Compare commits

...

524 Commits

Author SHA1 Message Date
Lakoja c4573caea2 3477: Make background darker in dark theme and highlight colors darker in light theme 2024-04-26 15:04:09 +02:00
Lakoja fdca12d85a 3477: (related) Group theme blocks 2024-04-26 15:04:09 +02:00
Lakoja 884a2fc211 3477: (related) Only use color references in theme color files 2024-04-26 15:04:09 +02:00
Weblate fe7103f2b9
Translations update from Weblate (#4388)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)

---------

Co-authored-by: Manuel <mannivuwiki@gmail.com>
Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
2024-04-22 19:46:06 +02:00
Weblate 7960db6c78
Translations update from Weblate (#4385)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)

---------

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
2024-04-21 10:26:36 +02:00
Konrad Pozniak 333beec17d
upgrade to gradle/actions/wrapper-validation@v3 in the other workflow as well 🙄 (#4378)
missed this in #4374
2024-04-21 09:35:59 +02:00
charlag 8e4aab8bae Add F-Droid changelog for Tusky 25 2024-04-20 19:13:49 +02:00
Conny Duck 0362a32163 update changelog 2024-04-20 19:13:49 +02:00
Conny Duck 3fe900c2dd improve changelog layout 2024-04-20 19:13:49 +02:00
Conny Duck 51b7745cee update screenshots 2024-04-20 19:13:49 +02:00
Conny Duck 0a9485f8e9 upgrade version to 25.0 beta 1 2024-04-20 19:13:49 +02:00
Conny Duck a9fe9ba079 update changelog 2024-04-20 19:13:49 +02:00
Christophe Beyls 72ee0b4292
Enable support for WebVTT and TTML subtitles for the player (#4377)
All subtitle formats used to be enabled by default in older versions of
media3.
After the media3 library was upgraded to 1.3.0, Extractors should
specify a `SubtitleParser.Factory` explicitly. By default, no subtitle
formats are supported when using the now deprecated empty constructor of
Extractors.

Limit support to **WebVTT** and **TTML** which are the only true web
standards amongst all the subtitle formats supported by ExoPlayer.

Note that only subtitles embedded in MP4 and WebM files are supported
until Mastodon provides a way to upload subtitles separately.
2024-04-17 21:29:21 +02:00
Konrad Pozniak 4e822c9a0a
larger background for toolbar icons in AccountActivity (#4375)
Looks way better.
[I also wanted to change the color of the status bar, but nobody seems
to like it](https://chaos.social/@ConnyDuck/112178196967742268), so
let's leave it.

before/after
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/4e93c722-c1a3-4fc4-808f-037a1398a944"
width="260"/> <img
src="https://github.com/tuskyapp/Tusky/assets/10157047/2a58785b-d3f4-4613-9bd9-0e09436f7142"
width="260"/>
2024-04-17 18:41:51 +02:00
renovate[bot] 703641bf06
chore(deps): update plugin com.gradle.develocity to v3.17.2 (#4376)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| com.gradle.develocity | `3.17.1` -> `3.17.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.gradle.develocity/3.17.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.gradle.develocity/3.17.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.gradle.develocity/3.17.1/3.17.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.gradle.develocity/3.17.1/3.17.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMDEuNCIsInVwZGF0ZWRJblZlciI6IjM3LjMwMS40IiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 18:40:35 +02:00
Konrad Pozniak e15516af68
upgrade to gradle/actions/wrapper-validation@v3 (#4374)
see
https://github.com/gradle/wrapper-validation-action/releases/tag/v3.3.0
2024-04-17 18:40:26 +02:00
Christophe Beyls f69cae2315
Optimize I/O code using Okio - part 2 (#4372)
- Read license resource using Okio inside a coroutine (instead of the
main thread) in `LicenseActivity`
- Use Okio and its buffer system to copy ContentProvider streams and
files to a temporary file in `MediaUploader.prepareMedia()`
- Properly close the input file after copying it to a temporary file in
`MediaUploader.prepareMedia()`
- Properly close sink in case of null body source during file copy in
`Uri.copyToFolder()` in `DraftHelper.kt`
- Add comment explaining the current value of `DEFAULT_CHUNK_SIZE` in
`UriRequestBody.kt` and indent the file properly
- Replace hardcoded `Charset` and `Int` byte size with the proper
constants, and align the `hashCode()` implementation with other
`BitmapTransformation` implementations in
`CompositeWithOpaqueBackground`
- Properly close `InputStream` in case of error during Bitmap size
decoding in `getImageSquarePixels()`
- return `Int` instead of `Long` in `getImageSquarePixels()`, since the
current code simply converts the `Int` result to a `Long` _after_
multiplication and not before (and `Int.MAX_VALUE` is already way above
the maximum number of pixels a decoded Bitmap could return)
- Simplify `getImageOrientation()`
- Add explicit dependency to the Okio library and upgrade it to its
latest version.
2024-04-14 16:39:29 +02:00
Konrad Pozniak 2504f42f7b
Apply window insets to SwipeRefreshLayout in AccountActivity to not obscure spinner (#4371)
before & after

<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/b029f5ff-9b17-48be-b306-a2e7e03ef6f7"
width="240"/>
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/ea9d3aa8-1f76-4709-9677-f478e2e0064a"
width="240"/>
2024-04-14 16:13:41 +02:00
renovate[bot] 546145da88
chore(deps): update dependency com.android.application to v8.3.2 (#4370)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.android.application](https://developer.android.com/studio/build)
([source](https://android.googlesource.com/platform/tools/base)) |
`8.3.1` -> `8.3.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.application/8.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.android.application/8.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.android.application/8.3.1/8.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.application/8.3.1/8.3.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-10 21:53:39 +02:00
renovate[bot] d87754fabe
fix(deps): update dependency org.mockito.kotlin:mockito-kotlin to v5.3.1 (#4369)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[org.mockito.kotlin:mockito-kotlin](https://togithub.com/mockito/mockito-kotlin)
| `5.2.1` -> `5.3.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.mockito.kotlin:mockito-kotlin/5.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.mockito.kotlin:mockito-kotlin/5.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.mockito.kotlin:mockito-kotlin/5.2.1/5.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.mockito.kotlin:mockito-kotlin/5.2.1/5.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>mockito/mockito-kotlin
(org.mockito.kotlin:mockito-kotlin)</summary>

###
[`v5.3.1`](https://togithub.com/mockito/mockito-kotlin/releases/tag/5.3.1)

[Compare
Source](https://togithub.com/mockito/mockito-kotlin/compare/5.2.1...5.3.1)

<sup><sup>*Changelog generated by [Shipkit Changelog Gradle
Plugin](https://togithub.com/shipkit/shipkit-changelog)*</sup></sup>

##### 5.3.1

- 2024-04-09 - [3
commit(s)](https://togithub.com/mockito/mockito-kotlin/compare/5.3.0...5.3.1)
by Oscar Guillén, Róbert Papp
- Follow-up on
[#&#8203;508](https://togithub.com/mockito/mockito-kotlin/issues/508):
add tests and remove unnecessary methods
[(#&#8203;516)](https://togithub.com/mockito/mockito-kotlin/pull/516)
- Fix broken release
([#&#8203;509](https://togithub.com/mockito/mockito-kotlin/issues/509))
[(#&#8203;514)](https://togithub.com/mockito/mockito-kotlin/pull/514)
- Add support for destructured parameters in answers
[(#&#8203;512)](https://togithub.com/mockito/mockito-kotlin/pull/512)
- Release is broken
[(#&#8203;509)](https://togithub.com/mockito/mockito-kotlin/issues/509)
- Port AdditionalMatchers matchers
[(#&#8203;508)](https://togithub.com/mockito/mockito-kotlin/pull/508)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-10 21:53:30 +02:00
renovate[bot] b5c9356851
fix(deps): update androidx.media3 to v1.3.1 (#4368)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [androidx.media3:media3-ui](https://togithub.com/androidx/media) |
`1.3.0` -> `1.3.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-ui/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-ui/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-ui/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-ui/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-datasource-okhttp](https://togithub.com/androidx/media)
| `1.3.0` -> `1.3.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-datasource-okhttp/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-datasource-okhttp/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-datasource-okhttp/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-datasource-okhttp/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-exoplayer](https://togithub.com/androidx/media)
| `1.3.0` -> `1.3.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer/1.3.0/1.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>androidx/media (androidx.media3:media3-ui)</summary>

###
[`v1.3.1`](https://togithub.com/androidx/media/blob/HEAD/RELEASENOTES.md#131-2024-04-11)

[Compare
Source](https://togithub.com/androidx/media/compare/1.3.0...1.3.1)

This release includes the following changes since the
[1.3.0 release](#&#8203;130-2024-03-06):

-   Common Library:
- Add `Format.labels` to allow localized or other alternative labels.
-   ExoPlayer:
- Fix issue where `PreloadMediaPeriod` cannot retain the streams when it
        is preloaded again.
- Apply the correct corresponding `TrackSelectionResult` to the playing
        period in track reselection.
- Start early-enabled renderers only after advancing the playing period
        when transitioning between media items
([#&#8203;1017](https://togithub.com/androidx/media/issues/1017)).
- Add missing return type to proguard `-keepclasseswithmembers` rule for
        `DefaultVideoFrameProcessor.Factory.Builder.build()`
([#&#8203;1187](https://togithub.com/androidx/media/issues/1187)).
-   Transformer:
- Add workaround for exception thrown due to `MediaMuxer` not supporting
        negative presentation timestamps before API 30.
-   Track Selection:
- `DefaultTrackSelector`: Prefer video tracks with a 'reasonable' frame
rate (>=10fps) over those with a lower or unset frame rate. This ensures
the player selects the 'real' video track in MP4s extracted from motion
        photos that can contain two HEVC tracks where one has a higher
        resolution but a very small number of frames
([#&#8203;1051](https://togithub.com/androidx/media/issues/1051)).
-   Extractors:
- Fix issue where padding was not skipped when reading odd-sized chunks
from WAV files
([#&#8203;1117](https://togithub.com/androidx/media/pull/1117)).
- MP3: Populate `Format.averageBitrate` from metadata frames such as
        `XING` and `VBRI`.
- MPEG-TS: Revert a change that aimed to ensure the last frame is
rendered
        by passing the last access unit of a stream to the sample queue
([#&#8203;7909](https://togithub.com/google/ExoPlayer/issues/7909)).
This is due
        to the change causing new problems with I-frame only HLS streams
([#&#8203;1150](https://togithub.com/google/ExoPlayer/issues/1150)) and
H.262 HLS
streams
([#&#8203;1126](https://togithub.com/google/ExoPlayer/issues/1126)).
-   Audio:
- Allow renderer recovery by disabling offload if audio track fails to
        initialize in offload mode.
-   Video:
- Add workaround for a device issue on Galaxy Tab S7 FE, Chromecast with
Google TV, and Lenovo M10 FHD Plus that causes 60fps H265 streams to be
        marked as unsupported
- Add workaround that ensures the first frame is always rendered while
tunneling even if the device does not do this automatically as required
by the API
([#&#8203;1169](https://togithub.com/androidx/media/issues/1169)).
        ([#&#8203;966](https://togithub.com/androidx/media/issues/966)).
- Fix issue where HDR color info handling causes codec misbehavior and
        prevents adaptive format switches for SDR video tracks
([#&#8203;1158](https://togithub.com/androidx/media/issues/1158)).
-   Text:
    -   WebVTT: Prevent directly consecutive cues from creating spurious
        additional `CuesWithTiming` instances from `WebvttParser.parse`
([#&#8203;1177](https://togithub.com/androidx/media/issues/1177)).
-   DRM:
- Work around a `NoSuchMethodError` which can be thrown by the
`MediaDrm`
        framework instead of `ResourceBusyException` or
        `NotProvisionedException` on some Android 14 devices
([#&#8203;1145](https://togithub.com/androidx/media/issues/1145)).
-   Effect:
    -   Improved PQ to SDR tone-mapping by converting color spaces.
-   Session:
- Fix issue where the current position jumps back when the controller
        replaces the current item
        ([#&#8203;951](https://togithub.com/androidx/media/issues/951)).
- Fix issue where `MediaMetadata` with just non-null `extras` is not
        transmitted between media controllers and sessions
([#&#8203;1176](https://togithub.com/androidx/media/issues/1176)).
-   UI:
    -   Fallback to include audio track language name if `Locale` cannot
        identify a display name
        ([#&#8203;988](https://togithub.com/androidx/media/issues/988)).
-   DASH Extension:
- Populate all `Label` elements from the manifest into `Format.labels`
        ([#&#8203;1054](https://togithub.com/androidx/media/pull/1054)).
-   RTSP Extension:
    -   Skip empty session information values (i-tags) in SDP parsing
([#&#8203;1087](https://togithub.com/androidx/media/issues/1087)).
-   Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
- Disable the MIDI extension as a local dependency by default because it
requires an additional Maven repository to be configured. Users who need
        this module from a local dependency
[can re-enable
it](https://togithub.com/androidx/media/blob/main/README.md#midi-module).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-10 21:53:14 +02:00
renovate[bot] 0a5d277623
chore(deps): update plugin com.gradle.develocity to v3.17.1 (#4367)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| com.gradle.develocity | `3.17` -> `3.17.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.gradle.develocity/3.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.gradle.develocity/3.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.gradle.develocity/3.17/3.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.gradle.develocity/3.17/3.17.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-10 21:53:05 +02:00
Christophe Beyls 65af26993b
Optimize I/O code using Okio (#4366)
This pull request takes advantage of the Okio library to simplify, fix
or improve performance of some I/O related code in Tusky.

- Return early or throw `FileNotFoundException` early in case
`contentResolver.openInputStream()` returns `null` instead of throwing
`NullPointerException` later. Change the signature of
`Closeable.closeQuietly()` to only accept a non-null `Closeable`.
- Reimplement `Uri.copyToFile()` using Okio. This takes advantage of the
built-in high-performance buffers of the library so a buffer doesn't
need to be allocated or managed manually. The new implementation also
makes sure that the input and output streams are always closed, as the
original code could in some cases return without properly closing a
stream.
- Reimplement `ProgressRequestBody` as `Uri.asRequestBody()` (adding to
the existing extension functions available in the Okio library to create
a `RequestBody`). The new implementation uses Okio's `Buffer` instead of
a manually managed byte array, which allows to avoid copying bytes from
one buffer to the next. The max number of bytes read at once was
increased from 2K to 8K to improve performance. Avoid division by zero
in case `contentLength` is `0`. Finally, this implementation now takes a
`Uri` as input instead of an `InputStream`, because a `RequestBody` must
be replayable in case Okio retries the request, and an `InputStream` can
only be used once.
2024-04-10 21:52:55 +02:00
Konrad Pozniak ee9a9fc51e
Translations update from Weblate (#4363)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-04-10 21:47:41 +02:00
Christophe Beyls ec599c8f8a
Replace java.util.Random with kotlin Random object (#4364)
This also improves randomness by avoiding to reinitialize the random
number generator repeatedly from a seed based on the current time.
Typically, if the number generator is reinitialized repeatedly at
non-random times (like multiple times in a row), then generated numbers
have a higher chance of repeating.

The Kotlin Random object is only initialized once, using the best seed
available for the current Android version.
2024-04-10 21:47:27 +02:00
Konrad Pozniak 2a4d60bed8
fix deserializing audio attachments (#4362)
closes #4361 

```
com.squareup.moshi.JsonDataException: Required value 'width' missing at $.statuses[0].media_attachments[0].meta.original
       at com.squareup.moshi.internal.Util.missingProperty(Util.java:660)
       at com.keylesspalace.tusky.entity.Attachment_SizeJsonAdapter.fromJson(Attachment_SizeJsonAdapter.kt:81)
       at com.keylesspalace.tusky.entity.Attachment_SizeJsonAdapter.fromJson(Attachment_SizeJsonAdapter.kt:23)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at com.keylesspalace.tusky.entity.Attachment_MetaDataJsonAdapter.fromJson(Attachment_MetaDataJsonAdapter.kt:64)
       at com.keylesspalace.tusky.entity.Attachment_MetaDataJsonAdapter.fromJson(Attachment_MetaDataJsonAdapter.kt:23)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at com.keylesspalace.tusky.entity.AttachmentJsonAdapter.fromJson(AttachmentJsonAdapter.kt:66)
       at com.keylesspalace.tusky.entity.AttachmentJsonAdapter.fromJson(AttachmentJsonAdapter.kt:22)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at com.squareup.moshi.CollectionJsonAdapter.fromJson(CollectionJsonAdapter.java:81)
       at com.squareup.moshi.CollectionJsonAdapter$2.fromJson(CollectionJsonAdapter.java:55)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at com.keylesspalace.tusky.entity.StatusJsonAdapter.fromJson(StatusJsonAdapter.kt:195)
       at com.keylesspalace.tusky.entity.StatusJsonAdapter.fromJson(StatusJsonAdapter.kt:26)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at com.squareup.moshi.CollectionJsonAdapter.fromJson(CollectionJsonAdapter.java:81)
       at com.squareup.moshi.CollectionJsonAdapter$2.fromJson(CollectionJsonAdapter.java:55)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at com.keylesspalace.tusky.entity.SearchResultJsonAdapter.fromJson(SearchResultJsonAdapter.kt:51)
       at com.keylesspalace.tusky.entity.SearchResultJsonAdapter.fromJson(SearchResultJsonAdapter.kt:21)
       at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
       at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:46)
       at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:27)
       at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:246)
       at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:156)
       at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
       at java.lang.Thread.run(Thread.java:1012)
```
2024-04-10 21:47:05 +02:00
Konrad Pozniak f1b0e0fbc2
enableDecoderFallback for ExoPlayer (#4360)
This helps playing some media even if there is a problem with the
primary decoder.
E.g. [this
video](https://mastodon.social/@krzyzanowskim/112208964123517040) fails
on my Fairphone 4 without this change.


<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/215d932c-9ed1-4ee8-8be7-e6ca28ddec23"
width="200"/>

<details>
  <summary>Stacktrace</summary>

```
androidx.media3.exoplayer.ExoPlaybackException: MediaCodecVideoRenderer error, index=0, format=Format(1, null, null, video/avc, avc1.640034, -1, null, [1920, 1440, 119.99593, ColorInfo(BT709, Limited range, sRGB, false, 8bit Luma, 8bit Chroma)], [-1, -1]), format_supported=YES
      at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:620)
      at android.os.Handler.dispatchMessage(Handler.java:102)
      at android.os.Looper.loopOnce(Looper.java:201)
      at android.os.Looper.loop(Looper.java:288)
      at android.os.HandlerThread.run(HandlerThread.java:67)
  Caused by: androidx.media3.exoplayer.mediacodec.MediaCodecRenderer$DecoderInitializationException: Decoder init failed: OMX.qcom.video.decoder.avc, Format(1, null, null, video/avc, avc1.640034, -1, null, [1920, 1440, 119.99593, ColorInfo(BT709, Limited range, sRGB, false, 8bit Luma, 8bit Chroma)], [-1, -1])
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1114)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecOrBypass(MediaCodecRenderer.java:551)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:1560)
      at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:1152)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.readSourceOmittingSampleData(MediaCodecRenderer.java:994)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:814)
      at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.render(MediaCodecVideoRenderer.java:940)
      at androidx.media3.exoplayer.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:1102)
      at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:541)
      at android.os.Handler.dispatchMessage(Handler.java:102) 
      at android.os.Looper.loopOnce(Looper.java:201) 
      at android.os.Looper.loop(Looper.java:288) 
      at android.os.HandlerThread.run(HandlerThread.java:67) 
  Caused by: android.media.MediaCodec$CodecException: Error 0xfffffff4
      at android.media.MediaCodec.native_configure(Native Method)
      at android.media.MediaCodec.configure(MediaCodec.java:2215)
      at android.media.MediaCodec.configure(MediaCodec.java:2131)
      at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecAdapter.initialize(AsynchronousMediaCodecAdapter.java:174)
      at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecAdapter.access$100(AsynchronousMediaCodecAdapter.java:54)
      at androidx.media3.exoplayer.mediacodec.AsynchronousMediaCodecAdapter$Factory.createAdapter(AsynchronousMediaCodecAdapter.java:119)
      at androidx.media3.exoplayer.mediacodec.DefaultMediaCodecAdapterFactory.createAdapter(DefaultMediaCodecAdapterFactory.java:117)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.initCodec(MediaCodecRenderer.java:1195)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecWithFallback(MediaCodecRenderer.java:1103)
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.maybeInitCodecOrBypass(MediaCodecRenderer.java:551) 
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:1560) 
      at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:1152) 
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.readSourceOmittingSampleData(MediaCodecRenderer.java:994) 
      at androidx.media3.exoplayer.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:814) 
      at androidx.media3.exoplayer.video.MediaCodecVideoRenderer.render(MediaCodecVideoRenderer.java:940) 
      at androidx.media3.exoplayer.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:1102) 
      at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:541) 
      at android.os.Handler.dispatchMessage(Handler.java:102) 
      at android.os.Looper.loopOnce(Looper.java:201) 
      at android.os.Looper.loop(Looper.java:288) 
      at android.os.HandlerThread.run(HandlerThread.java:67) 
```

</details>
2024-04-10 21:46:52 +02:00
Ümit Solmaz 9e0796ae6c Translated using Weblate (Turkish)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2024-04-10 19:09:08 +00:00
Konrad Pozniak 0d3b1b1c5a
show rules of the correct instance on the auth screen (#4358)
closes #4357
2024-04-05 12:01:54 +02:00
renovate[bot] b524d57d64
chore(deps): update plugin google-ksp to v1.9.23-1.0.20 (#4359)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.devtools.ksp](https://goo.gle/ksp)
([source](https://togithub.com/google/ksp)) | `1.9.23-1.0.19` ->
`1.9.23-1.0.20` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.devtools.ksp/1.9.23-1.0.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.devtools.ksp/1.9.23-1.0.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.devtools.ksp/1.9.23-1.0.19/1.9.23-1.0.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.devtools.ksp/1.9.23-1.0.19/1.9.23-1.0.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>google/ksp (com.google.devtools.ksp)</summary>

###
[`v1.9.23-1.0.20`](https://togithub.com/google/ksp/releases/tag/1.9.23-1.0.20)

[Compare
Source](https://togithub.com/google/ksp/compare/1.9.23-1.0.19...1.9.23-1.0.20)

#### KSP1 issues fixed

- performance optimization for certain workload consists of heavy Java
files, including
- Replace IdKey's impl with identityHashCode
[#&#8203;1804](https://togithub.com/google/ksp/issues/1804)
- Cache enclosed descriptors by name
[#&#8203;1808](https://togithub.com/google/ksp/issues/1808)
- Add excludedSources to the KSP extension object
[#&#8203;1793](https://togithub.com/google/ksp/issues/1793) thanks to
[@&#8203;bitspittle](https://togithub.com/bitspittle)

#### KSP2 issues fixed

- NoClassDefFoundError for LZ4Factory when trying KSP2
[#&#8203;1713](https://togithub.com/google/ksp/issues/1713)
- Calling KSType.replace() with original arguments results in <ERROR
TYPE> [#&#8203;1807](https://togithub.com/google/ksp/issues/1807)
- Static fields in base class appear in derived classes
[#&#8203;1744](https://togithub.com/google/ksp/issues/1744)
-   fix backing field checking for top level callables
-   support sealed inheritors lookup.
-   support type alias for getSymbolsWithAnnotation
-   support more types for reference elements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-05 12:01:38 +02:00
Zongle Wang d01d4b2157
Migrate to develocity plugin (#4356)
https://docs.gradle.com/enterprise/gradle-plugin/legacy/#develocity_migration
2024-04-04 19:25:25 +02:00
Konrad Pozniak a312492b1d
Translations update from Weblate (#4343)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-04-04 19:24:13 +02:00
renovate[bot] 721a2a6d03
fix(deps): update dependency org.robolectric:robolectric to v4.12.1 (#4355)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [org.robolectric:robolectric](http://robolectric.org)
([source](https://togithub.com/robolectric/robolectric)) | `4.12` ->
`4.12.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.robolectric:robolectric/4.12.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.robolectric:robolectric/4.12.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.robolectric:robolectric/4.12/4.12.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.robolectric:robolectric/4.12/4.12.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 21:08:19 +02:00
renovate[bot] ec065b7c2e
chore(deps): update plugin com.gradle.enterprise to v3.17 (#4354)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| com.gradle.enterprise | `3.16.2` -> `3.17` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.gradle.enterprise/3.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.gradle.enterprise/3.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.gradle.enterprise/3.16.2/3.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.gradle.enterprise/3.16.2/3.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 21:08:10 +02:00
XoseM 98864e8097 Translated using Weblate (Galician)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2024-04-03 18:30:31 +00:00
Konrad Pozniak fa3c7db919
add the correct information to regenerate the lint baseline file (#4351)
also regenenerate the file
2024-04-02 21:02:51 +02:00
Christophe Beyls df7b11afc3
Replace Gson library with Moshi (#4309)
**! ! Warning**: Do not merge before testing every API call and database
read involving JSON !

**Gson** is obsolete and has been superseded by **Moshi**. But more
importantly, parsing Kotlin objects using Gson is _dangerous_ because
Gson uses Java serialization and is **not Kotlin-aware**. This has two
main consequences:

- Fields of non-null types may end up null at runtime. Parsing will
succeed, but the code may crash later with a `NullPointerException` when
trying to access a field member;
- Default values of constructor parameters are always ignored. When
absent, reference types will be null, booleans will be false and
integers will be zero.

On the other hand, Kotlin-aware parsers like **Moshi** or **Kotlin
Serialization** will validate at parsing time that all received fields
comply with the Kotlin contract and avoid errors at runtime, making apps
more stable and schema mismatches easier to detect (as long as logs are
accessible):

- Receiving a null value for a non-null type will generate a parsing
error;
- Optional types are declared explicitly by adding a default value. **A
missing value with no default value declaration will generate a parsing
error.**

Migrating the entity declarations from Gson to Moshi will make the code
more robust but is not an easy task because of the semantic differences.

With Gson, both nullable and optional fields are represented with a null
value. After converting to Moshi, some nullable entities can become
non-null with a default value (if they are optional and not nullable),
others can stay nullable with no default value (if they are mandatory
and nullable), and others can become **nullable with a default value of
null** (if they are optional _or_ nullable _or_ both). That third option
is the safest bet when it's not clear if a field is optional or not,
except for lists which can usually be declared as non-null with a
default value of an empty list (I have yet to see a nullable array type
in the Mastodon API).

Fields that are currently declared as non-null present another
challenge. In theory, they should remain as-is and everything will work
fine. In practice, **because Gson is not aware of nullable types at
all**, it's possible that some non-null fields currently hold a null
value in some cases but the app does not report any error because the
field is not accessed by Kotlin code in that scenario. After migrating
to Moshi however, parsing such a field will now fail early if a null
value or no value is received.

These fields will have to be identified by heavily testing the app and
looking for parsing errors (`JsonDataException`) and/or by going through
the Mastodon documentation. A default value needs to be added for
missing optional fields, and their type could optionally be changed to
nullable, depending on the case.

Gson is also currently used to serialize and deserialize objects to and
from the local database, which is also challenging because backwards
compatibility needs to be preserved. Fortunately, by default Gson omits
writing null fields, so a field of type `List<T>?` could be replaced
with a field of type `List<T>` with a default value of `emptyList()` and
reading back the old data should still work. However, nullable lists
that are written directly (not as a field of another object) will still
be serialized to JSON as `"null"` so the deserializing code must still
be handling null properly.

Finally, changing the database schema is out of scope for this pull
request, so database entities that also happen to be serialized with
Gson will keep their original types even if they could be made non-null
as an improvement.

In the end this is all for the best, because the app will be more
reliable and errors will be easier to detect by showing up earlier with
a clear error message. Not to mention the performance benefits of using
Moshi compared to Gson.

- Replace Gson reflection with Moshi Kotlin codegen to generate all
parsers at compile time.
- Replace custom `Rfc3339DateJsonAdapter` with the one provided by
moshi-adapters.
- Replace custom `JsonDeserializer` classes for Enum types with
`EnumJsonAdapter.create(T).withUnknownFallback()` from moshi-adapters to
support fallback values.
- Replace `GuardedBooleanAdapter` with the more generic `GuardedAdapter`
which works with any type. Any nullable field may now be annotated with
`@Guarded`.
- Remove Proguard rules related to Json entities. Each Json entity needs
to be annotated with `@JsonClass` with no exception, and adding this
annotation will ensure that R8/Proguard will handle the entities
properly.
- Replace some nullable Boolean fields with non-null Boolean fields with
a default value where possible.
- Replace some nullable list fields with non-null list fields with a
default value of `emptyList()` where possible.
- Update `TimelineDao` to perform all Json conversions internally using
`Converters` so no Gson or Moshi instance has to be passed to its
methods.
- ~~Create a custom `DraftAttachmentJsonAdapter` to serialize and
deserialize `DraftAttachment` which is a special entity that supports
more than one json name per field. A custom adapter is necessary because
there is not direct equivalent of `@SerializedName(alternate = [...])`
in Moshi.~~ Remove alternate names for some `DraftAttachment` fields
which were used as a workaround to deserialize local data in 2-years old
builds of Tusky.
- Update tests to make them work with Moshi.
- Simplify a few `equals()` implementations.
- Change a few functions to `val`s
- Turn `NetworkModule` into an `object` (since it contains no abstract
methods).

Please test the app thoroughly before merging. There may be some fields
currently declared as mandatory that are actually optional.
2024-04-02 21:01:04 +02:00
Konrad Pozniak 5343766886
fix swipe-refresh spinner showing forever when refreshing AccountActivity (#4345)
The flow must emit every update even if the values are the same, so use
SharedFlow instead of StateFlow.

Regression from https://github.com/tuskyapp/Tusky/pull/4337 cc @Goooler
2024-03-30 11:31:29 +01:00
renovate[bot] 9491ebb031
fix(deps): update dependency org.robolectric:robolectric to v4.12 (#4350)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [org.robolectric:robolectric](http://robolectric.org)
([source](https://togithub.com/robolectric/robolectric)) | `4.11.1` ->
`4.12` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.robolectric:robolectric/4.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.robolectric:robolectric/4.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.robolectric:robolectric/4.11.1/4.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.robolectric:robolectric/4.11.1/4.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-30 07:38:36 +01:00
Konrad Pozniak b022767ae6
fix translating polls and spoilers (#4344)
The docs are wrong https://github.com/mastodon/documentation/pull/1423 🙄
2024-03-29 21:13:57 +01:00
Zongle Wang ba495f41a5
Remove redundant crossinline (#4348)
Seems we don't need them in newer Kotlin.
2024-03-29 21:12:49 +01:00
renovate[bot] af016c1766
fix(deps): update dagger to v2.51.1 (#4349)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.dagger:dagger](https://togithub.com/google/dagger) |
`2.51` -> `2.51.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-compiler](https://togithub.com/google/dagger)
| `2.51` -> `2.51.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-compiler/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-compiler/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-compiler/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-compiler/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-support](https://togithub.com/google/dagger)
| `2.51` -> `2.51.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-support/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-support/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-support/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-support/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-processor](https://togithub.com/google/dagger)
| `2.51` -> `2.51.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-processor/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-processor/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-processor/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-processor/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [com.google.dagger:dagger-android](https://togithub.com/google/dagger)
| `2.51` -> `2.51.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android/2.51/2.51.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-29 20:12:08 +01:00
Zongle Wang 1acae50845
Convert some sealed classes to interfaces (#4347)
There is no non-abstract field in them, we can just fall back to
interfaces.
2024-03-29 20:11:53 +01:00
Zongle Wang e865ffafde
Don't use mutable shared flows in UI (#4346) 2024-03-29 20:02:12 +01:00
renovate[bot] 06f283575d
fix(deps): update retrofit to v2.11.0 (#4342)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[com.squareup.retrofit2:retrofit](https://togithub.com/square/retrofit)
| `2.10.0` -> `2.11.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.squareup.retrofit2:retrofit/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.squareup.retrofit2:retrofit/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.squareup.retrofit2:retrofit/2.10.0/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.squareup.retrofit2:retrofit/2.10.0/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.squareup.retrofit2:converter-gson](https://togithub.com/square/retrofit)
| `2.10.0` -> `2.11.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.squareup.retrofit2:converter-gson/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.squareup.retrofit2:converter-gson/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.squareup.retrofit2:converter-gson/2.10.0/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.squareup.retrofit2:converter-gson/2.10.0/2.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>square/retrofit (com.squareup.retrofit2:retrofit)</summary>

###
[`v2.11.0`](https://togithub.com/square/retrofit/blob/HEAD/CHANGELOG.md#2110---2024-03-28)

[Compare
Source](https://togithub.com/square/retrofit/compare/2.10.0...2.11.0)

**New**

- The built-in `OptionalConverterFactory` is now public to allow
installing it before other converters which consume all types (e.g.,
Moshi, Gson, Jackson, etc.).

**Fixed**

- Ensure that exceptions thrown from failure to parse method annotations
can be observed by multiple threads/callers. Previously only the first
caller would see the actual parsing exception and other callers would
get a cryptic `ClassCastException`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjkuMiIsInVwZGF0ZWRJblZlciI6IjM3LjI2OS4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-29 10:16:03 +01:00
Konrad Pozniak b85ada930b
fix warnings in test runs (#4340)
Found while working on #4026 but not directly related.

Two cases of unmocked methods and one of unclosed resource.
2024-03-28 09:13:05 +01:00
Zongle Wang 211983c7ee
Remove unused Lifecycle dependencies (#4339) 2024-03-27 12:21:31 +01:00
Konrad Pozniak 50b84b6ee0
Translations update from Weblate (#4332)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-27 12:17:57 +01:00
Zongle Wang f029b7f84d
Migrate LiveData to Flow (#4337) 2024-03-27 11:34:17 +01:00
Zongle Wang a3d87de8ac
Don't use mutable state flows in UI (#4336) 2024-03-27 11:17:42 +01:00
Weblate efa29d37b2 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/
Translation: Tusky/Tusky
2024-03-27 09:57:03 +00:00
fin-w 3fdb2c14a9 Translated using Weblate (English (United Kingdom))
Currently translated at 2.5% (16 of 639 strings)

Translated using Weblate (Welsh)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: fin-w <fin-w@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/en_GB/
Translation: Tusky/Tusky
2024-03-27 09:57:03 +00:00
Ümit Solmaz adf4dcb3c8 Translated using Weblate (Turkish)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2024-03-27 09:57:03 +00:00
xzFantom eb75cf0818 Translated using Weblate (Belarusian)
Currently translated at 91.3% (584 of 639 strings)

Co-authored-by: xzFantom <xzfantom@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/be/
Translation: Tusky/Tusky
2024-03-27 09:57:03 +00:00
Konrad Pozniak 3274bd2660
Translations update from Weblate (#4327)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2024-03-26 19:14:02 +01:00
Konrad Pozniak c7a1ddd589
fix crash when instance info fails to load (#4335)
Steps to reproduce: Cold start the app while being logged in and
offline.
2024-03-26 18:25:34 +01:00
Deleted User 4849f6772e Translated using Weblate (German)
Currently translated at 87.8% (29 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/
2024-03-26 16:48:23 +00:00
renovate[bot] 00d7cc72b1
chore(deps): update dependency gradle to v8.7 (#4333)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [gradle](https://gradle.org)
([source](https://togithub.com/gradle/gradle)) | minor | `8.6` -> `8.7`
|

---

### Release Notes

<details>
<summary>gradle/gradle (gradle)</summary>

### [`v8.7`](https://togithub.com/gradle/gradle/compare/v8.6.0...v8.7.0)

[Compare
Source](https://togithub.com/gradle/gradle/compare/v8.6.0...v8.7.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNjEuMCIsInVwZGF0ZWRJblZlciI6IjM3LjI2MS4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-25 10:23:02 +01:00
Zongle Wang 83cbbe9ada
Retrofit 2.10.0 (#4330)
https://github.com/square/retrofit/releases/tag/2.10.0
2024-03-19 08:32:14 +01:00
renovate[bot] be8140d628
chore(deps): update dependency com.android.application to v8.3.1 (#4328)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.android.application](https://developer.android.com/studio/build)
([source](https://android.googlesource.com/platform/tools/base)) |
`8.3.0` -> `8.3.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.application/8.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.android.application/8.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.android.application/8.3.0/8.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.application/8.3.0/8.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yNDUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjI0NS4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 07:57:26 +01:00
Konrad Pozniak d4c8d6213c
Translations update from Weblate (#4325)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-18 14:07:49 +01:00
Deleted User 010a3ed5fb Translated using Weblate (German)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Deleted User <noreply+314@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/
Translation: Tusky/Tusky
2024-03-18 04:45:01 +00:00
fin-w 08f1525b6e Translated using Weblate (Welsh)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: fin-w <fin-w@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-03-18 04:45:00 +00:00
Ihor Hordiichuk fa43d7738f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2024-03-18 04:45:00 +00:00
Sveinn í Felli d516c6e3a1 Translated using Weblate (Icelandic)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2024-03-18 04:45:00 +00:00
fin-w fd78cc7355 Translated using Weblate (Welsh)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: fin-w <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-03-18 04:45:00 +00:00
Konrad Pozniak d0e6e4ba39
Translations update from Weblate (#4324)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-13 15:21:21 +01:00
fin-w 594b89408f Translated using Weblate (Welsh)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: fin-w <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-03-13 06:33:19 +00:00
Gera, Zoltan 55e763a34b Translated using Weblate (Hungarian)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Gera, Zoltan <gerazo@manioka.hu>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/
Translation: Tusky/Tusky
2024-03-13 06:33:19 +00:00
cuithon 6d7a66a441
chore: fix typo (#4321) 2024-03-12 08:50:55 +01:00
Konrad Pozniak 5396452e1c
provide more space for debug information in bug reports (#4318) 2024-03-11 17:18:52 +01:00
Konrad Pozniak 0b87ba2031
prevent media visibility from changing when refreshing timelines (#4319)
classic operator precendence issue

closes #4317
2024-03-11 17:18:43 +01:00
Konrad Pozniak d5eb37595c
Translations update from Weblate (#4320)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-11 17:18:32 +01:00
Hồ Nhất Duy 23dd0d447c Translated using Weblate (Vietnamese)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2024-03-10 04:45:01 +00:00
fin-w b05965923d Translated using Weblate (Welsh)
Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-03-10 04:45:01 +00:00
Willow fbb22799dc
Machine translation of posts (#4307) 2024-03-09 16:12:18 +01:00
renovate[bot] 80982d061e
Update androidx.media3 to v1.3.0 (#4313)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [androidx.media3:media3-ui](https://togithub.com/androidx/media) |
`1.2.1` -> `1.3.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-ui/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-ui/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-ui/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-ui/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-datasource-okhttp](https://togithub.com/androidx/media)
| `1.2.1` -> `1.3.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-datasource-okhttp/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-datasource-okhttp/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-datasource-okhttp/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-datasource-okhttp/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-exoplayer](https://togithub.com/androidx/media)
| `1.2.1` -> `1.3.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer/1.2.1/1.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>androidx/media (androidx.media3:media3-ui)</summary>

###
[`v1.3.0`](https://togithub.com/androidx/media/blob/HEAD/RELEASENOTES.md#130-2024-03-06)

[Compare
Source](https://togithub.com/androidx/media/compare/1.2.1...1.3.0)

This release includes the following changes since the
[1.2.1 release](#&#8203;121-2024-01-09):

-   Common Library:
- Implement support for `android.resource://package/[type/]name` raw
resource URIs where `package` is different to the package of the current
        application. This has always been documented to work, but wasn't
        correctly implemented until now.
- Normalize MIME types set by app code or read from media to be fully
        lower-case.
    -   Define ads with a full `MediaItem` instead of a single `Uri` in
        `AdPlaybackState`.
    -   Increase `minSdk` to 19 (Android KitKat). This is
[aligned with all other AndroidX
libraries](https://android-developers.googleblog.com/2023/10/androidx-minsdkversion-19.html),
and is required for us to upgrade to the latest versions of our AndroidX
        dependencies.
    -   Populate both `artworkUri` and `artworkData` in
`MediaMetadata.Builder.populate(MediaMetadata)` when at least one of
them is non-null
([#&#8203;964](https://togithub.com/androidx/media/issues/964)).
-   ExoPlayer:
- Add `PreloadMediaSource` and `PreloadMediaPeriod` that allows apps to
preload a content media source at a specific start position before
playback. `PreloadMediaSource` takes care of preparing the content media
source to receive the `Timeline`, preparing and caching the period at
the given start position, selecting tracks and loading media data for
        the period. Apps control the preload progress by implementing
`PreloadMediaSource.PreloadControl` and set the preloaded source to the
        player for playback.
    -   Add `ExoPlayer.setImageOutput` that allows apps to set
        `ImageRenderer.ImageOutput`.
- `DefaultRenderersFactory` now provides an `ImageRenderer` to the
player
by default with null `ImageOutput` and `ImageDecoder.Factory.DEFAULT`.
- Emit `Player.Listener.onPositionDiscontinuity` event when silence is
skipped ([#&#8203;765](https://togithub.com/androidx/media/issues/765)).
- Add experimental support for parsing subtitles during extraction. You
        can enable this using
`MediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`.
    -   Support adaptive media sources with `PreloadMediaSource`.
    -   Implement `HttpEngineDataSource`, an `HttpDataSource` using the

[HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
        API.
- Prevent subclassing `CompositeSequenceableLoader`. This component was
[previously made
extensible](0de57cbfae)
but was never subclassed within the library. Customizations can be done
        by wrapping an instance using the
[decorator pattern](https://en.wikipedia.org/wiki/Decorator_pattern) and
        implementing a custom `CompositeSequenceableLoaderFactory`.
- Fix issue where repeating the same time causes metadata from this item
to be cleared
([#&#8203;1007](https://togithub.com/androidx/media/issues/1007)).
    -   Rename `experimentalSetSubtitleParserFactory` methods on
`BundledChunkExtractor.Factory` and `DefaultHlsExtractorFactory` to
`setSubtitleParserFactory` and disallow passing `null`. Use the new
`experimentalParseSubtitlesDuringExtraction(boolean)` methods to control
        parsing behaviour.
- Add support for customising the `SubtitleParser.Factory` used during
        extraction. This can be achieved with
        `MediaSource.Factory.setSubtitleParserFactory()`.
    -   Add source prefix to all `Format.id` fields generated from
`MergingMediaSource`. This helps to identify which source produced a
`Format`
([#&#8203;883](https://togithub.com/androidx/media/issues/883)).
- Fix the regex used for validating custom Common Media Client Data
(CMCD)
        key names by modifying it to only check for hyphen
([#&#8203;1028](https://togithub.com/androidx/media/issues/1028)).
    -   Stop double-encoding CMCD query parameters
([#&#8203;1075](https://togithub.com/androidx/media/issues/1075)).
-   Transformer:
    -   Add support for flattening H.265/HEVC SEF slow motion videos.
    -   Increase transmuxing speed, especially for 'remove video' edits.
- Add API to ensure that the output file starts on a video frame. This
can
make the output of trimming operations more compatible with player
        implementations that don't show the first video frame until its
        presentation timestamp
        ([#&#8203;829](https://togithub.com/androidx/media/issues/829)).
    -   Add support for optimizing single asset mp4 trim operations.
- Add support to ensure a video frame has the first timestamp in the
output file. Fixes output files beginning with black frame on iOS based
players ([#&#8203;829](https://togithub.com/androidx/media/issues/829)).
-   Track Selection:
- Add `DefaultTrackSelector.selectImageTrack` to enable image track
        selection.
- Add `TrackSelectionParameters.isPrioritizeImageOverVideoEnabled` to
determine whether to select an image track if both an image track and a
video track are available. The default value is `false` which means
        selecting a video track is prioritized.
-   Extractors:
    -   Add additional AV1C parsing to MP4 extractor to retrieve
        `ColorInfo.colorSpace`, `ColorInfo.colorTransfer`, and
        `ColorInfo.colorRange` values
        ([#&#8203;692](https://togithub.com/androidx/media/pull/692)).
- MP3: Use constant bitrate (CBR) seeking for files with an `Info`
header
(the CBR equivalent of the `Xing` header). Previously we used the seek
table from the `Info` header, but this results in less precise seeking
        than if we ignore it and assume the file is CBR.
    -   MPEG2-TS: Add DTS, DTS-LBR and DTS:X Profile2 support
        ([#&#8203;275](https://togithub.com/androidx/media/pull/275)).
- Extract audio types from TS descriptors and map them to role flags,
        allowing users to make better-informed audio track selections
        ([#&#8203;973](https://togithub.com/androidx/media/pull/973)).
-   Audio:
- Improve silence skipping algorithm with smooth volume ramp; retained
        minimal silence and more natural silence durations
([#&#8203;7423](https://togithub.com/google/ExoPlayer/issues/7423)).
    -   Report the skipped silence more deterministically
([#&#8203;1035](https://togithub.com/androidx/media/issues/1035)).
-   Video:
    -   Change the `MediaCodecVideoRenderer` constructor that takes a
`VideoFrameProcessor.Factory` argument and replace it with a constructor
that takes a `VideoSinkProvider` argument. Apps that want to inject a
        custom `VideoFrameProcessor.Factory` can instantiate a
        `CompositingVideoSinkProvider` that uses the custom
`VideoFrameProcessor.Factory` and pass the video sink provider to
        `MediaCodecVideoRenderer`.
-   Text:
- Fix serialization of bitmap cues to resolve `Tried to marshall a
Parcel
        that contained Binder objects` error when using
        `DefaultExtractorsFactory.setTextTrackTranscodingEnabled`
        ([#&#8203;836](https://togithub.com/androidx/media/issues/836)).
- CEA-708: Ignore `rowLock` value. The CEA-708-E S-2023 spec states that
`rowLock` and `columnLock` should both be assumed to be true, regardless
        of the values present in the stream (`columnLock` support is not
        implemented, so it's effectively assumed to always be false).
-   Image:
- Add support for DASH thumbnails. Grid images are cropped and
individual
thumbnails are provided to `ImageOutput` close to their presentation
        times.
-   DRM:
- Play 'clear lead' unencrypted samples in DRM content immediately by
default, even if the keys for the later encrypted samples aren't ready
yet. This may lead to mid-playback stalls if the keys still aren't ready
when the playback position reaches the encrypted samples (but previously
playback wouldn't have started at all by this point). This behavior can
        be disabled with

[`MediaItem.DrmConfiguration.Builder.setPlayClearContentWithoutKey`](https://developer.android.com/reference/androidx/media3/common/MediaItem.DrmConfiguration.Builder#setPlayClearContentWithoutKey\(boolean\))
        or

[`DefaultDrmSessionManager.Builder.setPlayClearSamplesWithoutKeys`](https://developer.android.com/reference/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.Builder#setPlayClearSamplesWithoutKeys\(boolean\)).
-   IMA extension:
- Fix issue where DASH and HLS ads without the appropriate file
extension
        can't be played.
-   Session:
    -   Disable double-click detection for TV apps
        ([#&#8203;962](https://togithub.com/androidx/media/issues/962)).
- Fix issue where `MediaItem.RequestMetadata` with just non-null extras
is
        not transmitted between media controllers and sessions.
- Add constructor to `MediaLibrarySession.Builder` that only takes a
        `Context` instead of a `MediaLibraryService`.
-   HLS Extension:
    -   Reduce `HlsMediaPeriod` to package-private visibility. This type
        shouldn't be directly depended on from outside the HLS package.
    -   Resolve seeks to beginning of a segment more efficiently
        ([#&#8203;1031](https://togithub.com/androidx/media/pull/1031)).
-   Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
    -   MIDI decoder: Ignore SysEx event messages
        ([#&#8203;710](https://togithub.com/androidx/media/pull/710)).
-   Test Utilities:
- Don't pause playback in `TestPlayerRunHelper.playUntilPosition`. The
test keeps the playback in a playing state, but suspends progress until
        the test is able to add assertions and further actions.
-   Demo app:
- Add a shortform demo module to demo the usage of `PreloadMediaSource`
        with the short-form content use case.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMzAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIzMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 13:33:13 +01:00
renovate[bot] bcde8ea8be
Update Kotlin (#4312)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.devtools.ksp](https://goo.gle/ksp)
([source](https://togithub.com/google/ksp)) | `1.9.22-1.0.18` ->
`1.9.23-1.0.19` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.devtools.ksp/1.9.23-1.0.19?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.devtools.ksp/1.9.23-1.0.19?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.devtools.ksp/1.9.22-1.0.18/1.9.23-1.0.19?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.devtools.ksp/1.9.22-1.0.18/1.9.23-1.0.19?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.plugin.parcelize | `1.9.22` -> `1.9.23` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.22/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.22/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.kapt | `1.9.22` -> `1.9.23` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.kapt/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.kapt/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.kapt/1.9.22/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.kapt/1.9.22/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.android | `1.9.22` -> `1.9.23` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.android/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.android/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.android/1.9.22/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.android/1.9.22/1.9.23?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>google/ksp (com.google.devtools.ksp)</summary>

###
[`v1.9.23-1.0.19`](https://togithub.com/google/ksp/releases/tag/1.9.23-1.0.19)

##### New APIs

- [#&#8203;1708](https://togithub.com/google/ksp/issues/1708)
`SymbolProcessorEnvironment.kspVersion`
- [#&#8203;1707](https://togithub.com/google/ksp/issues/1707)
`CodeGenerator.associateWithFunctions` and
`CodeGenerator.associateWithProperties`

##### Issues fixed in KSP2

- [#&#8203;1691](https://togithub.com/google/ksp/issues/1691)
IllegalStateException: Value type not found for value
- [#&#8203;1719](https://togithub.com/google/ksp/issues/1719) The order
of symbols returned from Resolver.getSymbolsWithAnnotation() is
different
- [#&#8203;1718](https://togithub.com/google/ksp/issues/1718) Getting
packageName of Kotlin types returns empty String
- [#&#8203;1721](https://togithub.com/google/ksp/issues/1721)
KSAnnotation packageName is an empty String for a type from a different
module
- [#&#8203;1722](https://togithub.com/google/ksp/issues/1722)
Resolver.getDeclarationsFromPackage() returns declarations from the
default package when passing in an unknown package
- [#&#8203;1717](https://togithub.com/google/ksp/issues/1717)
Resolver.getJvmName() returns different results for annotation args
- [#&#8203;1725](https://togithub.com/google/ksp/issues/1725)
asMemberOf() throws exception with a generic type without arguments
- [#&#8203;1728](https://togithub.com/google/ksp/issues/1728)
ClassCastException when calling KSType.replace() with empty list
- [#&#8203;1713](https://togithub.com/google/ksp/issues/1713)
NoClassDefFoundError for LZ4Factory when trying KSP2
- [#&#8203;1665](https://togithub.com/google/ksp/issues/1665)
NoClassDefFound in ksp.useKSP2=true mode
- [#&#8203;1733](https://togithub.com/google/ksp/issues/1733) ABI
incompatibility with kotlin-compiler-embeddable in 2.0.0-Beta4
- [#&#8203;1714](https://togithub.com/google/ksp/issues/1714) Generated
resources are not added to KotlinCompilation inputs
- [#&#8203;1747](https://togithub.com/google/ksp/issues/1747)
isCompanionObject is false for companion objects from KOTLIN_LIB
- [#&#8203;1743](https://togithub.com/google/ksp/issues/1743) Incorrect
type parameter variances
- [#&#8203;1759](https://togithub.com/google/ksp/issues/1759)
IllegalStateException when getting the modifiers from properties in a
Java annotation declaration

##### Issues fixed in KSP Gradle Plugin

- [#&#8203;1712](https://togithub.com/google/ksp/issues/1712) Analysis
API artifacts have the wrong common-deps dependency
- [#&#8203;1775](https://togithub.com/google/ksp/issues/1775) 1.0.18
creates circular dependencies with kapt tasks
- [#&#8203;1772](https://togithub.com/google/ksp/issues/1772) Update
plugin com.google.devtools.ksp to v1.9.22-1.0.18 BUILD FAILED

##### Known issues

- [#&#8203;1776](https://togithub.com/google/ksp/issues/1776) KSP2 has
higher memory usage when the compile classpath is very large.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMzAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIzMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 13:33:03 +01:00
renovate[bot] 06a896e755
Update dependency androidx.browser:browser to v1.8.0 (#4311)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.browser:browser](https://developer.android.com/jetpack/androidx/releases/browser#1.8.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.7.0` -> `1.8.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.browser:browser/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.browser:browser/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.browser:browser/1.7.0/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.browser:browser/1.7.0/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMjcuMiIsInVwZGF0ZWRJblZlciI6IjM3LjIyNy4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 13:32:42 +01:00
renovate[bot] 8e435aaea2
Update dependency app.cash.turbine:turbine to v1.1.0 (#4310)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [app.cash.turbine:turbine](https://togithub.com/cashapp/turbine) |
`1.0.0` -> `1.1.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/app.cash.turbine:turbine/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/app.cash.turbine:turbine/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/app.cash.turbine:turbine/1.0.0/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/app.cash.turbine:turbine/1.0.0/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>cashapp/turbine (app.cash.turbine:turbine)</summary>

###
[`v1.1.0`](https://togithub.com/cashapp/turbine/blob/HEAD/CHANGELOG.md#110---2024-03-06)

[Compare
Source](https://togithub.com/cashapp/turbine/compare/1.0.0...1.1.0)

[1.1.0]: https://togithub.com/cashapp/turbine/releases/tag/1.1.0

##### Changed

-   Add `wasmJs` target, remove `iosArm32` and `watchosX86` targets.
-   Throw unconsumed events if scope is externally canceled.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMjcuMiIsInVwZGF0ZWRJblZlciI6IjM3LjIyNy4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 13:32:30 +01:00
Konrad Pozniak f4782489ff
Translations update from Weblate (#4315)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2024-03-09 13:32:17 +01:00
Konrad Pozniak ce196738bf
Translations update from Weblate (#4314)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-09 13:32:03 +01:00
Ümit Solmaz e47ae6e71f Translated using Weblate (Turkish)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/tr/
2024-03-09 11:56:11 +00:00
Konrad Pozniak be8b7c3a31
improve MainActivity / LoginActivity transitions (#4301)
I overlooked those in https://github.com/tuskyapp/Tusky/pull/4224
2024-03-09 11:04:29 +01:00
Christophe Beyls 9901376d38
Move ExoPlayer initialization to a Dagger module and optimize its dependencies (#4296)
Currently, ExoPlayer is initialized explicitly in `ViewMediaFragment`
with all its dependencies, including many that are not useful for
viewing Mastodon media attachments.

This pull request moves most ExoPlayer initialization and configuration
to a new Dagger module, and instead a `Provider<ExoPlayer>` factory is
injected in the Fragment so it can create new instances when needed.

The following ExoPlayer components will be configured:

- **Renderers**: all of them (audio, video, metadata, subtitles) except
for the `CameraMotionRenderer`.
- **Extractors**: FLAC, Wav, Mp4, Ogg, Matroska/WebM and MP3 containers,
to provide the same support as Firefox or Chrome browsers. Other
container formats that are either image formats (already covered by
Glide), not web-friendly or reserved for live streaming are skipped.
- **MediaSource**: only progressive download (through OkHttp) is
provided. Live streaming support using protocols like RTSP, MPEG/Dash or
HLS is skipped, because Mastodon servers don't use these protocols to
download attachments.

The Mastodon documentation mentions the [supported media formats for
attachments](https://docs.joinmastodon.org/user/posting/#media) and this
covers them and even more. The docs also mentions that the video and
audio files are transcoded to MP4 and MP3 upon upload but that was not
the case in the past (for example WebM was used) and it could change
again in the future.

Specifying these components manually allows reducing the APK size by
about 200 KB thanks to R8 shrinking.

There are also a few extra code changes:
- Remove the code specific to API < 24 since the min SDK of the app is
now 24.
- Add support for pausing a video when unplugging headphones.
- Specify the audio attributes according to content type to help the
Android audio mixer.
2024-03-09 11:04:04 +01:00
Ümit Solmaz 071ae0bed2 Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2024-03-09 09:28:37 +00:00
Konrad Pozniak f09a5b00e0
fix boost/reply filters not working correctly in home timelines (#4308)
closes #4306
2024-03-06 13:21:29 +01:00
Konrad Pozniak 109abc5ac3
Translations update from Weblate (#4302)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-06 13:08:34 +01:00
renovate[bot] 69938c7de9
Update dependency com.android.application to v8.3.0 (#4295)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.android.application](https://developer.android.com/studio/build)
([source](https://android.googlesource.com/platform/tools/base)) |
`8.2.2` -> `8.3.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.application/8.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.android.application/8.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.android.application/8.2.2/8.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.application/8.2.2/8.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMjAuMiIsInVwZGF0ZWRJblZlciI6IjM3LjIyMC4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-06 13:07:26 +01:00
renovate[bot] 3d5dfeb1c3
Update plugin google-ksp to v1.9.22-1.0.18 (#4294)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.devtools.ksp](https://goo.gle/ksp)
([source](https://togithub.com/google/ksp)) | `1.9.22-1.0.17` ->
`1.9.22-1.0.18` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.devtools.ksp/1.9.22-1.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.devtools.ksp/1.9.22-1.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.devtools.ksp/1.9.22-1.0.17/1.9.22-1.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.devtools.ksp/1.9.22-1.0.17/1.9.22-1.0.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMjAuMiIsInVwZGF0ZWRJblZlciI6IjM3LjIyMC4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-06 13:07:16 +01:00
XoseM 0132b758af Translated using Weblate (Galician)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2024-03-06 11:31:48 +00:00
Hồ Nhất Duy c4bc15c04f Translated using Weblate (Vietnamese)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2024-03-06 11:31:48 +00:00
Ümit Solmaz df75dd61c9 Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2024-03-06 11:31:48 +00:00
Konrad Pozniak 7a05530359
put r8 rules for enums back in to fix crash in AccountListActivity (#4299)
Regression from #4291 // cc @cbeyls 

<details>
  <summary>Stacktrace</summary>
  
  ```
Process: com.keylesspalace.tusky, PID: 31230
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.keylesspalace.tusky/com.keylesspalace.tusky.components.accountlist.AccountListActivity}:
java.lang.RuntimeException: java.lang.NoSuchMethodException: h4.a.values
[]
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3635)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792)
at
android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at
android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at
android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loopOnce(Looper.java:201)
	at android.os.Looper.loop(Looper.java:288)
	at android.app.ActivityThread.main(ActivityThread.java:7839)
	at java.lang.reflect.Method.invoke(Native Method)
at
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.RuntimeException: java.lang.NoSuchMethodException:
h4.a.values []
	at java.lang.Enum.enumValues(Enum.java:270)
	at java.lang.Enum.access$000(Enum.java:61)
	at java.lang.Enum$1.create(Enum.java:277)
	at java.lang.Enum$1.create(Enum.java:275)
	at libcore.util.BasicLruCache.get(BasicLruCache.java:63)
	at java.lang.Enum.getSharedConstants(Enum.java:289)
	at java.lang.Enum.valueOf(Enum.java:243)
	at java.io.ObjectInputStream.readEnum(ObjectInputStream.java:1841)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1409)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
	at android.os.Parcel.readSerializable(Parcel.java:3507)
	at android.os.Parcel.readValue(Parcel.java:3277)
	at android.os.Parcel.readArrayMapInternal(Parcel.java:3623)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
	at android.os.BaseBundle.unparcel(BaseBundle.java:236)
	at android.os.BaseBundle.getSerializable(BaseBundle.java:1268)
	at android.os.Bundle.getSerializable(Bundle.java:1104)
	at android.content.Intent.getSerializableExtra(Intent.java:8575)
at
com.keylesspalace.tusky.components.accountlist.AccountListActivity.onCreate(SourceFile:23)
	at android.app.Activity.performCreate(Activity.java:8051)
	at android.app.Activity.performCreate(Activity.java:8031)
at
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1329)
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3608)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792) 
at
android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) 
at
android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
at
android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210) 
	at android.os.Handler.dispatchMessage(Handler.java:106) 
	at android.os.Looper.loopOnce(Looper.java:201) 
	at android.os.Looper.loop(Looper.java:288) 
	at android.app.ActivityThread.main(ActivityThread.java:7839) 
	at java.lang.reflect.Method.invoke(Native Method) 
at
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) 
Caused by: java.lang.NoSuchMethodException: h4.a.values []
	at java.lang.Class.getMethod(Class.java:2103)
	at java.lang.Class.getDeclaredMethod(Class.java:2081)
	at java.lang.Enum.enumValues(Enum.java:267)
	at java.lang.Enum.access$000(Enum.java:61) 
	at java.lang.Enum$1.create(Enum.java:277) 
	at java.lang.Enum$1.create(Enum.java:275) 
	at libcore.util.BasicLruCache.get(BasicLruCache.java:63) 
	at java.lang.Enum.getSharedConstants(Enum.java:289) 
	at java.lang.Enum.valueOf(Enum.java:243) 
	at java.io.ObjectInputStream.readEnum(ObjectInputStream.java:1841) 
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1409) 
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427) 
	at android.os.Parcel.readSerializable(Parcel.java:3507) 
	at android.os.Parcel.readValue(Parcel.java:3277) 
	at android.os.Parcel.readArrayMapInternal(Parcel.java:3623) 
at
android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292) 
	at android.os.BaseBundle.unparcel(BaseBundle.java:236) 
	at android.os.BaseBundle.getSerializable(BaseBundle.java:1268) 
	at android.os.Bundle.getSerializable(Bundle.java:1104) 
	at android.content.Intent.getSerializableExtra(Intent.java:8575) 
at
com.keylesspalace.tusky.components.accountlist.AccountListActivity.onCreate(SourceFile:23) 
	at android.app.Activity.performCreate(Activity.java:8051) 
	at android.app.Activity.performCreate(Activity.java:8031) 
at
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1329) 
at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3608) 
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3792) 
at
android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) 
at
android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
at
android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2210) 
	at android.os.Handler.dispatchMessage(Handler.java:106) 
	at android.os.Looper.loopOnce(Looper.java:201) 
	at android.os.Looper.loop(Looper.java:288) 
	at android.app.ActivityThread.main(ActivityThread.java:7839) 
	at java.lang.reflect.Method.invoke(Native Method) 
at
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) 
  ```
</details>

closes #4297
2024-03-04 06:54:09 +01:00
Konrad Pozniak fd8d7db343
Translations update from Weblate (#4284)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-03-01 08:35:18 +01:00
renovate[bot] 7c15f5e8b0
Update dependency com.google.truth:truth to v1.4.2 (#4298)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.truth:truth](https://togithub.com/google/truth) | `1.4.1`
-> `1.4.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.truth:truth/1.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.truth:truth/1.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.truth:truth/1.4.1/1.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.truth:truth/1.4.1/1.4.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>google/truth (com.google.truth:truth)</summary>

### [`v1.4.2`](https://togithub.com/google/truth/releases/tag/v1.4.2):
1.4.2

This release is the final step of copying all our methods from `Truth8`
to `Truth`. If you have not already migrated your usages from `Truth8`
to `Truth`, you may see build errors:

OptionalSubjectTest.java:39: error: reference to assertThat is ambiguous
        assertThat(Optional.of("foo")).isPresent();
        ^
both method
assertThat(@&#8203;org.checkerframework.checker.nullness.qual.Nullable
Optional<?>) in Truth8 and method
assertThat(@&#8203;org.checkerframework.checker.nullness.qual.Nullable
Optional<?>) in Truth match

In most cases, you can migrate your whole project mechanically: `git
grep -l Truth8 | xargs perl -pi -e 's/\bTruth8\b/Truth/g;'`. (You can
make that change before upgrading to Truth 1.4.2 or as part of the same
commit.)

If you instead need to migrate your project incrementally (for example,
because it is very large), you may want to upgrade your version of Truth
incrementally, too, following our instructions for
[1.3.0](https://togithub.com/google/truth/releases/tag/v1.3.0) and
[1.4.0](https://togithub.com/google/truth/releases/tag/v1.4.0).

#### For help

Please feel welcome to [open an
issue](https://togithub.com/google/truth/issues/new) to report problems
or request help.

#### Changelog

- Removed temporary type parameters from `Truth.assertThat(Stream)` and
`Truth.assertThat(Optional)`. This can create build errors, which you
can fix by replacing all your references to `Truth8` with references to
`Truth`.
([`45782bd`](https://togithub.com/google/truth/commit/45782bd0e))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMjAuMiIsInVwZGF0ZWRJblZlciI6IjM3LjIyMC4yIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-01 08:07:49 +01:00
Sveinn í Felli 9084116305 Translated using Weblate (Icelandic)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2024-03-01 06:32:34 +00:00
Luna Jernberg f083c463a0 Translated using Weblate (Swedish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sv/
Translation: Tusky/Tusky
2024-03-01 06:32:34 +00:00
Ihor Hordiichuk b1ac7e587d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2024-03-01 06:32:34 +00:00
XoseM 4aa1980033 Translated using Weblate (Galician)
Currently translated at 99.6% (632 of 634 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2024-03-01 06:32:34 +00:00
fin-w 21e97faf78 Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-03-01 06:32:34 +00:00
Christophe Beyls 722b75e5c2
Optimize Proguard rules (#4291)
Using saner defaults for R8 while reducing the app size even further.
- Add Kotlin compiler options to skip adding assertions in release
builds
- Remove `optimizations`, `optimizationpasses` and `dontpreverify` rules
that are ignored by R8
- Only keep runtime annotations by default. If other attributes are
needed by a specific library, these will already be provided by the
library rules (for example Retrofit or coroutines)
- Remove the obsolete rule allowing a View to reflectively call any
arbitrary public Activity method accepting a View as argument. This has
always been a bad practice and is not used in this project anyway
- Remove the rules related to enums. R8 already optimizes enums properly
out-of-the-box and keeping these rules may prevent some of these
optimizations
- Add support for the `@Keep` annotation. Even if it's not currently
used in the code base, it can be handy in the future
- Add a missing rule to prevent generic signature of `NetworkResult`
class from being removed in `MastodonApi` so Retrofit works
- Allow obfuscation and shrinking of `kotlin.coroutines.Continuation`,
matching the rule defined in the next release of Retrofit
- Remove the rule forcing the removal of `String.format()`. This method
is actually used in the code (and in third-party libraries) for other
things than logging so forcing its removal can do more harm than good.
2024-02-29 15:29:05 +01:00
Christophe Beyls 40fde54e0b
Replace RxJava3 code with coroutines (#4290)
This pull request removes the remaining RxJava code and replaces it with
coroutine-equivalent implementations.

- Remove all duplicate methods in `MastodonApi`:
- Methods returning a RxJava `Single` have been replaced by suspending
methods returning a `NetworkResult` in order to be consistent with the
new code.
- _sync_/_async_ method variants are replaced with the _async_ version
only (suspending method), and `runBlocking{}` is used to make the async
variant synchronous.
- Create a custom coroutine-based implementation of `Single` for usage
in Java code where launching a coroutine is not possible. This class can
be deleted after remaining Java code has been converted to Kotlin.
- `NotificationsFragment.java` can subscribe to `EventHub` events by
calling the new lifecycle-aware `EventHub.subscribe()` method. This
allows using the `SharedFlow` as single source of truth for all events.
- Rx Autodispose is replaced by `lifecycleScope.launch()` which will
automatically cancel the coroutine when the Fragment view/Activity is
destroyed.
- Background work is launched in the existing injectable
`externalScope`, since using `GlobalScope` is discouraged.
`externalScope` has been changed to be a `@Singleton` and to use the
main dispatcher by default.
- Transform `ShareShortcutHelper` to an injectable utility class so it
can use the application `Context` and `externalScope` as provided
dependencies to launch a background coroutine.
- Implement a custom Glide extension method
`RequestBuilder.submitAsync()` to do the same thing as
`RequestBuilder.submit().get()` in a non-blocking way. This way there is
no need to switch to a background dispatcher and block a background
thread, and cancellation is supported out-of-the-box.
- An utility method `Fragment.updateRelativeTimePeriodically()` has been
added to remove duplicate logic in `TimelineFragment` and
`NotificationsFragment`, and the logic is now implemented using a simple
coroutine instead of `Observable.interval()`. Note that the periodic
update now happens between onStart and onStop instead of between
onResume and onPause, since the Fragment is not interactive but is still
visible in the started state.
- Rewrite `BottomSheetActivityTest` using coroutines tests.
- Remove all RxJava library dependencies.
2024-02-29 15:28:48 +01:00
renovate[bot] 91fe7a51cc
Update dagger to v2.51 (#4289)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.dagger:dagger](https://togithub.com/google/dagger) |
`2.50` -> `2.51` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-compiler](https://togithub.com/google/dagger)
| `2.50` -> `2.51` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-compiler/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-compiler/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-compiler/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-compiler/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-support](https://togithub.com/google/dagger)
| `2.50` -> `2.51` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-support/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-support/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-support/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-support/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-processor](https://togithub.com/google/dagger)
| `2.50` -> `2.51` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-processor/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-processor/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-processor/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-processor/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [com.google.dagger:dagger-android](https://togithub.com/google/dagger)
| `2.50` -> `2.51` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android/2.50/2.51?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMTIuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIxMi4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 15:10:57 +01:00
Konrad Pozniak 7448fd2416
change SendStatusService type to shortService (#4292)
This way the `FOREGROUND_SERVICE_REMOTE_MESSAGING` permission is not
needed and we should be able to publish on Google Play again. Drawback:
The service can get killed after a while (usually 3 mins) on Android 14.
I also tried using [user initiated data transfer
jobs](https://developer.android.com/about/versions/14/changes/user-initiated-data-transfers),
but that is not available on all api levels, and `WorkManager`, but that
is a huge refactoring and sending would probably work differently than
before.
2024-02-29 12:21:15 +01:00
renovate[bot] 1fab0b8460
Update dependency at.connyduck:networkresult-calladapter to v1.1.0 (#4286)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[at.connyduck:networkresult-calladapter](https://togithub.com/connyduck/networkresult-calladapter)
| `1.0.0` -> `1.1.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/at.connyduck:networkresult-calladapter/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/at.connyduck:networkresult-calladapter/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/at.connyduck:networkresult-calladapter/1.0.0/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/at.connyduck:networkresult-calladapter/1.0.0/1.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>connyduck/networkresult-calladapter
(at.connyduck:networkresult-calladapter)</summary>

###
[`v1.1.0`](https://togithub.com/connyduck/networkresult-calladapter/releases/tag/v1.1.0):
NetworkResult Calladapter 1.1.0

-   `NetworkResult` is now `Serializable`
-   The library is now build with Java 17
-   Dependency updates:
    -   Kotlin 1.9.22
    -   Kotlin Coroutines 1.8.0
    -   OkHttp 4.12.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMTIuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIxMi4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-25 20:40:17 +01:00
Konrad Pozniak 6249b53718
Fix some warnings & recreate lint-baseline.xml (#4278) 2024-02-25 16:20:26 +01:00
Willow c666a6b534
Better screen transitions (#4285)
I mostly took Android 13 transitions and removed the sliding for the
"deeper"/background one because "extend" animations are not available
until Android 13.

Here are the original ones:
https://cs.android.com/android/platform/superproject/+/android-13.0.0_r8:frameworks/base/core/res/res/anim/;bpv=1

Initially I've made separate versions fro Android 13+ that are close to
the original but I think it's not worth it to keep both.



https://github.com/tuskyapp/Tusky/assets/3099142/616fc40c-f944-45b4-bf6f-167f62d30493
2024-02-25 16:20:15 +01:00
Konrad Pozniak 9987a78044
new issue templates (#3789)
inspired by
https://github.com/mastodon/mastodon/tree/main/.github/ISSUE_TEMPLATE
2024-02-24 19:07:18 +01:00
renovate[bot] 847ea9975b
Update gradle/wrapper-validation-action action to v2 (#4283)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[gradle/wrapper-validation-action](https://togithub.com/gradle/wrapper-validation-action)
| action | major | `v1` -> `v2` |

---

### Release Notes

<details>
<summary>gradle/wrapper-validation-action
(gradle/wrapper-validation-action)</summary>

###
[`v2`](https://togithub.com/gradle/wrapper-validation-action/compare/v1...v2)

[Compare
Source](https://togithub.com/gradle/wrapper-validation-action/compare/v1...v2)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 07:49:54 +01:00
renovate[bot] 17379e662d
Update gradle/gradle-build-action action to v3 (#4282)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[gradle/gradle-build-action](https://togithub.com/gradle/gradle-build-action)
| action | major | `v2` -> `v3` |

---

### Release Notes

<details>
<summary>gradle/gradle-build-action
(gradle/gradle-build-action)</summary>

###
[`v3`](https://togithub.com/gradle/gradle-build-action/compare/v2...v3)

[Compare
Source](https://togithub.com/gradle/gradle-build-action/compare/v2...v3)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 07:49:45 +01:00
renovate[bot] 3660a10cac
Update actions/setup-java action to v4 (#4280)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-java](https://togithub.com/actions/setup-java) | action
| major | `v3` -> `v4` |

---

### Release Notes

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

### [`v4`](https://togithub.com/actions/setup-java/compare/v3...v4)

[Compare
Source](https://togithub.com/actions/setup-java/compare/v3...v4)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 07:48:53 +01:00
renovate[bot] f3f8017c43
Update actions/checkout action to v4 (#4279)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://togithub.com/actions/checkout) | action |
major | `v3` -> `v4` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

###
[`v4`](https://togithub.com/actions/checkout/blob/HEAD/CHANGELOG.md#v400)

[Compare Source](https://togithub.com/actions/checkout/compare/v3...v4)

- [Support fetching without the --progress
option](https://togithub.com/actions/checkout/pull/1067)
-   [Update to node20](https://togithub.com/actions/checkout/pull/1436)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-24 07:48:44 +01:00
Christophe Beyls a19540f0e4
Simplify and reduce overhead of lazy view binding in Fragments (#4269)
This reduces complexity of view binding inflation in Fragments, and also
reduces overhead (no `KProperty` objects need to be generated by the
compiler) by implementing `Lazy` instead of `ReadOnlyProperty`.

For a full explanation, see this [detailed blog
post](https://medium.com/@bladecoder/viewlifecyclelazy-and-other-ways-to-avoid-view-memory-leaks-in-android-fragments-4aa982e6e579).
2024-02-23 20:10:33 +01:00
renovate[bot] 7e5eef4060
Update plugin ktlint to v12.1.0 (#4277)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| org.jlleitschuh.gradle.ktlint | `12.0.3` -> `12.1.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jlleitschuh.gradle.ktlint/12.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jlleitschuh.gradle.ktlint/12.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jlleitschuh.gradle.ktlint/12.0.3/12.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jlleitschuh.gradle.ktlint/12.0.3/12.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 20:02:27 +01:00
renovate[bot] 3fbe6e9786
Update okhttp monorepo to v4.12.0 (#4276)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[com.squareup.okhttp3:logging-interceptor](https://square.github.io/okhttp/)
([source](https://togithub.com/square/okhttp)) | `4.11.0` -> `4.12.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.squareup.okhttp3:logging-interceptor/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.squareup.okhttp3:logging-interceptor/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.squareup.okhttp3:logging-interceptor/4.11.0/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.squareup.okhttp3:logging-interceptor/4.11.0/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [com.squareup.okhttp3:okhttp](https://square.github.io/okhttp/)
([source](https://togithub.com/square/okhttp)) | `4.11.0` -> `4.12.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.squareup.okhttp3:okhttp/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.squareup.okhttp3:okhttp/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.squareup.okhttp3:okhttp/4.11.0/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.squareup.okhttp3:okhttp/4.11.0/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [com.squareup.okhttp3:mockwebserver](https://square.github.io/okhttp/)
([source](https://togithub.com/square/okhttp)) | `4.11.0` -> `4.12.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.squareup.okhttp3:mockwebserver/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.squareup.okhttp3:mockwebserver/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.squareup.okhttp3:mockwebserver/4.11.0/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.squareup.okhttp3:mockwebserver/4.11.0/4.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 20:02:17 +01:00
renovate[bot] 5de9c5ce15
Update dependency org.robolectric:robolectric to v4.11.1 (#4273)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [org.robolectric:robolectric](http://robolectric.org)
([source](https://togithub.com/robolectric/robolectric)) | `4.10.3` ->
`4.11.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.robolectric:robolectric/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.robolectric:robolectric/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.robolectric:robolectric/4.10.3/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.robolectric:robolectric/4.10.3/4.11.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 16:50:58 +01:00
renovate[bot] 713da85340
Update dependency org.mockito.kotlin:mockito-kotlin to v5.2.1 (#4272)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[org.mockito.kotlin:mockito-kotlin](https://togithub.com/mockito/mockito-kotlin)
| `5.1.0` -> `5.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.mockito.kotlin:mockito-kotlin/5.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.mockito.kotlin:mockito-kotlin/5.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.mockito.kotlin:mockito-kotlin/5.1.0/5.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.mockito.kotlin:mockito-kotlin/5.1.0/5.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>mockito/mockito-kotlin
(org.mockito.kotlin:mockito-kotlin)</summary>

###
[`v5.2.1`](https://togithub.com/mockito/mockito-kotlin/releases/tag/5.2.1)

[Compare
Source](https://togithub.com/mockito/mockito-kotlin/compare/5.2.0...5.2.1)

<sup><sup>*Changelog generated by [Shipkit Changelog Gradle
Plugin](https://togithub.com/shipkit/shipkit-changelog)*</sup></sup>

##### 5.2.1

- 2023-12-02 - [1
commit(s)](https://togithub.com/mockito/mockito-kotlin/compare/5.2.0...5.2.1)
by Róbert Papp
- Increase visibility of Mockito dependency
[(#&#8203;498)](https://togithub.com/mockito/mockito-kotlin/pull/498)

###
[`v5.2.0`](https://togithub.com/mockito/mockito-kotlin/releases/tag/5.2.0)

[Compare
Source](https://togithub.com/mockito/mockito-kotlin/compare/5.1.0...5.2.0)

<sup><sup>*Changelog generated by [Shipkit Changelog Gradle
Plugin](https://togithub.com/shipkit/shipkit-changelog)*</sup></sup>

##### 5.2.0

- 2023-12-01 - [1
commit(s)](https://togithub.com/mockito/mockito-kotlin/compare/5.1.0...5.2.0)
by Sergei
- Main dependencies updated
[(#&#8203;496)](https://togithub.com/mockito/mockito-kotlin/pull/496)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 16:50:48 +01:00
Konrad Pozniak 7dca2fed58
fix sending posts on Api 34 (#4274)
```
java.lang.RuntimeException: Unable to start service com.keylesspalace.tusky.service.SendStatusService@1eb9198 with Intent { cmp=com.keylesspalace.tusky.test/com.keylesspalace.tusky.service.SendStatusService (has extras) }: android.app.MissingForegroundServiceTypeException: Starting FGS without a type  callerApp=ProcessRecord{18608f7 22134:com.keylesspalace.tusky.test/u0a193} targetSDK=34
           at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:4839)
           at android.app.ActivityThread.-$$Nest$mhandleServiceArgs(Unknown Source:0)
           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2289)
           at android.os.Handler.dispatchMessage(Handler.java:106)
           at android.os.Looper.loopOnce(Looper.java:205)
           at android.os.Looper.loop(Looper.java:294)
           at android.app.ActivityThread.main(ActivityThread.java:8177)
           at java.lang.reflect.Method.invoke(Native Method)
           at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
          Caused by: android.app.MissingForegroundServiceTypeException: Starting FGS without a type  callerApp=ProcessRecord{18608f7 22134:com.keylesspalace.tusky.test/u0a193} targetSDK=34
           at android.app.MissingForegroundServiceTypeException$1.createFromParcel(MissingForegroundServiceTypeException.java:53)
           at android.app.MissingForegroundServiceTypeException$1.createFromParcel(MissingForegroundServiceTypeException.java:49)
           at android.os.Parcel.readParcelableInternal(Parcel.java:4870)
           at android.os.Parcel.readParcelable(Parcel.java:4852)
           at android.os.Parcel.createExceptionOrNull(Parcel.java:3052)
           at android.os.Parcel.createException(Parcel.java:3041)
           at android.os.Parcel.readException(Parcel.java:3024)
           at android.os.Parcel.readException(Parcel.java:2966)
           at android.app.IActivityManager$Stub$Proxy.setServiceForeground(IActivityManager.java:6761)
           at android.app.Service.startForeground(Service.java:775)
           at com.keylesspalace.tusky.service.SendStatusService.onStartCommand(SendStatusService.kt:137)
           at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:4821)
           at android.app.ActivityThread.-$$Nest$mhandleServiceArgs(Unknown Source:0) 
           at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2289) 
           at android.os.Handler.dispatchMessage(Handler.java:106) 
           at android.os.Looper.loopOnce(Looper.java:205) 
           at android.os.Looper.loop(Looper.java:294) 
           at android.app.ActivityThread.main(ActivityThread.java:8177) 
           at java.lang.reflect.Method.invoke(Native Method) 
           at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) 
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) 
```
2024-02-23 16:50:33 +01:00
Konrad Pozniak 166e5ac20e
Translations update from Weblate (#4262)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-02-23 15:14:29 +01:00
renovate[bot] 498f682bfb
Update dependency com.google.android.material:material to v1.11.0 (#4268)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[com.google.android.material:material](https://togithub.com/material-components/material-components-android)
| `1.9.0` -> `1.11.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.android.material:material/1.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.android.material:material/1.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.android.material:material/1.9.0/1.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.android.material:material/1.9.0/1.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>material-components/material-components-android
(com.google.android.material:material)</summary>

###
[`v1.11.0`](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0)

[Compare
Source](https://togithub.com/material-components/material-components-android/compare/1.10.0...1.11.0)

### Important

Elevation Overlays within default component styles have been replaced by
the new [Tonal Surface Color
system](https://material.io/blog/tone-based-surface-color-m3). Instead
of blending the Primary color with the Surface color based on an
elevation value, components now use a specific Surface color role that
can be more easily understood and customized.

Documentation for the affected components and which Surface color roles
they use has been updated in
2114a11378.

### What's new since 1.10.0

- New [Tonal Surface Color
system](https://material.io/blog/tone-based-surface-color-m3)! Check out
the [design
guidance](https://material.io/blog/tone-based-surface-color-m3) and the
[commit with documentation
updates](2114a11378)
for more details.
- New Carousel variants! 1.11.0 introduces three new Carousel variants:
center-aligned hero, uncontained, and fullscreen. Check out the
[Carousel
documentation](https://togithub.com/material-components/material-components-android/blob/master/docs/components/Carousel.md)
for more details.

#### Dependency Updates

None.

*Note: Earlier versions of 1.11.0 used other versions of the
`androidx.activity` 1.8.0 library dependency, but there is no change in
the dependency from 1.10.0, the last stable MDC library version.*

#### Library Updates

-   `Badging`
- Adjusted badge vertical offset for certain components to fit design
requirements
([`78cc54b`](78cc54b8c3))
- Added note in attachBadgeDrawable method docs about menu item views
being re-used when there is a structural change in the menu. Since
badges are attached to the views, this may change the menu item that the
badge is intended for
([`5e6ea58`](5e6ea58280))
-   `BottomNavigationView`
- Set badges to be null instead of removing them from the sparse array
so that removing badges before restoring badge states will not override
the current state
([`9f2e686`](9f2e6864d2))
-   `BottomSheet`
- Fix sheet corners animation
([`de27132`](de271320e4))
-   `Carousel`
- Ensure that extra small size is not larger than the large size
([`17baf71`](17baf71972))
- Fixed crash when recyclerview has a size of zero.
([`26c3129`](26c3129201))
- When navigating with keyboard, scroll focused item to nearest focal
keyline, not the first focal keyline
([`fb9c1c6`](fb9c1c6edf))
- Force hero strategy to be start-aligned if there are not enough items
to make it center-aligned
([`9a2347b`](9a2347bda5))
- Add logic for multibrowse strategy to change strategy when number of
items is less than the number of keylines
([`cbb380d`](cbb380df61))
- Fixed focus order when using keyboard navigation for hero carousels.
([`0356f24`](0356f24a63))
- Cleaning up multi-browse strategy and removing compact arrangement
([`ed4647d`](ed4647d5df))
- Disallowing center aligned hero strategy with only 2 items since it
does not make any sense. With only 2 items there can only be a start
state and end state with the hero strategy.
([`d5d604d`](d5d604d0cc))
- Fix issue with uncontained carousel not having a proper end scroll
value due to assumption made in end scroll calculation
([`8cb444b`](8cb444b268))
- Added tests for hero and multibrowse strategies when the carousel
container is very small.
([`8312162`](8312162f53))
- Update scroll offset to scroll to the estimated position that it was
at upon an initial load
([`4a6ae4d`](4a6ae4d0b6))
- Fixed strategies crashing when there is not enough available space for
a large and a small item
([`c418063`](c418063205))
- Tweak uncontained strategy logic to adjust medium size items to
improve motion
([`93660d4`](93660d4241))
- Fixed formatted for KeylineState and KeylineStateList
([`b80d9a5`](b80d9a5ef4))
- Fixed keyline shifting in RTL for uncontained carousels
([`7151714`](7151714711))
- Reduce the number of truncations in intermediate calculations
([`4ce7e4c`](4ce7e4c8fa))
- Update vertical scroll speed to be faster
([`c6ea2d4`](c6ea2d4040))
- Fix some a11y bugs in Carousel
([`3d84841`](3d84841cad))
- Center aligned uncontained carousel
([`b6f6eb5`](b6f6eb555a))
- Add left-aligned uncontained strategy
([`9d81cac`](9d81cac125))
- Fix orientation not correct in item decoration calculations
([`966f7da`](966f7daec9))
- Update mask size on size change if mask x percentage has been set
([`dc91b39`](dc91b39d7c))
- Fix issue with next carousel item not being masked properly
([`a16f180`](a16f180ce7))
- Add a layout listener to recyclerview to refresh keyline state upon
size change
([`ff52862`](ff528621b3))
- Remove default list from Carousel catalog demos as it is not an
example of a carousel
([`0171624`](0171624c16))
- Add carousel alignment attribute
([`547156e`](547156e497))
- Fix issue with not refreshing keylines
([`674ec44`](674ec44429))
- Fixed MaskableFrameLayout not updating mask after size change when
setting the mask using setMaskXPercentage.
([`14023d2`](14023d2c85))
- Adding docs for fullscreen carousel strategy
([`7c40359`](7c40359d2b))
- Add fullscreen demo to catalog
([`ad6afbf`](ad6afbf6d8))
- Add full screen strategy
([`bc54f2e`](bc54f2e4b3))
-   `Catalog`
- Use BottomSheetDialogFragment
([`2c53952`](2c539524ec))
- Update navigation drawer state handling to be more accurate in demo
([`74ac87c`](74ac87cb61))
- Update navigation drawer state handling to be more accurate in custom
drawer demo
([`1252d4f`](1252d4f263))
- When bottom sheet is collapsed, back callback is enabled
([`98439df`](98439df4fb))
- Update side sheet state handling to be more accurate in demos
([`357cf2d`](357cf2d9ca))
- Update bottom sheet state handling to be more accurate in demos
([`f9102c7`](f9102c745a))
- Fixed demos in RTL
([`4e1b130`](4e1b130cdf))
- Make side sheet demo icon easier to see in dark mode
([`79a1953`](79a19537d7))
- Remove explicit Catalog dependency on androidx.activity now that
library depends on it
([`3bb69f5`](3bb69f501a))
- buid.gradle update.
([`4847799`](484779902e))
- Remove edge-to-edge to top app bar demos due to glitches in ActionBar
demo
([`c40e468`](c40e468820))
- Update slider position when carousel is scrolled in uncontained
carousel demo
([`480bbc6`](480bbc6a9b))
- Applied edge-to-edge to top app bar demos
([`aa5b5bc`](aa5b5bc157))
- Add uncontained carousel demo to catalog
([`ef9f918`](ef9f91864f))
- Update slider position when carousel is scrolled in catalog demos
([`3652fde`](3652fde271))
- Applied edge-to-edge to bottom sheet in Carousel demo
([`6c2dd5d`](6c2dd5d69e))
- Fix fullscreen vertical carousel to use vertical dividers
([`af7d09a`](af7d09a43d))
- Add alignment option to hero carousel
([`7cfd30f`](7cfd30f99b))
-   `Chip`
- Fix a typo in attributes table in documentation
([`7289aa6`](7289aa681d))
-   `CollapsingToolbarLayout`
- Fix issue where expanded text gets ellipsized too aggressively when
using title fade mode and the toolbar has menu items
([`094e3e2`](094e3e21ea))
- Fixed title collapse fade mode color to use a Tonal Surface role
instead of elevation overlay
([`5f2b4b2`](5f2b4b2531))
-   `Color`
- Delete unused contrast resources
([`541df07`](541df074a7))
- Updated focused and pressed state from 0.12 to 0.10 to increase the
contrast ratio with tonal surface color.
([`bc824b4`](bc824b4344))
- Updated colors of container transform demo
([`d16f223`](d16f22341e))
- Add ColorStateListDrawable support
([`0663019`](0663019f45))
- Removed layered drawable for the background after tonal surface color.
([`b5d6f1a`](b5d6f1ae45))
- Upgraded to v0.170 for tonal surface colors.
([`8204856`](8204856dd4))
- Update constructor comments
([`ba465a2`](ba465a2b17))
-   `Divider`
- Add RTL support
([`6b897c6`](6b897c6121))
-   `Documentation`
- Add note to clarify that automatic back handling in components is only
for API 33+
([`ad2b5f8`](ad2b5f8f8b))
- Update Predictive Back doc to recommend 1.10.0 stable version
([`b8b1a66`](b8b1a662de))
- Update doc to explain how to allow Top App Bar to grow taller in
response to system font setting
([`a01a68d`](a01a68de2d))
- Fix carousel docs
([`247240c`](247240c3cd))
- Add uncontained variant docs
([`9ee4aba`](9ee4aba007))
- Minor code block improvements
([`d7e75bb`](d7e75bba59))
- Add guidance in fullscreen strategy docs about portrait orientation
([`caec8d2`](caec8d2849))
- Update chip documentation to use `setOnCheckedStateChangeListener`
instead of `setOnCheckedChangeListener` which was deprecated in favour
of the former
([`0582b1a`](0582b1a094))
- Update example in docs to use `colorContainer` and `colorOnContainer`
instead of `colorPrimary` and `colorOnPrimary`.
([`44bfe2d`](44bfe2ddb8))
- Updated Color dev doc with tonal surface colors.
([`9a4c21d`](9a4c21d77a))
- Updated dev doc for tonal surface colors in affected components.
([`2114a11`](2114a11378))
- Crosslink github docs in javadocs
([`e3b255b`](e3b255b3a0))
- Cross-reference m.io and DAC in Github docs
([`1785bbf`](1785bbfabe))
-   `Material 3`
- Introduce U color tokens
([`ad63d3f`](ad63d3ff7e))
- Add Meizu to dynamic colors allowlist
([`faf9a32`](faf9a32770))
- Remove resources loader support for tonal surface update
([`1a9d54f`](1a9d54fa41))
- Added default framework text colors
([`d3dda60`](d3dda60296))
- Updated contrast documentation
([`1d3b8e1`](1d3b8e136d))
- Add shift to dynamic colors allowlist
([`fde37cf`](fde37cfba5))
- Expose attr contrastColorThemeOverlay
([`839b14c`](839b14cf0f))
- Add dynamic contrast support
([`862a7e1`](862a7e10c9))
-   `MaterialCardView`
- Support `android:duplicateParentState`.
([`31af945`](31af945caa))
-   `MaterialDatePicker`
- Added builder methods for customizing the positive and negative button
content descriptions
([`a00ee50`](a00ee50907))
- Fix header layout overlapping
([`5f1cab6`](5f1cab65b1))
- Fixing the "Column of Days:" announcement causing Talkback verbosity
([`a782e7a`](a782e7a1d8))
-   `NavigationView`
- Set material drawable background if the background has been set as a
ColorStateList
([`eba40e9`](eba40e9345))
-   `Predictive Back`
- Update to use decelerate interpolator
([`5559cbc`](5559cbc7c7))
- Fix issue where predictive back is not enabled in NavigationView after
rotating the screen or restarting the activity when the NavigationView
is already opened as a drawer
([`bccbd4f`](bccbd4f778))
- Fix issue where modal accessibility is not reset after collapsing
search view predictively, which caused the screen to appear frozen when
using TalkBack
([`8d83a31`](8d83a31b6b))
- Fixed IllegalStateException crashes caused by
MaterialBackAnimationHelper.
([`02dc779`](02dc77923b))
- Fixed `UnsupportedOperationException` `AnimatorSet` crash in
`SearchView`.
([`f101532`](f10153257f))
-   `ProgressIndicator`
- Allow extending LinearProgressIndicator and CircularProgressIndicator
(Closes
[#&#8203;2361](https://togithub.com/material-components/material-components-android/issues/2361))
([`3b0fd58`](3b0fd5868e))
-   `Search`
- Set the `editable` property within `SearchBar`'s
`AccessibilityNodeInfo` representation.
([`be1395b`](be1395bcef))
- Fix issue where predictive back is not enabled in SearchView after
rotating the screen or restarting the activity when the SearchView is
already expanded
([`a51561d`](a51561d8b8))
- Enforced outline variant to use colorSurface as container color.
([`720998d`](720998dcbd))
- Added support to set a default content description if a content
description is not set explicitly.
([`c15a323`](c15a323140))
-   `SideSheet`
- Add left sheet/sheet edge documentation.
([`d440e3c`](d440e3c4f8))
- Fix mixed language snippet
([`f91f17b`](f91f17bb4f))
-   `Switch`
- Fix drawable scaling for API < 23
([`a10c508`](a10c5083a9))
- Amendments to thumb icon size support
([`db9a641`](db9a6412ec))
-   `Tabs`
- Integrated divider token output into the background drawables.
([`6b627c2`](6b627c20e2))
-   `TextInputLayout`
- Fix editText paddings on pre-Lollipop
([`2590c42`](2590c42764))
- Fix onMeasure() infinite loop bug caused by posting requestLayout()
before endLayout is fully rendered.
([`93360a5`](93360a5a5d))
- Mutate cursor drawable before tinting it.
([`9a4888f`](9a4888f958))
-   `Theming`
- Fix swapped error colors for dynamic contrast theme in light mode
([`f2ccc11`](f2ccc116ea))
-   `TimePicker`
- Fix layout getting cut on narrow screens
([`d10201d`](d10201dc88))
-   `Tokens`
- Upgraded to v0.175.
([`bfee8a7`](bfee8a74d7))
-   `TopAppBar`
- Simplified logics to animate the container color between lifted state
and the default state.
([`1e9f5f0`](1e9f5f0730))
- Fix dynamic status bar foreground lift on scroll color when using
Tonal Surface Color on API Level 33
([`c4ae01a`](c4ae01a5a5))
- Fix dynamic status bar foreground lift on scroll color when using
Tonal Surface Color
([`569ddac`](569ddac527))
- Fixed that liftOnScrollColor doesn't respect to setLifted when
liftOnScroll is set to false.
([`43242f2`](43242f20de))
- Added liftOnScroll and lifted toggles to the catalog.
([`4e995d1`](4e995d1f81))
- Added getMaterialShapeBackground() to return a MaterialShapeDrawable
object of the background (non-lifted layer).
([`c7a0adf`](c7a0adf8dd))
- Updated the handling of liftOnScrollColor not override
android:background.
([`2b476b3`](2b476b3a96))
-   `Other`
- Cleanup date formats that specify the same field multiple times.
([`f1da3c3`](f1da3c384f))
- Update androidx.activity dependency to version 1.8.0
([`733fe20`](733fe20d88))
- Update androidx.activity dependency to version 1.8.0-rc01
([`2cfb127`](2cfb127095))
- Remove android:targetSdk where it's not needed
([`09382b8`](09382b896c))
- Update androidx.activity dependency to version 1.8.0-beta01
([`f93c65c`](f93c65c347))
- Added a comment before overridden resources.
([`7d8681f`](7d8681f71d))
- Delete BackLayer component
([`4c89301`](4c89301330))
- Stop running Robolectric tests on APIs < 19.
([`0a6a8a9`](0a6a8a971d))
- Fixed corrupt gradle jar file.
([`ed9b541`](ed9b54129d))
- Upgrade Gradle to 7.6.2.
([`04c0582`](04c05826e4))
- Consistently use window coordinates to calculate Snackbar offsets. In
one place screen and window coordinates are mixed, which leads to bad
behavior in split screen apps.
([`57b2663`](57b26639d8))
- 1.11.0-alpha01 Release.
([`76a681a`](76a681abf6))

#### Full list of release notes

-
[1.11.0-alpha01](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-alpha01)
-
[1.11.0-alpha02](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-alpha02)
-
[1.11.0-alpha03](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-alpha03)
-
[1.11.0-alpha04](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-alpha04)
-
[1.11.0-alpha05](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-alpha05)
-
[1.11.0-alpha06](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-alpha06)
-
[1.11.0-beta01](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-beta01)
-
[1.11.0-rc01](https://togithub.com/material-components/material-components-android/releases/tag/1.11.0-rc01)

#### Full list of changes

###
[`v1.10.0`](https://togithub.com/material-components/material-components-android/releases/tag/1.10.0)

[Compare
Source](https://togithub.com/material-components/material-components-android/compare/1.9.0...1.10.0)

### What's new since 1.9.0

- Added Predictive back support for search, bottom sheet, side sheet and
navigation drawer. Check out the [developer
documentation](https://togithub.com/material-components/material-components-android/blob/master/docs/foundations/PredictiveBack.md)
for more details.
- Add `Start-aligned` variant to Carousel component. Check out the
[developer
documentation](https://togithub.com/material-components/material-components-android/blob/master/docs/components/Carousel.md)
for more details.
- Badge component updates. Check out the [developer
documentation](https://togithub.com/material-components/material-components-android/blob/master/docs/components/BadgeDrawable.md)
for more details.
- Left & RTL side sheets. Check out the [developer
documentation](https://togithub.com/material-components/material-components-android/blob/master/docs/components/SideSheet.md)
for more details.

#### Important

##### New minimum requirements for your app's project:

-   Update `compileSdkVersion` to `34`

#### Dependency Updates

| Dependency | Previous version | New version |
| - | - | - |
| Gradle                                | 7.2.0 | 7.4.2 |
| androidx.activity                     | --    | 1.8.0 |
| androidx.appcompat                    | 1.5.0 | 1.6.1 |
| androidx.resourceinspection:resourceinspection-annotation | -- | 1.0.1
|
| androidx.resourceinspection:resourceinspection-processor | -- | 1.0.1
|

#### Library Updates

-   `A11y`
- Do not count headers for accessibility
([`917da52`](917da52393))
-   `Badging`
- Deprecate bottom badge gravities and update docs and usages in catalog
([`27abfdb`](27abfdba4f))
- Add new attribute for vertical offset when font is large
([`2362f4b`](2362f4b64b))
- Fix table formatting and remove unnecessary badge invalidation
([`44a97f4`](44a97f4a83))
- Fix crash caused by non-ascii strings
([`532b65d`](532b65d008))
- Badge cleanup/fixes:
([`4d50aa4`](4d50aa41e7))
- Add attribute to automatically adjust badge so that it is within the
anchor view's grandparent view's bounds
([`b706506`](b706506c3d))
- Add getters/setters for vertical and horizontal badge padding
([`a0d0b53`](a0d0b53472))
- Allow text strings in badges
([`c1ef52b`](c1ef52b8ea))
- Center badge content more correctly
([`fc0de1b`](fc0de1ba24))
- Add padding in between top and bottom edges of badge and text
([`8499b83`](8499b83ba1))
- Integrating tokens
([`68c844c`](68c844c91d))
- Add shape appearance for badges
([`2ddcfe4`](2ddcfe46b7))
- Update badge images
([`c1eba1b`](c1eba1b268))
-   `BottomAppBar`
- Fix bug with transparent top app bar when in bottom app bar layout
([`c22eb0d`](c22eb0d31b))
- Fix issue with pre-21 FAB elevation/shadow sometimes appearing
([`2ea3df9`](2ea3df991f))
-   `BottomNavigationView`
- Integrate tokens and add shape appearance support
([`7bd9724`](7bd9724078))
-   `BottomSheet`
- Fix sheet corners animation
([`a5ff190`](a5ff190f52))
- Update bottom sheet documentation
([`af1fa70`](af1fa70979))
- Integrate tokens
([`86cd9d7`](86cd9d70bf))
- Add method to allow programmatically changing
shouldRemoveExpandedCorners behavior
([`c8a0d47`](c8a0d47825))
- Update expanded corner removal to check if sheet view is actually at
top of screen
([`1c01e82`](1c01e82297))
- Fix for detached from bottom sheet behavior.
([`9c4b73d`](9c4b73da3a))
- Ignore ACTION_MOVE events in BottomSheetBehavior that weren't preceded
by an ACTION_DOWN event.
([`d8c01c1`](d8c01c1859))
-   `Carousel`
- Add orientation helper to clean up CarouselLayoutManager orientation
differences
([`7822ef8`](7822ef827b))
- Update docs to include hero variant
([`ec509cc`](ec509cc734))
- Add vertical scrolling capability
([`6b48d3b`](6b48d3bb80))
- Deprecate set/get mask x percentages in Maskable as they are no longer
used anywhere, and is a misleading method due to these methods not
actually having any effect on the Carousel as CarouselLayoutManager
overrides the values.
([`29d8742`](29d8742917))
- Ensure that masks are pushed out beyond the parent bounds if they are
*on* the parent bounds
([`9486de5`](9486de5f2f))
- Carousel updates and fixes
([`16c1575`](16c1575758))
- Fix contained mask logic to only update masks when it is still in
view, and remove restrictions on mask size with childWidth/2F. The only
restriction is that the right of the mask must be greater than the left
of the mask.
([`7d6a977`](7d6a977d50))
- Fixed multi browse strategy clipping extra small items before being
fully collapsed
([`85b6d50`](85b6d5018d))
- Add Carousel Hero strategy demo
([`b57dae5`](b57dae57aa))
- Add Hero carousel strategy
([`340cd44`](340cd44b07))
- Add option for snapping with multi-browse carousel demo
([`26c3779`](26c377962f))
- Add CarouselSnapHelper
([`8938da8`](8938da8c28))
- Refactor to reuse logic between different Carousel strategy classes
([`1c27404`](1c27404fc5))
- Fix item masking for API 21
([`7bc26e5`](7bc26e5070))
- Updated MultiBrowseCarouselStrategy to find best arrangments using a
cost function
([`0184b5b`](0184b5baa9))
- Fixed mutli-browse catalog demo crashing due to invalid position
slider values.
([`5bc7a50`](5bc7a50b35))
- Add support for transitions by forcing canvas clipping when detaching
from the window.
([`93ceb7e`](93ceb7edee))
- Updated MaskableFrameLayout to use Outline path clipping on 33+ only.
([`43c5077`](43c507775f))
- Updated setForceCompatClipping visibility for testing.
([`3856af1`](3856af1b65))
- Updated MaskableFrameLayout to clip more performantly.
([`733c9e0`](733c9e08c2))
- Changed Maskable.add/removeOnMaskChangedListener to
Maskable.setOnMaskChangedListener.
([`359580b`](359580b6c1))
- Fixed child index bug causing items to be ordered incorrectly.
([`9d0732b`](9d0732be9e))
-   `Catalog`
- Update side sheet state handling to be more accurate in demos
([`4442635`](4442635aec))
- Update bottom sheet state handling to be more accurate in demos
([`386d47b`](386d47b51b))
- Update to compileSdkVersion 34 and update catalog to use
androidx.activity:activity:1.8.0-alpha05
([`2336c23`](2336c23fab))
- Fixed catalog errors for some demos.
([`4c3e1d5`](4c3e1d513b))
- Fixes context menu are not themed on S
([`33e4f84`](33e4f841b2))
-   `Checkbox`
- Fixed checkmark icon not updating color on error correctly in pre 21.
([`62aa802`](62aa802f6c))
-   `Chip`
- Fix hand pointer icon not showing up on whole Chip when using mouse
input
([`2702b1a`](2702b1a8d5))
- Add theme overlays supported on API < 23
([`fd0c815`](fd0c81531c))
-   `Color`
- Temporarily remove v34 Android U color references which could be
causing resource NotFoundExceptions
([`2a1a67f`](2a1a67f076))
- Fixed lint error.
([`824d75e`](824d75e773))
- Integrate container color token
([`206928b`](206928b8f9))
- Made SearchBar and SearchView's container colors configurable in XML
style.
([`cf006c5`](cf006c5e10))
- Added U color resources for contrast mode support.
([`93f386c`](93f386c9f6))
-   `Dialog`
- Update DialogWhenLarge theme to have correct parent theme
([`dae89b7`](dae89b7ce2))
- Made dialog background color configurable in XML styles.
([`77cdc1e`](77cdc1e3ac))
- Made dialog background color configurable in XML styles.
([`303fabd`](303fabd331))
-   `Documentation`
- Fix SideSheetDialog class definition and source links
([`e4d0fd3`](e4d0fd3d6c))
- Revise version guidance for Search usage.
([`0bcb570`](0bcb57074d))
- Formatted tables in the eng doc.
([`ef57f69`](ef57f699cb))
- Fix doc formatting for github
([`e75654d`](e75654d4c2))
-   `ExposedDropdownMenu`
- Added attribute to set dropdown menu's container.
([`1562d0b`](1562d0b64f))
-   `FloatingActionButton`
- Correctly handle min touch target size
([`d6f36e8`](d6f36e89e2))
-   `Material 3`
- Remove resources loader support for tonal surface update
([`dfd9bfb`](dfd9bfb12f))
- Fix harmonization demo bug
([`836c51e`](836c51e44a))
- Updated content-based dynamic colors
([`e9b485d`](e9b485d619))
- Update Color doc
([`d7f9a06`](d7f9a067e3))
- Updated content-based dynamic colors
([`a511501`](a511501286))
- Adds Search class definition link to Search documentation.
([`e157608`](e157608050))
- Added ColorContrast API
([`a6cf098`](a6cf0985c4))
- Updated content-based dynamic with contrast levels
([`b335436`](b335436cf2))
- Updated SearchDemoUtils to mark the class and methods as public so
that it can be resued.
([`3b43d41`](3b43d41f16))
- Code style update
([`1fd695c`](1fd695c72a))
- Fix Search view prefix style.
([`e56e9b1`](e56e9b1dde))
- Updated search styles to set the default style attributes in the
related theme overlay.
([`9ffaa8d`](9ffaa8d1bf))
- Improve performance by preventing item change notifications during
inflation.
([`203d5ec`](203d5ec3a3))
- Fixed a crash in search components when view attributes inspection dev
setting is turned on.
([`1159923`](11599231a9))
- Color Component demo update
([`2aa1cf3`](2aa1cf3198))
- Color Component demo update
([`c786582`](c786582d6a))
-   `MaterialButton`
- Set up Android Studio resource inspection annotation and annotation
processor, and add [@&#8203;Attribute](https://togithub.com/Attribute)
annotation to MaterialButton#getIconPadding
([`2b5c75f`](2b5c75f966))
-   `MaterialDatePicker`
- Allow client app to access user selected inputMode
([`4d80434`](4d8043453e))
- Fix crash when clicking OK/Cancel on text input mode due to hiding
keyboard
([`7ccf670`](7ccf6708b0))
- Add getTextColor to DayViewDecorator
([`29b59c2`](29b59c21f5))
- a11y/i18n alignment
([`f4d0f56`](f4d0f5653a))
- ContentDescription for "DayName" is not properly getting read by
talkback for the German language
([`35bd1fc`](35bd1fc98f))
- Fix time zone when custom text input format is set
([`619d5a6`](619d5a6cad))
- Update screen width at which days size is 48dp in portrait mode.
([`ec511a5`](ec511a54cf))
- Made dialog container color configurable in xml style.
([`7b2c19b`](7b2c19b273))
- Fix hint for Korean
([`bcc97bf`](bcc97bfaf5))
- Integrated tokens.
([`30ea33e`](30ea33e948))
- Fix keyboard not showing at first in text input mode
([`4032a74`](4032a74112))
-   `NavigationRail`
- Added bigger padding in between items on the navigation rail when font
scale is large so there is more room for the badges when they are forced
inside the view bounds
([`8b016a0`](8b016a0e8b))
- Integrate tokens and add shapeAppearance attribute
([`3b2b827`](3b2b827597))
- Added attribute to control the application of start window inset
padding.
([`3f99392`](3f993923b2))
- Fixed active item focused state color not being visible.
([`1d2a59b`](1d2a59ba37))
-   `NavigationView`
- Updated NavigationView to use ViewOutlineProvider to handle corner
clipping when possible and remove drawerLayoutCornerClippingEnabled
attribute.
([`c031144`](c031144d26))
- Removed canvas clipping by default and added an option to
enabled/disable manually
([`e3b493f`](e3b493f5eb))
-   `Predictive Back`
- Fix issue where modal accessibility is not reset after collapsing
search view predictively, which caused the screen to appear frozen when
using TalkBack
([`e8af8f9`](e8af8f9460))
- Fixed IllegalStateException crashes caused by
MaterialBackAnimationHelper.
([`845007e`](845007e5e9))
- Fixed `UnsupportedOperationException` `AnimatorSet` crash in
`SearchView`.
([`6720e24`](6720e242b0))
- Fix custom predictive back implementations and update
androidx.activity dependency to 1.8.0-alpha06.
([`722b936`](722b93682c))
- Fixed MaterialBackAnimationHelper.onCancelBackProgress being called
twice.
([`ad60bbf`](ad60bbf5d9))
- Fixed possible NaN crashes in MaterialBottomContainerBackHelper,
MaterialMainContainerBackHelper and MaterialSideContainerBackHelper.
([`122c296`](122c2966fd))
- Update components to use BackEventCompat
([`a67a885`](a67a885668))
- Make MaterialMainContainerBackHelper collapsedView optional
([`fb56ab4`](fb56ab4130))
- Fix issue on Android U where clicking SearchView back button causes
SearchBar to disappear and back arrow animation to not run
([`e69a324`](e69a324a2f))
- Minor dev doc edits
([`82a91c8`](82a91c8455))
- Update dev docs
([`47c307d`](47c307d889))
- Fix expand/collapse animation for non-predictive back cases where
rootView bounds are not equal to searchView bounds
([`577d23e`](577d23eceb))
- Added updateBackProgress() predictive back support for coplanar side
sheets.
([`5ba704a`](5ba704a273))
- Added handleBackInvoked() predictive back support for coplanar side
sheets.
([`b984e64`](b984e64445))
- Fix subtle detached side sheet glitch where after predictive back the
sheet does not fully slide off screen
([`52f1737`](52f1737dd8))
- Fix custom nav drawer Catalog demo pre-T crash due to
OnBackAnimationCallback class not found
([`d8c5c2c`](d8c5c2c87d))
- Added predictive back support for modal side sheets.
([`f335a50`](f335a50907))
- Added predictive back support for standard side sheets.
([`562285e`](562285e051))
- Update side container back helper to support containers with arbitrary
child views
([`9405121`](9405121f62))
- Only use device corner radius if SearchView reaches edge of screen
([`a93c91a`](a93c91a2e2))
- Make back helpers support generified View type
([`69b5386`](69b5386e4b))
- Add ability to opt-out of back handling
([`176ce5e`](176ce5e5f0))
- Update SearchView to support predictive back when set up with
SearchBar
([`a4b6f46`](a4b6f46f0c))
- Fix bug where standard hideable bottom sheets don't stay hidden after
predictive back
([`2c23d2a`](2c23d2a158))
- Add top-level developer documentation
([`8105cb7`](8105cb7ed6))
- Update Bottom Sheet to support predictive back
([`d6fad95`](https://togithub.com/material-components/ma

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 14:57:43 +01:00
Conny Duck fef0535197 fix building nightly on bitrise 2024-02-23 14:57:19 +01:00
Zongle Wang efeb218003
Always publish build scans on CI (#4139)
This will let us easy to observe and share build statistics on CI.
2024-02-23 14:41:33 +01:00
renovate[bot] 5c3a029dd9
Update dependency gradle to v8.6 (#4271)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [gradle](https://gradle.org)
([source](https://togithub.com/gradle/gradle)) | minor | `8.5` -> `8.6`
|

---

### Release Notes

<details>
<summary>gradle/gradle (gradle)</summary>

### [`v8.6`](https://togithub.com/gradle/gradle/compare/v8.5.0...v8.6.0)

[Compare
Source](https://togithub.com/gradle/gradle/compare/v8.5.0...v8.6.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 14:41:10 +01:00
renovate[bot] 36a9b7a570
Update dependency com.google.truth:truth to v1.4.1 (#4270)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.truth:truth](https://togithub.com/google/truth) | `1.1.5`
-> `1.4.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.truth:truth/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.truth:truth/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.truth:truth/1.1.5/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.truth:truth/1.1.5/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>google/truth (com.google.truth:truth)</summary>

### [`v1.4.1`](https://togithub.com/google/truth/releases/tag/v1.4.1):
1.4.1

This release deprecates `Truth8`.

All its methods have become available on the main `Truth` class. In most
cases, you can migrate your whole project mechanically: `git grep -l
Truth8 | xargs perl -pi -e 's/\bTruth8\b/Truth/g;'`

While we do not plan to delete `Truth8`, we recommend migrating off it,
at least if you static import `assertThat`: If you do not migrate, such
static imports will become ambiguous in Truth 1.4.2, breaking your
build.

### [`v1.4.0`](https://togithub.com/google/truth/releases/tag/v1.4.0):
1.4.0

In this release, our assertions on Java 8 types continue to move from
the `Truth8` class to the main `Truth` class. This change should not
break compatibility for any supported JDK or Android version, even users
who test under old versions of Android without [API
desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).
Additionally, we will never break binary compatibility, though some
users will have to make changes to their source code in order for it to
compile against newer versions.

This release is likely to lead to more **build failures** than
[1.3.0](https://togithub.com/google/truth/releases/tag/v1.3.0) did.
However, those failures should be **straightforward to fix**.

#### Example build failure

    Foo.java:152: error: reference to assertThat is ambiguous
        assertThat(repo.findFileWithName("foo")).isNull();
        ^
both method assertThat(@&#8203;org.jspecify.nullness.Nullable Path) in
Truth8 and method assertThat(@&#8203;org.jspecify.nullness.Nullable
Path) in Truth match

#### Simplest upgrade strategy (if you can update all your code
atomically in the same commit as the Truth upgrade)

In the same commit:

1.  Upgrade Truth to 1.4.0.
2. Replace `import static com.google.common.truth.Truth8.assertThat;`
with `import static com.google.common.truth.Truth.assertThat;`.
- If you use Kotlin, replace `import
com.google.common.truth.Truth8.assertThat` with `import
com.google.common.truth.Truth.assertThat`.
3. Replace `import com.google.common.truth.Truth8;` with `import
com.google.common.truth.Truth;`.
    -   again, similarly for Kotlin if needed
4. Optionally replace remaining references to `Truth8` with references
to `Truth`.
- For example, replace `Truth8.assertThat(optional).isPresent()` with
`Truth.assertThat(optional).isPresent()`.

If you're feeling lucky, you can try this one-liner for the code
updates:

```sh
git grep -l Truth8 | xargs perl -pi -e 's/import static com.google.common.truth.Truth8.assertThat;/import static com.google.common.truth.Truth.assertThat;/g; s/import com.google.common.truth.Truth8.assertThat/import com.google.common.truth.Truth.assertThat/g; s/import com.google.common.truth.Truth8/import com.google.common.truth.Truth/g; s/\bTruth8[.]/Truth./g;'
```

In most cases, that can be further simplified to:

```sh
git grep -l Truth8 | xargs perl -pi -e 's/\bTruth8\b/Truth/g;'
```

After that process, it is possible that you'll still see build errors
from ambiguous usages of `assertThat` static imports. If so, you can
find a workaround in the section about overload ambiguity in the release
notes for
[1.3.0](https://togithub.com/google/truth/releases/tag/v1.3.0).
Alternatively, you can wait to upgrade until after a future Truth
release, which will eliminate the ambiguity by changing the signatures
of some `Truth.assertThat` overloads.

#### Incremental upgrade strategy

If you have a very large repo or you have other reasons to prefer to
upgrade incrementally, you can use the approach that we used inside
Google. Roughly, that approach was:

1. Make the optional changes discussed in the release notes for
[1.3.0](https://togithub.com/google/truth/releases/tag/v1.3.0).
2. For any remaining calls to `Truth8.assertThat`, change them to
*avoid* static import.
- That is, replace `assertThat(optional).isPresent()` with
`Truth8.assertThat(optional).isPresent()`.
3.  Upgrade Truth to 1.4.0.
4. Optionally replace references to `Truth8` with references to `Truth`
(including restoring static imports if desired), as discussed in section
about the simple upgrade strategy above.

#### Optional additional changes

- If you use `assertWithMessage(...).about(intStreams()).that(...)`,
`expect.about(optionalLongs()).that(...)`, or similar, you can remove
your call to `about`. This change will never be necessary; it is just a
simplification.
- This is similar to a previous optional change from
[1.3.0](https://togithub.com/google/truth/releases/tag/v1.3.0), except
that 1.3.0 solved this problem for `streams` and `optionals`, whereas
1.4.0 solves it for the other `Truth8` types.

#### For help

Please feel welcome to [open an
issue](https://togithub.com/google/truth/issues/new) to report problems
or request help.

#### Changelog

- Added the remaining `Truth8.assertThat` overloads to the main `Truth`
class. ([`9be8e77`](https://togithub.com/google/truth/commit/9be8e774c),
[`1f81827`](https://togithub.com/google/truth/commit/1f81827f1))
- Added more `that` overloads to make it possible to write type-specific
assertions when using the remaining Java 8 types.
([`7c65fc6`](https://togithub.com/google/truth/commit/7c65fc611))

### [`v1.3.0`](https://togithub.com/google/truth/releases/tag/v1.3.0):
1.3.0

In this release, our assertions on Java 8 types begin to move from the
`truth-java8-extensions` artifact and the `Truth8` class to the main
`truth` artifact and the `Truth` class. This change should not break
compatibility for anyone, even users who test under old versions of
Android without [API
desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring).
Additionally, we will never break binary compatibility, though some
users will have to make changes to their source code in order for it to
compile against newer versions.

This change will be routine for most users, but we're providing as much
information as we can for any users who do encounter problems.

We will post fuller instructions for migration later on, once we've
learned more from our internal migration efforts. For now, you may find
that you need to make one kind of change, and you may elect to make
others. (If we missed anything, please [open an
issue](https://togithub.com/google/truth/issues/new) to report problems
or request help.)

The change you might need to make:

- By adding new overloads of `Truth.assertThat`, we cause some code to
fail to compile because of an overload ambiguity. This is rare, but it
can happen if you static import both `Truth.assertThat` and some other
`assertThat` method that includes overloads for `Optional` or `Stream`.
(It does *not* happen for `Truth8.assertThat`, though, except with the
Eclipse compiler. Nor it does *necessarily* happen for other
`assertThat(Stream)` and `assertThat(Optional)` methods.) If this
happens to you, you'll need to remove one of the static imports,
changing the corresponding call sites from "`assertThat`" to
"`FooSubject.assertThat`."
- Alternatively, you may choose to wait until we make further changes to
the new `Truth.assertThat` overloads. Once we make those further
changes, you may be able to simultaneously replace all your imports of
`Truth8.assertThat` with imports of `Truth.assertThat` as you upgrade to
the new version, likely without introducing overload ambiguities.

The changes you might elect to make:

- If you use `Truth8.assertThat(Stream)` or
`Truth8.assertThat(Optional)`, you can migrate to the new overloads in
`Truth`. If you static import `Truth8.assertThat`, you can usually make
this change simply by replacing that static import with a static import
of `Truth.assertThat`—or, if you already have an import of
`Truth.assertThat`, by just removing the import of `Truth8.assertThat`.
(If you additionally use less common assertion methods, like
`assertThat(OptionalInt)`, you'll want to use *both* imports for now.
Later, we'll move `assertThat(OptionalInt)` and friends, too.) We
recommend making this change now, since your calls to
`Truth8.assertThat` will fail to compile against some future version of
Truth, unless you plan to wait to update your Truth dependency until
we've made all our changes for Java 8 types.

- If you use `assertWithMessage(...).about(streams()).that(...)`,
`expect.about(optionals()).that(...)`, or similar, you can remove your
call to `about`. This change will never be necessary; it is just a
simplification.

- If you depend on `truth-java8-extension`, you may remove it. All its
classes are now part of the main `truth` artifact. This change, too, is
not necessary; it is just a simplification. (OK, if your build system
has a concept of [strict
deps](https://blog.bazel.build/2017/06/28/sjd-unused_deps.html), there
is a chance that you'll *need* to add deps on `truth` to replace your
deps on `truth-java8-extension`.)

Finally, the changelog for this release:

- Made `StreamSubject` avoid collecting the `Stream` until necessary,
and made its `isEqualTo` and `isNotEqualTo` methods no longer always
throw. ([`f8ecaec`](https://togithub.com/google/truth/commit/f8ecaec69))
- Added `assertThat` overloads for `Optional` and `Stream` to the main
`Truth` class.
([`37fd8be`](https://togithub.com/google/truth/commit/37fd8bea9))
- Added `that` overloads to make it possible to write type-specific
assertions when using `expect.that(optional)` and `expect.that(stream)`.
([`ca7e8f4`](https://togithub.com/google/truth/commit/ca7e8f4c5))
- Moved the `truth-java8-extension` classes into the main `truth`
artifact. There is no longer any need to depend on
`truth-java8-extension`, which is now empty. (We've also removed the
`Truth8` [GWT](https://www.gwtproject.org/) module.)
([`eb0426e`](https://togithub.com/google/truth/commit/eb0426eb7))

Again, if you have any problems, please [let us
know](https://togithub.com/google/truth/issues/new).

### [`v1.2.0`](https://togithub.com/google/truth/releases/tag/v1.2.0):
1.2.0

- Fixed a bug that caused ProtoTruth to ignore the contents of unpacked
`Any` messages. This fix may cause tests to fail, since ProtoTruth will
now check whether the message contents match. If so, you may need to
change the values that your tests expect, or there may be a bug in the
code under test that had been hidden by the Truth bug. Sorry for the
trouble.
([`8bd3ef6`](https://togithub.com/google/truth/commit/8bd3ef613))
- Added `isWithin().of()` support to `IntegerSubject` and `LongSubject`.
([`6464cb5`](https://togithub.com/google/truth/commit/6464cb5ca),
[`0e99a27`](https://togithub.com/google/truth/commit/0e99a2711))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 14:40:42 +01:00
renovate[bot] d8b1a5a1dc
Update dagger to v2.50 (#4267)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.dagger:dagger](https://togithub.com/google/dagger) |
`2.48.1` -> `2.50` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-compiler](https://togithub.com/google/dagger)
| `2.48.1` -> `2.50` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-compiler/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-compiler/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-compiler/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-compiler/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-support](https://togithub.com/google/dagger)
| `2.48.1` -> `2.50` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-support/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-support/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-support/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-support/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-processor](https://togithub.com/google/dagger)
| `2.48.1` -> `2.50` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-processor/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-processor/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-processor/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-processor/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [com.google.dagger:dagger-android](https://togithub.com/google/dagger)
| `2.48.1` -> `2.50` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android/2.48.1/2.50?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 14:39:34 +01:00
Weblate 0dec496ccb Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/
Translation: Tusky/Tusky
2024-02-23 13:22:59 +00:00
renovate[bot] c04ed01b36
Update androidx.work to v2.9.0 (#4266)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.work:work-testing](https://developer.android.com/jetpack/androidx/releases/work#2.9.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.8.1` -> `2.9.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.work:work-testing/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.work:work-testing/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.work:work-testing/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.work:work-testing/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.work:work-runtime-ktx](https://developer.android.com/jetpack/androidx/releases/work#2.9.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.8.1` -> `2.9.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.work:work-runtime-ktx/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.work:work-runtime-ktx/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.work:work-runtime-ktx/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.work:work-runtime-ktx/2.8.1/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 12:52:36 +01:00
renovate[bot] fce8343a7f
Update androidx.lifecycle to v2.7.0 (#4265)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.lifecycle:lifecycle-viewmodel-ktx](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.7.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.2` -> `2.7.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.lifecycle:lifecycle-reactivestreams-ktx](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.7.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.2` -> `2.7.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.lifecycle:lifecycle-livedata-ktx](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.7.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.2` -> `2.7.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.lifecycle:lifecycle-common-java8](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.7.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.2` -> `2.7.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-common-java8/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-common-java8/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-common-java8/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-common-java8/2.6.2/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 12:33:18 +01:00
renovate[bot] 5dcb7f6e8e
Update dependency com.android.application to v8.2.2 (#4264)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.android.application](https://developer.android.com/studio/build)
([source](https://android.googlesource.com/platform/tools/base)) |
`8.2.1` -> `8.2.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.application/8.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.android.application/8.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.android.application/8.2.1/8.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.application/8.2.1/8.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 12:33:06 +01:00
Zongle Wang e3c68e0992
Use Java 21 (#4235) 2024-02-23 12:28:13 +01:00
Zongle Wang 88c75c8d9b
Room 2.6.1 with Kotlin code generation (#4081)
https://developer.android.com/jetpack/androidx/releases/room#2.6.0
2024-02-23 12:21:31 +01:00
renovate[bot] 5d5bc15f42
Update dependency androidx.browser:browser to v1.7.0 (#3945)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.browser:browser](https://developer.android.com/jetpack/androidx/releases/browser#1.7.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.5.0` -> `1.7.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.browser:browser/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.browser:browser/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.browser:browser/1.5.0/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.browser:browser/1.5.0/1.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi40MC4zIiwidXBkYXRlZEluVmVyIjoiMzcuMjAwLjAiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 11:37:16 +01:00
renovate[bot] 6071ad9ab0
Update emoji2 to v1.4.0 (#3948)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.emoji2:emoji2-views-helper](https://developer.android.com/jetpack/androidx/releases/emoji2#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.0` -> `1.4.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.emoji2:emoji2-views-helper/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.emoji2:emoji2-views-helper/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.emoji2:emoji2-views-helper/1.3.0/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.emoji2:emoji2-views-helper/1.3.0/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.emoji2:emoji2-views](https://developer.android.com/jetpack/androidx/releases/emoji2#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.0` -> `1.4.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.emoji2:emoji2-views/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.emoji2:emoji2-views/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.emoji2:emoji2-views/1.3.0/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.emoji2:emoji2-views/1.3.0/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.emoji2:emoji2](https://developer.android.com/jetpack/androidx/releases/emoji2#1.4.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.0` -> `1.4.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.emoji2:emoji2/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.emoji2:emoji2/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.emoji2:emoji2/1.3.0/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.emoji2:emoji2/1.3.0/1.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi40MC4zIiwidXBkYXRlZEluVmVyIjoiMzcuMjAwLjAiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 11:28:14 +01:00
renovate[bot] 1618ebdeff
Update Kotlin (#4263)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.devtools.ksp](https://goo.gle/ksp)
([source](https://togithub.com/google/ksp)) | `1.9.22-1.0.16` ->
`1.9.22-1.0.17` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.devtools.ksp/1.9.22-1.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.devtools.ksp/1.9.22-1.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.devtools.ksp/1.9.22-1.0.16/1.9.22-1.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.devtools.ksp/1.9.22-1.0.16/1.9.22-1.0.17?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[org.jetbrains.kotlinx:kotlinx-coroutines-test](https://togithub.com/Kotlin/kotlinx.coroutines)
| `1.7.3` -> `1.8.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.7.3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.7.3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[org.jetbrains.kotlinx:kotlinx-coroutines-rx3](https://togithub.com/Kotlin/kotlinx.coroutines)
| `1.7.3` -> `1.8.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-rx3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-rx3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-rx3/1.7.3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-rx3/1.7.3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[org.jetbrains.kotlinx:kotlinx-coroutines-android](https://togithub.com/Kotlin/kotlinx.coroutines)
| `1.7.3` -> `1.8.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.7.3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.7.3/1.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>google/ksp (com.google.devtools.ksp)</summary>

###
[`v1.9.22-1.0.17`](https://togithub.com/google/ksp/releases/tag/1.9.22-1.0.17)

[Compare
Source](https://togithub.com/google/ksp/compare/1.9.22-1.0.16...1.9.22-1.0.17)

##### Issues fixed

- Annotations missing on KSTypeArgument of typealias
[#&#8203;1679](https://togithub.com/google/ksp/issues/1679)
- unhandled visibility: private to this
[#&#8203;1515](https://togithub.com/google/ksp/issues/1515)
- `Resolver#getJvmCheckedException` results in `<ERROR TYPE>` when
throwing type variable.
[#&#8203;1460](https://togithub.com/google/ksp/issues/1460)
- Class annotation values with `$` in name are `null` when used in
Kotlin source
[#&#8203;1671](https://togithub.com/google/ksp/issues/1671)
- KSP 1.9.21-1.0.15 leaking memory and causing OOMs
[#&#8203;1653](https://togithub.com/google/ksp/issues/1653)
- KSP processing fails with Java enum
[#&#8203;1482](https://togithub.com/google/ksp/issues/1482)
- \[KSP2] Support Package annotations
[#&#8203;1641](https://togithub.com/google/ksp/issues/1641)

</details>

<details>
<summary>Kotlin/kotlinx.coroutines
(org.jetbrains.kotlinx:kotlinx-coroutines-test)</summary>

###
[`v1.8.0`](https://togithub.com/Kotlin/kotlinx.coroutines/blob/HEAD/CHANGES.md#Version-180)

[Compare
Source](https://togithub.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.0)

- Implement the library for the Web Assembly (Wasm) for JavaScript
([#&#8203;3713](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3713)).
Thanks [@&#8203;igoriakovlev](https://togithub.com/igoriakovlev)!
-   Major Kotlin version update: was 1.8.20, became 1.9.21.
- On Android, ensure that `Dispatchers.Main !=
Dispatchers.Main.immediate`
([#&#8203;3545](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3545),
[#&#8203;3963](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3963)).
- Fixed a bug that caused `Flow` operators that limit cancel the
upstream flow to forget that they were already finished if there is
another such operator upstream
([#&#8203;4035](https://togithub.com/Kotlin/kotlinx.coroutines/issues/4035),
[#&#8203;4038](https://togithub.com/Kotlin/kotlinx.coroutines/issues/4038))
- `kotlinx-coroutines-debug` is published with the correct Java 9 module
info
([#&#8203;3944](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3944)).
- `kotlinx-coroutines-debug` no longer requires manually setting
`DebugProbes.enableCoroutineCreationStackTraces` to `false`, it's the
default
([#&#8203;3783](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3783)).
- `kotlinx-coroutines-test`: set the default timeout of `runTest` to 60
seconds, added the ability to configure it on the JVM with the
`kotlinx.coroutines.test.default_timeout=10s`
([#&#8203;3800](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3800)).
- `kotlinx-coroutines-test`: fixed a bug that could lead to not all
uncaught exceptions being reported after some tests failed
([#&#8203;3800](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3800)).
- `delay(Duration)` rounds nanoseconds up to whole milliseconds and not
down
([#&#8203;3920](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3920)).
Thanks [@&#8203;kevincianfarini](https://togithub.com/kevincianfarini)!
- `Dispatchers.Default` and the default thread for background work are
guaranteed to use the same context classloader as the object containing
it them
([#&#8203;3832](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3832)).
- It is guaranteed that by the time `SharedFlow.collect` suspends for
the first time, it's registered as a subscriber for that `SharedFlow`
([#&#8203;3885](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3885)).
Before, it was also true, but not documented.
- Atomicfu version is updated to 0.23.1, and Kotlin/Native atomic
transformations are enabled, reducing the footprint of coroutine-heavy
code
([#&#8203;3954](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3954)).
- Added a workaround for miscompilation of `withLock` on JS
([#&#8203;3881](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3881)).
Thanks [@&#8203;CLOVIS-AI](https://togithub.com/CLOVIS-AI)!
-   Small tweaks and documentation fixes.

##### Changelog relative to version 1.8.0-RC2

- `kotlinx-coroutines-debug` no longer requires manually setting
`DebugProbes.enableCoroutineCreationStackTraces` to `false`, it's the
default
([#&#8203;3783](https://togithub.com/Kotlin/kotlinx.coroutines/issues/3783)).
- Fixed a bug that caused `Flow` operators that limit cancel the
upstream flow to forget that they were already finished if there is
another such operator upstream
([#&#8203;4035](https://togithub.com/Kotlin/kotlinx.coroutines/issues/4035),
[#&#8203;4038](https://togithub.com/Kotlin/kotlinx.coroutines/issues/4038))
-   Small documentation fixes.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 11:18:09 +01:00
renovate[bot] e8369c1d2a
Update androidx.media3 to v1.2.1 (#4216)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [androidx.media3:media3-ui](https://togithub.com/androidx/media) |
`1.1.1` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-ui/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-ui/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-ui/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-ui/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-datasource-okhttp](https://togithub.com/androidx/media)
| `1.1.1` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-datasource-okhttp/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-datasource-okhttp/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-datasource-okhttp/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-datasource-okhttp/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-exoplayer-rtsp](https://togithub.com/androidx/media)
| `1.1.1` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer-rtsp/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer-rtsp/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer-rtsp/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer-rtsp/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-exoplayer-hls](https://togithub.com/androidx/media)
| `1.1.1` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer-hls/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer-hls/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer-hls/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer-hls/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-exoplayer-dash](https://togithub.com/androidx/media)
| `1.1.1` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer-dash/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer-dash/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer-dash/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer-dash/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.media3:media3-exoplayer](https://togithub.com/androidx/media)
| `1.1.1` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer/1.1.1/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>androidx/media (androidx.media3:media3-ui)</summary>

###
[`v1.2.1`](https://togithub.com/androidx/media/blob/HEAD/RELEASENOTES.md#121-2024-01-09)

[Compare
Source](https://togithub.com/androidx/media/compare/1.2.0...1.2.1)

This release includes the following changes since the
[1.2.0 release](#&#8203;120-2023-11-15):

-   ExoPlayer:
    -   Fix issue where manual seeks outside of the
`LiveConfiguration.min/maxOffset` range keep adjusting the offset back
        to `min/maxOffset`.
- Fix issue that OPUS and VORBIS channel layouts are wrong for 3, 5, 6,
7
        and 8 channels
([#&#8203;8396](https://togithub.com/google/ExoPlayer/issues/8396)).
- Fix issue where track selections after seek to zero in a live stream
        incorrectly let the stream start at its default position
([#&#8203;9347](https://togithub.com/google/ExoPlayer/issues/9347)).
- Fix the issue where new instances of `CmcdData.Factory` were receiving
negative values for `bufferedDurationUs` from chunk sources, resulting
        in an `IllegalArgumentException`
        ([#&#8203;888](https://togithub.com/androidx/media/issues/888)).
-   Transformer:
- Work around an issue where the encoder would throw at configuration
time
        due to setting a high operating rate.
-   Extractors:
    -   Mark secondary (unplayable) HEVC tracks in JPEG motion photos as
`ROLE_FLAG_ALTERNATE` to prevent them being automatically selected for
        playback because of their higher resolution.
    -   Fix wrong keyframe detection for TS H264 streams
        ([#&#8203;864](https://togithub.com/androidx/media/pull/864)).
- Fix duration estimation of TS streams that are longer than 47721
seconds
        ([#&#8203;855](https://togithub.com/androidx/media/issues/855)).
-   Audio:
- Fix handling of EOS for `SilenceSkippingAudioProcessor` when called
multiple times
([#&#8203;712](https://togithub.com/androidx/media/issues/712)).
-   Video:
- Add workaround for a device issue on Galaxy Tab S7 FE, Chromecast with
Google TV, and Lenovo M10 FHD Plus that causes 60fps AVC streams to be
        marked as unsupported
        ([#&#8203;693](https://togithub.com/androidx/media/issues/693)).
-   Metadata:
- Fix bug where `MediaMetadata` was only populated from Vorbis comments
        with upper-case keys
        ([#&#8203;876](https://togithub.com/androidx/media/issues/876)).
- Catch `OutOfMemoryError` when parsing very large ID3 frames, meaning
playback can continue without the tag info instead of playback failing
        completely.
-   DRM:
- Extend workaround for spurious ClearKey `https://default.url` license
        URL to API 33+ (previously the workaround only applied on API 33
exactly) ([#&#8203;837](https://togithub.com/androidx/media/pull/837)).
- Fix `ERROR_DRM_SESSION_NOT_OPENED` when switching from encrypted to
clear content without a surface attached to the player. The error was
due to incorrectly using a secure decoder to play the clear content.
-   Session:
    -   Put the custom keys and values in `MediaMetadataCompat` to
        `MediaMetadata.extras` and `MediaMetadata.extras` to
        `MediaMetadataCompat`
        ([#&#8203;756](https://togithub.com/androidx/media/issues/756),
        [#&#8203;802](https://togithub.com/androidx/media/issues/802)).
    -   Fix broadcasting `notifyChildrenChanged` for legacy controllers
        ([#&#8203;644](https://togithub.com/androidx/media/issues/644)).
- Fix a bug where setting a negative time for a disabled `setWhen` timer
        of the notification caused a crash on some devices
        ([#&#8203;903](https://togithub.com/androidx/media/issues/903)).
- Fix `IllegalStateException` when the media notification controller
hasn't completed connecting when the first notification update is
requested
([#&#8203;917](https://togithub.com/androidx/media/issues/917)).
-   UI:
- Fix issue where forward and rewind buttons are not visible when used
        with Material Design in a BottomSheetDialogFragment
        ([#&#8203;511](https://togithub.com/androidx/media/issues/511)).
    -   Fix issue where the numbers in the fast forward button of the
        `PlayerControlView` were misaligned
        ([#&#8203;547](https://togithub.com/androidx/media/issues/547)).
-   DASH Extension:
    -   Parse "f800" as channel count of 5 for Dolby in DASH manifest
        ([#&#8203;688](https://togithub.com/androidx/media/issues/688)).
-   Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
- MIDI: Fix issue where seeking forward skips the Program Change events
        ([#&#8203;704](https://togithub.com/androidx/media/issues/704)).
    -   Migrate to FFmpeg 6.0 and update supported NDK to `r26b`
        ([#&#8203;707](https://togithub.com/androidx/media/pull/707),
        [#&#8203;867](https://togithub.com/androidx/media/pull/867)).
-   Cast Extension:
- Sanitize creation of a `Timeline` to not crash the app when loading
        media fails on the cast device
        ([#&#8203;708](https://togithub.com/androidx/media/issues/708)).

###
[`v1.2.0`](https://togithub.com/androidx/media/blob/HEAD/RELEASENOTES.md#120-2023-11-15)

[Compare
Source](https://togithub.com/androidx/media/compare/1.1.1...1.2.0)

This release includes the following changes since the
[1.1.1 release](#&#8203;111-2023-08-14):

-   Common Library:
- Add a `@Nullable Throwable` parameter to the methods in the
`Log.Logger`
interface. The `message` parameter to these methods no longer contains
any information about the `Throwable` passed to the `Log.{d,i,w,e}()`
        methods, so implementations will need to manually append this
        information if desired (possibly using
        `Logger.appendThrowableString(String, Throwable)`).
- Fix Kotlin compatibility issue where nullable generic type parameters
and nullable array element types are not detected as nullable. Examples
        are `TrackSelectorResult` and `SimpleDecoder` method parameters
([#&#8203;6792](https://togithub.com/google/ExoPlayer/issues/6792)).
    -   Change default UI and notification behavior in
`Util.shouldShowPlayButton` to show a "play" button while playback is
temporarily suppressed (e.g. due to transient audio focus loss). The
        legacy behavior can be maintained by using
        `PlayerView.setShowPlayButtonIfPlaybackIsSuppressed(false)` or
`MediaSession.Builder.setShowPlayButtonIfPlaybackIsSuppressed(false)`
([#&#8203;11213](https://togithub.com/google/ExoPlayer/issues/11213)).
- Upgrade `androidx.annotation:annotation-experimental` to `1.3.1` to
fix
        https://issuetracker.google.com/251172715.
    -   Move `ExoPlayer.setAudioAttributes` to the `Player` interface.
-   ExoPlayer:
- Fix seeking issues in AC4 streams caused by not identifying
decode-only
        samples correctly
([#&#8203;11000](https://togithub.com/google/ExoPlayer/issues/11000)).
- Add suppression of playback on unsuitable audio output devices (e.g.
the
built-in speaker on Wear OS devices) when this feature is enabled via
`ExoPlayer.Builder.setSuppressPlaybackOnUnsuitableOutput`. The playback
        suppression reason will be updated as
`Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` if playback
is attempted when no suitable audio outputs are available, or if all
suitable outputs are disconnected during playback. The suppression
        reason will be removed when a suitable output is connected.
- Add `MediaSource.canUpdateMediaItem` and `MediaSource.updateMediaItem`
        to accept `MediaItem` updates after creation via
        `Player.replaceMediaItem(s)`.
- Allow `MediaItem` updates for all `MediaSource` classes provided by
the
        library via `Player.replaceMediaItem(s)`
        ([#&#8203;33](https://togithub.com/androidx/media/issues/33),
[#&#8203;9978](https://togithub.com/google/ExoPlayer/issues/9978)).
    -   Rename `MimeTypes.TEXT_EXOPLAYER_CUES` to
        `MimeTypes.APPLICATION_MEDIA3_CUES`.
- Add `PngExtractor` that sends and reads a whole PNG file into the
        `TrackOutput` as one sample.
    -   Enhance `SequenceableLoader.continueLoading(long)` method in the
        `SequenceableLoader` interface to
        `SequenceableLoader.continueLoading(LoadingInfo loadingInfo)`.
`LoadingInfo` contains additional parameters, including `playbackSpeed`
        and `lastRebufferRealtimeMs` in addition to the existing
        `playbackPositionUs`.
- Enhance `ChunkSource.getNextChunk(long, long, List, ChunkHolder)`
method
in the `ChunkSource` interface to `ChunkSource.getNextChunk(LoadingInfo,
        long, List, ChunkHolder)`.
- Add additional fields to Common Media Client Data (CMCD) logging:
buffer
starvation (`bs`), deadline (`dl`), playback rate (`pr`) and startup
(`su`)
([#&#8203;8699](https://togithub.com/google/ExoPlayer/issues/8699)).
    -   Add luma and chroma bitdepth to `ColorInfo`
        ([#&#8203;491](https://togithub.com/androidx/media/pull/491)).
- Add additional fields to Common Media Client Data (CMCD) logging: next
        object request (`nor`) and next range request (`nrr`)
([#&#8203;8699](https://togithub.com/google/ExoPlayer/issues/8699)).
- Add functionality to transmit Common Media Client Data (CMCD) data
using
query parameters
([#&#8203;553](https://togithub.com/androidx/media/issues/553)).
- Fix `ConcurrentModificationException` in `ExperimentalBandwidthMeter`
        ([#&#8203;612](https://togithub.com/androidx/media/issues/612)).
    -   Add `MediaPeriodId` parameter to
        `CompositeMediaSource.getMediaTimeForChildMediaTime`.
- Support `ClippingMediaSource` (and other sources with period/window
time
        offsets) in `ConcatenatingMediaSource2`
([#&#8203;11226](https://togithub.com/google/ExoPlayer/issues/11226)).
    -   Change `BaseRenderer.onStreamChanged()` to also receive a
        `MediaPeriodId` argument.
-   Transformer:
    -   Parse EXIF rotation data for image inputs.
    -   Remove `TransformationRequest.HdrMode` annotation type and its
associated constants. Use `Composition.HdrMode` and its associated
        constants instead.
    -   Simplify the `OverlaySettings` to fix rotation issues.
    -   Changed `frameRate` and `durationUs` parameters of
        `SampleConsumer.queueInputBitmap` to `TimestampIterator`.
-   Track Selection:
- Add
`DefaultTrackSelector.Parameters.allowAudioNonSeamlessAdaptiveness`
to explicitly allow or disallow non-seamless adaptation. The default
        stays at its current behavior of `true`.
-   Extractors:
- MPEG-TS: Ensure the last frame is rendered by passing the last access
        unit of a stream to the sample queue
([#&#8203;7909](https://togithub.com/google/ExoPlayer/issues/7909)).
    -   Fix typo when determining `rotationDegrees`. Changed
        `projectionPosePitch` to `projectionPoseRoll`
        ([#&#8203;461](https://togithub.com/androidx/media/pull/461)).
    -   Remove the assumption that `Extractor` instances can be directly
        inspected with `instanceof`. If you want runtime access to the
        implementation details of an `Extractor` you must first call
        `Extractor.getUnderlyingInstance`.
    -   Add `BmpExtractor`.
    -   Add `WebpExtractor`.
    -   Add `HeifExtractor`.
    -   Add
[QuickTime classic](https://developer.apple.com/standards/qtff-2001.pdf)
        support to `Mp4Extractor`.
-   Audio:
- Add support for 24/32-bit big-endian PCM in MP4 and Matroska, and
parse
        PCM encoding for `lpcm` in MP4.
    -   Add support for extracting Vorbis audio in MP4.
- Add `AudioSink.getFormatOffloadSupport(Format)` that retrieves level
of
        offload support the sink can provide for the format through a
        `DefaultAudioOffloadSupportProvider`. It returns the new
        `AudioOffloadSupport` that contains `isFormatSupported`,
        `isGaplessSupported`, and `isSpeedChangeSupported`.
- Add `AudioSink.setOffloadMode()` through which the offload
configuration
        on the audio sink is configured. Default is
        `AudioSink.OFFLOAD_MODE_DISABLED`.
    -   Offload can be enabled through `setAudioOffloadPreference` in
`TrackSelectionParameters`. If the set preference is to enable, the
device supports offload for the format, and the track selection is a
        single audio track, then audio offload will be enabled.
    -   If `audioOffloadModePreference` is set to
        `AUDIO_OFFLOAD_MODE_PREFERENCE_REQUIRED`, then the
`DefaultTrackSelector` will only select an audio track and only if that
track's format is supported in offload. If no audio track is supported
        in offload, then no track will be selected.
- Disabling gapless support for offload when pre-API level 33 due to
        playback position issue after track transition.
    -   Remove parameter `enableOffload` from
        `DefaultRenderersFactory.buildAudioSink` method signature.
    -   Remove method `DefaultAudioSink.Builder.setOffloadMode`.
    -   Remove intdef value
`DefaultAudioSink.OffloadMode.OFFLOAD_MODE_ENABLED_GAPLESS_DISABLED`.
    -   Add support for Opus gapless metadata during offload playback.
- Allow renderer recovery by disabling offload if failed at first write
        ([#&#8203;627](https://togithub.com/androidx/media/issues/627)).
- Enable Offload Scheduling by default for audio-only offloaded
playback.
    -   Delete `ExoPlayer.experimentalSetOffloadSchedulingEnabled` and
`AudioOffloadListener.onExperimentalOffloadSchedulingEnabledChanged`.
    -   Renamed `onExperimentalSleepingForOffloadChanged` as
`onSleepingForOffloadChanged` and `onExperimentalOffloadedPlayback` as
        `onOffloadedPlayback`.
- Move audio offload mode related `TrackSelectionParameters` interfaces
        and definitions to an inner `AudioOffloadPreferences` class.
- Add `onAudioTrackInitialized` and `onAudioTrackReleased` callbacks to
        `AnalyticsListener`, `AudioRendererEventListener` and
        `AudioSink.Listener`.
    -   Fix DTS Express audio buffer underflow issue
        ([#&#8203;650](https://togithub.com/androidx/media/pull/650)).
    -   Fix bug where the capabilities check for E-AC3-JOC throws an
        `IllegalArgumentException`
        ([#&#8203;677](https://togithub.com/androidx/media/issues/677)).
-   Video:
    -   Allow `MediaCodecVideoRenderer` to use a custom
        `VideoFrameProcessor.Factory`.
- Fix bug where the first frame couldn't be rendered if the audio stream
        starts with negative timestamps
        ([#&#8203;291](https://togithub.com/androidx/media/issues/291)).
-   Text:
- Remove `ExoplayerCuesDecoder`. Text tracks with `sampleMimeType =
application/x-media3-cues` are now directly handled by `TextRenderer`
        without needing a `SubtitleDecoder` instance.
-   Metadata:
- `MetadataDecoder.decode` will no longer be called for "decode-only"
        samples as the implementation must return null anyway.
-   Effect:
- Add `VideoFrameProcessor.queueInputBitmap(Bitmap, Iterator<Long>)`
        queuing bitmap input by timestamp.
- Change `VideoFrameProcessor.registerInputStream()` to be non-blocking.
        Apps must implement
        `VideoFrameProcessor.Listener#onInputStreamRegistered()`.
    -   Changed `frameRate` and `durationUs` parameters of
        `VideoFrameProcessor.queueInputBitmap` to `TimestampIterator`.
-   IMA extension:
- Fix bug where a multi-period DASH live stream that is not the first
item
        in a playlist can throw an exception
        ([#&#8203;571](https://togithub.com/androidx/media/issues/571)).
    -   Release StreamManager before calling `AdsLoader.destroy()`
    -   Bump IMA SDK version to 3.31.0.
-   Session:
    -   Set the notifications foreground service behavior to
`FOREGROUND_SERVICE_IMMEDIATE` in `DefaultMediaNotificationProvider`
        ([#&#8203;167](https://togithub.com/androidx/media/issues/167)).
    -   Use only
`android.media.session.MediaSession.setMediaButtonBroadcastReceiver()`
above API 31 to avoid problems with deprecated API on Samsung devices
        ([#&#8203;167](https://togithub.com/androidx/media/issues/167)).
- Use the media notification controller as proxy to set available
commands
and custom layout used to populate the notification and the platform
        session.
    -   Convert media button events that are received by
`MediaSessionService.onStartCommand()` within Media3 instead of routing
them to the platform session and back to Media3. With this, the caller
controller is always the media notification controller and apps can
easily recognize calls coming from the notification in the same way on
        all supported API levels.
- Fix bug where `MediaController.getCurrentPosition()` is not advancing
        when connected to a legacy `MediaSessionCompat`.
    -   Add `MediaLibrarySession.getSubscribedControllers(mediaId)` for
        convenience.
- Override `MediaLibrarySession.Callback.onSubscribe()` to assert the
availability of the parent ID for which the controller subscribes. If
successful, the subscription is accepted and `notifyChildrenChanged()`
        is called immediately to inform the browser
        ([#&#8203;561](https://togithub.com/androidx/media/issues/561)).
- Add session demo module for Automotive OS and enable session demo for
        Android Auto.
    -   Do not set the queue of the framework session when
`COMMAND_GET_TIMELINE` is not available for the media notification
controller. With Android Auto as the client controller reading from the
framework session, this has the effect that the `queue` button in the UI
        of Android Auto is not displayed
        ([#&#8203;339](https://togithub.com/androidx/media/issues/339)).
- Use `DataSourceBitmapLoader` by default instead of
`SimpleBitmapLoader`
        ([#&#8203;271](https://togithub.com/androidx/media/issues/271),
        [#&#8203;327](https://togithub.com/androidx/media/issues/327)).
- Add `MediaSession.Callback.onMediaButtonEvent(Intent)` that allows
apps
        to override the default media button event handling.
-   UI:
- Add a `Player.Listener` implementation for Wear OS devices that
handles
        playback suppression due to
        `Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` by
launching a system dialog to allow a user to connect a suitable audio
output (e.g. bluetooth headphones). The listener will auto-resume
playback if a suitable device is connected within a configurable timeout
        (default is 5 minutes).
-   Downloads:
- Declare "data sync" foreground service type for `DownloadService` for
Android 14 compatibility. When using this service, the app also needs to
add `dataSync` as `foregroundServiceType` in the manifest and add the
        `FOREGROUND_SERVICE_DATA_SYNC` permission
([#&#8203;11239](https://togithub.com/google/ExoPlayer/issues/11239)).
-   HLS Extension:
- Refresh the HLS live playlist with an interval calculated from the
last
        load start time rather than the last load completed time
        ([#&#8203;663](https://togithub.com/androidx/media/issues/663)).
-   DASH Extension:
- Allow multiple of the same DASH identifier in segment template url.
- Add experimental support for parsing subtitles during extraction. This
        has better support for merging overlapping subtitles, including
resolving flickering when transitioning between subtitle segments. You
        can enable this using
`DashMediaSource.Factory.experimentalParseSubtitlesDuringExtraction()`
        ([#&#8203;288](https://togithub.com/androidx/media/issues/288)).
-   RTSP Extension:
- Fix a race condition that could lead to `IndexOutOfBoundsException`
when
        falling back to TCP, or playback hanging in some situations.
    -   Check state in RTSP setup when returning loading state of
        `RtspMediaPeriod`
        ([#&#8203;577](https://togithub.com/androidx/media/issues/577)).
- Ignore custom Rtsp request methods in Options response public header
        ([#&#8203;613](https://togithub.com/androidx/media/issues/613)).
- Use RTSP Setup Response timeout value in time interval of sending
        keep-alive RTSP Options requests
        ([#&#8203;662](https://togithub.com/androidx/media/issues/662)).
-   Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
- Release the MIDI decoder module, which provides support for playback
of
        standard MIDI files using the Jsyn library to synthesize audio.
- Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output
        buffers that don't need to be presented. This is preferred over
        `C.BUFFER_FLAG_DECODE_ONLY` that will be deprecated.
    -   Add `Decoder.setOutputStartTimeUs` and
`SimpleDecoder.isAtLeastOutputStartTimeUs` to allow decoders to drop
decode-only samples before the start time. This should be preferred to
        `Buffer.isDecodeOnly` that will be deprecated.
- Fix bug publishing MIDI decoder artifact to Maven repository. The
        artifact is renamed to `media3-exoplayer-midi`
        ([#&#8203;734](https://togithub.com/androidx/media/issues/734)).
-   Leanback extension:
- Fix bug where disabling a surface can cause an `ArithmeticException`
in
Leanback code
([#&#8203;617](https://togithub.com/androidx/media/issues/617)).
-   Test Utilities:
- Make `TestExoPlayerBuilder` and `FakeClock` compatible with Espresso
UI
tests and Compose UI tests. This fixes a bug where playback advances
non-deterministically during Espresso or Compose view interactions.
-   Remove deprecated symbols:
    -   Remove
`TransformationRequest.Builder.setEnableRequestSdrToneMapping(boolean)`
        and

`TransformationRequest.Builder.experimental_setEnableHdrEditing(boolean)`.
Use `Composition.Builder.setHdrMode(int)` and pass the `Composition` to
        `Transformer.start(Composition, String)` instead.
- Remove deprecated
`DownloadNotificationHelper.buildProgressNotification`
method, use a non deprecated method that takes a `notMetRequirements`
        parameter instead.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjEuMCIsInVwZGF0ZWRJblZlciI6IjM3LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 11:17:17 +01:00
renovate[bot] 5d52f0985e
Update dependency androidx.activity:activity-ktx to v1.8.2 (#4095)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.activity:activity-ktx](https://developer.android.com/jetpack/androidx/releases/activity#1.8.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.7.2` -> `1.8.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.activity:activity-ktx/1.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.activity:activity-ktx/1.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.activity:activity-ktx/1.7.2/1.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.activity:activity-ktx/1.7.2/1.8.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMjAwLjAiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 10:58:17 +01:00
renovate[bot] 5153e138fc
Update dependency com.github.UnifiedPush:android-connector to v2.4.0 (#4098)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[com.github.UnifiedPush:android-connector](https://togithub.com/UnifiedPush/android-connector)
| `2.1.1` -> `2.4.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.github.UnifiedPush:android-connector/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.github.UnifiedPush:android-connector/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.github.UnifiedPush:android-connector/2.1.1/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.github.UnifiedPush:android-connector/2.1.1/2.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>UnifiedPush/android-connector
(com.github.UnifiedPush:android-connector)</summary>

###
[`v2.4.0`](https://togithub.com/UnifiedPush/android-connector/releases/tag/2.4.0)

[Compare
Source](https://togithub.com/UnifiedPush/android-connector/compare/2.3.1...2.4.0)

-   Use jvmToolchain(8) with kotlin 1.7.20 to improve compatibility
- Remove FCM embedded distributor from distributors if PlayServices is
not installed

###
[`v2.3.1`](https://togithub.com/UnifiedPush/android-connector/releases/tag/2.3.1)

[Compare
Source](https://togithub.com/UnifiedPush/android-connector/compare/2.3.0...2.3.1)

-   Make DEFAULT_FEATURES static

##### 2.3.0 changes:

-   Change default features to bytes_message ⚠️
- Introduce getAckDistributor and getSavedDistributor, deprecate
getDistributor ⚠️
-   Fix inconsistent JVM-target compatibility
-   Bump dependencies

###
[`v2.3.0`](https://togithub.com/UnifiedPush/android-connector/releases/tag/2.3.0)

[Compare
Source](https://togithub.com/UnifiedPush/android-connector/compare/2.2.0...2.3.0)

-   Change default features to bytes_message ⚠️
- Introduce getAckDistributor and getSavedDistributor, deprecate
getDistributor ⚠️
-   Fix inconsistent JVM-target compatibility
-   Bump dependencies

###
[`v2.2.0`](https://togithub.com/UnifiedPush/android-connector/releases/tag/2.2.0)

[Compare
Source](https://togithub.com/UnifiedPush/android-connector/compare/2.1.1...2.2.0)

Bump dependencies
Target SDK 34
Avoid race conditions with token, instances and distributors

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMTUzLjIiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 10:53:31 +01:00
renovate[bot] 51133720c5
Update dependency androidx.core:core-ktx to v1.12.0 (#4096)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.core:core-ktx](https://developer.android.com/jetpack/androidx/releases/core#1.12.0)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.10.1` -> `1.12.0` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.core:core-ktx/1.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.core:core-ktx/1.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.core:core-ktx/1.10.1/1.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.core:core-ktx/1.10.1/1.12.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMjAwLjAiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 10:51:10 +01:00
renovate[bot] 7c391b8bba
Update dependency com.github.MikeOrtiz:TouchImageView to v3.6 (#4097)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[com.github.MikeOrtiz:TouchImageView](https://togithub.com/MikeOrtiz/TouchImageView)
| `3.5` -> `3.6` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.github.MikeOrtiz:TouchImageView/3.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.github.MikeOrtiz:TouchImageView/3.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.github.MikeOrtiz:TouchImageView/3.5/3.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.github.MikeOrtiz:TouchImageView/3.5/3.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>MikeOrtiz/TouchImageView
(com.github.MikeOrtiz:TouchImageView)</summary>

###
[`v3.6`](https://togithub.com/MikeOrtiz/TouchImageView/releases/tag/3.6)

[Compare
Source](https://togithub.com/MikeOrtiz/TouchImageView/compare/3.5...3.6)

<!-- Release notes generated using configuration in .github/release.yml
at 3.6 -->

#### What's Changed

##### Exciting New Features 🎉

- api34 by [@&#8203;hannesa2](https://togithub.com/hannesa2) in
[https://github.com/MikeOrtiz/TouchImageView/pull/555](https://togithub.com/MikeOrtiz/TouchImageView/pull/555)

##### Other Changes

- Bump androidx.recyclerview:recyclerview from 1.3.0 to 1.3.1 by
[@&#8203;dependabot](https://togithub.com/dependabot) in
[https://github.com/MikeOrtiz/TouchImageView/pull/546](https://togithub.com/MikeOrtiz/TouchImageView/pull/546)
- Bump Kotlin 1.9 by [@&#8203;hannesa2](https://togithub.com/hannesa2)
in
[https://github.com/MikeOrtiz/TouchImageView/pull/548](https://togithub.com/MikeOrtiz/TouchImageView/pull/548)
- Bump com.github.bumptech.glide:glide from 4.15.1 to 4.16.0 by
[@&#8203;dependabot](https://togithub.com/dependabot) in
[https://github.com/MikeOrtiz/TouchImageView/pull/549](https://togithub.com/MikeOrtiz/TouchImageView/pull/549)
- Bump kotlin_version from 1.9.0 to 1.9.10 by
[@&#8203;dependabot](https://togithub.com/dependabot) in
[https://github.com/MikeOrtiz/TouchImageView/pull/551](https://togithub.com/MikeOrtiz/TouchImageView/pull/551)
- Bump actions/checkout from 3 to 4 by
[@&#8203;dependabot](https://togithub.com/dependabot) in
[https://github.com/MikeOrtiz/TouchImageView/pull/553](https://togithub.com/MikeOrtiz/TouchImageView/pull/553)
- Fix compileSdk deprecation by
[@&#8203;hannesa2](https://togithub.com/hannesa2) in
[https://github.com/MikeOrtiz/TouchImageView/pull/556](https://togithub.com/MikeOrtiz/TouchImageView/pull/556)
- Remove Froyo method by
[@&#8203;hannesa2](https://togithub.com/hannesa2) in
[https://github.com/MikeOrtiz/TouchImageView/pull/557](https://togithub.com/MikeOrtiz/TouchImageView/pull/557)
- Bump com.android.tools.build:gradle from 8.1.0 to 8.1.1 by
[@&#8203;dependabot](https://togithub.com/dependabot) in
[https://github.com/MikeOrtiz/TouchImageView/pull/550](https://togithub.com/MikeOrtiz/TouchImageView/pull/550)
- Bump androidx.core:core-ktx from 1.10.1 to 1.12.0 by
[@&#8203;dependabot](https://togithub.com/dependabot) in
[https://github.com/MikeOrtiz/TouchImageView/pull/552](https://togithub.com/MikeOrtiz/TouchImageView/pull/552)

**Full Changelog**:
https://github.com/MikeOrtiz/TouchImageView/compare/3.5...3.6

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMTUzLjIiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-23 10:29:37 +01:00
Konrad Pozniak b976fe5296
full sdk 34 support (#4224)
builds upon work from #4082 

Additionally fixes some deprecations and adds support for [predictive
back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture).
I also refactored how the activity transitions work because they are
closely related to predictive back. The awkward
`finishWithoutSlideOutAnimation` is gone, activities that have been
started with slide in will now automatically close with slide out.

To test predictive back you need an emulator or device with Sdk 34
(Android 14) and then enable it in the developer settings.

Predictive back requires the back action to be determined before it
actually occurs so the system can play the right predictive animation,
which made a few reorganisations necessary.

closes #4082 
closes #4005 
unlocks a bunch of dependency upgrades that require sdk 34

---------

Co-authored-by: Goooler <wangzongler@gmail.com>
2024-02-23 10:27:19 +01:00
Konrad Pozniak fa8bede7d6
fix compose notification action not recreating MainActivity (#4249)
closes #4247
2024-02-23 10:27:07 +01:00
Konrad Pozniak 7d3aafdd65
fix quick replies from notifications (#4250)
While working on #4249 I noticed that quick replies also don't work as
expected. The notification just stays in the sending state forever.
There are actually 2 problems:
- Notifications are sent in `NotificationFetcher` with the id of the
Mastodon notification as tag and the current account id as id. The wrong
notification id was forwarded to `SendStatusBroadcastReceiver` so it
never had a chance of updating the notification.
- Notifications containing an active remote input can't be cancelled
(they just stop their animation when doing so). So instead I update the
notification with info that the reply is being sent and have it dismiss
automatically.

I also tried replacing the original notification with the "sending"
notification of `SendStatusService`, but that doesn't work because
`Service.startForeground` doesn't have a tag parameter, only an id.

---------

Co-authored-by: Willow <charlag@tuta.io>
2024-02-23 10:26:46 +01:00
Willow 22ec78c75a
Improve detailed status looks (#4260)
#4205 did change how the counters for the detailed posts behave and for
a good reason I believe.

However I find the changed order very confusing and not aesthetically
pleasing.

I have tried a few options, including reserving space for it but it was
confusing (when counters are not displayed there would be a danging
separator or if we show separator together with it it would be confusing
as well).

I propose we simply show the counters independent on the counts. I know
we try to de-emphasize the counters but I believe this is fine to do in
detailed view.

One disadvantage is that we need translators to update the translations.

Additionally I've done two spacing changes: I removed a separator
between the counters and the buttons, removed padding around the
counters and increased the space between the counters and the buttons
instead. I believe it's better to use space than separators. This also
makes the space above/below the media/counters separator balanced.

In the second commit I've also made the metadata/counters separators
thinner, I think it looks better.

here's the combined version:


![proposal_final](https://github.com/tuskyapp/Tusky/assets/3099142/ea9d4c0c-fe6a-4f2e-8427-673b2a833e6b)
2024-02-23 10:25:05 +01:00
Konrad Pozniak 48afcacd98
Translations update from Weblate (#4259)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-02-23 10:15:03 +01:00
Salif Mehmed 4e43feb9dc Translated using Weblate (Bulgarian)
Currently translated at 86.4% (548 of 634 strings)

Co-authored-by: Salif Mehmed <mail@salif.eu>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/bg/
Translation: Tusky/Tusky
2024-02-21 04:45:01 +00:00
Rhoslyn Prys 2c13805d84 Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Rhoslyn Prys <post@meddal.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-02-21 04:45:01 +00:00
Levi Bard 6994693496
Move the statistics bar in the detailed view below the button bar (#4205)
Rationale: In the current layout, when performing multiple interactions
(e.g. fav+boost, fav+reply) on a post that hasn't been interacted with
before, the statistics bar appears and pushes the buttons down, so the
second tap goes to the statistics bar instead
2024-02-20 10:17:13 +01:00
Konrad Pozniak 7173d5e1e7
make badge for new direct messages blue (#4257)
This makes the dot badge that we show on the direct messages tab when
there are unread messages blue instead of red. I prefer it that way
because its more subtle and doesn't look like there is some kind of
error.

before / after:

<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/f4b241a5-0fa4-4134-9790-18f74caa2dae"
width="240"/> <img
src="https://github.com/tuskyapp/Tusky/assets/10157047/56788d5c-f19c-4fa5-b83e-e824aed995f4"
width="240"/>
2024-02-19 09:22:14 +01:00
Konrad Pozniak 1cd8b497f7
set greenDebug as default flavor (#4251)
I had it happen multiple times recently that I was testing green Tusky
but Android Studio actually put blue Tusky on my device and I wasted a
lot of time until I found out 😣
This change should tell it that greenDebug is the preferred flavor for
developing.
2024-02-17 15:15:19 +01:00
Konrad Pozniak 17e99bbc2e
Revert "Migrate to Hilt KSP compiler (#4136)" (#4246)
This reverts commit 6494247301.

Seems like dagger/ksp is still a bit buggy, I'm getting one of these
errors every other build, so lets revert this for now.

https://github.com/google/dagger/issues/4181
https://github.com/google/ksp/issues/1196
2024-02-10 10:42:31 +01:00
Konrad Pozniak 7f7751624e
Translations update from Weblate (#4240)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-02-08 09:10:02 +01:00
Ihor Hordiichuk e2c2db1d31 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2024-02-04 04:45:01 +00:00
Hồ Nhất Duy dbe82fe1b0 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2024-02-04 04:45:01 +00:00
Ümit Solmaz b826ca15a9 Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2024-02-04 04:45:01 +00:00
fin-w 61de55efa7 Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-02-04 04:45:00 +00:00
XoseM c9d29fa92f Translated using Weblate (Galician)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2024-02-04 04:45:00 +00:00
Konrad Pozniak 7fef19efc6
Revert "make timestamp abbreviations plurals (#4202)" (#4230)
This reverts commit 5174c00558.

closes #4145
2024-01-28 19:48:35 +01:00
Konrad Pozniak 8a39fc643c
Translations update from Weblate (#4234)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-01-28 19:09:15 +01:00
Konrad Pozniak d66866648e
improve null safety of instance info (#4226)
according to crash logs there are seem to be some instances that don't
always return the expected json, so lets be extra safe here

```
Exception java.lang.NullPointerException:
  at com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository$getInstanceInfo$2.invokeSuspend (InstanceInfoRepository.kt:67)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:108)
  at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run (LimitedDispatcher.java:115)
  at kotlinx.coroutines.scheduling.TaskImpl.run (Tasks.kt:103)
  at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely (CoroutineScheduler.java:584)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask (CoroutineScheduler.kt:793)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker (CoroutineScheduler.kt:697)
  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run (CoroutineScheduler.kt:684)
```
2024-01-28 19:08:44 +01:00
Konrad Pozniak 0c2b8b114b
make sure link preview card is not shown when cw is collapsed (#4218)
The sensitive flag indicates sensitive media, but we want to check if
there is a contentwarning on the post. I think statuses that have a
contentwarning but no sensitive flag are rare so we never noticed this
bug.

closes #4201
2024-01-28 19:07:51 +01:00
Konrad Pozniak 0b9f61c100
bring back the notification filter preference (#4225)
It was probably forgotten when we restored the old notifications
behavior.
closes #4222
2024-01-28 19:07:29 +01:00
Konrad Pozniak 750e255029
fix check that ensures only one bookmark tab is added (#4217)
closes #4214
2024-01-28 19:07:17 +01:00
ButterflyOfFire 5ae6611072 Translated using Weblate (French)
Currently translated at 93.5% (592 of 633 strings)

Translated using Weblate (Arabic)

Currently translated at 91.4% (579 of 633 strings)

Co-authored-by: ButterflyOfFire <butterflyoffire@protonmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ar/
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/
Translation: Tusky/Tusky
2024-01-28 17:35:57 +00:00
Konrad Pozniak db9e6562cf
Translations update from Weblate (#4229)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2024-01-19 09:56:51 +01:00
Konrad Pozniak a4f931da83
Translations update from Weblate (#4227)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-01-19 09:56:39 +01:00
Hồ Nhất Duy 002852dc27 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/
2024-01-19 08:24:35 +00:00
Bruno Miguel 92879700a1 Translated using Weblate (Portuguese (Portugal))
Currently translated at 63.6% (21 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pt_PT/
2024-01-19 08:24:35 +00:00
Marco Baptista 3a551b48f6 Translated using Weblate (Portuguese (Portugal))
Currently translated at 51.5% (17 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pt_PT/
2024-01-19 08:24:35 +00:00
XoseM 88dbc8de12 Translated using Weblate (Galician)
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2024-01-17 04:45:01 +00:00
Sveinn í Felli 5b04a0e8e1 Translated using Weblate (Icelandic)
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2024-01-17 04:45:01 +00:00
Hồ Nhất Duy 9005e6525c Translated using Weblate (Vietnamese)
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2024-01-17 04:45:01 +00:00
Bruno Miguel 6f22072d0e Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Bruno Miguel <brunoalexandremiguel@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2024-01-17 04:45:01 +00:00
João Alves 8df7a58679 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: João Alves <joao.2003.couto+weblate@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2024-01-17 04:45:01 +00:00
Bruno Miguel a25f9f5823 Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Bruno Miguel <brunoalexandremiguel@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2024-01-17 04:45:01 +00:00
Newidyn a489be35b1 Translated using Weblate (Welsh)
Currently translated at 98.7% (625 of 633 strings)

Co-authored-by: Newidyn <grugallt@protonmail.ch>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-01-17 04:45:00 +00:00
fin-w c536b11072 Translated using Weblate (Welsh)
Currently translated at 98.7% (625 of 633 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-01-17 04:45:00 +00:00
Ihor Hordiichuk 111b301246 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2024-01-17 04:45:00 +00:00
Konrad Pozniak 5174c00558
make timestamp abbreviations plurals (#4202)
some languages require it

closes #4145

Changing all the strings was easy with the Regex replace feature
https://www.jetbrains.com/help/idea/tutorial-finding-and-replacing-text-using-regular-expressions.html
2024-01-09 20:35:40 +01:00
Maximilian Ertl 27a610bd48
feat: explicitly enable the share-button in Chrome Custom Tabs (#4223)
Chrome defaults to showing it anyways, but Firefox doesn't. By enabling
this feature, users across both browsers will now have the same
experience.

closes tuskyapp/Tusky/issues/4137
2024-01-09 20:08:41 +01:00
Zongle Wang c9f8b043c5
Polish Chinese translations (#4221)
- Insert blanks between Chinese chars with Ascii chars.
- Fix the typo of `账户`.
- Add full_description and short_description for Traditional Chinese.
2024-01-09 20:07:49 +01:00
Konrad Pozniak 36ff1b55d1
Translations update from Weblate (#4215)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-01-09 20:06:44 +01:00
Ümit Solmaz a08ef5d68f Translated using Weblate (Turkish)
Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (German)

Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2024-01-09 04:45:01 +00:00
Hồ Nhất Duy 4efc54246f Translated using Weblate (Vietnamese)
Currently translated at 100.0% (633 of 633 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2024-01-09 04:45:01 +00:00
Konrad Pozniak afbc183c02
add missing room schema file (#4211) 2024-01-07 10:45:30 +01:00
Konrad Pozniak aca3a638e9
Upgrade Gradle to 8.5 and Android Gradle Plugin to 8.2.1 (#4208) 2024-01-07 10:45:21 +01:00
renovate[bot] fb063dfb0f
fix(deps): update dependency androidx.fragment:fragment-ktx to v1.6.2 (#4209)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.fragment:fragment-ktx](https://developer.android.com/jetpack/androidx/releases/fragment#1.6.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.6.1` -> `1.6.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.fragment:fragment-ktx/1.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.fragment:fragment-ktx/1.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.fragment:fragment-ktx/1.6.1/1.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.fragment:fragment-ktx/1.6.1/1.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjEuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyMS4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 20:22:08 +01:00
renovate[bot] 520c202e55
fix(deps): update dependency androidx.exifinterface:exifinterface to v1.3.7 (#4207)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.exifinterface:exifinterface](https://developer.android.com/jetpack/androidx/releases/exifinterface#1.3.7)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.3.6` -> `1.3.7` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.exifinterface:exifinterface/1.3.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.exifinterface:exifinterface/1.3.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.exifinterface:exifinterface/1.3.6/1.3.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.exifinterface:exifinterface/1.3.6/1.3.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjEuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyMS4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 19:53:33 +01:00
renovate[bot] 1a3277bb85
chore(deps): update kotlin (#4204)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.devtools.ksp](https://goo.gle/ksp)
([source](https://togithub.com/google/ksp)) | `1.9.20-1.0.13` ->
`1.9.22-1.0.16` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.devtools.ksp/1.9.22-1.0.16?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.devtools.ksp/1.9.22-1.0.16?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.devtools.ksp/1.9.20-1.0.13/1.9.22-1.0.16?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.devtools.ksp/1.9.20-1.0.13/1.9.22-1.0.16?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.plugin.parcelize | `1.9.20` -> `1.9.22` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.20/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.20/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.android | `1.9.20` -> `1.9.22` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.android/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.android/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.android/1.9.20/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.android/1.9.20/1.9.22?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>google/ksp (com.google.devtools.ksp)</summary>

###
[`v1.9.22-1.0.16`](https://togithub.com/google/ksp/releases/tag/1.9.22-1.0.16)

[Compare
Source](https://togithub.com/google/ksp/compare/1.9.21-1.0.16...1.9.22-1.0.16)

This is KSP 1.0.16 for Kotlin compiler 1.9.22

###
[`v1.9.21-1.0.16`](https://togithub.com/google/ksp/releases/tag/1.9.21-1.0.16)

[Compare
Source](https://togithub.com/google/ksp/compare/1.9.21-1.0.15...1.9.21-1.0.16)

##### Issue fixed

[#&#8203;1653](https://togithub.com/google/ksp/issues/1653) KSP
1.9.21-1.0.15 leaking memory and causing OOMs

###
[`v1.9.21-1.0.15`](https://togithub.com/google/ksp/releases/tag/1.9.21-1.0.15)

[Compare
Source](https://togithub.com/google/ksp/compare/1.9.20-1.0.14...1.9.21-1.0.15)

This is a hot fix to
[#&#8203;1591](https://togithub.com/google/ksp/issues/1591) where
multiple KMP projects were affected.

##### Issues Fixed / PRs Merged

[#&#8203;1591](https://togithub.com/google/ksp/issues/1591) Error:
Rewrite at slice LEXICAL_SCOPE key: ANNOTATION_ENTRY old value
[#&#8203;1612](https://togithub.com/google/ksp/issues/1612) Filter out
the `-progressive` compiler flag in KSP tasks
[#&#8203;1614](https://togithub.com/google/ksp/issues/1614) Revert back
to the non-thread-local KSP compiler plugin implementation.

###
[`v1.9.20-1.0.14`](https://togithub.com/google/ksp/releases/tag/1.9.20-1.0.14)

[Compare
Source](https://togithub.com/google/ksp/compare/1.9.20-1.0.13...1.9.20-1.0.14)

##### issues fixed

- [#&#8203;1474](https://togithub.com/google/ksp/issues/1474)
Unresolvable types in annotation value are incorrectly displayed as null
values instead of error types.
- [#&#8203;1522](https://togithub.com/google/ksp/issues/1522)
KspTaskNative is not configuration cacheable
- [#&#8203;1554](https://togithub.com/google/ksp/issues/1554) KSP
Incremental Processing: Removing a file does not remove transitively
generated files
- [#&#8203;1555](https://togithub.com/google/ksp/issues/1555) KSP
Incremental Processing: Clean source files should have generated output
cache accessible via Resolver.
- [#&#8203;1568](https://togithub.com/google/ksp/issues/1568) KSP
resolves dependencies too early

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMDMuMSIsInVwZGF0ZWRJblZlciI6IjM3LjEwMy4xIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCJ9-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 19:30:12 +01:00
Konrad Pozniak 5192fb08a5
upgrade ktlint plugin to 12.0.3 (#4169)
There are some new rules, I think they mostly make sense, except for the
max line length which I had to disable because we are over it in a lot
of places.

---------

Co-authored-by: Goooler <wangzongler@gmail.com>
2024-01-04 17:00:55 +01:00
Konrad Pozniak 33cd6fdb98
Translations update from Weblate (#4199)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2024-01-04 09:08:11 +01:00
Konrad Pozniak d3c91904c8
Translations update from Weblate (#4198)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2024-01-04 09:07:57 +01:00
Sveinn í Felli 99e78eab78 Translated using Weblate (Icelandic)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
Rhoslyn Prys 305953358b Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Rhoslyn Prys <post@meddal.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
Eric 260f18ffce Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Eric <ekhfcxwuvxqfdb@hldrive.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
Ricardo 3193453775 Translated using Weblate (Portuguese (Portugal))
Currently translated at 96.3% (611 of 634 strings)

Co-authored-by: Ricardo <ricardojmv0@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
Danial Behzadi d822eb72c4 Translated using Weblate (Persian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
Quentí 6c80db6e03 Translated using Weblate (Occitan)
Currently translated at 98.7% (626 of 634 strings)

Co-authored-by: Quentí <quentinantonin@free.fr>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/oc/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
fin-w f9817633bd Translated using Weblate (Welsh)
Currently translated at 100.0% (633 of 633 strings)

Translated using Weblate (Welsh)

Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Welsh)

Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2024-01-04 07:37:01 +00:00
mcclure 70f8e8ba93
Implement new policy: The database version number is always even (#4128)
Posted this as issue #3999 before. The reasoning is personal experiments
and forks may add database fields and must bump the database number to
do so, but this causes massive merge difficulties when Tusky then
inevitably itself bumps the number. To alleviate this, Tusky official
should use only even database numbers, so odd versions are available for
third party scribbling.

There was little discussion positive or negative in #3999 (one proposal
we switch to a date-based number system, which would work but also could
be unnecessarily complicated). With PR #4115 we now have to make a
decision because that's the first post-proposal PR to bump the database
number odd. So, since I see no outright objections, I'd like to
implement this.

@connyduck suggested the best way to implement the proposal would be to
add a comment to the version number's home in AppDatabase.java.

Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
2024-01-03 22:21:18 +01:00
Danial Behzadi b6d0c19636 Translated using Weblate (Persian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fa/
2024-01-03 20:50:24 +00:00
fin-w 347552d200 Translated using Weblate (Welsh)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/cy/
2024-01-03 20:50:24 +00:00
renovate[bot] d87995ccd6
fix(deps): update dependency io.reactivex.rxjava3:rxjava to v3.1.8 (#4092)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [io.reactivex.rxjava3:rxjava](https://togithub.com/ReactiveX/RxJava) |
`3.1.6` -> `3.1.8` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/io.reactivex.rxjava3:rxjava/3.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/io.reactivex.rxjava3:rxjava/3.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/io.reactivex.rxjava3:rxjava/3.1.6/3.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/io.reactivex.rxjava3:rxjava/3.1.6/3.1.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>ReactiveX/RxJava (io.reactivex.rxjava3:rxjava)</summary>

###
[`v3.1.8`](https://togithub.com/ReactiveX/RxJava/releases/tag/v3.1.8)


[Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex.rxjava3%7Crxjava%7C3.1.8%7C)
[JavaDocs](http://reactivex.io/RxJava/3.x/javadoc/3.1.8)

##### Bugfixes

- Fix `ObservableSwitchMap` `NullPointerException` due to cancel race.
([https://github.com/ReactiveX/RxJava/pull/7597](https://togithub.com/ReactiveX/RxJava/pull/7597))

###
[`v3.1.7`](https://togithub.com/ReactiveX/RxJava/releases/tag/v3.1.7)


[Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex.rxjava3%7Crxjava%7C3.1.7%7C)
[JavaDocs](http://reactivex.io/RxJava/3.x/javadoc/3.1.7)

##### API changes

- Add `onDropped` callback for `onBackpressureLatest`. (<a
href='https://github.com/ReactiveX/RxJava/issues/7542'>[#&#8203;7542](https://togithub.com/ReactiveX/RxJava/issues/7542)</a>)
- Add `onDropped` callback to `onBackpressureBuffer`. (<a
href='https://github.com/ReactiveX/RxJava/issues/7567'>[#&#8203;7567](https://togithub.com/ReactiveX/RxJava/issues/7567)</a>)

##### Documentation

- Change summary to caption, because summary is obsolete in HTML5. (<a
href='https://github.com/ReactiveX/RxJava/issues/7534'>[#&#8203;7534](https://togithub.com/ReactiveX/RxJava/issues/7534)</a>)
- Try using https to access the reactive streams javadoc. (<a
href='https://github.com/ReactiveX/RxJava/issues/7535'>[#&#8203;7535](https://togithub.com/ReactiveX/RxJava/issues/7535)</a>)
-   Improve Javadoc of `Disposable`.

##### Other

- Add minimum GitHub token permissions for workflows. (<a
href='https://github.com/ReactiveX/RxJava/issues/7541'>[#&#8203;7541](https://togithub.com/ReactiveX/RxJava/issues/7541)</a>)
- Create SECURITY.md (<a
href='https://github.com/ReactiveX/RxJava/issues/7546'>[#&#8203;7546](https://togithub.com/ReactiveX/RxJava/issues/7546)</a>)
- Add copyright and license to jar. (<a
href='https://github.com/ReactiveX/RxJava/issues/7520'>[#&#8203;7520](https://togithub.com/ReactiveX/RxJava/issues/7520)</a>)
- Convert `CompletableOnErrorComplete$onError` inner class to static.
(<a
href='https://github.com/ReactiveX/RxJava/issues/7575'>[#&#8203;7575](https://togithub.com/ReactiveX/RxJava/issues/7575)</a>)
- Allow null accumulator type in the `collect(Collector)` operators. (<a
href='https://github.com/ReactiveX/RxJava/issues/7590'>[#&#8203;7590](https://togithub.com/ReactiveX/RxJava/issues/7590)</a>)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMTAzLjEiLCJ0YXJnZXRCcmFuY2giOiJkZXZlbG9wIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-03 21:33:58 +01:00
Konrad Pozniak 1f698e0732
show post language in metadata (#4127)
closes https://github.com/tuskyapp/Tusky/issues/3096

<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/417c55a9-168b-4ada-9636-de6314698def"
width="320">
2024-01-03 21:17:28 +01:00
UlrichKu 0698333665
3488 improve profile list (#3507)
Fixes #3488 

Working with lists from a profile page and in the normal "lists view"
from the drawer now use the same fragment view code.

(also) RFC regarding joining different list lists


![grafik](https://user-images.githubusercontent.com/1618905/229463168-397bd943-82d8-4e05-a8bf-9fcf22f6c1f9.png)
2024-01-03 21:17:03 +01:00
Zongle Wang 6494247301
Migrate to Hilt KSP compiler (#4136)
https://github.com/google/dagger/releases/tag/dagger-2.49

Closes #4012.
2024-01-03 21:16:05 +01:00
sanao e8e7bad110
feat: Change name of Preferences > Filters > Tabs and move them to Account Preferences(#3536) (#4115)
# Overview
In the previous code, when you open preferences, there is a section
headed "Filters" with a section called "Tabs"

This is confusing.

# Changes
- Change the section title from "Filters" to "Per-timeline preferences."
- Change the current "Tabs" section to "Home timeline" since it is only
for home timelines

# Screenshots
account preference screen | detail screen
:--: | :--:
|<image
src="https://github.com/tuskyapp/Tusky/assets/62137820/12694f24-b7e3-4ba3-90f5-53740e9c4269"
width="250" />|<image
src="https://github.com/tuskyapp/Tusky/assets/62137820/796e9ac1-76d6-43ef-a087-a1cd2d899ef8"
width="250" />

# Note
- Maybe string resources should have a new property? (for translation)

# Related link
 Fixes #3536

---------

Co-authored-by: mcc <andi.m.mcclure@gmail.com>
2024-01-03 21:14:13 +01:00
Konrad Pozniak 966ba38dbe
clarify updating the download link in Release.md (#4197) 2023-12-28 13:28:18 +01:00
Konrad Pozniak 7c5238a6bc
remove string check step from Release.md (#4196)
It is no longer necessary since we have lint checks.
Also fixed some typos.
2023-12-27 20:34:53 +01:00
charlag af6b3a3a33
Update CHANGELOG.md 2023-12-27 19:30:49 +01:00
charlag 3bea267407
Release 117 (24.1) 2023-12-27 19:25:49 +01:00
fin-w 78c7a105a6 Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-12-27 19:24:31 +01:00
Ihor Hordiichuk ad0e1b5c76 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/
2023-12-27 19:24:24 +01:00
Konrad Pozniak d2cb74b636
Translations update from Weblate (#4181)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-12-24 10:26:14 +01:00
Konrad Pozniak 9a0cdc5e52
Translations update from Weblate (#4183)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2023-12-24 10:25:28 +01:00
Rogljič b87a8d893b Translated using Weblate (Slovenian)
Currently translated at 58.6% (372 of 634 strings)

Co-authored-by: Rogljič <zala.roguljic@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sl/
Translation: Tusky/Tusky
2023-12-24 08:55:22 +00:00
Danial Behzadi bc8282496d Translated using Weblate (Persian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
2023-12-24 08:55:22 +00:00
ButterflyOfFire 0c4cc31c2d Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (French)

Currently translated at 95.1% (603 of 634 strings)

Co-authored-by: ButterflyOfFire <butterflyoffire@protonmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-12-24 08:55:22 +00:00
XoseM db3d1ec13d Translated using Weblate (Galician)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-12-24 08:55:22 +00:00
Konrad Pozniak 004659a54d
fix Glide IllegalArgumentException in DrawerImageLoader (#4189)
Turns out that the crash I thought will not occur with #4153 did occur
again. 😒
But this time I managed to reproduce it (faking a slow network for that
one request only and then quickly switching activities to get it
destroyed) so I'm sure it is fixed, and I did a check for possible side
effects but it seems Glide is clever enough to cancel all requests by
itself.

<details>
<summary>stacktrace</summary>

```
Exception java.lang.IllegalArgumentException: You cannot start a load for a destroyed activity
  at com.bumptech.glide.manager.RequestManagerRetriever.assertNotDestroyed (RequestManagerRetriever.java:236)
  at com.bumptech.glide.manager.RequestManagerRetriever.get (RequestManagerRetriever.java:110)
  at com.bumptech.glide.manager.RequestManagerRetriever.get (RequestManagerRetriever.java:176)
  at com.bumptech.glide.Glide.with (Glide.java:634)
  at com.keylesspalace.tusky.MainActivity$setupDrawer$2.cancel (MainActivity.kt:573)
  at com.mikepenz.materialdrawer.util.DrawerImageLoader.cancelImage (DrawerImageLoader.java:56)
  at com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem.unbindView (BaseDescribeableDrawerItem.kt:95)
  at com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem.unbindView (BaseDescribeableDrawerItem.kt:20)
  at com.mikepenz.fastadapter.listeners.OnBindViewHolderListenerImpl.unBindViewHolder (OnBindViewHolderListenerImpl.java:36)
  at com.mikepenz.fastadapter.FastAdapter.onViewRecycled (FastAdapter.kt:410)
  at androidx.recyclerview.widget.RecyclerView$Recycler.dispatchViewRecycled (RecyclerView.java:7346)
  at androidx.recyclerview.widget.RecyclerView$Recycler.addViewHolderToRecycledViewPool (RecyclerView.java:7108)
  at androidx.recyclerview.widget.RecyclerView$Recycler.recycleViewHolderInternal (RecyclerView.java:7059)
  at androidx.recyclerview.widget.RecyclerView.removeAnimatingView (RecyclerView.java:1556)
  at androidx.recyclerview.widget.RecyclerView$ItemAnimatorRestoreListener.onAnimationFinished (RecyclerView.java:13564)
  at androidx.recyclerview.widget.RecyclerView$ItemAnimator.dispatchAnimationFinished (RecyclerView.java:14066)
  at androidx.recyclerview.widget.SimpleItemAnimator.dispatchChangeFinished (SimpleItemAnimator.java:328)
  at androidx.recyclerview.widget.DefaultItemAnimator.endChangeAnimationIfNecessary (DefaultItemAnimator.java:437)
  at androidx.recyclerview.widget.DefaultItemAnimator.endChangeAnimationIfNecessary (DefaultItemAnimator.java:418)
  at androidx.recyclerview.widget.DefaultItemAnimator.endAnimations (DefaultItemAnimator.java:588)
  at androidx.recyclerview.widget.RecyclerView.onDetachedFromWindow (RecyclerView.java:3383)
  at android.view.View.dispatchDetachedFromWindow (View.java:20898)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3956)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewGroup.dispatchDetachedFromWindow (ViewGroup.java:3948)
  at android.view.ViewRootImpl.dispatchDetachedFromWindow (ViewRootImpl.java:5114)
  at android.view.ViewRootImpl.doDie (ViewRootImpl.java:8309)
  at android.view.ViewRootImpl.die (ViewRootImpl.java:8286)
  at android.view.WindowManagerGlobal.removeViewLocked (WindowManagerGlobal.java:538)
  at android.view.WindowManagerGlobal.removeView (WindowManagerGlobal.java:479)
  at android.view.WindowManagerImpl.removeViewImmediate (WindowManagerImpl.java:162)
  at android.app.ActivityThread.handleDestroyActivity (ActivityThread.java:5564)
  at android.app.ActivityThread.handleRelaunchActivityInner (ActivityThread.java:5842)
  at android.app.ActivityThread.handleRelaunchActivity (ActivityThread.java:5758)
  at android.app.servertransaction.ActivityRelaunchItem.execute (ActivityRelaunchItem.java:71)
  at android.app.servertransaction.ActivityTransactionItem.execute (ActivityTransactionItem.java:45)
  at android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.java:135)
  at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:95)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2293)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loopOnce (Looper.java:226)
  at android.os.Looper.loop (Looper.java:329)
  at android.app.ActivityThread.main (ActivityThread.java:8058)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:548)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1026)
```

</details>
2023-12-24 09:19:08 +01:00
Max Malekzadeh 5cce62bc6a
Add copyright notices in source file headers (Fixes issue #4188) (#4190) 2023-12-24 09:18:09 +01:00
fin-w 45b41cd592 Translated using Weblate (Welsh)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/cy/
2023-12-24 07:49:03 +00:00
XoseM 03550bb580 Translated using Weblate (Galician)
Currently translated at 78.7% (26 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/
2023-12-24 07:49:03 +00:00
Rogljič 44c9a108de Translated using Weblate (Slovenian)
Currently translated at 18.1% (6 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/sl/
2023-12-24 07:49:03 +00:00
Luna Jernberg 9c981c4d5e Translated using Weblate (Swedish)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/sv/
2023-12-24 07:49:03 +00:00
Konrad Pozniak 0f3f2238e2
fix ClassCastException in ClickableSpanTextView (#4185)
<details>
  <summary>Stacktrace</summary>
  
  ```
java.lang.ClassCastException: java.lang.String cannot be cast to
android.text.Spannable
at
com.keylesspalace.tusky.view.ClickableSpanTextView.dispatchTouchEvent(ClickableSpanTextView.kt:208)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2968)
				at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2600)
at
com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:448)
at
com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1829)
				at android.app.Activity.dispatchTouchEvent(Activity.java:3307)
at
androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:70)
at
com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:410)
				at android.view.View.dispatchPointerEvent(View.java:12015)
at
android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4795)
at
android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4609)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at
android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4200)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4166)
at
android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4293)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4174)
at
android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4350)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at
android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4200)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4166)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4174)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4147)
at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6661)
  ```
  
</details>

Could not reproduce with a regular build because I couldn't find the
place where we set a string into a ClickableSpanTextView, so I created
that scenario manually. It crashed instantly when trying to select text,
and with this fix it behaved as expected.
2023-12-23 15:23:16 +01:00
Konrad Pozniak 9781e43441
fix ANR caused by direct message badge (#4182)
Saw an ANR (app not responding) error being reported in the Play console
and then found this. Sorry but `runBlocking` in production code is an
absolute no go.
2023-12-21 19:02:28 +01:00
Conny Duck d8c436c268 fix broken lint-baseline.xml 2023-12-16 19:56:40 +01:00
Conny Duck dece567eb5 Release 116 (24.1) 2023-12-16 19:15:32 +01:00
Konrad Pozniak cce811e0a5
fix icon alignment in help text of empty timelines (#4179)
@Lakoja do you remember why you added that version check? Removing it
fixes the bug.

Before / after
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/b2c2f79e-7f30-47fb-89ef-b5e4c31b0d0e"
width="200"/> <img
src="https://github.com/tuskyapp/Tusky/assets/10157047/3bb746f8-97e7-4d60-a67e-175e02a6d929"
width="200"/>

closes #4175
2023-12-16 15:26:22 +01:00
Konrad Pozniak 6a6df01194
Translations update from Weblate (#4177)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-12-16 08:44:08 +01:00
Konrad Pozniak 13d42035c5
Translations update from Weblate (#4174)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2023-12-16 08:43:47 +01:00
Vladyslav Stepanov 7a4e09b2b7 Translated using Weblate (Russian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Vladyslav Stepanov <mittwerk@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/
Translation: Tusky/Tusky
2023-12-16 07:14:18 +00:00
Vladyslav Stepanov d1d2a26601 Translated using Weblate (Russian)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/ru/
2023-12-15 16:02:10 +00:00
Konrad Pozniak 8248030615
Translations update from Weblate (#4170)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-12-15 13:37:53 +01:00
Konrad Pozniak 6a98427fa5
fix crash when opening profile image (#4172)
```
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.keylesspalace.tusky.test/com.keylesspalace.tusky.ViewMediaActivity}: java.lang.NullPointerException
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2778)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
	at android.app.ActivityThread.-wrap11(Unknown Source:0)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
	at android.os.Handler.dispatchMessage(Handler.java:106)
	at android.os.Looper.loop(Looper.java:164)
	at android.app.ActivityThread.main(ActivityThread.java:6494)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
Caused by: java.lang.NullPointerException
	at com.keylesspalace.tusky.ViewMediaActivity.adjustScreenWakefulness(ViewMediaActivity.kt:351)
	at com.keylesspalace.tusky.ViewMediaActivity.onCreate(ViewMediaActivity.kt:161)
	at android.app.Activity.performCreate(Activity.java:7009)
	at android.app.Activity.performCreate(Activity.java:7000)
	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)
	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731)
	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856) 
	at android.app.ActivityThread.-wrap11(Unknown Source:0) 
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589) 
	at android.os.Handler.dispatchMessage(Handler.java:106) 
	at android.os.Looper.loop(Looper.java:164) 
	at android.app.ActivityThread.main(ActivityThread.java:6494) 
	at java.lang.reflect.Method.invoke(Native Method) 
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) 
```
2023-12-15 07:41:38 +01:00
Chaman Vétéran f3a6b7c8f6 Translated using Weblate (French)
Currently translated at 93.2% (591 of 634 strings)

Co-authored-by: Chaman Vétéran <nathm.va@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/
Translation: Tusky/Tusky
2023-12-15 06:12:14 +00:00
Ümit Solmaz 2293809986 Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-12-15 06:12:14 +00:00
Vladyslav Stepanov 86753857ea Translated using Weblate (Russian)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Vladyslav Stepanov <mittwerk@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/
Translation: Tusky/Tusky
2023-12-15 06:12:14 +00:00
Konrad Pozniak 331b13621e
Prevent device from dimming or sleeping screen while video/audio playing (but really) (#4168)
For reasons not totally clear to me, Github marked #4160 as "merged" and
will not let me reopen it.

I believe this should be included for 24.1 because it fixes a 24.0
regression in the media player.

I have tested this newest commit in a number of ways, and in my testing
I find (1) when viewing an image, it sleeps after about a minute (2)
when viewing video, it stays awake indefinitely (3) this is true whether
the image/video was opened directly, or reached by swiping from another
attachment. I have not tested swiping to/from audio but I am confident
it will work the same.
2023-12-13 19:23:11 +01:00
Konrad Pozniak 0c900842ec
fix text selection crash on older Androids (#4166)
I hate these workarounds for Android bugs, I'm always afraid the will
introduce other problems. I tested this on multiple Android versions, it
definitely fixes the problem and otherwise seems fine though.

closes #4164
2023-12-13 19:22:54 +01:00
Konrad Pozniak e30fd895db
Translations update from Weblate (#4157)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-12-13 19:22:42 +01:00
mcc 877e7c6ec1 Adjust ViewMediaActivity FLAG_KEEP_SCREEN_ON logic to support swipe gestures 2023-12-12 18:01:18 -05:00
Sveinn í Felli c1e704073e Translated using Weblate (Icelandic)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
Ricardo 3a602eb971 Translated using Weblate (Portuguese (Portugal))
Currently translated at 96.3% (611 of 634 strings)

Co-authored-by: Ricardo <ricardojmv0@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
Salif Mehmed de85c3bbf3 Translated using Weblate (Bulgarian)
Currently translated at 86.5% (549 of 634 strings)

Co-authored-by: Salif Mehmed <mail@salif.eu>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/bg/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
fin-w e7f50b6adb Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
João Alves be64ae288b Translated using Weblate (Portuguese (Portugal))
Currently translated at 87.6% (556 of 634 strings)

Co-authored-by: João Alves <joao.2003.couto+weblate@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
André Ferreira 244267225d Translated using Weblate (Portuguese (Portugal))
Currently translated at 87.6% (556 of 634 strings)

Co-authored-by: André Ferreira <andre@bravoferreira.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
Ricardo 8741baefe4 Translated using Weblate (Portuguese (Portugal))
Currently translated at 87.6% (556 of 634 strings)

Co-authored-by: Ricardo <ricardojmv0@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:01 +00:00
André Ferreira 17db97302c Translated using Weblate (Portuguese (Portugal))
Currently translated at 84.7% (537 of 634 strings)

Co-authored-by: André Ferreira <andre@bravoferreira.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:00 +00:00
Ricardo db8be6ca08 Translated using Weblate (Portuguese (Portugal))
Currently translated at 84.7% (537 of 634 strings)

Co-authored-by: Ricardo <ricardojmv0@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:00 +00:00
André Ferreira 3769c9d43a Translated using Weblate (Portuguese (Portugal))
Currently translated at 82.0% (520 of 634 strings)

Co-authored-by: André Ferreira <andre@bravoferreira.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:00 +00:00
Ricardo 3eeea0f442 Translated using Weblate (Portuguese (Portugal))
Currently translated at 82.0% (520 of 634 strings)

Co-authored-by: Ricardo <ricardojmv0@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-12 04:45:00 +00:00
mcc b9a593d6b0 ktLint fix 2023-12-10 18:13:46 -05:00
mcc 90f35ed009 Media3/exoplayer introduced a regression where the device can sleep while video/audio are playing. Patch restores v23 behavior 2023-12-10 13:56:02 -05:00
Konrad Pozniak 75c42cb5c1
prevent MainActivity from leaking through the DrawerImageLoader singleton (#4153)
Another fix for a memory leak. This one is not as big as #4150, but
still worth fixing for memory constrained devices imo.
The `DrawerImageLoader` implementation (a global singleton) references a
member of the `MainActivity`, causing the whole activity to leak.

This weird construct was introduced in #1989 to fix a crash, but I think
since we migrated to coroutines it is no longer necessary because all
calls get correctly cancelled. I tried reproducing the crash but could
not, so I'm pretty sure it is fine. I would appreciate it if someone
else could try it as well though.

(The crash could be reproduced on slow internet, when
`onFetchUserInfoSuccess` was called while the activity was being
destroyed, causing Glide to crash the app because it can't use destroyed
activities. `onFetchUserInfoSuccess` is now no longer called in this
case because it is inside a `lifecycleScope.launch` block.)
2023-12-10 09:44:53 +01:00
Konrad Pozniak ee3760fcc9
correctly count emojis when composing a post (#4152)
Thx to @evant for the help 

closes #4140
2023-12-10 07:38:25 +01:00
Konrad Pozniak 4f38678be7
Translations update from Weblate (#4151)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2023-12-10 07:38:12 +01:00
Konrad Pozniak db27186b5c
fix memory leak in CompositeWithOpaqueBackground (#4150)
Found with Leak canary: The transformation ends up in Glide's memory
cache and leaks whole Activities through the view -> context reference.

This fixes the problem by removing the background detection logic (so
the view reference is no longer needed) and setting the background
directly instead. Looks exactly as before.
2023-12-10 07:37:54 +01:00
Konrad Pozniak 05742ce02e
Translations update from Weblate (#4147)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-12-10 07:37:44 +01:00
Ihor Hordiichuk 0789f8e48b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/
2023-12-10 06:08:52 +00:00
XoseM 898775d577 Translated using Weblate (Galician)
Currently translated at 78.1% (25 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/
2023-12-10 06:08:52 +00:00
fin-w dd53910ebe Translated using Weblate (Welsh)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/cy/
2023-12-10 06:08:52 +00:00
Luna Jernberg ef4e22f40d Translated using Weblate (Swedish)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/sv/
2023-12-10 06:08:52 +00:00
André Ferreira 0081b903a4 Translated using Weblate (Portuguese (Portugal))
Currently translated at 79.0% (501 of 634 strings)

Co-authored-by: André Ferreira <andre@bravoferreira.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-10 04:45:01 +00:00
Hồ Nhất Duy 5ab20dd998 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-12-10 04:45:01 +00:00
Chaman Vétéran e9c6b98556 Translated using Weblate (French)
Currently translated at 92.9% (589 of 634 strings)

Co-authored-by: Chaman Vétéran <nathm.va@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/
Translation: Tusky/Tusky
2023-12-10 04:45:01 +00:00
Bruno Miguel 3538b3d318 Translated using Weblate (Portuguese (Portugal))
Currently translated at 78.2% (496 of 634 strings)

Co-authored-by: Bruno Miguel <brunoalexandremiguel@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/
Translation: Tusky/Tusky
2023-12-10 04:45:01 +00:00
Ihor Hordiichuk e8ed4deb29 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2023-12-10 04:45:01 +00:00
XoseM 15a52f10e5 Translated using Weblate (Galician)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-12-10 04:45:00 +00:00
Luna Jernberg 6d1ccaba9f Translated using Weblate (Swedish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sv/
Translation: Tusky/Tusky
2023-12-10 04:45:00 +00:00
TAKAHASHI Shuuji ec5083ebba Translated using Weblate (Japanese)
Currently translated at 98.1% (622 of 634 strings)

Co-authored-by: TAKAHASHI Shuuji <shuuji3@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/
Translation: Tusky/Tusky
2023-12-10 04:45:00 +00:00
fin-w 98654c3035 Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: fin-w <puf@users.noreply.weblate.tusky.app>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-12-10 04:45:00 +00:00
Conny Duck a7e0fa26b7 Release 115 (24.0) 2023-12-05 20:19:35 +01:00
Konrad Pozniak c9d96a15c3
Translations update from Weblate (#4122)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2023-12-05 19:47:00 +01:00
Konrad Pozniak 7a0cdc6d01
Translations update from Weblate (#4121)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-12-05 19:46:45 +01:00
Konrad Pozniak 6463e99c53
don't rehide filtered posts on interaction (#4130)
closes #4125
2023-12-05 19:29:01 +01:00
Konrad Pozniak e1f2d639aa
fix image scaling after swipe down gesture ended in ViewImageFragment (#4135)
closes #4132
2023-12-05 19:28:52 +01:00
fin-w 390ae00cb5
Reword the string "pref_failed_to_sync" (#4134)
This changes one word in the string `pref_failed_to_sync` in the file
`values/strings.xml`.

See my reasoning here #4133 

"Failed to sync settings" changes to "Failed to sync preferences".
2023-12-05 19:28:22 +01:00
XoseM 98fa798556 Translated using Weblate (Galician)
Currently translated at 78.1% (25 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/
2023-12-05 18:18:04 +00:00
Ümit Solmaz 9e663f2a84 Translated using Weblate (Turkish)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/tr/
2023-12-05 18:18:04 +00:00
Hồ Nhất Duy c5a0897ff1 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/
2023-12-05 18:18:04 +00:00
Luna Jernberg 2d6a4ac151 Translated using Weblate (Swedish)
Currently translated at 100.0% (32 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/sv/
2023-12-05 18:18:04 +00:00
Deleted User 919f15b45b Translated using Weblate (German)
Currently translated at 90.6% (29 of 32 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/
2023-12-05 18:18:04 +00:00
Anatoly Bogomolov d63f0bb8f3 Translated using Weblate (Russian)
Currently translated at 68.6% (435 of 634 strings)

Co-authored-by: Anatoly Bogomolov <tolya.bogomolov2019@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Ümit Solmaz cef04f9f96 Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
puf f6abd72e0d Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Sveinn í Felli 605df0f8db Translated using Weblate (Icelandic)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Icelandic)

Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Ihor Hordiichuk c5b546ab7e Translated using Weblate (Ukrainian)
Currently translated at 99.8% (633 of 634 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Kalle Kniivilä da6e114938 Translated using Weblate (Finnish)
Currently translated at 44.1% (280 of 634 strings)

Co-authored-by: Kalle Kniivilä <kalle.kniivila@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fi/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
XoseM e360e3d498 Translated using Weblate (Galician)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Hồ Nhất Duy a94db89303 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Deleted User baea6e0678 Translated using Weblate (German)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Deleted User <noreply+305@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/
Translation: Tusky/Tusky
2023-12-05 18:00:32 +00:00
Konrad Pozniak 5dbdce2e2f
fix images in ViewImageFragment disappearing when switching apps (#4126)
I tried to ind out why the imageview was cleared here but to no avail.
Removing it also doesn't seems to have unwanted sideeffects 🤷
2023-11-29 18:39:10 +01:00
Konrad Pozniak 6616df4a82
fix crash when rotating caption dialog (#4123)
Steps to reproduce: Open the dialog to set a catption on an image.
Rotate the screen.

<details>
  <summary>Stacktrace</summary>
  
```
Exception java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
  at com.keylesspalace.tusky.components.compose.dialog.CaptionDialog.onCreateView (CaptionDialog.kt:61)
  at androidx.fragment.app.Fragment.performCreateView (Fragment.java:3114)
  at androidx.fragment.app.DialogFragment.performCreateView (DialogFragment.java:775)
  at androidx.fragment.app.FragmentStateManager.createView (FragmentStateManager.java:557)
  at androidx.fragment.app.FragmentStateManager.moveToExpectedState (FragmentStateManager.java:272)
  at androidx.fragment.app.FragmentStore.moveToExpectedState (FragmentStore.java:114)
  at androidx.fragment.app.FragmentManager.moveToState (FragmentManager.java:1455)
  at androidx.fragment.app.FragmentManager.dispatchStateChange (FragmentManager.java:3034)
  at androidx.fragment.app.FragmentManager.dispatchActivityCreated (FragmentManager.java:2952)
  at androidx.fragment.app.FragmentController.dispatchActivityCreated (FragmentController.java:263)
  at androidx.fragment.app.FragmentActivity.onStart (FragmentActivity.java:350)
  at androidx.appcompat.app.AppCompatActivity.onStart (AppCompatActivity.java:251)
  at android.app.Instrumentation.callActivityOnStart (Instrumentation.java:1543)
  at android.app.Activity.performStart (Activity.java:8682)
  at android.app.ActivityThread.handleStartActivity (ActivityThread.java:4219)
  at android.app.servertransaction.TransactionExecutor.performLifecycleSequence (TransactionExecutor.java:221)
  at android.app.servertransaction.TransactionExecutor.cycleToPath (TransactionExecutor.java:201)
  at android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:173)
  at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:97)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2584)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loopOnce (Looper.java:226)
  at android.os.Looper.loop (Looper.java:313)
  at android.app.ActivityThread.main (ActivityThread.java:8810)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:604)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)
```
  
</details>

Restoring the saved caption after the view was created fixes the
problem.
2023-11-29 18:38:55 +01:00
Goooler 1313371051
Optimise repositories declarations (#4113) 2023-11-23 09:41:46 +01:00
Levi Bard 2d663d8966
Translations update from Weblate (#4119)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-11-23 09:41:08 +01:00
mcclure 70725fd75b
Regularize show/hide logic for video player scrub/play controls (fixes #4073) (#4117)
When viewing a video in Tusky, there is a top toolbar where the
description is shown and the bottom toolbar where play, forward,
backward, and scrub controls are found. In both Tusky 23 and the new
media3 video player code, the logic for showing these toolbars is
*unrelated*; Tusky catches tap events and shows and hides the
description, and the Android media library separately catches tap events
and shows and hides the bottom toolbar. Meanwhile, Tusky and the Android
media library each separately manage a set of logic for auto-hiding
their respective toolbars after a certain number of seconds has passed.
This all results in several problems:

- The top and bottom toolbars can desync, so that one is visible and the
other is not, and tapping to show/hide after this will only swap which
one is visible. This happens *every* time you switch to another
application then back to Tusky while the video player is up.
- You can also desync the top and bottom toolbars in this way by simply
tapping very rapidly.
- The autohide logic was difficult for us to control or customize,
because it was partially hidden inside the Android libraries (relevant
because under media3, the autohide delay increased from 3 to something
like 5 or 6 seconds).

In this patch, I disabled all auto- and tap-based show/hide logic in
media3 and set the Tusky-side show/hide to directly control the media3
toolbar. I then audited the code with printfs until I understood the
state machine of show/hide, and removed anything irrational (some code
was either unreachable, or redundant; either these lines were broken in
the media3 transition, or they never worked).¹

While doing this, I made two policy changes:

- As discussed on Matrix, the autohide delay is now 4 seconds. (In
discussions with users on Mastodon, some complained the previous 3
seconds was too short; but in my opinion and [I think?] charlag's, the
new 5 seconds is too long).
- In the pre-existing code, if the user has hidden the controls, and
they switch to another app and back, the controls display for 4 seconds
then re-hide themselves, just like if the video had been presented for
the first time. I think this is good and kept it— *however* I made a
decision if the user intentionally taps to display the controls, *then*
switches to another app and back, the controls should *not* auto-hide,
because the user most recently requested those controls be shown.

Tests I performed on the final PR (successfully):

- Start video. Expect: toolbar+description hides after 4 seconds.
- Start video. Pause. Resume. Expect: t+d hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap to show t+d. Switch to
other app. Switch back. Expect: t+d appear, do NOT autohide.
- Start video. Before 4 seconds up, switch to other app. Switch back.
Expect: t+d reappears, then hides after 4 seconds.
- Start video. Pause. Resume. Before 4 seconds up, switch to other app.
Switch back. Expect: t+d reappears, then hides after 4 seconds.
- Start video. Wait 4 seconds until t+d hide. Tap rapidly over and over
for many seconds. Expect: Nothing weird
- Start *audio*. Expect: At no point does controller autohide, not even
if I switch to another app and back, but I can hide it by manually
tapping

These tests were performed on Android 13. There is an entirely separate
`Build.VERSION.SDK_INT <= 23` path I did not test, but Android Studio
says this is dead code (I think it thinks our minimum SDK is higher than
that?)

---

<small>¹ Incidentally, the underlying cause of #4073 (the show/resume
part of it anyway) turned out to be that the STATE_READY event was being
received not just on video load but also a second time on app resume,
causing certain parts of the initialization code to run a second time
although the fragment had already been fully initialized.</small>
2023-11-23 08:32:01 +01:00
UlrichKu 8efe3a96b7
3820: Group android notifications and properly use the "group summary alert" (#3821)
Fixes #3820 

This mainly corrects the "first of batch" logic.
(Each group/batch has a list so the question "only one per this group?"
can be answered.)
2023-11-23 08:25:00 +01:00
Levi Bard f9ef0d36c2
Translations update from Weblate (#4111)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-11-23 08:24:26 +01:00
Ümit Solmaz 4e37d4f10f Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-11-23 07:04:18 +00:00
Conny Duck 11e7845ee3 Release 114 (24.0 beta 1) 2023-11-22 20:15:11 +01:00
Quentí ca1b04db60 Translated using Weblate (Occitan)
Currently translated at 97.9% (621 of 634 strings)

Co-authored-by: Quentí <quentinantonin@free.fr>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/oc/
Translation: Tusky/Tusky
2023-11-20 04:45:01 +00:00
Manuel 63c708e594 Translated using Weblate (Italian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Manuel <mannivuwiki@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/
Translation: Tusky/Tusky
2023-11-20 04:45:00 +00:00
Danial Behzadi 47cf36c9fe Translated using Weblate (Persian)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
2023-11-20 04:45:00 +00:00
puf e26a383495 Translated using Weblate (Welsh)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Welsh)

Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-11-20 04:45:00 +00:00
Hồ Nhất Duy cb9bf588ab Translated using Weblate (Vietnamese)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-11-20 04:45:00 +00:00
Luna Jernberg 6722a32504 Translated using Weblate (Swedish)
Currently translated at 100.0% (634 of 634 strings)

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sv/
Translation: Tusky/Tusky
2023-11-20 04:45:00 +00:00
Levi Bard 21a4308fef
Fix deserialization of the response from friendica on api/v2/instance (#4103)
Fixes #4100

---------

Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
2023-11-13 10:05:28 +01:00
sanao ff39f9b3c2
feat: Hide self-boosts (#3534) (#4101)
# Overview
Some Mastodon posters have a annoying habit of boosting their own posts
some time after they've posted them.
 No need to see the same toot over and over again.

# Changes made
- Add an additional option to the "Filters > Tabs" preference to show
these self-boosts (default: on)
- If "Show boosts" is turned off, self-boosts are automatically hidden.
    
# Screenshot
***screen of "Filters > Tabs" preference***
before | after
:--: | :--:
| <image
src="https://github.com/tuskyapp/Tusky/assets/62137820/16ed2f4a-0776-4f60-afe6-29827acf5bbd"
width="300"> |<image
src="https://github.com/tuskyapp/Tusky/assets/62137820/9d4e1457-f71d-440c-959f-b91f7433b29a"
width="300" />

***screen of Home Timeline***
***switch-on(self-boosts are displayed)*** | ***swith-off(self-boosts
are not displayed)***
:--: | :--:
| <image
src="https://github.com/tuskyapp/Tusky/assets/62137820/3bb80791-a81f-4cbc-98ad-8a14602e53a4"
width="300" />|<image
src="https://github.com/tuskyapp/Tusky/assets/62137820/a7964da8-d106-4209-b911-460ef8988831"
width="300" />

# Related issue
 Fixes #3534
2023-11-13 10:04:39 +01:00
Levi Bard c6948878a8
Translations update from Weblate (#4105)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2023-11-13 10:02:56 +01:00
Levi Bard a326134ed9
Translations update from Weblate (#4099)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-11-13 09:30:45 +01:00
Luna Jernberg 03be46014a Translated using Weblate (Swedish)
Currently translated at 100.0% (31 of 31 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/sv/
2023-11-13 08:03:46 +00:00
Levi Bard dc26e6bb7b
Update ksp to track kotlin version update (#4104) 2023-11-09 18:54:09 +01:00
Luna Jernberg 9ebd484e24 Translated using Weblate (Swedish)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: Luna Jernberg <droidbittin@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sv/
Translation: Tusky/Tusky
2023-11-09 17:27:27 +00:00
Rhoslyn Prys 71e8f1e213 Translated using Weblate (Welsh)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: Rhoslyn Prys <post@meddal.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-11-09 17:27:27 +00:00
XoseM 6942c7b9b6 Translated using Weblate (Galician)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-11-09 17:27:27 +00:00
Hồ Nhất Duy 3952b99a88 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-11-09 17:27:27 +00:00
renovate[bot] e919584682
Update dagger to v2.48.1 (#4094)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.dagger:dagger](https://togithub.com/google/dagger) |
`2.47` -> `2.48.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-compiler](https://togithub.com/google/dagger)
| `2.47` -> `2.48.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-compiler/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-compiler/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-compiler/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-compiler/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-support](https://togithub.com/google/dagger)
| `2.47` -> `2.48.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-support/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-support/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-support/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-support/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.google.dagger:dagger-android-processor](https://togithub.com/google/dagger)
| `2.47` -> `2.48.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android-processor/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android-processor/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android-processor/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android-processor/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [com.google.dagger:dagger-android](https://togithub.com/google/dagger)
| `2.47` -> `2.48.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:dagger-android/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:dagger-android/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:dagger-android/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:dagger-android/2.47/2.48.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 12:37:02 +01:00
Nick Schonning bf50d9a712
Use Renovate recommended baseline (#3977)
Notice this was done upstream in
https://github.com/mastodon/mastodon/pull/26306 by one of the Renovate
people, so thought it would make sense here too
2023-11-01 11:39:20 +01:00
renovate[bot] 955ad2df37
Update dependency androidx.preference:preference-ktx to v1.2.1 (#3944)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.preference:preference-ktx](https://developer.android.com/jetpack/androidx/releases/preference#1.2.1)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `1.2.0` -> `1.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.preference:preference-ktx/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.preference:preference-ktx/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.preference:preference-ktx/1.2.0/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.preference:preference-ktx/1.2.0/1.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi40MC4zIiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 11:39:08 +01:00
renovate[bot] b7841f2ff2
Update autodispose to v2.2.1 (#3873)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[com.uber.autodispose2:autodispose](https://togithub.com/uber/AutoDispose)
| `2.1.1` -> `2.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.uber.autodispose2:autodispose/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.uber.autodispose2:autodispose/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.uber.autodispose2:autodispose/2.1.1/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.uber.autodispose2:autodispose/2.1.1/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[com.uber.autodispose2:autodispose-androidx-lifecycle](https://togithub.com/uber/AutoDispose)
| `2.1.1` -> `2.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.uber.autodispose2:autodispose-androidx-lifecycle/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.uber.autodispose2:autodispose-androidx-lifecycle/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.uber.autodispose2:autodispose-androidx-lifecycle/2.1.1/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.uber.autodispose2:autodispose-androidx-lifecycle/2.1.1/2.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>uber/AutoDispose (com.uber.autodispose2:autodispose)</summary>

###
[`v2.2.1`](https://togithub.com/uber/AutoDispose/blob/HEAD/CHANGELOG.md#Version-221)

[Compare
Source](https://togithub.com/uber/AutoDispose/compare/2.2.0...2.2.1)

*2023-07-25*

- **Fix**: Don't hold a reference to the view if detach happens before
`DetachEventCompletable` is disposed.

###
[`v2.2.0`](https://togithub.com/uber/AutoDispose/blob/HEAD/CHANGELOG.md#Version-220)

[Compare
Source](https://togithub.com/uber/AutoDispose/compare/2.1.1...2.2.0)

*2023-07-23*

- **Fix**: Dispose the handle returned by `Job.invokeOnCompletion` when
Rx subscription is disposed.
- **Fix**: The `withScope()` lint now correctly handles different named
arguments order.
- **Fix**: Support kotlin if/when/lambda expressions in lint lenient
mode.
-   Update AndroidX Lifecycle to `2.6.1`.
-   Update Kotlin to `1.9.0`.
-   Update RxJava to `3.1.6`.
-   Update RxAndroid to `3.0.2`.
-   Update lint to `8.0.2`/`31.0.2`.
-   Update error-prone to `2.20.0`.
- Update dokka + fresh coat of paint on API docs on the project site:
https://uber.github.io/AutoDispose/api/2.x/

Special thanks to [@&#8203;alexfu](https://togithub.com/alexfu),
[@&#8203;sanggggg](https://togithub.com/sanggggg), and
[@&#8203;psteiger](https://togithub.com/@&#8203;psteiger) for
contributing to this release!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi4xMS4wIiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 09:26:49 +01:00
renovate[bot] 00e5795c86
Update Kotlin to v1.9.20 (#4018)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| org.jetbrains.kotlin.plugin.parcelize | `1.9.0` -> `1.9.20` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.0/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.plugin.parcelize/1.9.0/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.kapt | `1.9.0` -> `1.9.20` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.kapt/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.kapt/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.kapt/1.9.0/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.kapt/1.9.0/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| org.jetbrains.kotlin.android | `1.9.0` -> `1.9.20` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlin.android/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlin.android/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlin.android/1.9.0/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlin.android/1.9.0/1.9.20?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi44My4wIiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 09:24:28 +01:00
renovate[bot] 227ddf6a71
Update androidx.lifecycle to v2.6.2 (#4020)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.lifecycle:lifecycle-viewmodel-ktx](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.1` -> `2.6.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-viewmodel-ktx/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.lifecycle:lifecycle-reactivestreams-ktx](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.1` -> `2.6.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-reactivestreams-ktx/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.lifecycle:lifecycle-livedata-ktx](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.1` -> `2.6.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-livedata-ktx/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[androidx.lifecycle:lifecycle-common-java8](https://developer.android.com/jetpack/androidx/releases/lifecycle#2.6.2)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `2.6.1` -> `2.6.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.lifecycle:lifecycle-common-java8/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.lifecycle:lifecycle-common-java8/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.lifecycle:lifecycle-common-java8/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.lifecycle:lifecycle-common-java8/2.6.1/2.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi44My4wIiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 09:24:01 +01:00
renovate[bot] 97cf9a0ea4
Update dependency androidx.paging:paging-runtime-ktx to v3.2.1 (#4021)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[androidx.paging:paging-runtime-ktx](https://developer.android.com/jetpack/androidx/releases/paging#3.2.1)
([source](https://cs.android.com/androidx/platform/frameworks/support))
| `3.2.0` -> `3.2.1` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.paging:paging-runtime-ktx/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.paging:paging-runtime-ktx/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.paging:paging-runtime-ktx/3.2.0/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.paging:paging-runtime-ktx/3.2.0/3.2.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNi44My4wIiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 09:23:43 +01:00
renovate[bot] 2596aa1e0e
Update dependency com.android.application to v8.1.2 (#4078)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.android.application](https://developer.android.com/studio/build)
([source](https://android.googlesource.com/platform/tools/base)) |
`8.1.1` -> `8.1.2` |
[![age](https://developer.mend.io/api/mc/badges/age/maven/com.android.application/8.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.android.application/8.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.android.application/8.1.1/8.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.android.application/8.1.1/8.1.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

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

---

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

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/tuskyapp/Tusky).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zMS41IiwidXBkYXRlZEluVmVyIjoiMzcuMzEuNSIsInRhcmdldEJyYW5jaCI6ImRldmVsb3AifQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 09:23:27 +01:00
Konrad Pozniak de70f08066
make blockquotes pretty (#4091)
closes https://github.com/tuskyapp/Tusky/issues/1271

before:
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/bfb4dcab-a6a7-4373-acf2-337363d63908"
width="380"/>

after:
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/79d90c22-dfd6-44fa-a600-a0e13d992bfc"
width="380"/>
2023-11-01 09:22:48 +01:00
Konrad Pozniak 6773342b60
Support code blocks (#4090)
before
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/452b959f-7f97-4d04-a464-0dcf0bf56f79"
width="380"/>

after
<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/0fb5b41c-dda3-4d46-878e-689d6ae51b0a"
width="380"/>
2023-11-01 09:22:23 +01:00
mcclure ede66c4eb8
Exoplayer: Increase space between rewind, pause, ffwd buttons (#4077)
I [posted our new video player layout] on Mastodon for comments and
multiple people said the buttons were too close together. I agree. I
added some space (I eyeballed it, I made it bigger until it felt too big
and then I narrowed it), I think we have now increased the space from
10dp to 25dp. I added the space by wrapping the buttons in
LinearLayouts, because they are <include>s and could theoretically
insert more than one button.

Concerns: If the "next"/"prev" buttons ever become active, the space
will not be correctly applied to those. We can fix that if it ever comes
up (we don't display those buttons). If people think the buttons should
be placed even further apart we can do this by just increasing the
number in styles.xml.

This is what it looks like now. See previous look and comparison with
23.0 in #4071

<img width=400
src="https://files.mastodon.social/media_attachments/files/111/293/547/524/867/101/original/91b83e1717111444.png">
2023-11-01 09:21:43 +01:00
Levi Bard 0f0ad6d5b0
Translations update from Weblate (#4088)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky-app/horizontal-auto.svg)
2023-11-01 09:20:10 +01:00
Levi Bard 26220dc22b
Translations update from Weblate (#4087)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-11-01 08:40:39 +01:00
Ümit Solmaz 7e2b7b4f93 Translated using Weblate (Turkish)
Currently translated at 100.0% (31 of 31 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/tr/
2023-11-01 07:14:36 +00:00
XoseM f7b821acf1 Translated using Weblate (Galician)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-11-01 04:45:00 +00:00
Ümit Solmaz c9bc6c3fdf Translated using Weblate (Turkish)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-11-01 04:45:00 +00:00
Levi Bard 0301bed0ac
Fix showing filtered (warn) boosts in timelines (#4079) 2023-10-30 08:42:16 +01:00
Levi Bard c26abaea5d
Translations update from Weblate (#4080)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-10-30 08:41:52 +01:00
Eric b870088050 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: Eric <ekhfcxwuvxqfdb@hldrive.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/
Translation: Tusky/Tusky
2023-10-30 04:45:00 +00:00
puf 685c51db53 Translated using Weblate (Welsh)
Currently translated at 100.0% (632 of 632 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-10-30 04:45:00 +00:00
Levi Bard 55ed6841ff
Support "replies policy" for lists (#4072) 2023-10-26 11:21:04 +02:00
mcclure 0f1d63e3c3
Upgrade all exoplayer/media3 packages to 1.1.1 stable (#4076)
Works in my testing
2023-10-25 13:45:33 -04:00
mcc a7cf5fcbc7 Upgrade all media3 packages to 1.1.1 stable 2023-10-25 11:59:46 -04:00
Levi Bard 70c67b9003
Translations update from Weblate (#4074)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widget/tusky/tusky/horizontal-auto.svg)
2023-10-25 13:24:40 +02:00
Levi Bard 131ebabe85
Add support for v2/instance (#4062)
…with fallback to v1
2023-10-25 12:53:10 +02:00
Markus Unterwaditzer 6e6cf05d11
Remove garbage alt text from images attached via Gboard (#4068)
Steps to reproduce:

1. install Gboard

(https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin)

2. open a direct link to any image in Firefox
3. long-press the image to get a "Copy Image" dialogue (and copy the
   image)
4. compose a new post in Tusky
5. Gboard will suggest to paste the image from clipboard
6. paste image, see that when opening alt text editor, it is filled out
   with this garbage string

Why is this bad? It's not when I just fix the alt text. But it breaks
every mechanism that is supposed to remind me of adding alt text.

It's hard to argue that this is within scope of Tusky but I also don't
see it getting fixed in Gboard, so here we go.
2023-10-25 12:52:42 +02:00
Weblate 99e16e9918 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/
Translation: Tusky/Tusky
2023-10-25 10:27:37 +00:00
UlrichKu 73ce9ffda5
4063: Make dialog size more stable (#4066)
Fixes: #4063

Switching from an AlertDialog to only a DialogFragment.

I didn't get the AlertDialog to be sized correctly.
It also opens now directly with the right (full screen) size. When the
imageView fails to load (i.e. with an audio file) it will be hidden.

This changes the button layout somewhat.

One observation: The placeholder text "... visually impaired..." is not
quite right as a description for an audio file is not intended for the
visually impaired. But I couldn't think of a better text just yet.


![grafik](https://github.com/tuskyapp/Tusky/assets/1618905/fd49d5bd-b86c-4659-abb9-f1776cbb2a55)
2023-10-25 11:59:59 +02:00
Levi Bard dd250717b2
Translations update from Weblate (#4064)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)
2023-10-25 11:59:15 +02:00
Weblate 85af732ec6 Added translation using Weblate (French (Belgium))
Added translation using Weblate (Telugu)

Added translation using Weblate (Punjabi)

Added translation using Weblate (Luxembourgish)

Added translation using Weblate (English (Australia))

Co-authored-by: Weblate <noreply@weblate.org>
2023-10-25 04:45:14 +00:00
puf eb849194e0 Translated using Weblate (Welsh)
Currently translated at 100.0% (629 of 629 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-10-25 04:45:14 +00:00
mcclure 1a10d5fd7a
Customize exoplayer to remove dark "curtain" (#4071)
In the new Tusky version, we switched from "old and busted" Android
video player to new "Exoplayer" (PR #3857). This introduced a dark
"curtain" covering the entire screen. This patch restores the 23.0 look.

This is done by creating a "magic" exo_player_control_view.xml override file.
2023-10-24 12:56:43 -04:00
mcc 98c3946ad9 Put semitransparent curtain back behind controls. This requires moving the stanza up in the file. 2023-10-24 12:32:26 -04:00
mcc 7590456dc1 Hide dark layer behind exoplayer controls 2023-10-23 17:15:47 -04:00
mcc 04ae4800ad Custom exo_player_control_view.xml file 2023-10-23 17:09:40 -04:00
UlrichKu b286255630
3532: Show badge on conversations tab on new conversations (#3890)
Fixes #3532

(Old PR, now closed: https://github.com/tuskyapp/Tusky/pull/3533)

Listens on new notifications and if a "direct mention" is detected a
badge (red dot) is shown on the conversations tab if present.

I am missing things like this a lot and also big accounts are unhappy
with the usability so far:
https://mastodon.social/@pallenberg/110129889996182814
2023-10-15 21:39:38 +02:00
Konrad Pozniak ff1c4a4b27
fix theme preference defaults (#4061)
closes #4060 

Also I noticed that we had the theme defaults twice in the code so I
refactored a bit so only one are still in there.
2023-10-14 14:20:20 +02:00
UlrichKu 8935b554d7
2512: Fix random tab colors (#4058)
Tries to fix the random colors for tabs on the bottom after #4051.

Does not work yet: The "selected" color does not take effect.
2023-10-11 23:11:49 +02:00
Lakoja 948d3b30cf 2512: Set style properly 2023-10-11 21:00:59 +02:00
Lakoja 7841209286 2512: Some in-vain efforts to fix wrong tab color 2023-10-11 19:48:21 +02:00
Levi Bard a39d210692
Translations update from Weblate (#4057)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)
2023-10-11 10:52:42 +02:00
UlrichKu 81a04c8977
2512: Allow more than 5 tabs (#4051)
Fixes #2512 


![grafik](https://github.com/tuskyapp/Tusky/assets/1618905/f8199d10-e26a-4f14-93c3-95cb890ea663)

Can add an arbitrary number of tabs.
Graphical behavior is unchanged for small numbers: the whole space if
filled with the tabs - they are enlarged if needed.

If there are more the mode switches to "scrollable".
This does not, however, look very differently (see screenshot with the
current tab scrolled out).

---------

Co-authored-by: Konrad Pozniak <connyduck@users.noreply.github.com>
2023-10-11 09:54:01 +02:00
Levi Bard 1f8bf728db
Translations update from Weblate (#4056)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)
2023-10-11 09:53:28 +02:00
Levi Bard b634f22efb
Translations update from Weblate (#4055)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky-app/horizontal-auto.svg)
2023-10-11 09:33:23 +02:00
Weblate eb61ab4900 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/
Translation: Tusky/Tusky
2023-10-11 07:29:36 +00:00
Ümit Solmaz a3081bff5a Translated using Weblate (Turkish)
Currently translated at 100.0% (630 of 630 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-10-11 07:08:57 +00:00
Deleted User 5ff70923b7 Translated using Weblate (German)
Currently translated at 100.0% (630 of 630 strings)

Co-authored-by: Deleted User <noreply+304@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/
Translation: Tusky/Tusky
2023-10-11 07:08:57 +00:00
sanao 3f7480fb73
Displayed the date of each announcement. (#4041)
## Issue
Close #3967 

# What I did
- Displayed the date of each announcement.
  - Date is placed in the lower left corner of the Announcement
- Supported date format internationalization using
getBestDateTimePattern

# Screenshot
<image
src="https://github.com/tuskyapp/Tusky/assets/62137820/7c124183-1a13-4cae-8667-ff82ca99b60c"
width="500"/>

## Note
I am not good at English so I use machine translation a bit. So, you may
find my writing style a little strange...
2023-10-10 19:17:06 +02:00
Ümit Solmaz 33eed2177d Translated using Weblate (Turkish)
Currently translated at 100.0% (31 of 31 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/tr/
2023-10-10 16:52:51 +00:00
Konrad Pozniak 150d3854bc
don't cut off names on profiles (#4052)
We cut them off everywhere but there should be at least one place where
one can read full display/usernames

closes #4000 

<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/21d22eb3-a6fc-4397-bdd0-551a3cce12d5"
width="400"/>
2023-10-09 15:43:02 +02:00
Levi Bard 119b14cb7f
Translations update from Weblate (#4050)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)
2023-10-09 15:42:37 +02:00
Salif Mehmed ef3bd4f919 Translated using Weblate (Bulgarian)
Currently translated at 87.4% (551 of 630 strings)

Co-authored-by: Salif Mehmed <mail@salif.eu>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/bg/
Translation: Tusky/Tusky
2023-10-07 15:40:54 +00:00
puf ab64525f4c Translated using Weblate (Welsh)
Currently translated at 99.8% (629 of 630 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-10-07 15:40:54 +00:00
Rhys Wynne 7b0b3d8f0d Translated using Weblate (Welsh)
Currently translated at 99.8% (629 of 630 strings)

Co-authored-by: Rhys Wynne <rhysw1@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-10-07 15:40:54 +00:00
Konrad Pozniak 9412ffba0f
fix boost button not updating when boosting (#4048) 2023-10-07 08:59:36 +02:00
Levi Bard 0fad9729ef
Translations update from Weblate (#4040)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)
2023-10-05 12:24:06 +02:00
Rhoslyn Prys 99cbd3a8f6 Translated using Weblate (Welsh)
Currently translated at 99.8% (629 of 630 strings)

Co-authored-by: Rhoslyn Prys <post@meddal.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-10-04 11:40:54 +00:00
Weblate 0efe314279 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/
Translation: Tusky/Tusky
2023-10-04 11:40:54 +00:00
Levi Bard 7036551b18
Fix weblate again (#4039)
Don't squash&merge or weblate won't pick it up :D
2023-09-29 07:16:05 +02:00
Conny Duck 74b391ce97 Merge remote-tracking branch 'weblate/develop' into fix-weblate 2023-09-28 20:05:08 +02:00
Levi Bard 1badf68531
Refresh timelines when filters are added/edited/removed (#3552)
Fixes #3546
2023-09-28 20:01:44 +02:00
Konrad Pozniak 14d4934172
Fix weblate conflict (#4038)
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Oliebol <schrijfmedan@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Co-authored-by: Manuel <mannivuwiki@gmail.com>
Co-authored-by: xzFantom <xzfantom@gmail.com>
Co-authored-by: Yusuke Matsubara <whym@whym.org>
Co-authored-by: XoseM <xosem@disroot.org>
Co-authored-by: Deleted User <noreply+143@weblate.org>
Co-authored-by: Salif Mehmed <mail@salif.eu>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: ButterflyOfFire <butterflyoffire@protonmail.com>
2023-09-27 19:39:23 +02:00
UlrichKu 24985f0206
#3570: Also provide a "direct" back with the hardware button (#3571)
Fixes #3570
2023-09-27 08:56:09 +02:00
UlrichKu 2dc27bca2a
use link icon instead of emoji when showing hidden urls (#4031)
Looks way better imho. Also closes #4028.

Before vs After:


![Screenshot_20230924_163714](https://github.com/tuskyapp/Tusky/assets/10157047/5b0b745a-4574-4e37-988e-b04997ac55f1)


![Screenshot_20230924_162657](https://github.com/tuskyapp/Tusky/assets/10157047/58a3482f-6afb-4b10-9891-f7a86af7f2bf)
2023-09-26 21:46:05 +02:00
alcapurrias 6c8cf0191d
Update contributing.md with payment policy link (#4037)
Updated to add the payment policy link and capitalized "API" where used
2023-09-26 20:17:18 +02:00
Levi Bard 2dceecb591
Fix build after logical conflict (#4036)
Ugh, I didn't notice that #3480 was affected by the notification
fragment rollback
2023-09-26 20:04:06 +02:00
ButterflyOfFire 29ebe191a6 Translated using Weblate (Arabic)
Currently translated at 93.5% (590 of 631 strings)

Co-authored-by: ButterflyOfFire <butterflyoffire@protonmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ar/
Translation: Tusky/Tusky
2023-09-26 17:53:58 +00:00
Sveinn í Felli 149f8e5e39 Translated using Weblate (Icelandic)
Currently translated at 100.0% (631 of 631 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Salif Mehmed 6681ed20a2 Translated using Weblate (Bulgarian)
Currently translated at 87.1% (550 of 631 strings)

Co-authored-by: Salif Mehmed <mail@salif.eu>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/bg/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Deleted User 0240c61fcd Translated using Weblate (Japanese)
Currently translated at 93.5% (590 of 631 strings)

Co-authored-by: Deleted User <noreply+143@weblate.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
XoseM 6dcf05b089 Translated using Weblate (Galician)
Currently translated at 100.0% (631 of 631 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Yusuke Matsubara bf3de32800 Translated using Weblate (Japanese)
Currently translated at 93.3% (589 of 631 strings)

Co-authored-by: Yusuke Matsubara <whym@whym.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
xzFantom 4bb4a56e0d Translated using Weblate (Belarusian)
Currently translated at 92.0% (581 of 631 strings)

Co-authored-by: xzFantom <xzfantom@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/be/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Manuel 563cd95db4 Translated using Weblate (Italian)
Currently translated at 100.0% (631 of 631 strings)

Co-authored-by: Manuel <mannivuwiki@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Hồ Nhất Duy 40cfaeeb18 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (630 of 630 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Ihor Hordiichuk d00a893853 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (630 of 630 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Oliebol 1f2b032822 Translated using Weblate (Dutch)
Currently translated at 95.7% (603 of 630 strings)

Co-authored-by: Oliebol <schrijfmedan@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Danial Behzadi 76786d9e59 Translated using Weblate (Persian)
Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (630 of 630 strings)

Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Eric 0a80834e7d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (631 of 631 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (630 of 630 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (629 of 629 strings)

Co-authored-by: Eric <alchemillatruth@purelymail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/
Translation: Tusky/Tusky
2023-09-26 17:53:57 +00:00
Konrad Pozniak 54e92b2156
improve local status updates (#3480)
The idea here is: Everytime we get hold of a new version of a post, we
update everything about that post everywhere.
This makes the distincion between different event types unnecessary, as
everythng is just a `StatusChangedEvent`.
The main benefit is that posts should be up-to-date more often, which is
important considering there is now editing and #3413
2023-09-26 09:08:58 +02:00
alcapurrias 2f39f87cc7
Payment Policy Document (#4035)
Document copied from CryptPad as approved by the team in the 2023-09-25
meeting.
2023-09-26 08:55:28 +02:00
Konrad Pozniak fbd99717c0
move Release.md to doc folder, remove ViewModelInterface.md (#4034) 2023-09-26 08:54:16 +02:00
Konrad Pozniak 5764c903e1
add role badges (#4029)
This adds support for the new Mastodon 4.2 role badges. Admins can
define if a role should be visible in the interface and then we get it
delivered by the Api on the `Account` object like this:
```
"roles": [
        {
              "id": "4",
              "name": "TEST",
              "color": "#ffee00"
        }
  ]
```

- keeps compatibility with older Mastodon version and non Mastodon
servers
- Took me a while, but I figured out a way to use the color and have it
look ok on all backgrounds (Mastodon itself ignores the color and just
always uses its brand color)
- falls back to Tusky blue in case no color is configured
- I adjusted the "Follows you" and "Bot" badges so they match the new
badge style
- In case the "Follows you" and "Bot" badges are visible at the same
time, "Follows you" gets its own line and "Bot" goes into the same line
as the role badge.
- Will work even with a lot of role badges (right now users can only
have 1 role at once though)
- Will work even when the badges federate (right now they don't)

<img
src="https://github.com/tuskyapp/Tusky/assets/10157047/24cbe889-ae46-408e-bfa0-cf3fd3c24f74"
width="320" />
2023-09-25 09:47:27 +02:00
UlrichKu 82bc48c3ae
Avoid crash on notification worker (#4025)
Not quite sure why/when this happens - every stack trace is not our
code, but I do get an ClassNotFoundException for Notification$Type with
the new reverted code.

The notification fetching (worker) then stops/crashes so I never get an
Android notification.

It might have something to do with
https://github.com/tuskyapp/Tusky/pull/3732 ?
2023-09-25 09:44:01 +02:00
Conny Duck 31ad946e1a use link icon instead of emoji when showing hidden urls 2023-09-24 16:28:53 +02:00
Angelo Suzuki fa80a0123a
Add "Trending posts" (statuses) feed (#4007)
Add "Trending posts" (statuses) feed.

This feed is a good source of interesting accounts to follow and,
personally, a sort of "Front page of the Fediverse".

Since #3908 and #3910 (which would provide a more thorough, albeit
complex, access to trending things) won't get merged, I'd like to
address this missing feed by simply adding another tab/feed.

~~If desired, I can move the second commit (fixing lint) to another
PR.~~

## Screenshots
### Tab
<img
src="https://github.com/tuskyapp/Tusky/assets/1063155/6a71a97e-673e-44c7-b67d-9b1df0bed4f5"
width=320 /> <img
src="https://github.com/tuskyapp/Tusky/assets/1063155/9bf60b23-d2f3-4dd8-8af6-e96647b02121"
width=320 />

### Activity
<img
src="https://github.com/tuskyapp/Tusky/assets/1063155/4e07dea3-d97f-42c6-8551-492a3116fcfa"
width=320 /> <img
src="https://github.com/tuskyapp/Tusky/assets/1063155/ad00a134-d622-43f4-8305-84cfa7fed706"
width=320 />
2023-09-14 22:37:41 +02:00
Mylloon bb1868fd67
Add system black theme (#3957)
Close #1222
2023-09-13 11:14:20 +02:00
UlrichKu f63c662275
Add the special appbar for the notifications again (#4022) 2023-09-13 09:21:08 +02:00
Levi Bard f99cb6d1d5
Fix lint warnings (#4019)
Clears the baseline of issues in our code, and resolves most of the
straightforward warnings from the report
2023-09-13 09:20:53 +02:00
Konrad Pozniak 7dfc8790c7
update minSdk to 24, cleanup code (#4014)
closes https://github.com/tuskyapp/Tusky/issues/2607
redo of https://github.com/tuskyapp/Tusky/pull/3593
2023-09-12 19:25:45 +02:00
Konrad Pozniak 0768dcd374
Cleanup unused resources (#4013)
redo of https://github.com/tuskyapp/Tusky/pull/3599
2023-09-12 18:11:06 +02:00
Levi Bard 8fb8fc8f02
Gradle 8.3 & AGP 8.1.1 (#3984) 2023-09-12 11:14:24 +02:00
Levi Bard c39769f6dd
Update Spanish translations (#4004)
Fix some typos and translations, hope it helps 👍🏻
2023-09-12 11:09:49 +02:00
Levi Bard 525adf3b81
Translations update from Weblate (#3998)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)
2023-09-12 11:07:33 +02:00
Levi Bard e5f0d00c9b
Translations update from Weblate (#3986)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky
description](https://weblate.tusky.app/projects/tusky/tusky-app/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky-app/horizontal-auto.svg)
2023-09-12 10:36:24 +02:00
Ümit Solmaz 210423da8b Translated using Weblate (Turkish)
Currently translated at 100.0% (628 of 628 strings)

Co-authored-by: Ümit Solmaz <usnotv@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Sveinn í Felli 2ba0d60896 Translated using Weblate (Icelandic)
Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.6% (626 of 628 strings)

Translated using Weblate (Icelandic)

Currently translated at 96.6% (607 of 628 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Ihor Hordiichuk e6481b5ac0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (628 of 628 strings)

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Eduardo 38b060f933 Translated using Weblate (Portuguese (Brazil))
Currently translated at 89.1% (560 of 628 strings)

Co-authored-by: Eduardo <edu200399lim@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_BR/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
XoseM d313bedcbc Translated using Weblate (Galician)
Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (626 of 626 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
eira lindgren 8fbed6c864 Translated using Weblate (Swedish)
Currently translated at 99.6% (624 of 626 strings)

Co-authored-by: eira lindgren <newt@nonbinary.me>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sv/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Hồ Nhất Duy 44875a0cbc Translated using Weblate (Vietnamese)
Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (626 of 626 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Eric 120abbc63e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (626 of 626 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Eric <alchemillatruth@purelymail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Oliebol 4266731bb0 Translated using Weblate (Dutch)
Currently translated at 97.4% (603 of 619 strings)

Co-authored-by: Oliebol <schrijfmedan@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Manuel dc7ae0bce5 Translated using Weblate (Italian)
Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Manuel <mannivuwiki@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Danial Behzadi 1a032d2c70 Translated using Weblate (Persian)
Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Persian)

Currently translated at 99.0% (620 of 626 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
puf 6380163de0 Translated using Weblate (Welsh)
Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-09-12 08:14:09 +00:00
Levi Bard eaeb673e19
use PrefKeys everywhere (#4016) 2023-09-12 09:57:05 +02:00
Levi Bard 924db72380
update SparkButton to 4.2.0 (#4010)
I moved the library from Jitpack to Maven Central and changed the group
id (so renovate won't pick that up), other than that there is only a
dependency upgrade to `androidx.appcompat:appcompat:1.6.1` included
2023-09-12 09:53:15 +02:00
Levi Bard 765ee605c2
upgrade glide to 4.16.0 (#4017)
They added some nullability annotations to their Java code, which is
nice but requires some changes in our Kotlin code.
closes #3975
2023-09-12 09:52:05 +02:00
Levi Bard a525cab52b
Rollback aggressive linting (#4003)
- Revise linting rules
  - Make lint failures opt-in (for the project) instead of opt-out
  - Reduce noise
- Add explicit errors for things we would ask somebody to change in a
code review
  - Update baseline to only include the new errors
  - Remove baseline autoupdate task since:
    - We want this to happen very rarely
    - The autoupdater also adds warnings
- Remove reviewdog github action (that autoadds lint comments to PRs)
2023-09-12 09:46:59 +02:00
Levi Bard f7b2962f58
Resets the paging3 changes of 3159 back to the (java) fragment code (#4015) 2023-09-12 09:46:24 +02:00
XoseM da630b858b Translated using Weblate (Galician)
Currently translated at 74.1% (23 of 31 strings)

Translation: Tusky/Tusky description
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/
2023-09-12 07:34:51 +00:00
Lakoja 3ad8588019 Remove unneeded code 2023-09-11 22:41:57 +02:00
Lakoja 4f865ec95f Add a bunch of "old" methods to be able to still use java code 2023-09-11 22:19:34 +02:00
Lakoja 4af160853d Remove unneeded code 2023-09-11 21:58:56 +02:00
Conny Duck 42c033c5ae upgrade glide to 4.16.0 2023-09-11 19:29:50 +02:00
Conny Duck af5ec068e8 use PrefKeys everywhere 2023-09-11 19:12:33 +02:00
Conny Duck 4f9baf8e3e update SparkButton to 4.2.0 2023-09-10 10:05:11 +02:00
Lakoja add62129f8 Resets the paging3 changes of 3159 back to the (java) fragment code before.
Should be the basis for further not-so-rattling improvements.
2023-09-09 21:29:24 +02:00
Tak! 1887511b18 Update contributor documentation 2023-09-08 15:14:16 +02:00
Levi Bard 40d771d60f
Kotlin - Enable trailing comma (#3959)
Trailing commas on Kotlin sources [has many
advantages](https://pinterest.github.io/ktlint/0.49.0/rules/standard/#trailing-comma-on-call-site):
- It makes version-control diffs cleaner – as all the focus is on the
changed value.
- It makes it easy to add and reorder elements – there is no need to add
or delete the comma if you manipulate elements.
- It simplifies code generation, for example, for object initializers.
The last element can also have a comma.

This PR doesn't go as far as require it, but tweaks KtLint to at least
allow it.

The two `.kt` files prove that the KtLint rules have been properly
disabled.
2023-09-08 09:17:50 +02:00
Elena.NET 54be828d6c
Update Spanish translations 2023-09-07 17:20:55 +02:00
Tak! 1e553ebd8a Remove newLintBaseline task since we only want errors in the baseline 2023-09-07 14:20:30 +02:00
Tak! 26d963dda9 Make linting failures opt-in instead of opt-out 2023-09-07 14:20:30 +02:00
Tak! e416a3e43d Remove reviewdog github action 2023-09-07 10:18:52 +02:00
Levi Bard 84969586c8
refactor domain blocks to paging (#3801)
Adds Paging, ViewModel, error snackbars, removes Rx, renames everything
to "domain blocks" to match the Masto Api

part of #2992
2023-09-06 10:11:17 +02:00
Tak! a96460cb16 Merge branch 'develop' into refactor_instancemute 2023-09-06 08:55:34 +02:00
Levi Bard b0150212c3
3891 better hashtag filtering (#3893)
Fixes #3891 (the first two observerations; the rest is either ok or has
another issue)
2023-09-05 09:35:33 +02:00
Levi Bard 5f0f4b82b9
3530: Provide a help text on the empty conversations view (#3531)
Fixes #3530
2023-09-05 09:34:50 +02:00
Levi Bard 7016fa3abc
don't crash on unexpected json responses (#3635) 2023-09-05 09:34:20 +02:00
Levi Bard 02404564e0
Detect Bookwyrm URL formats (#3936)
While helping test an issue with
[Bookwyrm](https://github.com/bookwyrm-social/bookwyrm) I noticed that
the URL formats used by that project aren't checked as possible profile
or post links. They're quite close to a couple of others, so I just
copied close examples and edited a couple of terms.

It's pretty minor, I just used a previous commit as a reference. Let me
know if it needs anything more though. I've only quickly tested it on a
local build with a couple of links against a live Bookwyrm and it picks
them up as expected now.
2023-09-05 09:33:51 +02:00
Levi Bard 710ce32a02
fix links sometimes being underlined (#3990)
closes #3989 

before:

![before](https://github.com/tuskyapp/Tusky/assets/10157047/a9004b44-dfd3-414d-838e-538df1eee2a7)

after:

![after](https://github.com/tuskyapp/Tusky/assets/10157047/f98790ac-80f1-459a-b726-b14b112fe0b1)
2023-09-05 09:33:39 +02:00
Levi Bard a6ab185790
Fix weblate conflict (#3996)
Here this should fix the weblate conflict. No idea why this happened.
This PR must not be squashed or Weblate won't find its commits (as for
all Weblate PRs).
2023-09-04 11:38:08 +02:00
Conny Duck 27a8683627 fix weblate conflict 2023-09-04 11:20:37 +02:00
mcclure 6566f89997
Fix link to Matrix in README (#3995)
Updated Matrix chat link to correct Matrix Space link.
2023-09-03 16:02:08 +01:00
Conny Duck ee4f6f4b7f fix links sometimes being underlined 2023-08-30 18:49:59 +02:00
Goooler 1e9bb3a980
Merge branch 'tuskyapp:develop' into agp811 2023-08-29 19:53:44 +08:00
Levi Bard 10a6b1616a
Editing profile: No change warning on leave. (#3972)
### Objective
* Prevent data loss when the user inadvertently hits back or wants to
leave the profile edition with unsaved changes.

### Description
* To limit the number of changes to the existing codebase, I merely
re-used the same method used by `save()` in the ViewModel to decide
whether to make a network request or simply return the profile as-is.
* ~A bit of code juggling around in the ViewModel and I was able to use
the logic for all the encoding of each profile field (Which is what the
ViewModel caches in memory).~ Thanks @Lakoja for improving this in the
VM.
* A couple of internal data classes used as helpers to move all the
fields around (now that they are no longer used in one single place)
were introduced.

### Potential Optimizations
* ~The profile encoding is done twice (once for checking, and then again
if the user has to actually save it). I'd say this is a negligible price
to pay, since the alternative would be to create a different set of
comparisons and/or keeping another profile in memory for the purpose of
comparison.~

### Visual Improvement
* I believe the Dialog is difficult to see, but it's being displayed
with Tusky's theme. Perhaps there's a better style to apply in this
case? (or maybe the edit profile activity shouldn't have the same
background color as dialogs?!)


### Issue
* #3486
2023-08-28 14:13:24 +02:00
Goooler 952e83959a Remove lint version overriding 2023-08-25 10:27:14 +08:00
Goooler 0b67be9932 Use configureEach 2023-08-25 10:22:32 +08:00
Goooler 268a689f3d Remove compileOptions 2023-08-25 10:21:47 +08:00
Goooler 6a829da010 AGP 8.1.1 2023-08-25 10:21:27 +08:00
Goooler d694f8f495 Migrate deprecated buildDir 2023-08-25 10:20:18 +08:00
Goooler 4c853a8689 Gradle 8.3 2023-08-25 10:19:40 +08:00
Mike Haynes 5764efa5d4
Allow the user to add a "bookmarks" tab (#3983)
Fixes #2368
2023-08-24 16:21:43 +02:00
Martin Marconcini 780dae00a6
Merge branch 'develop' into prompt_to_save_before_leaving_changed_profile 2023-08-24 15:14:10 +02:00
Lakoja 70d86345a8 3891: Match (only) title for muting state 2023-08-24 15:00:52 +02:00
Nik Clayton f118088161
Revert "Enable gradle build cache for Bitrise builds" (#3920)
Reverts tuskyapp/Tusky#3840. Turns out this needs to be enabled at
Bitrise, and it's not on our plan. So every build is running this extra
workflow, but it's not providing any benefit, just slowing things down.
2023-08-24 13:01:03 +02:00
Martin Marconcini b0294a8b93
Merge branch 'develop' into prompt_to_save_before_leaving_changed_profile 2023-08-24 10:06:43 +02:00
Martin Marconcini 387c3989d7
Apply PR suggestions:
* Remove dialog title and string
* Rename Data Class
* Use liveData.value directly when possible
2023-08-24 10:06:25 +02:00
Nik Clayton 0c892cf56f
Leave comments on PRs with fixes for lint errors (#3224)
Runs `ktlintFormat`, and adds comments to the PR if that generates any
diffs. The comments include the fix, which can be accepted immediately
through the GitHub UI.
2023-08-23 22:49:44 +02:00
Martin Marconcini 2d52ab9072
Merge branch 'develop' into prompt_to_save_before_leaving_changed_profile 2023-08-23 15:50:43 +02:00
Martin Marconcini a58f29fa57
Merge pull request #1 from Lakoja/3486-separate-diff-from-encode
3486 separate diff from encode
2023-08-23 15:50:26 +02:00
Lakoja 45d2fa1570 3486: (Appease linter) 2023-08-23 15:06:08 +02:00
SpaceFox 85888ac238
Uses the system theme as default theme (#3813)
Set the "System Design" as the default theme.

This ensures that the app's initial behaviour respect's the user's system-wide theme choice, while still allowing the user to adjust it later.

This is only done for new installs of Tusky. If the user is upgrading from a previous release and they did not have an explicit theme set then the dark theme is used, and the UX does not change.
2023-08-23 15:04:24 +02:00
Weblate dd0cf9c366
Translations update from Weblate (#3971)
Translations update from [Weblate](https://weblate.tusky.app) for
[Tusky/Tusky](https://weblate.tusky.app/projects/tusky/tusky/).



Current translation status:

![Weblate translation
status](https://weblate.tusky.app/widgets/tusky/-/tusky/horizontal-auto.svg)

---------

Co-authored-by: ButterflyOfFire <butterflyoffire@protonmail.com>
Co-authored-by: puf <puffinux@tutanota.com>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Oliebol <schrijfmedan@gmail.com>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Co-authored-by: XoseM <xosem@disroot.org>
Co-authored-by: Nik Clayton <nik@ngo.org.uk>
2023-08-22 22:23:07 +02:00
Lakoja 3a40274003 3486: Re-introduce separate check method to not need a synthetic accessor (lint error) 2023-08-22 21:17:22 +02:00
Nik Clayton f49b1cc744
Fix exception when updating summary notifications (#3976)
dc9e9f2aeb
modifed the code that fetched the value of EXTRA_NOTIFICATION_TYPE in an
intent, to use getSerializable().

However, the value was being placed in to the intent using putString().

This caused an exception when trying to update the summary notification,
so it would never update.

```
java.lang.ClassCastException: java.lang.String cannot be cast to com.keylesspalace.tusky.entity.Notification$Type
    at com.keylesspalace.tusky.components.notifications.NotificationHelper.updateSummaryNotifications(NotificationHelper.java:321)
    at com.keylesspalace.tusky.components.notifications.NotificationFetcher.fetchAndShow(NotificationFetcher.kt:87)
    at com.keylesspalace.tusky.components.notifications.NotificationFetcher$fetchAndShow$1.invokeSuspend(Unknown Source:14)
```

Fix this by placing the value in to the intent using putSerializable(),
to match how it will be fetched.
2023-08-22 18:22:45 +02:00
Lakoja ba50ff5686 3486: Separate diff and encoding 2023-08-22 16:12:03 +02:00
Lakoja f09f464667 3486: Rename stuff 2023-08-22 15:52:09 +02:00
Martin Marconcini 84915e6af5
Merge branch 'develop' into prompt_to_save_before_leaving_changed_profile 2023-08-22 13:06:27 +02:00
Martin Marconcini e56c0cb5a3
Avoid synthetic accessors. 2023-08-22 12:49:33 +02:00
Martin Marconcini 8edc8d6422
Make profileData internal so there's no synthetic accessor required. 2023-08-22 12:19:38 +02:00
Martin Marconcini c446d510e4
Fix lint double space. 2023-08-22 12:08:13 +02:00
Martin Marconcini 06239bb8a1
Fix synthetic accessor lint error. 2023-08-22 12:05:11 +02:00
Martin Marconcini 634f020ffa
Apply klint recommendations. 2023-08-19 17:57:25 +02:00
Martin Marconcini 461ec8d722
Prompt user before leaving edit profile when any field has been modified. 2023-08-19 17:36:00 +02:00
Nik Clayton 059352f471
Display notification filter/clear actions as menu items (#3877)
Previously the notification filter and clear actions were shown as
buttons in the UI, with a preference that determined whether they were
displayed.

Remove this preference, and display them as menu items.

- "Filter notifications" is shown as an icon, if possible
- "Clear notifications" is only ever shown as a menu item, to reduce the
chance the user inadvertently selects it

To ensure that the options menu appears correctly, remove the code that
creates a "fake" action bar, and adjust the layouts so that there are
three toolbars;

- mainToolbar -- displays the icons, and the current "location" (Home,
Notifications, etc)
- topNav -- displays the row of tabs at the top
- bottomNav -- displays the row of tabs at the bottom

Only one of them is set as the support action bar (depending on the
user's preferences). This provides the "show a logo" and "show the
options menu" functionality as standard, without needing to re-implement
as the previous code did.
2023-08-19 14:41:10 +02:00
Angelo Suzuki c7ffc6ad93
Hide option to mute own domain from account profile page (it is a no-op). (#3937)
Muting your own server domain is a no-op. Check for this case, and remove the "mute" menu item if true.

Fixes #3798
2023-08-19 14:17:04 +02:00
XoseM 02b0bdb266 Translated using Weblate (Galician)
Currently translated at 99.8% (618 of 619 strings)

Co-authored-by: XoseM <xosem@disroot.org>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
Hồ Nhất Duy e08582ded1 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Hồ Nhất Duy <mastoduy@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
Eric 2d9d472464 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Eric <alchemillatruth@purelymail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
Oliebol 25d7bfab5c Translated using Weblate (Dutch)
Currently translated at 96.9% (600 of 619 strings)

Co-authored-by: Oliebol <schrijfmedan@gmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
Danial Behzadi e53a0f0644 Translated using Weblate (Persian)
Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
puf 8eefdb56e2 Translated using Weblate (Welsh)
Currently translated at 100.0% (619 of 619 strings)

Co-authored-by: puf <puffinux@tutanota.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
ButterflyOfFire 106410d04b Translated using Weblate (French)
Currently translated at 96.2% (594 of 617 strings)

Co-authored-by: ButterflyOfFire <butterflyoffire@protonmail.com>
Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/
Translation: Tusky/Tusky
2023-08-19 11:56:38 +00:00
Nik Clayton b6102a755a
Rename "Trending" to "TrendingTags" or similar where necessary (#3906)
The "trending" functionality will expand to include trending links and
posts. But at the moment the "Trending" references in the code are
exclusively to hashtags.

Rename "Trending" to "TrendingTags" or similar everywhere necessary in
order to prepare for this.

This includes a database migration, as the identifier for the "Trending
tags" tab in the account preferences was changed from "Trending" to
"TrendingTags". The migration updates the stored value if necessary.
2023-08-19 12:54:35 +02:00
Konrad Pozniak dc9e9f2aeb
Improve account switching intents (#3732)
Before, intent creation was scattered across multiple sites, with account switching logic in both `ComposeActivity` and `MainActivity`.

Now, intents are only created in `MainActivity` Companion, and account switching only occurs in `MainActivity`

Fixes #3695
2023-08-19 12:31:47 +02:00
Martin Marconcini 85f0b1f320
Prompt the user before deleting a filter
Prevent users from accidentally deleting filters by prompting them to confirm.

Add an AlertDialog extension that converts AlertDialog callbacks to linear control flow.

Fixes #3736.
2023-08-17 22:26:46 +02:00
Angelo Suzuki cf42b687e6 Kotlin - Enable trailing comma 2023-08-16 20:28:19 +02:00
Lakoja 35fd702472 3891: Remove unnecessary if (use "this.hashtag" consistently) 2023-08-15 09:09:59 +02:00
Lakoja 6bd1c234d2 3891: Change length to short consistently 2023-08-15 09:09:59 +02:00
Lakoja 96028c841a 3891: (use string templates) 2023-08-15 09:09:59 +02:00
Lakoja e5456b0197 3891: Directly start the edit filter activity 2023-08-15 09:09:59 +02:00
Lakoja a883ec71d5 3891: Only mute the actual hashtag (with #) 2023-08-15 09:09:58 +02:00
Lakoja b0a50f9f50 3891: Add feedback (snackbars) for hashtag actions 2023-08-15 09:09:58 +02:00
Mike Barnes 5596e31769
Merge branch 'tuskyapp:develop' into bookwyrm-urls 2023-08-08 20:05:04 +10:00
Mike Barnes 83de22950d
Update LinkHelperTest.kt
Remove negative test for a URL that's invalid for Pleroma, but valid for Bookwyrm
2023-08-08 19:10:36 +10:00
Mike Barnes e290df5499
Update LinkHelperTest.kt
Add test URLs for Bookwyrm
2023-08-07 19:43:03 +10:00
Mike Barnes 28fd9911b0
Update LinkHelper.kt
Add URL formats used by Bookwyrm
2023-08-07 19:39:54 +10:00
Lakoja 8de23d33b8 3530: Change (remove) quotes 2023-08-07 09:44:58 +02:00
Lakoja 7347ab96dd 3530: Hide help text if the message view is reconfigured (ie an error occurred). 2023-08-07 09:44:58 +02:00
Lakoja 0d1340781b 3530: Add help text for empty lists view. 2023-08-07 09:44:58 +02:00
Lakoja c9f59aace4 3530: Provide a help text on the empty conversations view 2023-08-07 09:44:58 +02:00
Conny Duck f919252ede remove import wrongly added by auto refactoring 2023-07-05 20:43:24 +02:00
Conny Duck 1a849a7385 update lint-baseline.xml 2023-07-05 20:21:32 +02:00
Conny Duck c84815525f ui improvements 2023-07-05 20:11:12 +02:00
Conny Duck f8a6e0d8b8 introduce DomainBlocksRepository 2023-07-05 20:09:16 +02:00
Conny Duck 8e56b5429b introduce SnackbarEvent instead of DomainBlockEvent 2023-07-05 19:42:30 +02:00
Conny Duck 2393ff1e4d rename item_muted_domain to item_blocked_domain 2023-07-05 19:23:04 +02:00
Conny Duck 4670964362 rename module and classes to domainblocks 2023-07-04 19:41:36 +02:00
Conny Duck 626a8760ae refactor instance blocks to paging 2023-07-04 19:30:57 +02:00
Conny Duck b18193e6c3 add logging to NetworkTimelineRemoteMediator 2023-05-08 19:27:00 +02:00
Conny Duck 3ac78c82c9 don't crash on unexpected json responses 2023-05-08 19:24:57 +02:00
575 changed files with 19217 additions and 17153 deletions

View File

@ -7,11 +7,21 @@ indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.{java,kt}]
ij_kotlin_imports_layout = *
# Disable wildcard imports
[*.{java, kt}]
ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999
ij_java_class_count_to_use_import_on_demand = 999
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}]
indent_size = 2

41
.github/ISSUE_TEMPLATE/1.bug_report.yml vendored Normal file
View File

@ -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

View File

@ -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

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: true

View File

@ -13,21 +13,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v3
- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Gradle Build Action
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}

View File

@ -13,20 +13,20 @@ jobs:
name: app:buildGreenDebug
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-java@v3
- uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
- name: Gradle Wrapper Validation
uses: gradle/wrapper-validation-action@v1
uses: gradle/actions/wrapper-validation@v3
- name: Copy CI 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:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}

View File

@ -6,6 +6,77 @@
### Significant bug fixes
## 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)
## v24.1
- 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)
- 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)
- 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
### New features and other improvements
- The number of tabs that can be configured is no longer limited. [PR#4058](https://github.com/tuskyapp/Tusky/pull/4058)
- Blockquotes and code blocks in posts now look nicer [PR#4090](https://github.com/tuskyapp/Tusky/pull/4090) [PR#4091](https://github.com/tuskyapp/Tusky/pull/4091)
- The old behavior of the notification tab (pre Tusky 22.0) has been restored. [PR#4015](https://github.com/tuskyapp/Tusky/pull/4015)
- Role badges are now shown on profiles (Mastodon 4.2 feature). [PR#4029](https://github.com/tuskyapp/Tusky/pull/4029)
- The video player has been upgraded to Google Jetpack Media3; video compatibility should be improved, and you can now adjust playback speed. [PR#3857](https://github.com/tuskyapp/Tusky/pull/3857)
- New theme option to use the black theme when following the system design. [PR#3957](https://github.com/tuskyapp/Tusky/pull/3957)
- Following the system design is now the default theme setting. [PR#3813](https://github.com/tuskyapp/Tusky/pull/3957)
- A new view to see trending posts is available both in the menu and as custom tab. [PR#4007](https://github.com/tuskyapp/Tusky/pull/4007)
- A new option to hide self boosts has been added. [PR#4101](https://github.com/tuskyapp/Tusky/pull/4101)
- The `api/v2/instance` endpoint is now supported. [PR#4062](https://github.com/tuskyapp/Tusky/pull/4062)
- New settings for lists:
- Hide from the home timeline [PR#3932](https://github.com/tuskyapp/Tusky/pull/3932)
- Decide which replies should be shown in the list [PR#4072](https://github.com/tuskyapp/Tusky/pull/4072)
- The oldest supported Android version is now Android 7 Nougat [PR#4014](https://github.com/tuskyapp/Tusky/pull/4014)
### Significant bug fixes
- **Empty trends no longer causes Tusky to crash**, [PR#3853](https://github.com/tuskyapp/Tusky/pull/3853)
## v23.0
### New features and other improvements

View File

@ -22,8 +22,9 @@ We try to follow the [Guide to app architecture](https://developer.android.com/t
### Kotlin
Tusky was originally written in Java, but is in the process of migrating to Kotlin. All new code must be written in Kotlin.
We try to follow the [Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) and make format the code according to the default [ktlint codestyle](https://github.com/pinterest/ktlint).
You can check the codestyle by running `./gradlew ktlintCheck`.
We try to follow the [Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) and format the code according to the default [ktlint codestyle](https://github.com/pinterest/ktlint).
You can check the codestyle by running `./gradlew ktlintCheck lint`. This will fail if you have any errors, and produces a detailed report which also lists warnings.
We intentionally have very few hard linting errors, so that new contributors can focus on what they want to achieve instead of fighting the linter.
### Text
All English text that will be visible to users must be put in `app/src/main/res/values/strings.xml` so it is translateable into other languages.
@ -42,12 +43,15 @@ All icons are from the Material iconset, find new icons [here](https://fonts.goo
We try to make Tusky as accessible as possible for as many people as possible. Please make sure that all touch targets are at least 48dpx48dp in size, Text has sufficient contrast and images or icons have a image description. See [this guide](https://developer.android.com/guide/topics/ui/accessibility/apps) for more information.
### Supported servers
Tusky is primarily a Mastodon client and aims to always support the newest Mastodon version. Other platforms implementing the Mastodon Api, e.g. Akkoma, GoToSocial or Pixelfed should also work with Tusky but no special effort is made to support their quirks or additional features.
Tusky is primarily a Mastodon client and aims to always support the newest Mastodon version. Other platforms implementing the Mastodon API, e.g. Akkoma, GoToSocial or Pixelfed should also work with Tusky, but no special effort is made to support their quirks or additional features.
### Payment Policy
Our payment policy may be viewed [here](https://github.com/tuskyapp/Tusky/blob/develop/doc/PaymentPolicy.md).
## Troubleshooting / FAQ
- Tusky should be built with the newest version of Android Studio
- Tusky should be built with the newest version of Android Studio.
- Tusky comes with two sets of build variants, "blue" and "green", which can be installed simultaneously and are distinguished by the colors of their icons. Green is intended for local development and testing, whereas blue is for releases.
## Resources
- [Mastodon Api documentation](https://docs.joinmastodon.org/api/)
- [Mastodon API documentation](https://docs.joinmastodon.org/api/)

View File

@ -1,9 +0,0 @@
[Issue text goes here].
* * * *
- Tusky Version:
- Android Version:
- Android Device:
- Mastodon instance (if applicable):
- [ ] I searched or browsed the repos other issues to ensure this is not a duplicate.

View File

@ -32,4 +32,4 @@ If you have any bug reports, feature requests or questions please open an issue
We always welcome new contributors! Please read our [contribution guide](https://github.com/tuskyapp/Tusky/blob/develop/CONTRIBUTING.md) to get started.
### Development chatroom
https://riot.im/app/#/room/#Tusky:matrix.org
https://matrix.to/#/#Tusky:matrix.org

View File

@ -22,15 +22,15 @@ final def CUSTOM_INSTANCE = ""
final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky"
android {
compileSdk 33
compileSdk 34
namespace "com.keylesspalace.tusky"
defaultConfig {
applicationId APP_ID
namespace "com.keylesspalace.tusky"
minSdk 23
targetSdk 33
versionCode 113
versionName "23.0"
minSdk 24
targetSdk 34
versionCode 118
versionName "25.0 beta 1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -41,10 +41,21 @@ android {
buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"")
}
buildTypes {
debug {
isDefault true
}
release {
minifyEnabled true
shrinkResources true
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"
applicationIdSuffix ".test"
versionNameSuffix "-" + gitSha
isDefault true
}
}
lint {
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")
}
@ -99,12 +111,6 @@ android {
includeInApk false
includeInBundle false
}
// Can remove this once https://issuetracker.google.com/issues/260059413 is fixed.
// https://kotlinlang.org/docs/gradle-configure-project.html#gradle-java-toolchains-support
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
applicationVariants.configureEach { variant ->
variant.outputs.configureEach {
outputFileName = "Tusky_${variant.versionName}_${variant.versionCode}_${gitSha}_" +
@ -115,6 +121,7 @@ android {
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.generateKotlin", "true")
arg("room.incremental", "true")
}
@ -128,7 +135,6 @@ configurations {
// library versions are in PROJECT_ROOT/gradle/libs.versions.toml
dependencies {
implementation libs.kotlinx.coroutines.android
implementation libs.kotlinx.coroutines.rx3
implementation libs.bundles.androidx
implementation libs.bundles.room
@ -136,22 +142,20 @@ dependencies {
implementation libs.android.material
implementation libs.gson
implementation libs.bundles.moshi
ksp libs.moshi.kotlin.codegen
implementation libs.bundles.retrofit
implementation libs.networkresult.calladapter
implementation libs.bundles.okhttp
implementation libs.okio
implementation libs.conscrypt.android
implementation libs.bundles.glide
ksp libs.glide.compiler
implementation libs.bundles.rxjava3
implementation libs.bundles.autodispose
implementation libs.bundles.dagger
kapt libs.bundles.dagger.processors
@ -189,7 +193,7 @@ dependencies {
// 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) {
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",
@ -202,18 +206,3 @@ tasks.withType(org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask) {
"--add-opens", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
"--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"])
}
tasks.register("newLintBaseline") {
description 'Deletes and then recreates the lint baseline'
// This task should always run, irrespective of caching
notCompatibleWithConfigurationCache("Is always out of date")
outputs.upToDateWhen { false }
doLast {
delete android.lint.baseline.path
}
// Regenerate the lint baseline
it.finalizedBy tasks.named("lintBlueDebug")
}

File diff suppressed because one or more lines are too long

View File

@ -29,26 +29,48 @@
Disable these for the time being. -->
<issue id="UnusedIds" severity="ignore" />
<issue id="UnusedResources" severity="ignore" />
<!-- Logs are stripped in release builds. -->
<issue id="LogConditional" severity="ignore" />
<!-- Newer dependencies are handled by Renovate, and don't need a warning -->
<issue id="GradleDependency" severity="ignore" />
<issue id="NewerVersionAvailable" severity="ignore" />
<!-- Typographical quotes are 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="TypographyDashes" severity="ignore" />
<issue id="TypographyEllipsis" severity="ignore" />
<!-- Ensure we are warned about errors in the baseline -->
<issue id="LintBaseline" severity="warning" />
<!-- Translations come from external parties -->
<issue id="MissingQuantity" severity="ignore" />
<issue id="ImpliedQuantity" severity="ignore" />
<!-- Most alleged typos are in translations -->
<issue id="Typos" severity="ignore" />
<!-- Warn about typos. The typo database in lint is not exhaustive, and it's unclear
how to add to it when it's wrong. -->
<issue id="Typos" severity="warning" />
<!-- Basically all of our vectors are external -->
<issue id="VectorPath" severity="ignore" />
<issue id="Overdraw" severity="ignore" />
<!-- Set OldTargetApi back to warning -->
<issue id="OldTargetApi" severity="warning" />
<!-- Irrelevant api version warnings -->
<issue id="OldTargetApi" severity="ignore" />
<issue id="UnusedAttribute" severity="ignore" />
<!-- Mark all other lint issues as errors -->
<issue id="all" severity="error" />
<!-- We do not *want* all the text in the app to be selectable -->
<issue id="SelectableText" severity="ignore" />
<!-- This is heavily used by the viewbinding helper -->
<issue id="SyntheticAccessor" severity="ignore" />
<!-- Things we would actually question in a code review -->
<issue id="MissingPermission" severity="error" />
<issue id="InvalidPackage" severity="error" />
<issue id="UseCompatLoadingForDrawables" severity="error" />
<issue id="UseCompatTextViewDrawableXml" severity="error" />
<issue id="Recycle" severity="error" />
<issue id="KeyboardInaccessibleWidget" severity="error" />
<!-- Mark all other lint issues as warnings -->
<issue id="all" severity="warning" />
</lint>

View File

@ -1,83 +1,44 @@
# 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
-dontpreverify
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-keepattributes *Annotation*
# Preserve some attributes that may be required for reflection.
-keepattributes RuntimeVisible*Annotations, AnnotationDefault
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
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
-keepclassmembers enum * {
-keepclassmembers,allowoptimization enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
-keepclassmembers class **.R$* {
public static <fields>;
# Preserve annotated Javascript interface methods.
-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
# 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
-keepattributes SourceFile,LineNumberTable
-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

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ class MigrationsTest {
@Rule
var helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
AppDatabase::class.java.canonicalName!!,
FrameworkSQLiteOpenHelperFactory()
)

View File

@ -18,7 +18,8 @@
android:supportsRtl="true"
android:theme="@style/TuskyTheme"
android:usesCleartextTraffic="false"
android:localeConfig="@xml/locales_config">
android:localeConfig="@xml/locales_config"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".SplashActivity"
@ -130,7 +131,7 @@
android:theme="@style/Base.Theme.AppCompat" />
<activity
android:name=".components.search.SearchActivity"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
@ -148,7 +149,7 @@
<activity
android:name=".components.report.ReportActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity android:name=".components.instancemute.InstanceListActivity" />
<activity android:name=".components.domainblocks.DomainBlocksActivity" />
<activity android:name=".components.scheduled.ScheduledStatusActivity" />
<activity android:name=".components.announcements.AnnouncementsActivity" />
<activity android:name=".components.drafts.DraftsActivity" />
@ -188,14 +189,14 @@
android:icon="@drawable/ic_quicksettings"
android:label="@string/tusky_compose_post_quicksetting_label"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:exported="true"
tools:targetApi="24">
android:exported="true">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service android:name=".service.SendStatusService"
android:foregroundServiceType="shortService"
android:exported="false" />
<provider

View File

@ -21,8 +21,9 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import kotlinx.coroutines.launch
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import javax.inject.Inject
import kotlinx.coroutines.launch
class AboutActivity : BottomSheetActivity(), Injectable {
@Inject
@ -54,7 +55,7 @@ class AboutActivity : BottomSheetActivity(), Injectable {
lifecycleScope.launch {
accountManager.activeAccount?.let { account ->
val instanceInfo = instanceInfoRepository.getInstanceInfo()
val instanceInfo = instanceInfoRepository.getUpdatedInstanceInfoOrFallback()
binding.accountInfo.text = getString(
R.string.about_account_info,
account.username,
@ -70,9 +71,15 @@ class AboutActivity : BottomSheetActivity(), Injectable {
binding.aboutPoweredByTusky.hide()
}
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license)
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_tusky_license
)
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_project_site
)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_bug_feature_request_site
)
binding.tuskyProfileButton.setOnClickListener {
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)

View File

@ -45,8 +45,8 @@ import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
@ -82,11 +82,18 @@ class AccountsInListFragment : DialogFragment(), Injectable {
super.onStart()
dialog?.apply {
// 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? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_accounts_in_list, container, false)
}
@ -164,15 +171,27 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
override fun areContentsTheSame(
oldItem: TimelineAccount,
newItem: TimelineAccount
): Boolean {
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> {
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding)
binding.notificationTextView.hide()
@ -186,7 +205,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
override fun onBindViewHolder(
holder: BindingHolder<ItemFollowRequestBinding>,
position: Int
) {
val account = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
@ -204,10 +226,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> {
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding)
binding.notificationTextView.hide()
@ -224,7 +255,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
override fun onBindViewHolder(
holder: BindingHolder<ItemFollowRequestBinding>,
position: Int
) {
val (account, inAList) = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)

View File

@ -47,7 +47,9 @@ import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.interfaces.PermissionRequester;
import com.keylesspalace.tusky.settings.AppTheme;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.ActivityExtensions;
import com.keylesspalace.tusky.util.ThemeUtils;
import java.util.ArrayList;
@ -56,10 +58,17 @@ import java.util.List;
import javax.inject.Inject;
import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME;
import static com.keylesspalace.tusky.util.ActivityExtensions.supportsOverridingActivityTransitions;
public abstract class BaseActivity extends AppCompatActivity implements Injectable {
public static final String OPEN_WITH_SLIDE_IN = "OPEN_WITH_SLIDE_IN";
private static final String TAG = "BaseActivity";
@Inject
@NonNull
public AccountManager accountManager;
private static final int REQUESTER_NONE = Integer.MAX_VALUE;
@ -69,14 +78,19 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (supportsOverridingActivityTransitions() && activityTransitionWasRequested()) {
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, R.anim.activity_open_enter, R.anim.activity_open_exit);
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, R.anim.activity_close_enter, R.anim.activity_close_exit);
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
/* 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
* views are created. */
String theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT);
String theme = preferences.getString(APP_THEME, AppTheme.DEFAULT.getValue());
Log.d("activeTheme", theme);
if (theme.equals("black")) {
if (ThemeUtils.isBlack(getResources().getConfiguration(), theme)) {
setTheme(R.style.TuskyBlackTheme);
}
@ -87,7 +101,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor));
int style = textStyle(preferences.getString("statusTextSize", "medium"));
int style = textStyle(preferences.getString(PrefKeys.STATUS_TEXT_SIZE, "medium"));
getTheme().applyStyle(style, true);
if(requiresLogin()) {
@ -97,6 +111,10 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
requesters = new HashMap<>();
}
private boolean activityTransitionWasRequested() {
return getIntent().getBooleanExtra(OPEN_WITH_SLIDE_IN, false);
}
@Override
protected void attachBaseContext(Context newBase) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(newBase);
@ -162,13 +180,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
return style;
}
public void startActivityWithSlideInAnimation(Intent intent) {
super.startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
return true;
@ -179,11 +192,10 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right);
}
public void finishWithoutSlideOutAnimation() {
super.finish();
// if this activity was opened with slide-in, close it with slide out
if (!supportsOverridingActivityTransitions() && activityTransitionWasRequested()) {
overridePendingTransition(R.anim.activity_close_enter, R.anim.activity_close_exit);
}
}
protected void redirectIfNotLoggedIn() {
@ -191,12 +203,12 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
if (account == null) {
Intent intent = new Intent(this, LoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityWithSlideInAnimation(intent);
ActivityExtensions.startActivityWithSlideInAnimation(this, intent);
finish();
}
}
protected void showErrorDialog(View anyView, @StringRes int descriptionId, @StringRes int actionId, View.OnClickListener listener) {
protected void showErrorDialog(@Nullable View anyView, @StringRes int descriptionId, @StringRes int actionId, @Nullable View.OnClickListener listener) {
if (anyView != null) {
Snackbar bar = Snackbar.make(anyView, getString(descriptionId), Snackbar.LENGTH_SHORT);
bar.setAction(actionId, listener);
@ -204,7 +216,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
}
}
public void showAccountChooserDialog(CharSequence dialogTitle, boolean showActiveAccount, AccountSelectionListener listener) {
public void showAccountChooserDialog(@Nullable CharSequence dialogTitle, boolean showActiveAccount, @NonNull AccountSelectionListener listener) {
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
AccountEntity activeAccount = accountManager.getActiveAccount();
@ -231,9 +243,9 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
adapter.addAll(accounts);
new AlertDialog.Builder(this)
.setTitle(dialogTitle)
.setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
.show();
.setTitle(dialogTitle)
.setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
.show();
}
public @Nullable String getOpenAsText() {
@ -256,11 +268,10 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
public void openAsAccount(@NonNull String url, @NonNull AccountEntity account) {
accountManager.setActiveAccount(account.getId());
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra(MainActivity.REDIRECT_URL, url);
Intent intent = MainActivity.redirectIntent(this, account.getId(), url);
startActivity(intent);
finishWithoutSlideOutAnimation();
finish();
}
@Override
@ -272,7 +283,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
}
}
public void requestPermissions(String[] permissions, PermissionRequester requester) {
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) {

View File

@ -22,17 +22,17 @@ import android.view.View
import android.widget.LinearLayout
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
import autodispose2.autoDispose
import androidx.lifecycle.lifecycleScope
import at.connyduck.calladapter.networkresult.fold
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.looksLikeMastodonUrl
import com.keylesspalace.tusky.util.openLink
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import javax.inject.Inject
import kotlinx.coroutines.launch
/** 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
@ -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)) {
openLink(url)
return
}
mastodonApi.searchObservable(
query = url,
resolve = true
).observeOn(AndroidSchedulers.mainThread())
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ (accounts, statuses) ->
lifecycleScope.launch {
mastodonApi.search(
query = url,
resolve = true
).fold(
onSuccess = { (accounts, statuses) ->
if (getCancelSearchRequested(url)) {
return@subscribe
return@launch
}
onEndSearch(url)
if (statuses.isNotEmpty()) {
viewThread(statuses[0].id, statuses[0].url)
return@subscribe
return@launch
}
accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let { account ->
// Some servers return (unrelated) accounts for url searches (#2804)
// Verify that the account's url matches the query
viewAccount(account.id)
return@subscribe
return@launch
}
performUrlFallbackAction(url, lookupFallbackBehavior)
},
{
onFailure = {
if (!getCancelSearchRequested(url)) {
onEndSearch(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}
}
)
}
onBeginSearch(url)
}
@ -121,10 +124,17 @@ abstract class BottomSheetActivity : BaseActivity() {
startActivityWithSlideInAnimation(intent)
}
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
protected open fun performUrlFallbackAction(
url: String,
fallbackBehavior: PostLookupFallbackBehavior
) {
when (fallbackBehavior) {
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()
}
}

View File

@ -25,9 +25,11 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.LiveData
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
@ -46,15 +48,18 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.await
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
import com.keylesspalace.tusky.viewmodel.ProfileDataInUi
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class EditProfileActivity : BaseActivity(), Injectable {
@ -96,6 +101,14 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
private val currentProfileData
get() = ProfileDataInUi(
displayName = binding.displayNameEditText.text.toString(),
note = binding.noteEditText.text.toString(),
locked = binding.lockedCheckBox.isChecked,
fields = accountFieldEditAdapter.getFieldData()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -114,9 +127,17 @@ class EditProfileActivity : BaseActivity(), Injectable {
binding.fieldList.layoutManager = LinearLayoutManager(this)
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 {
accountFieldEditAdapter.addField()
@ -131,52 +152,64 @@ class EditProfileActivity : BaseActivity(), Injectable {
viewModel.obtainProfile()
viewModel.profileData.observe(this) { profileRes ->
when (profileRes) {
is Success -> {
val me = profileRes.data
if (me != null) {
binding.displayNameEditText.setText(me.displayName)
binding.noteEditText.setText(me.source?.note)
binding.lockedCheckBox.isChecked = me.locked
lifecycleScope.launch {
viewModel.profileData.collect { profileRes ->
if (profileRes == null) return@collect
when (profileRes) {
is Success -> {
val me = profileRes.data
if (me != null) {
binding.displayNameEditText.setText(me.displayName)
binding.noteEditText.setText(me.source?.note)
binding.lockedCheckBox.isChecked = me.locked
accountFieldEditAdapter.setFields(me.source?.fields.orEmpty())
binding.addFieldButton.isVisible =
(me.source?.fields?.size ?: 0) < maxAccountFields
accountFieldEditAdapter.setFields(me.source?.fields.orEmpty())
binding.addFieldButton.isVisible =
(me.source?.fields?.size ?: 0) < maxAccountFields
if (viewModel.avatarData.value == null) {
Glide.with(this)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
.into(binding.avatarPreview)
}
if (viewModel.avatarData.value == null) {
Glide.with(this@EditProfileActivity)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.transform(
FitCenter(),
RoundedCorners(
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
)
)
.into(binding.avatarPreview)
}
if (viewModel.headerData.value == null) {
Glide.with(this)
.load(me.header)
.into(binding.headerPreview)
if (viewModel.headerData.value == null) {
Glide.with(this@EditProfileActivity)
.load(me.header)
.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 {
viewModel.instanceData.collect { instanceInfo ->
maxAccountFields = instanceInfo.maxFields
accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength)
accountFieldEditAdapter.setFieldLimits(
instanceInfo.maxFieldNameLength,
instanceInfo.maxFieldValueLength
)
binding.addFieldButton.isVisible =
accountFieldEditAdapter.itemCount < maxAccountFields
}
@ -185,60 +218,85 @@ class EditProfileActivity : BaseActivity(), Injectable {
observeImage(viewModel.avatarData, binding.avatarPreview, true)
observeImage(viewModel.headerData, binding.headerPreview, false)
viewModel.saveData.observe(
this
) {
when (it) {
is Success -> {
finish()
}
is Loading -> {
binding.saveProgressBar.visibility = View.VISIBLE
}
is Error -> {
onSaveFailure(it.errorMessage)
lifecycleScope.launch {
viewModel.saveData.collect {
if (it == null) return@collect
when (it) {
is Success -> {
finish()
}
is Loading -> {
binding.saveProgressBar.visibility = View.VISIBLE
}
is Error -> {
onSaveFailure(it.errorMessage)
}
}
}
}
binding.displayNameEditText.doAfterTextChanged {
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)
lifecycleScope.launch {
viewModel.isChanged.collect { dataWasChanged ->
onBackCallback.isEnabled = dataWasChanged
}
}
}
override fun onStop() {
super.onStop()
if (!isFinishing) {
viewModel.updateProfile(
binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData()
)
viewModel.updateProfile(currentProfileData)
}
}
private fun observeImage(
liveData: LiveData<Uri>,
flow: StateFlow<Uri?>,
imageView: ImageView,
roundedCorners: Boolean
) {
liveData.observe(
this
) { imageUri ->
lifecycleScope.launch {
flow.collect { imageUri ->
// skipping all caches so we can always reuse the same uri
val glide = Glide.with(imageView)
.load(imageUri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
// skipping all caches so we can always reuse the same uri
val glide = Glide.with(imageView)
.load(imageUri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
if (roundedCorners) {
glide.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
).into(imageView)
} else {
glide.into(imageView)
if (roundedCorners) {
glide.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
).into(imageView)
} else {
glide.into(imageView)
}
imageView.show()
}
imageView.show()
}
}
@ -287,14 +345,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
return super.onOptionsItemSelected(item)
}
private fun save() {
viewModel.save(
binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData()
)
}
private fun save() = viewModel.save(currentProfileData)
private fun onSaveFailure(msg: String?) {
val errorMsg = msg ?: getString(R.string.error_media_upload_sending)
@ -304,6 +355,22 @@ class EditProfileActivity : BaseActivity(), Injectable {
private fun onPickFailure(throwable: 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 {
when (launchSaveDialog()) {
AlertDialog.BUTTON_POSITIVE -> save()
else -> finish()
}
}
private suspend fun launchSaveDialog() = AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_save_profile_changes_message))
.create()
.await(R.string.action_save, R.string.action_discard)
}

View File

@ -19,11 +19,14 @@ import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.annotation.RawRes
import androidx.lifecycle.lifecycleScope
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import com.keylesspalace.tusky.util.closeQuietly
import java.io.BufferedReader
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
class LicenseActivity : BaseActivity() {
@ -44,23 +47,15 @@ class LicenseActivity : BaseActivity() {
}
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
val sb = StringBuilder()
val br = BufferedReader(InputStreamReader(resources.openRawResource(fileId)))
try {
var line: String? = br.readLine()
while (line != null) {
sb.append(line)
sb.append('\n')
line = br.readLine()
lifecycleScope.launch {
textView.text = withContext(Dispatchers.IO) {
try {
resources.openRawResource(fileId).source().buffer().use { it.readUtf8() }
} catch (e: IOException) {
Log.w("LicenseActivity", e)
""
}
}
} catch (e: IOException) {
Log.w("LicenseActivity", e)
}
br.closeQuietly()
textView.text = sb.toString()
}
}

View File

@ -1,4 +1,4 @@
/* Copyright 2017 Andrew Dawson
/* Copyright Tusky contributors
*
* This file is a part of Tusky.
*
@ -23,9 +23,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.TextView
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
@ -35,16 +33,17 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
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.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.databinding.DialogListBinding
import com.keylesspalace.tusky.databinding.ItemListBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
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.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewmodel.ListsViewModel
@ -54,18 +53,12 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import com.mikepenz.iconics.IconicsDrawable
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 javax.inject.Inject
import kotlinx.coroutines.launch
/**
* Created by charlag on 1/4/18.
*/
// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?)
class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
@ -124,7 +117,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
private fun showlistNameDialog(list: MastoList?) {
val binding = DialogListBinding.inflate(layoutInflater)
val binding = DialogListBinding.inflate(layoutInflater).apply {
replyPolicySpinner.setSelection(MastoList.ReplyPolicy.from(list?.repliesPolicy).ordinal)
}
val dialog = AlertDialog.Builder(this)
.setView(binding.root)
.setPositiveButton(
@ -134,7 +129,12 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
R.string.action_rename_list
}
) { _, _ ->
onPickedDialogName(binding.nameText.text.toString(), list?.id, binding.exclusiveCheckbox.isChecked)
onPickedDialogName(
binding.nameText.text.toString(),
list?.id,
binding.exclusiveCheckbox.isChecked,
MastoList.ReplyPolicy.entries[binding.replyPolicySpinner.selectedItemPosition].policy
)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
@ -192,6 +192,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
R.string.message_empty,
null
)
binding.messageView.showHelp(R.string.help_empty_lists)
} else {
binding.messageView.hide()
}
@ -206,9 +207,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
).show()
}
private fun onListSelected(listId: String, listTitle: String) {
private fun onListSelected(list: MastoList) {
startActivityWithSlideInAnimation(
StatusListActivity.newListIntent(this, listId, listTitle)
StatusListActivity.newListIntent(this, list.id, list.title)
)
}
@ -247,51 +248,42 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
private inner class ListsAdapter :
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
ListAdapter<MastoList, BindingHolder<ItemListBinding>>(ListsDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
.let(this::ListViewHolder)
.apply {
val iconColor = MaterialColors.getColor(nameTextView, android.R.attr.textColorTertiary)
val context = nameTextView.context
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemListBinding> {
return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
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) {
if (v == itemView) {
val list = getItem(bindingAdapterPosition)
onListSelected(list.id, list.title)
} else {
onMore(getItem(bindingAdapterPosition), v)
}
holder.itemView.setOnClickListener {
onListSelected(item)
}
}
}
private fun onPickedDialogName(name: String, listId: String?, exclusive: Boolean) {
private fun onPickedDialogName(
name: String,
listId: String?,
exclusive: Boolean,
replyPolicy: String
) {
if (listId == null) {
viewModel.createNewList(name, exclusive)
viewModel.createNewList(name, exclusive, replyPolicy)
} else {
viewModel.updateList(listId, name, exclusive)
viewModel.updateList(listId, name, exclusive, replyPolicy)
}
}

View File

@ -16,6 +16,8 @@
package com.keylesspalace.tusky
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@ -26,6 +28,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
@ -33,6 +36,7 @@ import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.View
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
@ -41,15 +45,18 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.GravityCompat
import androidx.core.view.MenuProvider
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.MarginPageTransformer
import at.connyduck.calladapter.networkresult.fold
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.FixedSizeDrawable
@ -60,8 +67,11 @@ import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
import com.keylesspalace.tusky.appstore.CacheUpdater
import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
@ -81,8 +91,10 @@ import com.keylesspalace.tusky.components.trending.TrendingActivity
import com.keylesspalace.tusky.databinding.ActivityMainBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftsAlert
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.FabFragment
@ -91,16 +103,17 @@ import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase
import com.keylesspalace.tusky.usecase.LogoutUsecase
import com.keylesspalace.tusky.util.ShareShortcutHelper
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDimension
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.supportsOverridingActivityTransitions
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.updateShortcut
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@ -131,9 +144,10 @@ import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
@Inject
@ -154,6 +168,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
@Inject
lateinit var developerToolsUseCase: DeveloperToolsUseCase
@Inject
lateinit var shareShortcutHelper: ShareShortcutHelper
@Inject
@ApplicationScope
lateinit var externalScope: CoroutineScope
private val binding by viewBinding(ActivityMainBinding::inflate)
private lateinit var header: AccountHeaderView
@ -164,8 +185,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private val preferences by unsafeLazy { PreferenceManager.getDefaultSharedPreferences(this) }
private lateinit var glide: RequestManager
// We need to know if the emoji pack has been changed
private var selectedEmojiPack: String? = null
@ -175,37 +194,68 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
/** Adapter for the different timeline tabs */
private lateinit var tabAdapter: MainPagerAdapter
private var directMessageTab: TabLayout.Tab? = null
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
when {
binding.mainDrawerLayout.isOpen -> {
binding.mainDrawerLayout.close()
}
binding.viewPager.currentItem != 0 -> {
binding.viewPager.currentItem = 0
}
}
}
}
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activeAccount = accountManager.activeAccount
?: return // will be redirected to LoginActivity by BaseActivity
if (supportsOverridingActivityTransitions() && explodeAnimationWasRequested()) {
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, R.anim.explode, R.anim.activity_open_exit)
}
var showNotificationTab = false
if (intent != null) {
// check for savedInstanceState in order to not handle intent events more than once
if (intent != null && savedInstanceState == null) {
val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
if (notificationId != -1) {
// opened from a notification action, cancel the notification
val notificationManager = getSystemService(
NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId)
}
/** there are two possibilities the accountId can be passed to MainActivity:
* - from our code as long 'account_id'
* - from our code as Long Intent Extra TUSKY_ACCOUNT_ID
* - from share shortcuts as String 'android.intent.extra.shortcut.ID'
*/
var accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1)
if (accountId == -1L) {
var tuskyAccountId = intent.getLongExtra(TUSKY_ACCOUNT_ID, -1)
if (tuskyAccountId == -1L) {
val accountIdString = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID)
if (accountIdString != null) {
accountId = accountIdString.toLong()
tuskyAccountId = accountIdString.toLong()
}
}
val accountRequested = accountId != -1L
if (accountRequested && accountId != activeAccount.id) {
accountManager.setActiveAccount(accountId)
val accountRequested = tuskyAccountId != -1L
if (accountRequested && tuskyAccountId != activeAccount.id) {
accountManager.setActiveAccount(tuskyAccountId)
}
val openDrafts = intent.getBooleanExtra(OPEN_DRAFTS, false)
if (canHandleMimeType(intent.type)) {
if (canHandleMimeType(intent.type) || intent.hasExtra(COMPOSE_OPTIONS)) {
// Sharing to Tusky from an external app
if (accountRequested) {
// The correct account is already active
forwardShare(intent)
forwardToComposeActivity(intent)
} else {
// No account was provided, show the chooser
showAccountChooserDialog(
@ -216,10 +266,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val requestedId = account.id
if (requestedId == activeAccount.id) {
// The correct account is already active
forwardShare(intent)
forwardToComposeActivity(intent)
} else {
// A different account was requested, restart the activity
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId)
intent.putExtra(TUSKY_ACCOUNT_ID, requestedId)
changeAccount(requestedId, intent)
}
}
@ -229,11 +279,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} else if (openDrafts) {
val intent = DraftsActivity.newIntent(this)
startActivity(intent)
} else if (accountRequested && savedInstanceState == null) {
} else if (accountRequested && intent.hasExtra(NOTIFICATION_TYPE)) {
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
// otherwise show notification tab
if (intent.getStringExtra(NotificationHelper.TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS)
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
val intent = AccountListActivity.newIntent(
this,
AccountListActivity.Type.FOLLOW_REQUESTS
)
startActivityWithSlideInAnimation(intent)
} else {
showNotificationTab = true
@ -242,17 +295,27 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(binding.root)
setSupportActionBar(binding.mainToolbar)
glide = Glide.with(this)
binding.composeButton.setOnClickListener {
val composeIntent = Intent(applicationContext, ComposeActivity::class.java)
startActivity(composeIntent)
}
// Determine which of the three toolbars should be the supportActionBar (which hosts
// the options menu).
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
binding.mainToolbar.visible(!hideTopToolbar)
if (hideTopToolbar) {
when (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top")) {
"top" -> setSupportActionBar(binding.topNav)
"bottom" -> setSupportActionBar(binding.bottomNav)
}
binding.mainToolbar.hide()
// There's not enough space in the top/bottom bars to show the title as well.
supportActionBar?.setDisplayShowTitleEnabled(false)
} else {
setSupportActionBar(binding.mainToolbar)
binding.mainToolbar.show()
}
loadDrawerAvatar(activeAccount.profilePictureUrl, true)
@ -263,7 +326,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupDrawer(
savedInstanceState,
addSearchButton = hideTopToolbar,
addTrendingButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING)
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
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
@ -288,47 +356,57 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
is MainTabsChangedEvent -> {
refreshMainDrawerItems(
addSearchButton = hideTopToolbar,
addTrendingButton = !event.newTabs.hasTab(TRENDING)
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES)
)
setupTabs(false)
}
is AnnouncementReadEvent -> {
unreadAnnouncementsCount--
updateAnnouncementsBadge()
}
is NewNotificationsEvent -> {
directMessageTab?.let {
if (event.accountId == activeAccount.accountId) {
val hasDirectMessageNotification =
event.notifications.any {
it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT
}
if (hasDirectMessageNotification) {
showDirectMessageBadge(true)
}
}
}
}
is NotificationsLoadingEvent -> {
if (event.accountId == activeAccount.accountId) {
showDirectMessageBadge(false)
}
}
is ConversationsLoadingEvent -> {
if (event.accountId == activeAccount.accountId) {
showDirectMessageBadge(false)
}
}
}
}
}
Schedulers.io().scheduleDirect {
externalScope.launch(Dispatchers.IO) {
// Flush old media that was cached for sharing
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
}
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
when {
binding.mainDrawerLayout.isOpen -> {
binding.mainDrawerLayout.close()
}
binding.viewPager.currentItem != 0 -> {
binding.viewPager.currentItem = 0
}
else -> {
finish()
}
}
}
}
)
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
if (
Build.VERSION.SDK_INT >= 33 &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
@ -340,6 +418,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
draftsAlert.observeInContext(this, true)
}
private fun showDirectMessageBadge(showBadge: Boolean) {
directMessageTab?.let { tab ->
tab.badge?.isVisible = showBadge
// TODO a bit cumbersome (also for resetting)
lifecycleScope.launch(Dispatchers.IO) {
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge != showBadge) {
it.hasDirectMessageBadge = showBadge
accountManager.saveAccount(it)
}
}
}
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_main, menu)
menu.findItem(R.id.action_search)?.apply {
@ -350,6 +444,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
// 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
if (!binding.mainToolbar.isVisible) {
menu.forEach {
it.setShowAsAction(
SHOW_AS_ACTION_NEVER
)
}
}
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
@ -422,12 +530,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
private fun forwardShare(intent: Intent) {
val composeIntent = Intent(this, ComposeActivity::class.java)
composeIntent.action = intent.action
composeIntent.type = intent.type
composeIntent.putExtras(intent)
composeIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
private fun forwardToComposeActivity(intent: Intent) {
val composeOptions = IntentCompat.getParcelableExtra(
intent,
COMPOSE_OPTIONS,
ComposeActivity.ComposeOptions::class.java
)
val composeIntent = if (composeOptions != null) {
ComposeActivity.startIntent(this, composeOptions)
} else {
Intent(this, ComposeActivity::class.java).apply {
action = intent.action
type = intent.type
putExtras(intent)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
startActivity(composeIntent)
finish()
}
@ -435,13 +554,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(
savedInstanceState: Bundle?,
addSearchButton: Boolean,
addTrendingButton: Boolean
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean
) {
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener)
binding.topNavAvatar.setOnClickListener(drawerOpenClickListener)
binding.bottomNavAvatar.setOnClickListener(drawerOpenClickListener)
binding.topNav.setNavigationOnClickListener(drawerOpenClickListener)
binding.bottomNav.setNavigationOnClickListener(drawerOpenClickListener)
header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@ -465,17 +585,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.currentProfileName.ellipsize = TextUtils.TruncateAt.END
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
header.accountHeaderBackground.setBackgroundColor(MaterialColors.getColor(header, R.attr.colorBackgroundAccent))
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
header.accountHeaderBackground.setBackgroundColor(
MaterialColors.getColor(header, R.attr.colorBackgroundAccent)
)
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
if (animateAvatars) {
glide.load(uri)
Glide.with(imageView)
.load(uri)
.placeholder(placeholder)
.into(imageView)
} else {
glide.asBitmap()
Glide.with(imageView)
.asBitmap()
.load(uri)
.placeholder(placeholder)
.into(imageView)
@ -483,12 +607,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
override fun cancel(imageView: ImageView) {
glide.clear(imageView)
// nothing to do, Glide already handles cancellation automatically
}
override fun placeholder(ctx: Context, tag: String?): Drawable {
if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) {
return ctx.getDrawable(R.drawable.avatar_default)!!
return AppCompatResources.getDrawable(ctx, R.drawable.avatar_default)!!
}
return super.placeholder(ctx, tag)
@ -496,12 +620,33 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
})
binding.mainDrawer.apply {
refreshMainDrawerItems(addSearchButton, addTrendingButton)
refreshMainDrawerItems(
addSearchButton = addSearchButton,
addTrendingTagsButton = addTrendingTagsButton,
addTrendingStatusesButton = addTrendingStatusesButton
)
setSavedInstance(savedInstanceState)
}
binding.mainDrawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) { }
override fun onDrawerOpened(drawerView: View) {
onBackPressedCallback.isEnabled = true
}
override fun onDrawerClosed(drawerView: View) {
onBackPressedCallback.isEnabled = binding.tabLayout.selectedTabPosition > 0
}
override fun onDrawerStateChanged(newState: Int) { }
})
}
private fun refreshMainDrawerItems(addSearchButton: Boolean, addTrendingButton: Boolean) {
private fun refreshMainDrawerItems(
addSearchButton: Boolean,
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean
) {
binding.mainDrawer.apply {
itemAdapter.clear()
tintStatusBar = true
@ -618,7 +763,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
)
}
if (addTrendingButton) {
if (addTrendingTagsButton) {
binding.mainDrawer.addItemsAtPosition(
5,
primaryDrawerItem {
@ -630,6 +775,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
}
if (addTrendingStatusesButton) {
binding.mainDrawer.addItemsAtPosition(
6,
primaryDrawerItem {
nameRes = R.string.title_public_trending_statuses
iconicsIcon = GoogleMaterial.Icon.gmd_local_fire_department
onClick = {
startActivityWithSlideInAnimation(StatusListActivity.newTrendingIntent(context))
}
}
)
}
}
if (BuildConfig.DEBUG) {
@ -699,6 +857,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Detach any existing mediator before changing tab contents and attaching a new mediator
tabLayoutMediator?.detach()
directMessageTab = null
tabAdapter.tabs = tabs
tabAdapter.notifyItemRangeChanged(0, tabs.size)
@ -709,6 +869,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
LIST -> tabs[position].arguments[1]
else -> getString(tabs[position].text)
}
if (tabs[position].id == DIRECT) {
val badge = tab.orCreateBadge
badge.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false
badge.backgroundColor = MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorPrimary)
directMessageTab = tab
}
}.also { it.attach() }
// Selected tab is either
@ -734,9 +900,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
onTabSelectedListener = object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
onBackPressedCallback.isEnabled = tab.position > 0 || binding.mainDrawerLayout.isOpen
binding.mainToolbar.title = tab.contentDescription
refreshComposeButtonState(tabAdapter, tab.position)
if (tab == directMessageTab) {
tab.badge?.isVisible = false
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge) {
it.hasDirectMessageBadge = false
accountManager.saveAccount(it)
}
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
@ -755,7 +934,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
supportActionBar?.title = tabs[position].title(this@MainActivity)
binding.mainToolbar.setOnClickListener {
(tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
(
tabAdapter.getFragment(
activeTabLayout.selectedTabPosition
) as? ReselectableFragment
)?.onReselect()
}
updateProfiles()
@ -786,7 +969,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
// open LoginActivity to add new 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
}
// change Account
@ -798,15 +983,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
cacheUpdater.stop()
accountManager.setActiveAccount(newSelectedId)
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) {
intent.type = forward.type
intent.action = forward.action
intent.putExtras(forward)
}
startActivity(intent)
finishWithoutSlideOutAnimation()
overridePendingTransition(R.anim.explode, R.anim.explode)
finish()
if (!supportsOverridingActivityTransitions()) {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.explode, R.anim.activity_open_exit)
}
}
private fun logout() {
@ -829,7 +1017,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
}
startActivity(intent)
finishWithoutSlideOutAnimation()
finish()
}
}
.setNegativeButton(android.R.string.cancel, null)
@ -849,17 +1037,26 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun onFetchUserInfoSuccess(me: Account) {
glide.asBitmap()
Glide.with(header.accountHeaderBackground)
.asBitmap()
.load(me.header)
.into(header.accountHeaderBackground)
loadDrawerAvatar(me.avatar, false)
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
NotificationHelper.createNotificationChannelsForAccount(
accountManager.activeAccount!!,
this
)
// Setup push notifications
showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager)
showMigrationNoticeIfNecessary(
this,
binding.mainCoordinatorLayout,
binding.composeButton,
accountManager
)
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
lifecycleScope.launch {
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
@ -869,119 +1066,93 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
updateProfiles()
updateShortcut(this, accountManager.activeAccount!!)
shareShortcutHelper.updateShortcut(accountManager.activeAccount!!)
}
@SuppressLint("CheckResult")
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
if (hideTopToolbar) {
val navOnBottom = preferences.getString("mainNavPosition", "top") == "bottom"
val avatarView = if (navOnBottom) {
binding.bottomNavAvatar.show()
binding.bottomNavAvatar
val activeToolbar = if (hideTopToolbar) {
val navOnBottom = preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom"
if (navOnBottom) {
binding.bottomNav
} else {
binding.topNavAvatar.show()
binding.topNavAvatar
}
if (animateAvatars) {
Glide.with(this)
.load(avatarUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarView)
} else {
Glide.with(this)
.asBitmap()
.load(avatarUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarView)
binding.topNav
}
} else {
binding.bottomNavAvatar.hide()
binding.topNavAvatar.hide()
binding.mainToolbar
}
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
if (animateAvatars) {
glide.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
if (animateAvatars) {
Glide.with(this)
.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default)
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
if (resource is Animatable) resource.start()
activeToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
if (resource is Animatable) {
resource.start()
}
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
} else {
glide.asBitmap()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
override fun onLoadCleared(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
}
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
})
} else {
Glide.with(this)
.asBitmap()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default)
}
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(
BitmapDrawable(resources, resource),
navIconSize,
navIconSize
)
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
activeToolbar.navigationIcon = FixedSizeDrawable(
BitmapDrawable(resources, resource),
navIconSize,
navIconSize
)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
})
}
}
})
}
}
@ -1001,7 +1172,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
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() {
@ -1035,16 +1211,93 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
private fun explodeAnimationWasRequested(): Boolean {
return intent.getBooleanExtra(OPEN_WITH_EXPLODE_ANIMATION, false)
}
override fun getActionButton() = binding.composeButton
override fun androidInjector() = androidInjector
companion object {
const val OPEN_WITH_EXPLODE_ANIMATION = "explode"
private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val REDIRECT_URL = "redirectUrl"
const val OPEN_DRAFTS = "draft"
private const val REDIRECT_URL = "redirectUrl"
private const val OPEN_DRAFTS = "draft"
private const val TUSKY_ACCOUNT_ID = "tuskyAccountId"
private const val COMPOSE_OPTIONS = "composeOptions"
private const val NOTIFICATION_TYPE = "notificationType"
private const val NOTIFICATION_TAG = "notificationTag"
private const val NOTIFICATION_ID = "notificationId"
/**
* Switches the active account to the provided accountId and then stays on MainActivity
*/
@JvmStatic
fun accountSwitchIntent(context: Context, tuskyAccountId: Long): Intent {
return Intent(context, MainActivity::class.java).apply {
putExtra(TUSKY_ACCOUNT_ID, tuskyAccountId)
}
}
/**
* Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked
*/
@JvmStatic
fun openNotificationIntent(
context: Context,
tuskyAccountId: Long,
type: Notification.Type
): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(NOTIFICATION_TYPE, type.name)
}
}
/**
* Switches the active account to the accountId and then opens ComposeActivity with the provided options
* @param tuskyAccountId the id of the Tusky account to open the screen with. Set to -1 for current account.
* @param notificationId optional id of the notification that should be cancelled when this intent is opened
* @param notificationTag optional tag of the notification that should be cancelled when this intent is opened
*/
@JvmStatic
fun composeIntent(
context: Context,
options: ComposeActivity.ComposeOptions,
tuskyAccountId: Long = -1,
notificationTag: String? = null,
notificationId: Int = -1
): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
action = Intent.ACTION_SEND // so it can be opened via shortcuts
putExtra(COMPOSE_OPTIONS, options)
putExtra(NOTIFICATION_TAG, notificationTag)
putExtra(NOTIFICATION_ID, notificationId)
}
}
/**
* switches the active account to the accountId and then tries to resolve and show the provided url
*/
@JvmStatic
fun redirectIntent(context: Context, tuskyAccountId: Long, url: String): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(REDIRECT_URL, url)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
/**
* switches the active account to the provided accountId and then opens drafts
*/
fun draftIntent(context: Context, tuskyAccountId: Long): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(OPEN_DRAFTS, true)
}
}
}
}

View File

@ -27,17 +27,20 @@ import at.connyduck.calladapter.networkresult.fold
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.filters.EditFilterActivity
import com.keylesspalace.tusky.components.filters.FiltersActivity
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterV1
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import retrofit2.HttpException
import javax.inject.Inject
import kotlinx.coroutines.launch
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -47,7 +50,9 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject
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 var hashtag: String? = null
private var followTagItem: MenuItem? = null
@ -74,6 +79,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
Kind.FAVOURITES -> getString(R.string.title_favourites)
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
Kind.PUBLIC_TRENDING_STATUSES -> getString(R.string.title_public_trending_statuses)
else -> intent.getStringExtra(EXTRA_LIST_TITLE)
}
@ -132,9 +138,19 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
{
followTagItem?.isVisible = false
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.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)
}
)
@ -152,9 +168,19 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
{
followTagItem?.isVisible = true
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.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)
}
)
@ -169,6 +195,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
*/
private fun updateMuteTagMenuItems() {
val tag = hashtag ?: return
val hashedTag = "#$tag"
muteTagItem?.isVisible = true
muteTagItem?.isEnabled = false
@ -178,18 +205,17 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
mastodonApi.getFilters().fold(
{ filters ->
mutedFilter = filters.firstOrNull { filter ->
filter.context.contains(Filter.Kind.HOME.kind) && filter.keywords.any {
it.keyword == tag
}
// TODO shouldn't this be an exact match (only one keyword; exactly the hashtag)?
filter.context.contains(Filter.Kind.HOME.kind) && filter.title == hashedTag
}
updateTagMuteState(mutedFilter != null)
},
{ throwable ->
if (throwable is HttpException && throwable.code() == 404) {
if (throwable.isHttpNotFound()) {
mastodonApi.getFiltersV1().fold(
{ filters ->
mutedFilterV1 = filters.firstOrNull { filter ->
tag == filter.phrase && filter.context.contains(FilterV1.HOME)
hashedTag == filter.phrase && filter.context.contains(FilterV1.HOME)
}
updateTagMuteState(mutedFilterV1 != null)
},
@ -221,6 +247,9 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
val tag = hashtag ?: return true
lifecycleScope.launch {
var filterCreateSuccess = false
val hashedTag = "#$tag"
mastodonApi.createFilter(
title = "#$tag",
context = listOf(FilterV1.HOME),
@ -228,19 +257,31 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
expiresInSeconds = null
).fold(
{ filter ->
if (mastodonApi.addFilterKeyword(filterId = filter.id, keyword = tag, wholeWord = true).isSuccess) {
mutedFilter = filter
updateTagMuteState(true)
if (mastodonApi.addFilterKeyword(
filterId = filter.id,
keyword = hashedTag,
wholeWord = true
).isSuccess
) {
// must be requested again; otherwise does not contain the keyword (but server does)
mutedFilter = mastodonApi.getFilter(filter.id).getOrNull()
// TODO the preference key here ("home") is not meaningful; should probably be another event if any
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
filterCreateSuccess = true
} 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 ->
if (throwable is HttpException && throwable.code() == 404) {
if (throwable.isHttpNotFound()) {
mastodonApi.createFilterV1(
tag,
hashedTag,
listOf(FilterV1.HOME),
irreversible = false,
wholeWord = true,
@ -248,20 +289,50 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
).fold(
{ filter ->
mutedFilterV1 = filter
updateTagMuteState(true)
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
filterCreateSuccess = true
},
{ throwable ->
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)
}
)
} 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)
}
}
)
if (filterCreateSuccess) {
updateTagMuteState(true)
Snackbar.make(
binding.root,
getString(R.string.muting_hashtag_success_format, tag),
Snackbar.LENGTH_LONG
).apply {
setAction(R.string.action_view_filter) {
val intent = if (mutedFilter != null) {
Intent(this@StatusListActivity, EditFilterActivity::class.java).apply {
putExtra(EditFilterActivity.FILTER_TO_EDIT, mutedFilter)
}
} else {
Intent(this@StatusListActivity, FiltersActivity::class.java)
}
startActivityWithSlideInAnimation(intent)
}
show()
}
}
}
return true
@ -307,9 +378,19 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
eventHub.dispatch(PreferenceChangedEvent(Filter.Kind.HOME.kind))
mutedFilterV1 = null
mutedFilter = null
Snackbar.make(
binding.root,
getString(R.string.unmuting_hashtag_success_format, tag),
Snackbar.LENGTH_SHORT
).show()
},
{ 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)
}
)
@ -351,5 +432,10 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
putExtra(EXTRA_KIND, Kind.TAG.name)
putExtra(EXTRA_HASHTAG, hashtag)
}
fun newTrendingIntent(context: Context) =
Intent(context, StatusListActivity::class.java).apply {
putExtra(EXTRA_KIND, Kind.PUBLIC_TRENDING_STATUSES.name)
}
}
}

View File

@ -20,10 +20,10 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
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.viewmodel.TimelineViewModel
import com.keylesspalace.tusky.components.trending.TrendingFragment
import com.keylesspalace.tusky.components.trending.TrendingTagsFragment
import com.keylesspalace.tusky.fragment.NotificationsFragment
import java.util.Objects
/** this would be a good case for a sealed class, but that does not work nice with Room */
@ -33,9 +33,11 @@ const val NOTIFICATIONS = "Notifications"
const val LOCAL = "Local"
const val FEDERATED = "Federated"
const val DIRECT = "Direct"
const val TRENDING = "Trending"
const val TRENDING_TAGS = "TrendingTags"
const val TRENDING_STATUSES = "TrendingStatuses"
const val HASHTAG = "Hashtag"
const val LIST = "List"
const val BOOKMARKS = "Bookmarks"
data class TabData(
val id: String,
@ -92,11 +94,21 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
icon = R.drawable.ic_reblog_direct_24dp,
fragment = { ConversationsFragment.newInstance() }
)
TRENDING -> TabData(
id = TRENDING,
TRENDING_TAGS -> TabData(
id = TRENDING_TAGS,
text = R.string.title_public_trending_hashtags,
icon = R.drawable.ic_trending_up_24px,
fragment = { TrendingFragment.newInstance() }
fragment = { TrendingTagsFragment.newInstance() }
)
TRENDING_STATUSES -> TabData(
id = TRENDING_STATUSES,
text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_hot_24dp,
fragment = {
TimelineFragment.newInstance(
TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
)
}
)
HASHTAG -> TabData(
id = HASHTAG,
@ -104,16 +116,31 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
icon = R.drawable.ic_hashtag,
fragment = { args -> TimelineFragment.newHashtagInstance(args) },
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(
id = LIST,
text = R.string.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,
title = { arguments.getOrNull(1).orEmpty() }
)
BOOKMARKS -> TabData(
id = BOOKMARKS,
text = R.string.title_bookmarks,
icon = R.drawable.ic_bookmark_active_24dp,
fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.BOOKMARKS) }
)
else -> throw IllegalArgumentException("unknown tab type")
}
}

View File

@ -15,18 +15,10 @@
package com.keylesspalace.tusky
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatEditText
@ -38,34 +30,29 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
import at.connyduck.calladapter.networkresult.fold
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.MaterialContainerTransform
import com.keylesspalace.tusky.adapter.ItemInteractionListener
import com.keylesspalace.tusky.adapter.TabAdapter
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.components.account.list.ListSelectionFragment
import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.MastoList
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.viewBinding
import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListener {
class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener {
@Inject
lateinit var mastodonApi: MastodonApi
@ -73,6 +60,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
@Inject
lateinit var eventHub: EventHub
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
private val binding by viewBinding(ActivityTabPreferenceBinding::inflate)
private lateinit var currentTabs: MutableList<TabData>
@ -82,9 +72,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
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) {
override fun handleOnBackPressed() {
@ -109,14 +103,19 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
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)
binding.addTabRecyclerView.adapter = addTabAdapter
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
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)
}
@ -128,7 +127,11 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
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]
currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
currentTabs[target.bindingAdapterPosition] = temp
@ -148,7 +151,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)
viewHolder.itemView.elevation = 0f
}
@ -164,18 +170,12 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
toggleFab(false)
}
binding.maxTabsInfo.text = resources.getQuantityString(R.plurals.max_tab_number_reached, MAX_TAB_COUNT, MAX_TAB_COUNT)
updateAvailableTabs()
onBackPressedDispatcher.addCallback(onFabDismissedCallback)
}
override fun onTabAdded(tab: TabData) {
if (currentTabs.size >= MAX_TAB_COUNT) {
return
}
toggleFab(false)
if (tab.id == HASHTAG) {
@ -273,81 +273,24 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
editText.requestFocus()
}
private var listSelectDialog: ListSelectionFragment? = null
private fun showSelectListDialog() {
val adapter = object : ArrayAdapter<MastoList>(this, android.R.layout.simple_list_item_1) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
getItem(position)?.let { item -> (view as TextView).text = item.title }
return view
}
}
listSelectDialog = ListSelectionFragment.newInstance(null)
listSelectDialog?.show(supportFragmentManager, null)
val statusLayout = LinearLayout(this)
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()
}
)
}
return
}
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
start = CoroutineStart.LAZY
) {
try {
delay(delayMs)
progressView.show()
awaitCancellation()
} finally {
progressView.hide()
}
override fun onListSelected(list: MastoList) {
listSelectDialog?.dismiss()
listSelectDialog = null
val newTab = createTabDataFromId(LIST, listOf(list.id, list.title))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
updateAvailableTabs()
saveTabs()
}
private fun validateHashtag(input: CharSequence?): Boolean {
@ -378,17 +321,23 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
if (!currentTabs.contains(directMessagesTab)) {
addableTabs.add(directMessagesTab)
}
val trendingTab = createTabDataFromId(TRENDING)
if (!currentTabs.contains(trendingTab)) {
addableTabs.add(trendingTab)
val trendingTagsTab = createTabDataFromId(TRENDING_TAGS)
if (!currentTabs.contains(trendingTagsTab)) {
addableTabs.add(trendingTagsTab)
}
val bookmarksTab = createTabDataFromId(BOOKMARKS)
if (!currentTabs.contains(bookmarksTab)) {
addableTabs.add(bookmarksTab)
}
val trendingStatusesTab = createTabDataFromId(TRENDING_STATUSES)
if (!currentTabs.contains(trendingStatusesTab)) {
addableTabs.add(trendingStatusesTab)
}
addableTabs.add(createTabDataFromId(HASHTAG))
addableTabs.add(createTabDataFromId(LIST))
addTabAdapter.updateData(addableTabs)
binding.maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT)
currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT)
}
@ -419,8 +368,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
}
}
override fun androidInjector() = dispatchingAndroidInjector
companion object {
private const val MIN_TAB_COUNT = 2
private const val MAX_TAB_COUNT = 5
}
}

View File

@ -22,12 +22,13 @@ import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.settings.AppTheme
import com.keylesspalace.tusky.settings.NEW_INSTALL_SCHEMA_VERSION
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
import com.keylesspalace.tusky.settings.SCHEMA_VERSION
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.setAppNightMode
import com.keylesspalace.tusky.worker.PruneCacheWorker
@ -37,11 +38,10 @@ import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.conscrypt.Conscrypt
import java.security.Security
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import org.conscrypt.Conscrypt
class TuskyApplication : Application(), HasAndroidInjector {
@Inject
@ -71,12 +71,13 @@ class TuskyApplication : Application(), HasAndroidInjector {
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.
val oldVersion = sharedPreferences.getInt(PrefKeys.SCHEMA_VERSION, 0)
val oldVersion = sharedPreferences.getInt(
PrefKeys.SCHEMA_VERSION,
NEW_INSTALL_SCHEMA_VERSION
)
if (oldVersion != SCHEMA_VERSION) {
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
}
@ -87,15 +88,11 @@ class TuskyApplication : Application(), HasAndroidInjector {
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
// init night mode
val theme = sharedPreferences.getString("appTheme", APP_THEME_DEFAULT)
val theme = sharedPreferences.getString(APP_THEME, AppTheme.DEFAULT.value)
setAppNightMode(theme)
localeManager.setLocale()
RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it)
}
NotificationHelper.createWorkerNotificationChannel(this)
WorkManager.initialize(
@ -130,6 +127,27 @@ class TuskyApplication : Application(), HasAndroidInjector {
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) {
// 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
// theme does not unexpectedly change.
if (!sharedPreferences.contains(APP_THEME)) {
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)
}
editor.putInt(PrefKeys.SCHEMA_VERSION, newVersion)
editor.apply()
}

View File

@ -35,19 +35,17 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.core.content.IntentCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
import autodispose2.autoDispose
import com.bumptech.glide.Glide
import com.bumptech.glide.request.FutureTarget
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
@ -57,23 +55,28 @@ import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
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.viewdata.AttachmentViewData
import dagger.android.DispatchingAndroidInjector
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.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
class ViewMediaActivity :
BaseActivity(),
HasAndroidInjector,
ViewImageFragment.PhotoActionsListener,
ViewVideoFragment.VideoActionsListener {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -102,7 +105,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
supportPostponeEnterTransition()
// Gather the parameters.
attachments = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)
attachments = IntentCompat.getParcelableArrayListExtra(
intent,
EXTRA_ATTACHMENTS,
AttachmentViewData::class.java
)
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
@ -124,6 +131,7 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
binding.toolbar.title = getPageTitle(position)
adjustScreenWakefulness()
}
})
@ -155,6 +163,8 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
window.sharedElementEnterTransition.removeListener(this)
}
})
adjustScreenWakefulness()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -211,7 +221,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun downloadMedia() {
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
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 request = DownloadManager.Request(Uri.parse(url))
@ -221,8 +235,13 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun requestDownloadMedia() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) { _, grantResults ->
if (
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
downloadMedia()
} else {
showErrorDialog(
@ -239,7 +258,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun onOpenStatus() {
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() {
@ -272,7 +293,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun shareFile(file: File, mimeType: String?) {
ShareCompat.IntentBuilder(this)
.setType(mimeType)
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
.addStream(
FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)
)
.setChooserTitle(R.string.send_media_to)
.startChooser()
}
@ -283,46 +306,37 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
isCreating = true
binding.progressBarShare.visibility = View.VISIBLE
invalidateOptionsMenu()
val file = File(directory, getTemporaryMediaFilename("png"))
val futureTask: FutureTarget<Bitmap> =
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit()
Single.fromCallable {
val bitmap = futureTask.get()
try {
val stream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
stream.close()
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")
lifecycleScope.launch {
val file = File(directory, getTemporaryMediaFilename("png"))
val result = try {
val bitmap =
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submitAsync()
try {
FileOutputStream(file).use { stream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
}
},
{ error ->
isCreating = false
invalidateOptionsMenu()
binding.progressBarShare.visibility = View.GONE
Log.e(TAG, "Failed to download image", error)
true
} catch (ioe: IOException) {
// FileNotFoundException is covered by IOException
Log.e(TAG, "Error writing temporary media.")
false
}.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) {
@ -342,6 +356,17 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
shareFile(file, mimeType)
}
// Prevent this activity from dimming or sleeping the screen if, and only if, it is playing video or audio
private fun adjustScreenWakefulness() {
attachments?.run {
if (get(binding.viewPager.currentItem).attachment.type == Attachment.Type.IMAGE) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
override fun androidInjector() = androidInjector
companion object {
@ -351,7 +376,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private const val TAG = "ViewMediaActivity"
@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)
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index)

View File

@ -24,7 +24,9 @@ import com.keylesspalace.tusky.entity.StringField
import com.keylesspalace.tusky.util.BindingHolder
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 var maxNameLength: Int? = null
@ -62,8 +64,15 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
override fun getItemCount() = fieldData.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEditFieldBinding> {
val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemEditFieldBinding> {
val binding = ItemEditFieldBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
@ -83,10 +92,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
onFieldsChanged()
}
holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
onFieldsChanged()
}
// Ensure the textview contents are selectable

View File

@ -28,7 +28,10 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(
context,
R.layout.item_autocomplete_account
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = if (convertView == null) {
@ -47,7 +50,7 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
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 animateAvatar = pm.getBoolean("animateGifAvatars", false)
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
}

View File

@ -31,13 +31,20 @@ class EmojiAdapter(
private val animate: Boolean
) : 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) }
override fun getItemCount() = emojiList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEmojiButtonBinding> {
val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemEmojiButtonBinding> {
val binding = ItemEmojiButtonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}

View File

@ -21,12 +21,10 @@ import android.text.Spanned
import android.text.style.StyleSpan
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.notifications.NotificationsPagingAdapter
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.loadAvatar
@ -35,33 +33,12 @@ import com.keylesspalace.tusky.util.setClickableText
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.NotificationViewData
class FollowRequestViewHolder(
private val binding: ItemFollowRequestBinding,
private val accountActionListener: AccountActionListener,
private val linkListener: LinkListener,
private val showHeader: Boolean
) : NotificationsPagingAdapter.ViewHolder, RecyclerView.ViewHolder(binding.root) {
override fun bind(
viewData: NotificationViewData,
payloads: List<*>?,
statusDisplayOptions: StatusDisplayOptions
) {
// Skip updates with payloads. That indicates a timestamp update, and
// this view does not have timestamps.
if (!payloads.isNullOrEmpty()) return
setupWithAccount(
viewData.account,
statusDisplayOptions.animateAvatars,
statusDisplayOptions.animateEmojis,
statusDisplayOptions.showBotOverlay
)
setupActionListener(accountActionListener, viewData.account.id)
}
) : RecyclerView.ViewHolder(binding.root) {
fun setupWithAccount(
account: TimelineAccount,
@ -82,16 +59,14 @@ class FollowRequestViewHolder(
wrappedName
)
binding.notificationTextView.text = SpannableStringBuilder(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)
}
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
if (account.note.isEmpty()) {
binding.accountNote.hide()
@ -102,7 +77,9 @@ class FollowRequestViewHolder(
.emojify(account.emojis, binding.accountNote, animateEmojis)
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)
binding.avatarBadge.visible(showBotOverlay && account.bot)
}

View File

@ -26,7 +26,11 @@ import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.modernLanguageCode
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 {
return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))

View File

@ -0,0 +1,708 @@
/* 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) {
}
}

View File

@ -67,7 +67,10 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
.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)
return BindingHolder(binding)
}

View File

@ -40,7 +40,11 @@ class PreviewPollOptionsAdapter : RecyclerView.Adapter<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

View File

@ -20,48 +20,20 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.notifications.NotificationActionListener
import com.keylesspalace.tusky.components.notifications.NotificationsPagingAdapter
import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionListener
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
import com.keylesspalace.tusky.entity.Report
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.viewdata.NotificationViewData
import java.util.Date
class ReportNotificationViewHolder(
private val binding: ItemReportNotificationBinding,
private val notificationActionListener: NotificationActionListener
) : NotificationsPagingAdapter.ViewHolder, RecyclerView.ViewHolder(binding.root) {
private val binding: ItemReportNotificationBinding
) : RecyclerView.ViewHolder(binding.root) {
override fun bind(
viewData: NotificationViewData,
payloads: List<*>?,
statusDisplayOptions: StatusDisplayOptions
) {
// Skip updates with payloads. That indicates a timestamp update, and
// this view does not have timestamps.
if (!payloads.isNullOrEmpty()) return
setupWithReport(
viewData.account,
viewData.report!!,
statusDisplayOptions.animateAvatars,
statusDisplayOptions.animateEmojis
)
setupActionListener(
notificationActionListener,
viewData.report.targetAccount.id,
viewData.account.id,
viewData.report.id
)
}
private fun setupWithReport(
fun setupWithReport(
reporter: TimelineAccount,
report: Report,
animateAvatar: Boolean,
@ -69,7 +41,7 @@ class ReportNotificationViewHolder(
) {
val reporterName = reporter.name.unicodeWrap().emojify(
reporter.emojis,
binding.root,
itemView,
animateEmojis
)
val reporteeName = report.targetAccount.name.unicodeWrap().emojify(
@ -77,19 +49,11 @@ class ReportNotificationViewHolder(
itemView,
animateEmojis
)
val icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_flag_24dp)
val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp)
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
binding.notificationTopText.text = itemView.context.getString(
R.string.notification_header_report_format,
reporterName,
reporteeName
)
binding.notificationSummary.text = itemView.context.getString(
R.string.notification_summary_report_format,
getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time),
report.status_ids?.size ?: 0
)
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, getRelativeTimeSpanString(itemView.context, report.createdAt.time, System.currentTimeMillis()), report.statusIds?.size ?: 0)
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
// Fancy avatar inset
@ -110,7 +74,7 @@ class ReportNotificationViewHolder(
)
}
private fun setupActionListener(
fun setupActionListener(
listener: NotificationActionListener,
reporteeId: String,
reporterId: String,

View File

@ -48,6 +48,7 @@ import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.FilterResult;
import com.keylesspalace.tusky.entity.HashTag;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.Translation;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
import com.keylesspalace.tusky.util.AttachmentHelper;
@ -56,6 +57,7 @@ import com.keylesspalace.tusky.util.CompositeWithOpaqueBackground;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.LocaleUtilsKt;
import com.keylesspalace.tusky.util.NumberUtils;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.TimestampUtils;
@ -66,11 +68,13 @@ import com.keylesspalace.tusky.viewdata.PollOptionViewData;
import com.keylesspalace.tusky.viewdata.PollViewData;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.keylesspalace.tusky.viewdata.TranslationViewData;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.helpers.Utils;
@ -120,6 +124,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected final TextView filteredPlaceholderLabel;
protected final Button filteredPlaceholderShowButton;
protected final ConstraintLayout statusContainer;
private final TextView translationStatusView;
private final Button untranslateButton;
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
@ -130,7 +137,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private final Drawable mediaPreviewUnloaded;
protected StatusBaseViewHolder(View itemView) {
protected StatusBaseViewHolder(@NonNull View itemView) {
super(itemView);
displayName = itemView.findViewById(R.id.status_display_name);
username = itemView.findViewById(R.id.status_username);
@ -151,10 +158,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
mediaLabels = new TextView[]{
itemView.findViewById(R.id.status_media_label_0),
itemView.findViewById(R.id.status_media_label_1),
itemView.findViewById(R.id.status_media_label_2),
itemView.findViewById(R.id.status_media_label_3)
itemView.findViewById(R.id.status_media_label_0),
itemView.findViewById(R.id.status_media_label_1),
itemView.findViewById(R.id.status_media_label_2),
itemView.findViewById(R.id.status_media_label_3)
};
mediaDescriptions = new CharSequence[mediaLabels.length];
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()));
((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.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
@ -191,14 +201,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton));
}
protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) {
protected void setDisplayName(@NonNull String name, @Nullable List<Emoji> customEmojis, @NonNull StatusDisplayOptions statusDisplayOptions) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
);
displayName.setText(emojifiedName);
}
protected void setUsername(String name) {
protected void setUsername(@Nullable String name) {
Context context = username.getContext();
String usernameText = context.getString(R.string.post_username_format, name);
username.setText(usernameText);
@ -210,7 +220,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected void setSpoilerAndContent(@NonNull StatusViewData.Concrete status,
@NonNull StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener) {
final @NonNull StatusActionListener listener) {
Status actionable = status.getActionable();
String spoilerText = status.getSpoilerText();
@ -221,7 +231,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (sensitive) {
CharSequence emojiSpoiler = CustomEmojiHelper.emojify(
spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis()
spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis()
);
contentWarningDescription.setText(emojiSpoiler);
contentWarningDescription.setVisibility(View.VISIBLE);
@ -271,9 +281,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Status actionable = status.getActionable();
Spanned content = status.getContent();
List<Status.Mention> mentions = actionable.getMentions();
List<HashTag> tags =actionable.getTags();
List<HashTag> tags = actionable.getTags();
List<Emoji> emojis = actionable.getEmojis();
PollViewData poll = PollViewDataKt.toViewData(actionable.getPoll());
PollViewData poll = PollViewDataKt.toViewData(status.getPoll());
if (expanded) {
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,
@Nullable String rebloggedUrl,
@Nullable String rebloggedUrl,
boolean isBot,
StatusDisplayOptions statusDisplayOptions) {
@ -315,8 +325,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (statusDisplayOptions.showBotOverlay() && isBot) {
avatarInset.setVisibility(View.VISIBLE);
Glide.with(avatarInset)
.load(R.drawable.bot_badge)
.into(avatarInset);
.load(R.drawable.bot_badge)
.into(avatarInset);
} else {
avatarInset.setVisibility(View.GONE);
}
@ -330,17 +340,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
avatarInset.setVisibility(View.VISIBLE);
avatarInset.setBackground(null);
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp,
statusDisplayOptions.animateAvatars(), null);
statusDisplayOptions.animateAvatars(), null);
avatarRadius = avatarRadius36dp;
}
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius,
ImageLoadingHelper.loadAvatar(
url,
avatar,
avatarRadius,
statusDisplayOptions.animateAvatars(),
Collections.singletonList(new CompositeWithOpaqueBackground(avatar)));
Collections.singletonList(new CompositeWithOpaqueBackground(MaterialColors.getColor(avatar, android.R.attr.colorBackground)))
);
}
protected void setMetaData(StatusViewData.Concrete statusViewData, StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
protected void setMetaData(@NonNull StatusViewData.Concrete statusViewData, @NonNull StatusDisplayOptions statusDisplayOptions, @NonNull StatusActionListener listener) {
Status status = statusViewData.getActionable();
Date createdAt = status.getCreatedAt();
@ -379,8 +393,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
long then = createdAt.getTime();
long now = System.currentTimeMillis();
return DateUtils.getRelativeTimeSpanString(then, now,
DateUtils.SECOND_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE);
DateUtils.SECOND_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE);
}
}
}
@ -463,9 +477,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
imageView.removeFocalPoint();
Glide.with(imageView)
.load(placeholder)
.centerInside()
.into(imageView);
.load(placeholder)
.centerInside()
.into(imageView);
} else {
Focus focus = meta != null ? meta.getFocus() : null;
@ -473,29 +487,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
imageView.setFocalPoint(focus);
Glide.with(imageView.getContext())
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(imageView)
.into(imageView);
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(imageView)
.into(imageView);
} else {
imageView.removeFocalPoint();
Glide.with(imageView)
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.into(imageView);
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.into(imageView);
}
}
}
protected void setMediaPreviews(
final List<Attachment> attachments,
boolean sensitive,
final StatusActionListener listener,
boolean showingContent,
boolean useBlurhash
final @NonNull List<Attachment> attachments,
boolean sensitive,
final @NonNull StatusActionListener listener,
boolean showingContent,
boolean useBlurhash
) {
mediaPreview.setVisibility(View.VISIBLE);
@ -514,10 +528,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
loadImage(
imageView,
showingContent ? previewUrl : null,
attachment.getMeta(),
useBlurhash ? attachment.getBlurhash() : null
imageView,
showingContent ? previewUrl : null,
attachment.getMeta(),
useBlurhash ? attachment.getBlurhash() : null
);
final Attachment.Type type = attachment.getType();
@ -579,13 +593,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
Context context = itemView.getContext();
CharSequence label = (sensitive && !showingContent) ?
context.getString(R.string.post_sensitive_media_title) :
mediaDescriptions[index];
context.getString(R.string.post_sensitive_media_title) :
mediaDescriptions[index];
mediaLabels[index].setText(label);
}
protected void setMediaLabel(List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent) {
protected void setMediaLabel(@NonNull List<Attachment> attachments, boolean sensitive,
final @NonNull StatusActionListener listener, boolean showingContent) {
Context context = itemView.getContext();
for (int i = 0; i < mediaLabels.length; i++) {
TextView mediaLabel = mediaLabels[i];
@ -606,7 +620,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
private void setAttachmentClickListener(View view, StatusActionListener listener,
private void setAttachmentClickListener(View view, @NonNull StatusActionListener listener,
int index, Attachment attachment, boolean animateTransition) {
view.setOnClickListener(v -> {
int position = getBindingAdapterPosition();
@ -630,10 +644,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaShow.setVisibility(View.GONE);
}
protected void setupButtons(final StatusActionListener listener,
final String accountId,
final String statusContent,
StatusDisplayOptions statusDisplayOptions) {
protected void setupButtons(final @NonNull StatusActionListener listener,
final @NonNull String accountId,
final @Nullable String statusContent,
@NonNull StatusDisplayOptions statusDisplayOptions) {
View.OnClickListener profileButtonClickListener = button -> listener.onViewAccount(accountId);
avatar.setOnClickListener(profileButtonClickListener);
@ -723,8 +737,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
popup.setOnMenuItemClickListener(item -> {
listener.onReblog(!buttonState, position);
if(!buttonState) {
if (!buttonState) {
reblogButton.playAnimation();
reblogButton.setChecked(true);
}
return true;
});
@ -744,16 +759,17 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
popup.setOnMenuItemClickListener(item -> {
listener.onFavourite(!buttonState, position);
if(!buttonState) {
if (!buttonState) {
favouriteButton.playAnimation();
favouriteButton.setChecked(true);
}
return true;
});
popup.show();
}
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions) {
public void setupWithStatus(@NonNull StatusViewData.Concrete status, final @NonNull StatusActionListener listener,
@NonNull StatusDisplayOptions statusDisplayOptions) {
this.setupWithStatus(status, listener, statusDisplayOptions, null);
}
@ -764,16 +780,16 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (payloads == null) {
Status actionable = status.getActionable();
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
setUsername(status.getUsername());
setUsername(actionable.getAccount().getUsername());
setMetaData(status, statusDisplayOptions, listener);
setIsReply(actionable.getInReplyToId() != null);
setReplyCount(actionable.getRepliesCount(), statusDisplayOptions.showStatsInline());
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
actionable.getAccount().getBot(), statusDisplayOptions);
actionable.getAccount().getBot(), statusDisplayOptions);
setReblogged(actionable.getReblogged());
setFavourited(actionable.getFavourited());
setBookmarked(actionable.getBookmarked());
List<Attachment> attachments = actionable.getAttachments();
List<Attachment> attachments = status.getAttachments();
boolean sensitive = actionable.getSensitive();
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
@ -795,8 +811,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setupCard(status, status.isExpanded(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener);
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
statusDisplayOptions);
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
statusDisplayOptions);
setTranslationStatus(status, listener);
setRebloggingEnabled(actionable.isRebloggingAllowed(), actionable.getVisibility());
setSpoilerAndContent(status, statusDisplayOptions, listener);
@ -821,6 +840,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) {
if (status.getFilterAction() != Filter.Action.WARN) {
showFilteredPlaceholder(false);
@ -843,7 +885,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
filteredPlaceholderShowButton.setOnClickListener(view -> listener.clearWarningAction(getBindingAdapterPosition()));
}
protected static boolean hasPreviewableAttachment(List<Attachment> attachments) {
protected static boolean hasPreviewableAttachment(@NonNull List<Attachment> attachments) {
for (Attachment attachment : attachments) {
if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) {
return false;
@ -858,54 +900,84 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Status actionable = status.getActionable();
String description = context.getString(R.string.description_status,
actionable.getAccount().getDisplayName(),
getContentWarningDescription(context, status),
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
getReblogDescription(context, status),
status.getUsername(),
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "",
actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "",
getMediaDescription(context, status),
getVisibilityDescription(context, actionable.getVisibility()),
getFavsText(context, actionable.getFavouritesCount()),
getReblogsText(context, actionable.getReblogsCount()),
getPollDescription(status, context, statusDisplayOptions)
// 1 display_name
actionable.getAccount().getDisplayName(),
// 2 CW?
getContentWarningDescription(context, status),
// 3 content?
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
// 4 date
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
// 5 edited?
actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
// 6 reposted_by?
getReblogDescription(context, status),
// 7 username
actionable.getAccount().getUsername(),
// 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);
}
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,
@NonNull StatusViewData.Concrete status) {
@Nullable
Status reblog = status.getRebloggingStatus();
if (reblog != null) {
return context
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
} else {
return "";
}
}
private static CharSequence getMediaDescription(Context context,
@NonNull StatusViewData.Concrete status) {
if (status.getActionable().getAttachments().isEmpty()) {
@NonNull StatusViewData.Concrete viewData) {
if (viewData.getAttachments().isEmpty()) {
return "";
}
StringBuilder mediaDescriptions = CollectionsKt.fold(
status.getActionable().getAttachments(),
new StringBuilder(),
(builder, a) -> {
if (a.getDescription() == null) {
String placeholder =
context.getString(R.string.description_post_media_no_description_placeholder);
return builder.append(placeholder);
} else {
builder.append("; ");
return builder.append(a.getDescription());
}
});
viewData.getAttachments(),
new StringBuilder(),
(builder, a) -> {
if (a.getDescription() == null) {
String placeholder =
context.getString(R.string.description_post_media_no_description_placeholder);
return builder.append(placeholder);
} else {
builder.append("; ");
return builder.append(a.getDescription());
}
});
return context.getString(R.string.description_post_media, mediaDescriptions);
}
@ -918,7 +990,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
protected static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) {
@NonNull
protected static CharSequence getVisibilityDescription(@NonNull Context context, @Nullable Status.Visibility visibility) {
if (visibility == null) {
return "";
@ -947,7 +1020,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
Context context,
StatusDisplayOptions statusDisplayOptions) {
PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll());
PollViewData poll = PollViewDataKt.toViewData(status.getPoll());
if (poll == null) {
return "";
} else {
@ -962,27 +1035,21 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions,
context);
context);
return context.getString(R.string.description_poll, args);
}
}
protected CharSequence getFavsText(Context context, int count) {
if (count > 0) {
String countString = numberFormat.format(count);
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
} else {
return "";
}
@NonNull
protected CharSequence getFavsText(@NonNull Context context, int count) {
String countString = numberFormat.format(count);
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
}
protected CharSequence getReblogsText(Context context, int count) {
if (count > 0) {
String countString = numberFormat.format(count);
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
} else {
return "";
}
@NonNull
protected CharSequence getReblogsText(@NonNull Context context, int count) {
String countString = numberFormat.format(count);
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
}
private void setupPoll(PollViewData poll, List<Emoji> emojis,
@ -1005,26 +1072,26 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
};
pollAdapter.setup(
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
PollAdapter.RESULT,
viewThreadListener,
statusDisplayOptions.animateEmojis()
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
PollAdapter.RESULT,
viewThreadListener,
statusDisplayOptions.animateEmojis()
);
pollButton.setVisibility(View.GONE);
} else {
// voting possible
pollAdapter.setup(
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE,
null,
statusDisplayOptions.animateEmojis()
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE,
null,
statusDisplayOptions.animateEmojis()
);
pollButton.setVisibility(View.VISIBLE);
@ -1077,11 +1144,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
protected void setupCard(
final StatusViewData.Concrete status,
boolean expanded,
final CardViewMode cardViewMode,
final StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener
final @NonNull StatusViewData.Concrete status,
boolean expanded,
final @NonNull CardViewMode cardViewMode,
final @NonNull StatusDisplayOptions statusDisplayOptions,
final @NonNull StatusActionListener listener
) {
if (cardView == null) {
return;
@ -1095,7 +1162,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
actionable.getPoll() == null &&
card != null &&
!TextUtils.isEmpty(card.getUrl()) &&
(!actionable.getSensitive() || expanded) &&
(TextUtils.isEmpty(actionable.getSpoilerText()) || expanded) &&
(!status.isCollapsible() || !status.isCollapsed())) {
cardView.setVisibility(View.VISIBLE);
@ -1119,14 +1186,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
int radius = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_radius);
.getDimensionPixelSize(R.dimen.card_radius);
ShapeAppearanceModel.Builder cardImageShape = ShapeAppearanceModel.builder();
if (card.getWidth() > card.getHeight()) {
cardView.setOrientation(LinearLayout.VERTICAL);
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;
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
@ -1136,7 +1203,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
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().width = ViewGroup.LayoutParams.MATCH_PARENT;
cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius);
@ -1148,40 +1215,40 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
RequestBuilder<Drawable> builder = Glide.with(cardImage.getContext())
.load(card.getImage())
.dontTransform();
.load(card.getImage())
.dontTransform();
if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
builder = builder.placeholder(decodeBlurHash(card.getBlurhash()));
}
builder.into(cardImage);
} else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) {
int radius = cardImage.getContext().getResources()
.getDimensionPixelSize(R.dimen.card_radius);
.getDimensionPixelSize(R.dimen.card_radius);
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
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().width = ViewGroup.LayoutParams.MATCH_PARENT;
ShapeAppearanceModel cardImageShape = ShapeAppearanceModel.builder()
.setTopLeftCorner(CornerFamily.ROUNDED, radius)
.setBottomLeftCorner(CornerFamily.ROUNDED, radius)
.build();
.setTopLeftCorner(CornerFamily.ROUNDED, radius)
.setBottomLeftCorner(CornerFamily.ROUNDED, radius)
.build();
cardImage.setShapeAppearanceModel(cardImageShape);
cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
Glide.with(cardImage.getContext())
.load(decodeBlurHash(card.getBlurhash()))
.dontTransform()
.into(cardImage);
.load(decodeBlurHash(card.getBlurhash()))
.dontTransform()
.into(cardImage);
} else {
cardView.setOrientation(LinearLayout.HORIZONTAL);
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
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().width = ViewGroup.LayoutParams.MATCH_PARENT;
@ -1190,8 +1257,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardImage.setScaleType(ImageView.ScaleType.CENTER);
Glide.with(cardImage.getContext())
.load(R.drawable.card_image_placeholder)
.into(cardImage);
.load(R.drawable.card_image_placeholder)
.into(cardImage);
}
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
@ -1199,8 +1266,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
cardView.setOnClickListener(visitLink);
// View embedded photos in our image viewer instead of opening the browser
cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbedUrl()) ?
v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbedUrl())) :
visitLink);
v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbedUrl())) :
visitLink);
cardView.setClipToOutline(true);
} else {

View File

@ -9,11 +9,13 @@ import android.text.method.LinkMovementMethod;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.ViewUtils;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
@ -23,6 +25,7 @@ import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.NoUnderlineURLSpan;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ViewExtensionsKt;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
@ -35,7 +38,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
private static final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
public StatusDetailedViewHolder(View view) {
public StatusDetailedViewHolder(@NonNull View view) {
super(view);
reblogs = view.findViewById(R.id.status_reblogs);
favourites = view.findViewById(R.id.status_favourites);
@ -43,7 +46,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
protected void setMetaData(StatusViewData.Concrete statusViewData, StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
protected void setMetaData(@NonNull StatusViewData.Concrete statusViewData, @NonNull StatusDisplayOptions statusDisplayOptions, @NonNull StatusActionListener listener) {
Status status = statusViewData.getActionable();
@ -57,8 +60,8 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
if (visibilityIcon != null) {
ImageSpan visibilityIconSpan = new ImageSpan(
visibilityIcon,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? DynamicDrawableSpan.ALIGN_CENTER : DynamicDrawableSpan.ALIGN_BASELINE
visibilityIcon,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? DynamicDrawableSpan.ALIGN_CENTER : DynamicDrawableSpan.ALIGN_BASELINE
);
sb.setSpan(visibilityIconSpan, 0, visibilityString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@ -67,7 +70,6 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
Date createdAt = status.getCreatedAt();
if (createdAt != null) {
sb.append(" ");
sb.append(dateFormat.format(createdAt));
}
@ -95,10 +97,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();
if (app != null) {
sb.append(metadataJoiner);
if (app.getWebsite() != null) {
@ -114,25 +122,8 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) {
if (reblogCount > 0) {
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.setText(getReblogsText(reblogs.getContext(), reblogCount));
favourites.setText(getFavsText(favourites.getContext(), favCount));
reblogs.setOnClickListener(v -> {
int position = getBindingAdapterPosition();
@ -155,8 +146,8 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
@Nullable Object payloads) {
// We never collapse statuses in the detail view
StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ?
status.copyWithCollapsed(false) :
status;
status.copyWithCollapsed(false) :
status;
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads);
setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
@ -165,7 +156,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
if (!statusDisplayOptions.hideStats()) {
setReblogAndFavCount(actionable.getReblogsCount(),
actionable.getFavouritesCount(), listener);
actionable.getFavouritesCount(), listener);
} else {
hideQuantitativeStats();
}
@ -197,7 +188,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
final Drawable visibilityDrawable = AppCompatResources.getDrawable(
this.metaInfo.getContext(), visibilityIcon
this.metaInfo.getContext(), visibilityIcon
);
if (visibilityDrawable == null) {
return null;
@ -205,10 +196,10 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
final int size = (int) this.metaInfo.getTextSize();
visibilityDrawable.setBounds(
0,
0,
size,
size
0,
0,
size,
size
);
visibilityDrawable.setTint(this.metaInfo.getCurrentTextColor());

View File

@ -51,7 +51,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
private final TextView favouritedCountLabel;
private final TextView reblogsCountLabel;
public StatusViewHolder(View itemView) {
public StatusViewHolder(@NonNull View itemView) {
super(itemView);
statusInfo = itemView.findViewById(R.id.status_info);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);

View File

@ -56,7 +56,11 @@ class TabAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
val binding = if (small) {
ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ItemTabPreferenceSmallBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
} else {
ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
}

View File

@ -1,20 +1,18 @@
package com.keylesspalace.tusky.appstore
import com.google.gson.Gson
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
class CacheUpdater @Inject constructor(
eventHub: EventHub,
accountManager: AccountManager,
appDatabase: AppDatabase,
gson: Gson
appDatabase: AppDatabase
) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@ -26,22 +24,20 @@ class CacheUpdater @Inject constructor(
eventHub.events.collect { event ->
val accountId = accountManager.activeAccount?.id ?: return@collect
when (event) {
is FavoriteEvent ->
timelineDao.setFavourited(accountId, event.statusId, event.favourite)
is ReblogEvent ->
timelineDao.setReblogged(accountId, event.statusId, event.reblog)
is BookmarkEvent ->
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark)
is StatusChangedEvent -> {
val status = event.status
timelineDao.update(
accountId = accountId,
status = status
)
}
is UnfollowEvent ->
timelineDao.removeAllByUser(accountId, event.accountId)
is StatusDeletedEvent ->
timelineDao.delete(accountId, event.statusId)
is PollVoteEvent -> {
val pollString = gson.toJson(event.poll)
timelineDao.setVoted(accountId, event.statusId, pollString)
timelineDao.setVoted(accountId, event.statusId, event.poll)
}
is PinEvent ->
timelineDao.setPinned(accountId, event.statusId, event.pinned)
}
}
}

View File

@ -2,12 +2,11 @@ package com.keylesspalace.tusky.appstore
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Event
data class ReblogEvent(val statusId: String, val reblog: Boolean) : Event
data class BookmarkEvent(val statusId: String, val bookmark: Boolean) : Event
data class StatusChangedEvent(val status: Status) : Event
data class MuteConversationEvent(val statusId: String, val mute: Boolean) : Event
data class UnfollowEvent(val accountId: String) : Event
data class BlockEvent(val accountId: String) : Event
@ -15,11 +14,16 @@ data class MuteEvent(val accountId: String) : Event
data class StatusDeletedEvent(val statusId: String) : Event
data class StatusComposedEvent(val status: Status) : Event
data class StatusScheduledEvent(val status: Status) : Event
data class StatusEditedEvent(val originalId: String, val status: Status) : Event
data class ProfileEditedEvent(val newProfileData: Account) : Event
data class PreferenceChangedEvent(val preferenceKey: String) : Event
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event
data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
data class DomainMuteEvent(val instance: String) : Event
data class AnnouncementReadEvent(val announcementId: String) : Event
data class PinEvent(val statusId: String, val pinned: Boolean) : Event
data class FilterUpdatedEvent(val filterContext: List<String>) : Event
data class NewNotificationsEvent(
val accountId: String,
val notifications: List<Notification>
) : Event
data class ConversationsLoadingEvent(val accountId: String) : Event
data class NotificationsLoadingEvent(val accountId: String) : Event

View File

@ -1,19 +1,33 @@
package com.keylesspalace.tusky.appstore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import java.util.function.Consumer
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
interface Event
@Singleton
class EventHub @Inject constructor() {
private val sharedEventFlow: MutableSharedFlow<Event> = MutableSharedFlow()
val events: Flow<Event> = sharedEventFlow
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun dispatch(event: Event) {
sharedEventFlow.emit(event)
_events.emit(event)
}
// TODO remove as soon as NotificationsFragment is Kotlin
fun subscribe(lifecycleOwner: LifecycleOwner, consumer: Consumer<Event>) {
lifecycleOwner.lifecycleScope.launch {
events.collect { event ->
consumer.accept(event)
}
}
}
}

View File

@ -22,9 +22,11 @@ import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.LayerDrawable
import android.graphics.Typeface
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.text.style.StyleSpan
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
@ -32,10 +34,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.Px
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.MenuProvider
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
@ -43,11 +47,13 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.updatePadding
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.shape.MaterialShapeDrawable
@ -60,7 +66,7 @@ import com.keylesspalace.tusky.EditProfileActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity
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.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity
@ -86,6 +92,7 @@ import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
import com.keylesspalace.tusky.util.setClickableText
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
@ -102,6 +109,7 @@ import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs
import kotlinx.coroutines.launch
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, HasAndroidInjector, LinkListener {
@ -173,9 +181,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
animateAvatar = sharedPrefs.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
hideFab = sharedPrefs.getBoolean("fabHide", false)
hideFab = sharedPrefs.getBoolean(PrefKeys.FAB_HIDE, false)
handleWindowInsets()
setupToolbar()
@ -261,9 +269,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountFragmentViewPager.adapter = adapter
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]
}.attach()
@ -295,7 +312,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val right = insets.getInsets(systemBars()).right
val bottom = insets.getInsets(systemBars()).bottom
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
}
@ -312,30 +337,24 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
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)
binding.accountToolbar.background = toolbarBackground
// Provide a non-transparent background to the navigation and overflow icons to ensure
// they remain visible over whatever the profile background image might be.
val backgroundCircle = AppCompatResources.getDrawable(this, R.drawable.background_circle)!!
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.accountToolbar.setNavigationIcon(R.drawable.ic_arrow_back_with_background)
binding.accountToolbar.setOverflowIcon(
AppCompatResources.getDrawable(this, R.drawable.ic_more_with_background)
)
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)
elevation = appBarElevation
shapeAppearanceModel = ShapeAppearanceModel.builder()
@ -375,11 +394,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
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
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)
@ -397,31 +422,46 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
* Subscribe to data loaded at the view model
*/
private fun subscribeObservables() {
viewModel.accountData.observe(this) {
when (it) {
is Success -> onAccountChanged(it.data)
is Error -> {
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
lifecycleScope.launch {
viewModel.accountData.collect {
if (it == null) return@collect
when (it) {
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() }
.show()
}
is Loading -> { }
}
}
viewModel.relationshipData.observe(this) {
val relation = it?.data
if (relation != null) {
onRelationshipChanged(relation)
lifecycleScope.launch {
viewModel.noteSaved.collect {
binding.saveNoteInfo.visible(it, View.INVISIBLE)
}
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
@ -438,10 +478,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
*/
private fun setupRefreshLayout() {
binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() }
viewModel.isRefreshing.observe(
this
) { isRefreshing ->
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
lifecycleScope.launch {
viewModel.isRefreshing.collect { isRefreshing ->
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
}
}
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
@ -460,25 +500,33 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val fullUsername = getFullUsername(loadedAccount)
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, fullUsername))
Snackbar.make(binding.root, getString(R.string.account_username_copied), Snackbar.LENGTH_SHORT)
Snackbar.make(
binding.root,
getString(R.string.account_username_copied),
Snackbar.LENGTH_SHORT
)
.show()
}
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)
accountFieldAdapter.fields = account.fields.orEmpty()
accountFieldAdapter.emojis = account.emojis.orEmpty()
accountFieldAdapter.fields = account.fields
accountFieldAdapter.emojis = account.emojis
accountFieldAdapter.notifyDataSetChanged()
binding.accountLockedImageView.visible(account.locked)
binding.accountBadgeTextView.visible(account.bot)
updateAccountAvatar()
updateToolbar()
updateBadges()
updateMovedAccount()
updateRemoteAccount()
updateAccountJoinedDate()
@ -491,6 +539,39 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
}
}
private fun updateBadges() {
binding.accountBadgeContainer.removeAllViews()
val isLight = resources.getBoolean(R.bool.lightNavigationBar)
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
)
binding.accountBadgeContainer.addView(badgeView)
}
loadedAccount?.roles?.forEach { role ->
val badgeColor = if (role.color.isNotBlank()) {
Color.parseColor(role.color)
} else {
// sometimes the color is not set for a role, in this case fall back to our default blue
getColor(R.color.tusky_blue)
}
val sb = SpannableStringBuilder("${role.name} ${viewModel.domain}")
sb.setSpan(StyleSpan(Typeface.BOLD), 0, role.name.length, 0)
val badgeView = getBadge(badgeColor, R.drawable.profile_badge_person_24dp, sb, isLight)
binding.accountBadgeContainer.addView(badgeView)
}
}
private fun updateAccountJoinedDate() {
loadedAccount?.let { account ->
try {
@ -579,7 +660,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
*/
private fun updateRemoteAccount() {
loadedAccount?.let { account ->
if (account.isRemote()) {
if (account.isRemote) {
binding.accountRemoveView.show()
binding.accountRemoveView.setOnClickListener {
openLink(account.url)
@ -772,13 +853,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
loadedAccount?.let { loadedAccount ->
val muteDomain = menu.findItem(R.id.action_mute_domain)
domain = getDomain(loadedAccount.url)
if (domain.isEmpty()) {
when {
// If we can't get the domain, there's no way we can mute it anyway...
menu.removeItem(R.id.action_mute_domain)
} else {
if (blockingDomain) {
// If the account is from our own domain, muting it is no-op
domain.isEmpty() || viewModel.isFromOwnDomain -> {
menu.removeItem(R.id.action_mute_domain)
}
blockingDomain -> {
muteDomain.title = getString(R.string.action_unmute_domain, domain)
} else {
}
else -> {
muteDomain.title = getString(R.string.action_mute_domain, domain)
}
}
@ -837,7 +921,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
} else {
AlertDialog.Builder(this)
.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)
.show()
}
@ -930,7 +1016,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, url)
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
}
@ -942,7 +1033,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername)
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
}
@ -955,7 +1051,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
return true
}
R.id.action_add_or_remove_from_list -> {
ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
ListSelectionFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
return true
}
R.id.action_mute_domain -> {
@ -973,7 +1069,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
}
R.id.action_report -> {
loadedAccount?.let { loadedAccount ->
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username))
startActivity(
ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)
)
}
return true
}
@ -990,7 +1088,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
}
private fun getFullUsername(account: Account): String {
return if (account.isRemote()) {
return if (account.isRemote) {
"@" + account.username
} else {
val localUsername = account.localUsername
@ -1000,6 +1098,51 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
}
}
private fun getBadge(
@ColorInt baseColor: Int,
@DrawableRes icon: Int,
text: CharSequence,
isLight: Boolean
): Chip {
val badge = Chip(this)
// text color with maximum contrast
val textColor = if (isLight) Color.BLACK else Color.WHITE
// 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)
)
// a color between the text color and the badge color
val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)
// configure the badge
badge.text = text
badge.setTextColor(textColor)
badge.chipStrokeWidth = resources.getDimension(R.dimen.profile_badge_stroke_width)
badge.chipStrokeColor = ColorStateList.valueOf(outlineColor)
badge.setChipIconResource(icon)
badge.isChipIconVisible = true
badge.chipIconSize = resources.getDimension(R.dimen.profile_badge_icon_size)
badge.chipIconTint = ColorStateList.valueOf(outlineColor)
badge.chipBackgroundColor = ColorStateList.valueOf(backgroundColor)
// badge isn't clickable, so disable all related behavior
badge.isClickable = false
badge.isFocusable = false
badge.setEnsureMinTouchTargetSize(false)
// reset some chip defaults so it looks better for our badge usecase
badge.iconStartPadding = resources.getDimension(R.dimen.profile_badge_icon_start_padding)
badge.iconEndPadding = resources.getDimension(R.dimen.profile_badge_icon_end_padding)
badge.minHeight = resources.getDimensionPixelSize(R.dimen.profile_badge_min_height)
badge.chipMinHeight = resources.getDimension(R.dimen.profile_badge_min_height)
badge.updatePadding(top = 0, bottom = 0)
return badge
}
override fun androidInjector() = dispatchingAndroidInjector
companion object {

View File

@ -38,8 +38,15 @@ class AccountFieldAdapter(
override fun getItemCount() = fields.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountFieldBinding> {
val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAccountFieldBinding> {
val binding = ItemAccountFieldBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
@ -51,11 +58,20 @@ class AccountFieldAdapter(
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
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)
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 {
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}

View File

@ -33,7 +33,11 @@ class AccountPagerAdapter(
override fun createFragment(position: Int): Fragment {
return when (position) {
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)
3 -> AccountMediaFragment.newInstance(accountId)
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")

View File

@ -1,7 +1,6 @@
package com.keylesspalace.tusky.components.account
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold
@ -19,58 +18,83 @@ import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.keylesspalace.tusky.util.getDomain
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
class AccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
private val eventHub: EventHub,
private val accountManager: AccountManager
accountManager: AccountManager
) : ViewModel() {
val accountData = MutableLiveData<Resource<Account>>()
val relationshipData = MutableLiveData<Resource<Relationship>>()
private val _accountData = MutableStateFlow(null as Resource<Account>?)
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
lateinit var accountId: String
var isSelf = false
/** the domain of the viewed account **/
var domain = ""
/** True if the viewed account has the same domain as the active account */
var isFromOwnDomain = false
private var noteUpdateJob: Job? = null
private val activeAccount = accountManager.activeAccount!!
init {
viewModelScope.launch {
eventHub.events.collect { event ->
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
accountData.postValue(Success(event.newProfileData))
if (event is ProfileEditedEvent && event.newProfileData.id == _accountData.value?.data?.id) {
_accountData.value = Success(event.newProfileData)
}
}
}
}
private fun obtainAccount(reload: Boolean = false) {
if (accountData.value == null || reload) {
if (_accountData.value == null || reload) {
isDataLoading = true
accountData.postValue(Loading())
_accountData.value = Loading()
viewModelScope.launch {
mastodonApi.account(accountId)
.fold(
{ account ->
accountData.postValue(Success(account))
domain = getDomain(account.url)
isFromOwnDomain = domain == activeAccount.domain
_accountData.value = Success(account)
isDataLoading = false
isRefreshing.postValue(false)
_isRefreshing.emit(false)
},
{ t ->
Log.w(TAG, "failed obtaining account", t)
accountData.postValue(Error(cause = t))
_accountData.value = Error(cause = t)
isDataLoading = false
isRefreshing.postValue(false)
_isRefreshing.emit(false)
}
)
}
@ -78,18 +102,25 @@ class AccountViewModel @Inject constructor(
}
private fun obtainRelationship(reload: Boolean = false) {
if (relationshipData.value == null || reload) {
relationshipData.postValue(Loading())
if (_relationshipData.value == null || reload) {
_relationshipData.value = Loading()
viewModelScope.launch {
mastodonApi.relationships(listOf(accountId))
.fold(
{ relationships ->
relationshipData.postValue(if (relationships.isNotEmpty()) Success(relationships[0]) else Error())
_relationshipData.value =
if (relationships.isNotEmpty()) {
Success(
relationships[0]
)
} else {
Error()
}
},
{ t ->
Log.w(TAG, "failed obtaining relationships", t)
relationshipData.postValue(Error(cause = t))
_relationshipData.value = Error(cause = t)
}
)
}
@ -97,7 +128,7 @@ class AccountViewModel @Inject constructor(
}
fun changeFollowState() {
val relationship = relationshipData.value?.data
val relationship = _relationshipData.value?.data
if (relationship?.following == true || relationship?.requested == true) {
changeRelationship(RelationShipAction.UNFOLLOW)
} else {
@ -106,7 +137,7 @@ class AccountViewModel @Inject constructor(
}
fun changeBlockState() {
if (relationshipData.value?.data?.blocking == true) {
if (_relationshipData.value?.data?.blocking == true) {
changeRelationship(RelationShipAction.UNBLOCK)
} else {
changeRelationship(RelationShipAction.BLOCK)
@ -122,9 +153,9 @@ class AccountViewModel @Inject constructor(
}
fun changeSubscribingState() {
val relationship = relationshipData.value?.data
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
relationship?.subscribing == true /* Pleroma */
val relationship = _relationshipData.value?.data
if (relationship?.notifying == true || // Mastodon 3.3.0rc1
relationship?.subscribing == true // Pleroma
) {
changeRelationship(RelationShipAction.UNSUBSCRIBE)
} else {
@ -136,9 +167,9 @@ class AccountViewModel @Inject constructor(
viewModelScope.launch {
mastodonApi.blockDomain(instance).fold({
eventHub.dispatch(DomainMuteEvent(instance))
val relation = relationshipData.value?.data
val relation = _relationshipData.value?.data
if (relation != null) {
relationshipData.postValue(Success(relation.copy(blockingDomain = true)))
_relationshipData.value = Success(relation.copy(blockingDomain = true))
}
}, { e ->
Log.e(TAG, "Error muting $instance", e)
@ -149,9 +180,9 @@ class AccountViewModel @Inject constructor(
fun unblockDomain(instance: String) {
viewModelScope.launch {
mastodonApi.unblockDomain(instance).fold({
val relation = relationshipData.value?.data
val relation = _relationshipData.value?.data
if (relation != null) {
relationshipData.postValue(Success(relation.copy(blockingDomain = false)))
_relationshipData.value = Success(relation.copy(blockingDomain = false))
}
}, { e ->
Log.e(TAG, "Error unmuting $instance", e)
@ -160,7 +191,7 @@ class AccountViewModel @Inject constructor(
}
fun changeShowReblogsState() {
if (relationshipData.value?.data?.showingReblogs == true) {
if (_relationshipData.value?.data?.showingReblogs == true) {
changeRelationship(RelationShipAction.FOLLOW, false)
} else {
changeRelationship(RelationShipAction.FOLLOW, true)
@ -175,9 +206,9 @@ class AccountViewModel @Inject constructor(
parameter: Boolean? = null,
duration: Int? = null
) = viewModelScope.launch {
val relation = relationshipData.value?.data
val account = accountData.value?.data
val isMastodon = relationshipData.value?.data?.notifying != null
val relation = _relationshipData.value?.data
val account = _accountData.value?.data
val isMastodon = _relationshipData.value?.data?.notifying != null
if (relation != null && account != null) {
// optimistically post new state for faster response
@ -210,7 +241,7 @@ class AccountViewModel @Inject constructor(
}
}
}
relationshipData.postValue(Loading(newRelation))
_relationshipData.value = Loading(newRelation)
}
val relationshipCall = when (relationshipAction) {
@ -245,7 +276,7 @@ class AccountViewModel @Inject constructor(
relationshipCall.fold(
{ relationship ->
relationshipData.postValue(Success(relationship))
_relationshipData.value = Success(relationship)
when (relationshipAction) {
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
@ -256,22 +287,22 @@ class AccountViewModel @Inject constructor(
},
{ t ->
Log.w(TAG, "failed loading relationship", t)
relationshipData.postValue(Error(relation, cause = t))
_relationshipData.value = Error(relation, cause = t)
}
)
}
fun noteChanged(newNote: String) {
noteSaved.postValue(false)
_noteSaved.value = false
noteUpdateJob?.cancel()
noteUpdateJob = viewModelScope.launch {
delay(1500)
mastodonApi.updateAccountNote(accountId, newNote)
.fold(
{
noteSaved.postValue(true)
_noteSaved.value = true
delay(4000)
noteSaved.postValue(false)
_noteSaved.value = false
},
{ t ->
Log.w(TAG, "Error updating note", t)
@ -298,12 +329,19 @@ class AccountViewModel @Inject constructor(
fun setAccountInfo(accountId: String) {
this.accountId = accountId
this.isSelf = accountManager.activeAccount?.accountId == accountId
this.isSelf = activeAccount.accountId == accountId
reload(false)
}
enum class RelationShipAction {
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE
FOLLOW,
UNFOLLOW,
BLOCK,
UNBLOCK,
MUTE,
UNMUTE,
SUBSCRIBE,
UNSUBSCRIBE
}
companion object {

View File

@ -0,0 +1,260 @@
/* 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.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
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 javax.inject.Inject
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ListSelectionFragment : DialogFragment(), Injectable {
interface ListSelectionListener {
fun onListSelected(list: MastoList)
}
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory }
private var _binding: FragmentListsListBinding? = null
// This property is only valid between onCreateDialog and onDestroyView
private val binding get() = _binding!!
private val adapter = Adapter()
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()
_binding = FragmentListsListBinding.inflate(layoutInflater)
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() }
}
}
}
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()
}
return dialog
}
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
start = CoroutineStart.LAZY
) {
try {
delay(delayMs)
progressView.show()
awaitCancellation()
} finally {
progressView.hide()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun load() {
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 }
}
}
}

View File

@ -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 }
}
}
}

View File

@ -24,14 +24,13 @@ import at.connyduck.calladapter.networkresult.onSuccess
import at.connyduck.calladapter.networkresult.runCatching
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
data class AccountListState(
val list: MastoList,
@ -54,35 +53,30 @@ class ListsForAccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi
) : ViewModel() {
private lateinit var accountId: String
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)
val loadError: SharedFlow<Throwable> = _loadError
val loadError: SharedFlow<Throwable> = _loadError.asSharedFlow()
private val _actionError = MutableSharedFlow<ActionError>(1)
val actionError: SharedFlow<ActionError> = _actionError
val actionError: SharedFlow<ActionError> = _actionError.asSharedFlow()
fun setup(accountId: String) {
this.accountId = accountId
}
fun load() {
fun load(accountId: String?) {
_loadError.resetReplayCache()
viewModelScope.launch {
runCatching {
val (all, includes) = listOf(
async { mastodonApi.getLists() },
async { mastodonApi.getListsIncludesAccount(accountId) }
).awaitAll()
val all = mastodonApi.getLists().getOrThrow()
var includes: List<MastoList> = emptyList()
if (accountId != null) {
includes = mastodonApi.getListsIncludesAccount(accountId).getOrThrow()
}
_states.emit(
all.getOrThrow().map { list ->
all.map { listState ->
AccountListState(
list = list,
includesAccount = includes.getOrThrow().any { it.id == list.id }
list = listState,
includesAccount = includes.any { it.id == listState.id }
)
}
)
@ -93,7 +87,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()
viewModelScope.launch {
mastodonApi.addAccountToList(listId, listOf(accountId))
@ -114,7 +110,7 @@ class ListsForAccountViewModel @Inject constructor(
}
}
fun removeAccountFromList(listId: String) {
fun removeAccountFromList(accountId: String, listId: String) {
_actionError.resetReplayCache()
viewModelScope.launch {
mastodonApi.deleteAccountFromList(listId, listOf(accountId))

View File

@ -49,9 +49,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Fragment with multiple columns of media previews for the specified account.
@ -92,9 +92,13 @@ class AccountMediaFragment :
)
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.adapter = adapter
@ -124,7 +128,11 @@ class AccountMediaFragment :
is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
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 -> {
@ -175,11 +183,19 @@ class AccountMediaFragment :
Attachment.Type.GIFV,
Attachment.Type.VIDEO,
Attachment.Type.AUDIO -> {
val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex)
val intent = ViewMediaActivity.newIntent(
context,
attachmentsFromSameStatus,
currentIndex
)
if (activity != null) {
val url = selected.attachment.url
ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
view,
url
)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)

View File

@ -21,7 +21,7 @@ import com.keylesspalace.tusky.util.getFormattedDescription
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import java.util.Random
import kotlin.random.Random
class AccountMediaGridAdapter(
private val useBlurhash: Boolean,
@ -29,27 +29,49 @@ class AccountMediaGridAdapter(
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
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
}
override fun areContentsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
override fun areContentsTheSame(
oldItem: AttachmentViewData,
newItem: AttachmentViewData
): Boolean {
return oldItem == newItem
}
}
) {
private val baseItemBackgroundColor = MaterialColors.getColor(context, 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 baseItemBackgroundColor = MaterialColors.getColor(
context,
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 random = Random()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> {
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAccountMediaBinding> {
val binding = ItemAccountMediaBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
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))
return BindingHolder(binding)
}
@ -71,7 +93,11 @@ class AccountMediaGridAdapter(
if (item.attachment.type == Attachment.Type.AUDIO) {
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)
.load(R.drawable.ic_music_box_preview_24dp)

View File

@ -59,7 +59,7 @@ class AccountMediaRemoteMediator(
}
val attachments = statuses.flatMap { status ->
AttachmentViewData.list(status, activeAccount.alwaysShowSensitiveMedia ?: false)
AttachmentViewData.list(status, activeAccount.alwaysShowSensitiveMedia)
}
if (loadType == LoadType.REFRESH) {

View File

@ -27,7 +27,7 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData
import javax.inject.Inject
class AccountMediaViewModel @Inject constructor(
private val accountManager: AccountManager,
accountManager: AccountManager,
api: MastodonApi
) : ViewModel() {

View File

@ -19,7 +19,6 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ConcatAdapter
@ -28,10 +27,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import at.connyduck.calladapter.networkresult.fold
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.PostLookupFallbackBehavior
import com.keylesspalace.tusky.R
@ -56,13 +52,12 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import javax.inject.Inject
import kotlinx.coroutines.launch
import retrofit2.Response
import java.io.IOException
import javax.inject.Inject
class AccountListFragment :
Fragment(R.layout.fragment_account_list),
@ -97,7 +92,9 @@ class AccountListFragment :
val layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.layoutManager = layoutManager
(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() }
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
@ -117,7 +114,8 @@ class AccountListFragment :
instanceName = activeAccount.domain,
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)
followRequestsAdapter
}
@ -142,15 +140,13 @@ class AccountListFragment :
}
override fun onViewTag(tag: String) {
(activity as BaseActivity?)
?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
activity?.startActivityWithSlideInAnimation(
StatusListActivity.newHashtagIntent(requireContext(), tag)
)
}
override fun onViewAccount(id: String) {
(activity as BaseActivity?)?.let {
val intent = AccountActivity.getIntent(it, id)
it.startActivityWithSlideInAnimation(intent)
}
activity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id))
}
override fun onViewUrl(url: String) {
@ -226,7 +222,11 @@ class AccountListFragment :
val unblockedUser = blocksAdapter.removeItem(position)
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) {
blocksAdapter.addItem(unblockedUser, position)
onBlock(true, id, position)
@ -244,22 +244,17 @@ class AccountListFragment :
Log.e(TAG, "Failed to $verb account accountId $accountId")
}
override fun onRespondToFollowRequest(
accept: Boolean,
accountId: String,
position: Int
) {
if (accept) {
api.authorizeFollowRequest(accountId)
} else {
api.rejectFollowRequest(accountId)
}.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{
override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) {
viewLifecycleOwner.lifecycleScope.launch {
if (accept) {
api.authorizeFollowRequest(accountId)
} else {
api.rejectFollowRequest(accountId)
}.fold(
onSuccess = {
onRespondToFollowRequestSuccess(position)
},
{ throwable ->
onFailure = { throwable ->
val verb = if (accept) {
"accept"
} else {
@ -268,6 +263,7 @@ class AccountListFragment :
Log.e(TAG, "Failed to $verb account id $accountId.", throwable)
}
)
}
}
private fun onRespondToFollowRequestSuccess(position: Int) {
@ -332,7 +328,7 @@ class AccountListFragment :
val linkHeader = response.headers()["Link"]
onFetchAccountsSuccess(accountList, linkHeader)
} catch (exception: IOException) {
} catch (exception: Exception) {
onFetchAccountsFailure(exception)
}
}

View File

@ -60,9 +60,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
}
}
private fun createFooterViewHolder(
parent: ViewGroup
): RecyclerView.ViewHolder {
private fun createFooterViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
}

View File

@ -39,16 +39,27 @@ class BlocksAdapter(
) {
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)
}
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemBlockedUserBinding>, position: Int) {
override fun onBindAccountViewHolder(
viewHolder: BindingHolder<ItemBlockedUserBinding>,
position: Int
) {
val account = accountList[position]
val binding = viewHolder.binding
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
val formattedUsername = context.getString(R.string.post_username_format, account.username)
binding.blockedUserUsername.text = formattedUsername

View File

@ -44,7 +44,6 @@ class FollowRequestsAdapter(
)
return FollowRequestViewHolder(
binding,
accountActionListener,
linkListener,
showHeader = false
)

View File

@ -27,12 +27,22 @@ class FollowRequestsHeaderAdapter(
private val accountLocked: Boolean
) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestsHeaderBinding> {
val binding = ItemFollowRequestsHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestsHeaderBinding> {
val binding = ItemFollowRequestsHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
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)
}

View File

@ -42,18 +42,29 @@ class MutesAdapter(
private val mutingNotificationsMap = HashMap<String, Boolean>()
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)
}
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemMutedUserBinding>, position: Int) {
override fun onBindAccountViewHolder(
viewHolder: BindingHolder<ItemMutedUserBinding>,
position: Int
) {
val account = accountList[position]
val binding = viewHolder.binding
val context = binding.root.context
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
val formattedUsername = context.getString(R.string.post_username_format, account.username)

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.components.announcements
import android.annotation.SuppressLint
import android.os.Build
import android.text.SpannableStringBuilder
import android.view.ContextThemeWrapper
@ -29,6 +30,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.EmojiSpan
import com.keylesspalace.tusky.util.emojify
@ -50,19 +52,35 @@ class AnnouncementAdapter(
private val animateEmojis: Boolean = false
) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false)
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAnnouncementBinding> {
val binding = ItemAnnouncementBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: BindingHolder<ItemAnnouncementBinding>, position: Int) {
val item = items[position]
holder.binding.announcementDate.text = absoluteTimeFormatter.format(item.publishedAt, false)
val text = holder.binding.text
val chips = holder.binding.chipGroup
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)
@ -100,7 +118,13 @@ class AnnouncementAdapter(
spanBuilder.setSpan(span, 0, 1, 0)
Glide.with(this)
.asDrawable()
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl })
.load(
if (animateEmojis) {
reaction.url
} else {
reaction.staticUrl
}
)
.into(span.getTarget(animateEmojis))
this.text = spanBuilder
}

View File

@ -26,6 +26,7 @@ import android.view.View
import android.widget.PopupWindow
import androidx.activity.viewModels
import androidx.core.view.MenuProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@ -44,6 +45,7 @@ import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker
@ -52,6 +54,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.launch
class AnnouncementsActivity :
BottomSheetActivity(),
@ -110,35 +113,46 @@ class AnnouncementsActivity :
binding.announcementsList.adapter = adapter
viewModel.announcements.observe(this) {
when (it) {
is Success -> {
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) {
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
binding.errorMessageView.show()
} else {
lifecycleScope.launch {
viewModel.announcements.collect {
if (it == null) return@collect
when (it) {
is Success -> {
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) {
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()
}
adapter.updateList(it.data ?: listOf())
}
is Loading -> {
binding.errorMessageView.hide()
}
is Error -> {
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.errorMessageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
refreshAnnouncements()
is Error -> {
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.errorMessageView.setup(
R.drawable.errorphant_error,
R.string.error_generic
) {
refreshAnnouncements()
}
binding.errorMessageView.show()
}
binding.errorMessageView.show()
}
}
}
viewModel.emojis.observe(this) {
picker.adapter = EmojiAdapter(it, this, animateEmojis)
lifecycleScope.launch {
viewModel.emoji.collect {
picker.adapter = EmojiAdapter(it, this@AnnouncementsActivity, animateEmojis)
}
}
viewModel.load()

View File

@ -16,8 +16,6 @@
package com.keylesspalace.tusky.components.announcements
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold
@ -31,8 +29,11 @@ import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AnnouncementsViewModel @Inject constructor(
private val instanceInfoRepo: InstanceInfoRepository,
@ -40,31 +41,33 @@ class AnnouncementsViewModel @Inject constructor(
private val eventHub: EventHub
) : ViewModel() {
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
private val _announcements = MutableStateFlow(null as Resource<List<Announcement>>?)
val announcements: StateFlow<Resource<List<Announcement>>?> = _announcements.asStateFlow()
private val emojisMutable = MutableLiveData<List<Emoji>>()
val emojis: LiveData<List<Emoji>> = emojisMutable
private val _emoji = MutableStateFlow(emptyList<Emoji>())
val emoji: StateFlow<List<Emoji>> = _emoji.asStateFlow()
init {
viewModelScope.launch {
emojisMutable.postValue(instanceInfoRepo.getEmojis())
_emoji.value = instanceInfoRepo.getEmojis()
}
}
fun load() {
viewModelScope.launch {
announcementsMutable.postValue(Loading())
_announcements.value = Loading()
mastodonApi.listAnnouncements()
.fold(
{
announcementsMutable.postValue(Success(it))
_announcements.value = Success(it)
it.filter { announcement -> !announcement.read }
.forEach { announcement ->
mastodonApi.dismissAnnouncement(announcement.id)
.fold(
{
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
eventHub.dispatch(
AnnouncementReadEvent(announcement.id)
)
},
{ throwable ->
Log.d(
@ -77,7 +80,7 @@ class AnnouncementsViewModel @Inject constructor(
}
},
{
announcementsMutable.postValue(Error(cause = it))
_announcements.value = Error(cause = it)
}
)
}
@ -88,9 +91,9 @@ class AnnouncementsViewModel @Inject constructor(
mastodonApi.addAnnouncementReaction(announcementId, name)
.fold(
{
announcementsMutable.postValue(
_announcements.value =
Success(
announcements.value!!.data!!.map { announcement ->
announcements.value?.data?.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
@ -107,7 +110,7 @@ class AnnouncementsViewModel @Inject constructor(
} else {
listOf(
*announcement.reactions.toTypedArray(),
emojis.value!!.find { emoji -> emoji.shortcode == name }!!.run {
emoji.value.find { emoji -> emoji.shortcode == name }!!.run {
Announcement.Reaction(
name,
1,
@ -124,7 +127,6 @@ class AnnouncementsViewModel @Inject constructor(
}
}
)
)
},
{
Log.w(TAG, "Failed to add reaction to the announcement.", it)
@ -138,7 +140,7 @@ class AnnouncementsViewModel @Inject constructor(
mastodonApi.removeAnnouncementReaction(announcementId, name)
.fold(
{
announcementsMutable.postValue(
_announcements.value =
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
@ -163,7 +165,6 @@ class AnnouncementsViewModel @Inject constructor(
}
}
)
)
},
{
Log.w(TAG, "Failed to remove reaction from the announcement.", it)

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.components.compose
import android.Manifest
import android.app.NotificationManager
import android.app.ProgressDialog
import android.content.ClipData
import android.content.Context
@ -26,6 +25,7 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.icu.text.BreakIterator
import android.net.Uri
import android.os.Build
import android.os.Bundle
@ -70,6 +70,7 @@ import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
import com.canhub.cropper.options
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.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
@ -94,8 +95,9 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.AppTheme
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
import com.keylesspalace.tusky.util.MentionSpan
import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.getInitialLanguages
@ -114,11 +116,6 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.io.File
import java.io.IOException
import java.text.DecimalFormat
@ -126,6 +123,11 @@ import java.util.Locale
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class ComposeActivity :
BaseActivity(),
@ -162,14 +164,23 @@ class ComposeActivity :
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
pickMedia(photoUploadUri!!)
private val takePicture =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
pickMedia(photoUploadUri!!)
}
}
}
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
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 {
uris.forEach { uri ->
pickMedia(uri)
@ -190,7 +201,8 @@ class ComposeActivity :
uriNew,
size,
itemOld.description,
null, // Intentionally reset focus when cropping
// Intentionally reset focus when cropping
null,
itemOld
)
}
@ -204,27 +216,30 @@ class ComposeActivity :
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?) {
super.onCreate(savedInstanceState)
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)
if (notificationId != -1) {
// ComposeActivity was opened from a notification, delete the notification
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(notificationId)
}
activeAccount = accountManager.activeAccount ?: return
// If started from an intent then compose as the account ID from the intent.
// Otherwise use the active account. If null then the user is not logged in,
// and return from the activity.
val intentAccountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1)
activeAccount = if (intentAccountId != -1L) {
accountManager.getAccountById(intentAccountId)
} else {
accountManager.activeAccount
} ?: return
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
val theme = preferences.getString(APP_THEME, AppTheme.DEFAULT.value)
if (theme == "black") {
setTheme(R.style.TuskyDialogActivityBlackTheme)
}
@ -236,7 +251,11 @@ class ComposeActivity :
val mediaAdapter = MediaPreviewAdapter(
this,
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 ->
makeFocusDialog(item.focus, item.uri) { newFocus ->
@ -254,7 +273,11 @@ class ComposeActivity :
/* 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. */
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS_EXTRA, ComposeOptions::class.java)
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(
intent,
COMPOSE_OPTIONS_EXTRA,
ComposeOptions::class.java
)
viewModel.setup(composeOptions)
setupButtons()
@ -280,7 +303,7 @@ class ComposeActivity :
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, accountManager.activeAccount))
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, activeAccount))
setupComposeField(preferences, viewModel.startingText)
setupContentWarningField(composeOptions?.contentWarning)
setupPollView()
@ -317,12 +340,20 @@ class ComposeActivity :
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
when (intent.action) {
Intent.ACTION_SEND -> {
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri ->
IntentCompat.getParcelableExtra(
intent,
Intent.EXTRA_STREAM,
Uri::class.java
)?.let { uri ->
pickMedia(uri)
}
}
Intent.ACTION_SEND_MULTIPLE -> {
IntentCompat.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.forEach { uri ->
IntentCompat.getParcelableArrayListExtra(
intent,
Intent.EXTRA_STREAM,
Uri::class.java
)?.forEach { uri ->
pickMedia(uri)
}
}
@ -342,7 +373,13 @@ class ComposeActivity :
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
val left = min(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
binding.composeEditField.text.insert(0, "\n")
binding.composeEditField.setSelection(0)
@ -355,23 +392,48 @@ class ComposeActivity :
if (replyingStatusAuthor != null) {
binding.composeReplyView.show()
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)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowDownIcon,
null
)
binding.composeReplyView.setOnClickListener {
TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup)
TransitionManager.beginDelayedTransition(
binding.composeReplyContentView.parent as ViewGroup
)
if (binding.composeReplyContentView.isVisible) {
binding.composeReplyContentView.hide()
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowDownIcon,
null
)
} else {
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)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowUpIcon,
null
)
}
}
}
@ -382,13 +444,21 @@ class ComposeActivity :
if (startingContentWarning != null) {
binding.composeContentWarningField.setText(startingContentWarning)
}
binding.composeContentWarningField.doOnTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
binding.composeContentWarningField.doOnTextChanged { newContentWarning, _, _, _ ->
updateVisibleCharactersLeft()
viewModel.updateContentWarning(newContentWarning?.toString())
}
}
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
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(
ComposeAutoCompleteAdapter(
@ -408,6 +478,7 @@ class ComposeActivity :
binding.composeEditField.doAfterTextChanged { editable ->
highlightSpans(editable!!, mentionColour)
updateVisibleCharactersLeft()
viewModel.updateContent(editable.toString())
}
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
@ -433,7 +504,9 @@ class ComposeActivity :
}
lifecycleScope.launch {
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive ->
viewModel.showContentWarning.combine(
viewModel.markMediaAsSensitive
) { showContentWarning, markSensitive ->
updateSensitiveMediaToggle(markSensitive, showContentWarning)
showContentWarning(showContentWarning)
}.collect()
@ -448,7 +521,10 @@ class ComposeActivity :
mediaAdapter.submitList(media)
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value)
updateSensitiveMediaToggle(
viewModel.markMediaAsSensitive.value,
viewModel.showContentWarning.value
)
}
}
@ -494,6 +570,12 @@ class ComposeActivity :
}
}
}
lifecycleScope.launch {
viewModel.closeConfirmation.collect {
updateOnBackPressedCallbackState()
}
}
}
private fun setupButtons() {
@ -504,6 +586,17 @@ class ComposeActivity :
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
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)
// Setup the interface buttons.
@ -524,46 +617,58 @@ class ComposeActivity :
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply {
colorInt = textColor
sizeDp = 18
}
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(
cameraIcon,
null,
null,
null
)
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 }
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null)
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply {
colorInt = textColor
sizeDp = 18
}
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(
imageIcon,
null,
null,
null
)
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 }
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null)
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply {
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.actionPhotoPick.setOnClickListener { onMediaPick() }
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
onBackPressedDispatcher.addCallback(
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()
}
}
)
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private fun setupLanguageSpinner(initialLanguages: List<String>) {
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
}
@ -593,7 +698,7 @@ class ComposeActivity :
a.getDimensionPixelSize(0, 1)
}
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
loadAvatar(
activeAccount.profilePictureUrl,
binding.composeAvatar,
@ -606,10 +711,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) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
val start = binding.composeEditField.selectionStart.coerceAtMost(
binding.composeEditField.selectionEnd
)
val end = binding.composeEditField.selectionStart.coerceAtLeast(
binding.composeEditField.selectionEnd
)
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
" $text"
} else {
@ -623,8 +741,12 @@ class ComposeActivity :
fun prependSelectedWordsWith(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
val start = binding.composeEditField.selectionStart.coerceAtMost(
binding.composeEditField.selectionEnd
)
val end = binding.composeEditField.selectionStart.coerceAtLeast(
binding.composeEditField.selectionEnd
)
val editorText = binding.composeEditField.text
if (start == end) {
@ -692,7 +814,10 @@ class ComposeActivity :
this.viewModel.toggleMarkSensitive()
}
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
private fun updateSensitiveMediaToggle(
markMediaSensitive: Boolean,
contentWarningShown: Boolean
) {
if (viewModel.media.value.isEmpty()) {
binding.composeHideMediaButton.hide()
binding.descriptionMissingWarningButton.hide()
@ -709,7 +834,10 @@ class ComposeActivity :
getColor(R.color.tusky_blue)
} else {
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)
@ -731,7 +859,10 @@ class ComposeActivity :
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
} else {
@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 {
getColor(R.color.tusky_blue)
}
@ -762,7 +893,11 @@ class ComposeActivity :
binding.composeToggleVisibilityButton.setImageResource(iconRes)
if (viewModel.editing) {
// Can't update visibility on published status
enableButton(binding.composeToggleVisibilityButton, clickable = false, colorActive = false)
enableButton(
binding.composeToggleVisibilityButton,
clickable = false,
colorActive = false
)
}
}
@ -799,7 +934,11 @@ class ComposeActivity :
private fun showEmojis() {
binding.emojiView.adapter?.let {
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)
} else {
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
@ -827,7 +966,7 @@ class ComposeActivity :
private fun onMediaPick() {
addMediaBehavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
// Wait until bottom sheet is not collapsed and show next screen after
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
@ -866,9 +1005,14 @@ class ComposeActivity :
private fun setupPollView() {
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)
binding.pollPreview.layoutParams = layoutParams
@ -890,13 +1034,13 @@ class ComposeActivity :
}
private fun removePoll() {
viewModel.poll.value = null
viewModel.updatePoll(null)
binding.pollPreview.hide()
}
override fun onVisibilityChanged(visibility: Status.Visibility) {
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
viewModel.statusVisibility.value = visibility
viewModel.changeStatusVisibility(visibility)
}
@VisibleForTesting
@ -919,7 +1063,10 @@ class ComposeActivity :
val textColor = if (remainingLength < 0) {
getColor(R.color.tusky_red)
} else {
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
MaterialColors.getColor(
binding.composeCharactersLeftView,
android.R.attr.textColorTertiary
)
}
binding.composeCharactersLeftView.setTextColor(textColor)
}
@ -931,7 +1078,9 @@ class ComposeActivity :
}
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() {
@ -981,7 +1130,11 @@ class ComposeActivity :
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
@ -1056,14 +1209,20 @@ class ComposeActivity :
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
// "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
cropImage.launch(
options(uri = item.uri) {
setOutputUri(uriNew)
setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG)
setOutputCompressFormat(
if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
)
}
)
}
@ -1072,9 +1231,28 @@ class ComposeActivity :
viewModel.removeMediaFromQueue(item)
}
private fun sanitizePickMediaDescription(description: String?): String? {
if (description == null) {
return null
}
// The Gboard android keyboard attaches this text whenever the user
// pastes something from the keyboard's suggestion bar.
// Due to different end user locales, the exact text may vary, but at
// least in version 13.4.08, all of the translations contained the
// string "Gboard".
if ("Gboard" in description) {
return null
}
return description
}
private fun pickMedia(uri: Uri, description: String? = null) {
val sanitizedDescription = sanitizePickMediaDescription(description)
lifecycleScope.launch {
viewModel.pickMedia(uri, description).onFailure { throwable ->
viewModel.pickMedia(uri, sanitizedDescription).onFailure { throwable ->
val errorString = when (throwable) {
is FileSizeException -> {
val decimalFormat = DecimalFormat("0.##")
@ -1082,7 +1260,9 @@ class ComposeActivity :
val formattedSize = decimalFormat.format(allowedSizeInMb)
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)
}
displayTransientMessage(errorString)
@ -1091,16 +1271,23 @@ class ComposeActivity :
}
private fun showContentWarning(show: Boolean) {
TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup)
TransitionManager.beginDelayedTransition(
binding.composeContentWarningBar.parent as ViewGroup
)
@ColorInt val color = if (show) {
binding.composeContentWarningBar.show()
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
binding.composeContentWarningField.setSelection(
binding.composeContentWarningField.text.length
)
binding.composeContentWarningField.requestFocus()
getColor(R.color.tusky_blue)
} else {
binding.composeContentWarningBar.hide()
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)
}
@ -1135,10 +1322,10 @@ class ComposeActivity :
private fun handleCloseButton() {
val contentText = binding.composeEditField.text.toString()
val contentWarning = binding.composeContentWarningField.text.toString()
when (viewModel.handleCloseButton(contentText, contentWarning)) {
when (viewModel.closeConfirmation.value) {
ConfirmationKind.NONE -> {
viewModel.stopUploads()
finishWithoutSlideOutAnimation()
finish()
}
ConfirmationKind.SAVE_OR_DISCARD ->
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
@ -1154,7 +1341,10 @@ class ComposeActivity :
/**
* 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()) {
R.string.compose_save_draft_loses_media
} else {
@ -1177,7 +1367,10 @@ class ComposeActivity :
* User is editing an existing draft, and can either update the draft with the new changes or
* 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()) {
R.string.compose_save_draft_loses_media
} else {
@ -1192,7 +1385,7 @@ class ComposeActivity :
}
.setNegativeButton(R.string.action_discard) { _, _ ->
viewModel.stopUploads()
finishWithoutSlideOutAnimation()
finish()
}
}
@ -1208,7 +1401,7 @@ class ComposeActivity :
}
.setNegativeButton(R.string.action_discard) { _, _ ->
viewModel.stopUploads()
finishWithoutSlideOutAnimation()
finish()
}
}
@ -1222,7 +1415,7 @@ class ComposeActivity :
.setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteDraft()
viewModel.stopUploads()
finishWithoutSlideOutAnimation()
finish()
}
.setNegativeButton(R.string.action_continue_edit) { _, _ ->
// Do nothing, dialog will dismiss, user can continue editing
@ -1231,7 +1424,7 @@ class ComposeActivity :
private fun deleteDraftAndFinish() {
viewModel.deleteDraft()
finishWithoutSlideOutAnimation()
finish()
}
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
@ -1249,7 +1442,7 @@ class ComposeActivity :
}
viewModel.saveDraft(contentText, contentWarning)
dialog?.cancel()
finishWithoutSlideOutAnimation()
finish()
}
}
@ -1281,10 +1474,15 @@ class ComposeActivity :
val state: State
) {
enum class Type {
IMAGE, VIDEO, AUDIO;
IMAGE,
VIDEO,
AUDIO
}
enum class State {
UPLOADING, UNPROCESSED, PROCESSED, PUBLISHED
UPLOADING,
UNPROCESSED,
PROCESSED,
PUBLISHED
}
}
@ -1355,8 +1553,6 @@ class ComposeActivity :
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
private const val VISIBILITY_KEY = "VISIBILITY"
private const val SCHEDULED_TIME_KEY = "SCHEDULE"
@ -1364,26 +1560,12 @@ class ComposeActivity :
/**
* @param options ComposeOptions to configure the ComposeActivity
* @param notificationId the id of the notification that starts the Activity
* @param accountId the id of the account to compose with, null for the current account
* @return an Intent to start the ComposeActivity
*/
@JvmStatic
@JvmOverloads
fun startIntent(
context: Context,
options: ComposeOptions,
notificationId: Int? = null,
accountId: Long? = null
): Intent {
fun startIntent(context: Context, options: ComposeOptions): Intent {
return Intent(context, ComposeActivity::class.java).apply {
putExtra(COMPOSE_OPTIONS_EXTRA, options)
if (notificationId != null) {
putExtra(NOTIFICATION_ID_EXTRA, notificationId)
}
if (accountId != null) {
putExtra(ACCOUNT_ID_EXTRA, accountId)
}
}
}
@ -1415,7 +1597,7 @@ class ComposeActivity :
*/
@JvmStatic
fun statusLength(body: Spanned, contentWarning: Spanned?, urlLength: Int): Int {
var length = body.length - body.getSpans(0, body.length, URLSpan::class.java)
var length = body.toString().perceivedCharacterLength() - body.getSpans(0, body.length, URLSpan::class.java)
.fold(0) { acc, span ->
// Accumulate a count of characters to be *ignored* in the final length
acc + when (span) {
@ -1428,15 +1610,25 @@ class ComposeActivity :
}
else -> {
// Expected to be negative if the URL length < maxUrlLength
span.url.length - urlLength
span.url.perceivedCharacterLength() - urlLength
}
}
}
// Content warning text is treated as is, URLs or mentions there are not special
contentWarning?.let { length += it.length }
contentWarning?.let { length += it.toString().perceivedCharacterLength() }
return length
}
// String.length would count emojis as multiple characters but Mastodon counts them as 1, so we need this workaround
private fun String.perceivedCharacterLength(): Int {
val breakIterator = BreakIterator.getCharacterInstance()
breakIterator.setText(this)
var count = 0
while (breakIterator.next() != BreakIterator.DONE) {
count++
}
return count
}
}
}

View File

@ -108,7 +108,9 @@ class ComposeAutoCompleteAdapter(
val account = accountResult.account
binding.username.text = context.getString(R.string.post_username_format, account.username)
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(
account.avatar,
binding.avatar,
@ -143,12 +145,12 @@ class ComposeAutoCompleteAdapter(
}
}
sealed class AutocompleteResult {
class AccountResult(val account: TimelineAccount) : AutocompleteResult()
sealed interface 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 {

View File

@ -38,19 +38,23 @@ import com.keylesspalace.tusky.service.MediaToSend
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.StatusToSend
import com.keylesspalace.tusky.util.randomAlphanumericString
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ComposeViewModel @Inject constructor(
private val api: MastodonApi,
@ -76,22 +80,43 @@ class ComposeViewModel @Inject constructor(
private var modifiedInitialState: 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)
val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow()
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val markMediaAsSensitive: MutableStateFlow<Boolean> =
private val _markMediaAsSensitive =
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
val markMediaAsSensitive: StateFlow<Boolean> = _markMediaAsSensitive.asStateFlow()
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
private val _statusVisibility = MutableStateFlow(Status.Visibility.UNKNOWN)
val statusVisibility: StateFlow<Status.Visibility> = _statusVisibility.asStateFlow()
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val _showContentWarning = MutableStateFlow(false)
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
@ -100,10 +125,16 @@ class ComposeViewModel @Inject constructor(
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 {
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
val mediaItems = media.value
val mediaItems = _media.value
if (type != QueuedMedia.Type.IMAGE &&
mediaItems.isNotEmpty() &&
mediaItems[0].type == QueuedMedia.Type.IMAGE
@ -128,7 +159,7 @@ class ComposeViewModel @Inject constructor(
): QueuedMedia {
var stashMediaItem: QueuedMedia? = null
media.update { mediaList ->
_media.update { mediaList ->
val mediaItem = QueuedMedia(
localId = mediaUploader.getNewLocalMediaId(),
uri = uri,
@ -155,7 +186,7 @@ class ComposeViewModel @Inject constructor(
mediaUploader
.uploadMedia(mediaItem, instanceInfo.first())
.collect { event ->
val item = media.value.find { it.localId == mediaItem.localId }
val item = _media.value.find { it.localId == mediaItem.localId }
?: return@collect
val newMediaItem = when (event) {
is UploadEvent.ProgressEvent ->
@ -164,15 +195,19 @@ class ComposeViewModel @Inject constructor(
item.copy(
id = event.mediaId,
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 -> {
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
uploadError.emit(event.error)
_media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
_uploadError.emit(event.error)
return@collect
}
}
media.update { mediaList ->
_media.update { mediaList ->
mediaList.map { mediaItem ->
if (mediaItem.localId == newMediaItem.localId) {
newMediaItem
@ -183,11 +218,22 @@ class ComposeViewModel @Inject constructor(
}
}
}
updateCloseConfirmation()
return mediaItem
}
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
media.update { mediaList ->
fun changeStatusVisibility(visibility: Status.Visibility) {
_statusVisibility.value = visibility
}
private fun addUploadedMedia(
id: String,
type: QueuedMedia.Type,
uri: Uri,
description: String?,
focus: Attachment.Focus?
) {
_media.update { mediaList ->
val mediaItem = QueuedMedia(
localId = mediaUploader.getNewLocalMediaId(),
uri = uri,
@ -205,22 +251,38 @@ class ComposeViewModel @Inject constructor(
fun removeMediaFromQueue(item: QueuedMedia) {
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() {
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true
}
fun handleCloseButton(contentText: String?, contentWarning: String?): ConfirmationKind {
return if (didChange(contentText, contentWarning)) {
fun updateContent(newContent: String?) {
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) {
ComposeKind.NEW -> if (isEmpty(contentText, contentWarning)) {
ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) {
ConfirmationKind.NONE
} else {
ConfirmationKind.SAVE_OR_DISCARD
}
ComposeKind.EDIT_DRAFT -> if (isEmpty(contentText, contentWarning)) {
ComposeKind.EDIT_DRAFT -> if (isEmpty(currentContent, contentWarning)) {
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
} else {
ConfirmationKind.UPDATE_OR_DISCARD
@ -236,20 +298,21 @@ class ComposeViewModel @Inject constructor(
private fun didChange(content: String?, contentWarning: String?): Boolean {
val textChanged = content.orEmpty() != startingText.orEmpty()
val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning
val mediaChanged = media.value.isNotEmpty()
val pollChanged = poll.value != null
val mediaChanged = _media.value.isNotEmpty()
val pollChanged = _poll.value != null
val didScheduledTimeChange = hasScheduledTimeChanged
return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange
}
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) {
showContentWarning.value = value
_showContentWarning.value = value
contentWarningStateChanged = true
updateCloseConfirmation()
}
fun deleteDraft() {
@ -261,12 +324,12 @@ class ComposeViewModel @Inject constructor(
}
fun stopUploads() {
mediaUploader.cancelUploadScope(*media.value.map { it.localId }.toIntArray())
mediaUploader.cancelUploadScope(*_media.value.map { it.localId }.toIntArray())
}
fun shouldShowSaveDraftDialog(): Boolean {
// 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"
}
}
@ -275,7 +338,7 @@ class ComposeViewModel @Inject constructor(
val mediaUris: MutableList<String> = mutableListOf()
val mediaDescriptions: MutableList<String?> = mutableListOf()
val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf()
media.value.forEach { item ->
for (item in _media.value) {
mediaUris.add(item.uri.toString())
mediaDescriptions.add(item.description)
mediaFocus.add(item.focus)
@ -287,15 +350,15 @@ class ComposeViewModel @Inject constructor(
inReplyToId = inReplyToId,
content = content,
contentWarning = contentWarning,
sensitive = markMediaAsSensitive.value,
visibility = statusVisibility.value,
sensitive = _markMediaAsSensitive.value,
visibility = _statusVisibility.value,
mediaUris = mediaUris,
mediaDescriptions = mediaDescriptions,
mediaFocus = mediaFocus,
poll = poll.value,
poll = _poll.value,
failedToSend = false,
failedToSendAlert = false,
scheduledAt = scheduledAt.value,
scheduledAt = _scheduledAt.value,
language = postLanguage,
statusId = originalStatusId
)
@ -305,16 +368,12 @@ class ComposeViewModel @Inject constructor(
* Send status to the server.
* Uses current state plus provided arguments.
*/
suspend fun sendStatus(
content: String,
spoilerText: String,
accountId: Long
) {
suspend fun sendStatus(content: String, spoilerText: String, accountId: Long) {
if (!scheduledTootId.isNullOrEmpty()) {
api.deleteScheduledStatus(scheduledTootId!!)
}
val attachedMedia = media.value.map { item ->
val attachedMedia = _media.value.map { item ->
MediaToSend(
localId = item.localId,
id = item.id,
@ -327,12 +386,12 @@ class ComposeViewModel @Inject constructor(
val tootToSend = StatusToSend(
text = content,
warningText = spoilerText,
visibility = statusVisibility.value.serverString(),
sensitive = attachedMedia.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value),
visibility = _statusVisibility.value.serverString,
sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value),
media = attachedMedia,
scheduledAt = scheduledAt.value,
scheduledAt = _scheduledAt.value,
inReplyToId = inReplyToId,
poll = poll.value,
poll = _poll.value,
replyingStatusContent = null,
replyingStatusAuthorUsername = null,
accountId = accountId,
@ -347,7 +406,7 @@ class ComposeViewModel @Inject constructor(
}
private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) {
media.update { mediaList ->
_media.update { mediaList ->
mediaList.map { mediaItem ->
if (mediaItem.localId == localId) {
mutator(mediaItem)
@ -371,9 +430,9 @@ class ComposeViewModel @Inject constructor(
}
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
when (token[0]) {
'@' -> {
return api.searchAccountsSync(query = token.substring(1), limit = 10)
return when (token[0]) {
'@' -> runBlocking {
api.searchAccounts(query = token.substring(1), limit = 10)
.fold({ accounts ->
accounts.map { AutocompleteResult.AccountResult(it) }
}, { e ->
@ -381,8 +440,12 @@ class ComposeViewModel @Inject constructor(
emptyList()
})
}
'#' -> {
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
'#' -> runBlocking {
api.search(
query = token,
type = SearchType.Hashtag.apiParameter,
limit = 10
)
.fold({ searchResult ->
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
}, { e ->
@ -394,7 +457,7 @@ class ComposeViewModel @Inject constructor(
val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList()
val incomplete = token.substring(1)
return emojiList.filter { emoji ->
emojiList.filter { emoji ->
emoji.shortcode.contains(incomplete, ignoreCase = true)
}.sortedBy { emoji ->
emoji.shortcode.indexOf(incomplete, ignoreCase = true)
@ -404,7 +467,7 @@ class ComposeViewModel @Inject constructor(
}
else -> {
Log.w(TAG, "Unexpected autocompletion token: $token")
return emptyList()
emptyList()
}
}
}
@ -432,7 +495,7 @@ class ComposeViewModel @Inject constructor(
startingContentWarning = contentWarning
}
if (!contentWarningStateChanged) {
showContentWarning.value = !contentWarning.isNullOrBlank()
_showContentWarning.value = !contentWarning.isNullOrBlank()
}
// recreate media list
@ -466,7 +529,7 @@ class ComposeViewModel @Inject constructor(
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
startingVisibility = tootVisibility
}
statusVisibility.value = startingVisibility
_statusVisibility.value = startingVisibility
val mentionedUsernames = composeOptions?.mentionedUsernames
if (mentionedUsernames != null) {
val builder = StringBuilder()
@ -478,30 +541,33 @@ class ComposeViewModel @Inject constructor(
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
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
this.poll.value = poll
this._poll.value = poll
}
replyingStatusContent = composeOptions?.replyingStatusContent
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
updateCloseConfirmation()
setupComplete = true
}
fun updatePoll(newPoll: NewPoll) {
poll.value = newPoll
fun updatePoll(newPoll: NewPoll?) {
_poll.value = newPoll
updateCloseConfirmation()
}
fun updateScheduledAt(newScheduledAt: String?) {
if (newScheduledAt != scheduledAt.value) {
if (newScheduledAt != _scheduledAt.value) {
hasScheduledTimeChanged = true
}
scheduledAt.value = newScheduledAt
_scheduledAt.value = newScheduledAt
}
val editing: Boolean

View File

@ -42,7 +42,7 @@ fun downsizeImage(
tempFile: File
): Boolean {
val decodeBoundsInputStream = try {
contentResolver.openInputStream(uri)
contentResolver.openInputStream(uri) ?: return false
} catch (e: FileNotFoundException) {
return false
}
@ -54,10 +54,10 @@ fun downsizeImage(
// Get EXIF data, for orientation info.
val orientation = getImageOrientation(uri, contentResolver)
/* 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
* 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
* sure it gets downsized to below the limit. */
* 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
* many cases, so it should only iterate once, but the loop is used to be absolutely
* sure it gets downsized to below the limit. */
var scaledImageSize = 1024
do {
val outputStream = try {
@ -66,7 +66,7 @@ fun downsizeImage(
return false
}
val decodeBitmapInputStream = try {
contentResolver.openInputStream(uri)
contentResolver.openInputStream(uri) ?: return false
} catch (e: FileNotFoundException) {
return false
}

View File

@ -113,11 +113,17 @@ class MediaPreviewAdapter(
private val differ = AsyncListDiffer(
this,
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
}
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
override fun areContentsTheSame(
oldItem: ComposeActivity.QueuedMedia,
newItem: ComposeActivity.QueuedMedia
): Boolean {
return oldItem == newItem
}
}

View File

@ -30,12 +30,16 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
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.getImageSquarePixels
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.getServerErrorMessage
import com.keylesspalace.tusky.util.randomAlphanumericString
import java.io.File
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -52,21 +56,20 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.shareIn
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okio.buffer
import okio.sink
import okio.source
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 class UploadEvent {
data class ProgressEvent(val percentage: Int) : UploadEvent()
data class FinishedEvent(val mediaId: String, val processed: Boolean) : UploadEvent(), FinalUploadEvent
data class ErrorEvent(val error: Throwable) : UploadEvent(), FinalUploadEvent
sealed interface UploadEvent {
data class ProgressEvent(val percentage: Int) : UploadEvent
data class FinishedEvent(
val mediaId: String,
val processed: Boolean
) : UploadEvent, FinalUploadEvent
data class ErrorEvent(val error: Throwable) : UploadEvent, FinalUploadEvent
}
data class UploadData(
@ -79,11 +82,7 @@ fun createNewImageFile(context: Context, suffix: String = ".jpg"): File {
val randomId = randomAlphanumericString(12)
val imageFileName = "Tusky_${randomId}_"
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
imageFileName, /* prefix */
suffix, /* suffix */
storageDir /* directory */
)
return File.createTempFile(imageFileName, suffix, storageDir)
}
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
@ -163,22 +162,22 @@ class MediaUploader @Inject constructor(
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
contentResolver.openInputStream(inUri).use { input ->
contentResolver.openInputStream(inUri)?.source().use { input ->
if (input == null) {
Log.w(TAG, "Media input is null")
uri = inUri
return@use
}
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out)
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
file.absoluteFile.sink().buffer().use { out ->
out.writeAll(input)
}
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
}
}
ContentResolver.SCHEME_FILE -> {
@ -191,17 +190,18 @@ class MediaUploader @Inject constructor(
val suffix = inputFile.name.substringAfterLast('.', "tmp")
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
val input = FileInputStream(inputFile)
FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out)
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
inputFile.source().use { input ->
file.absoluteFile.sink().buffer().use { out ->
out.writeAll(input)
}
}
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri)
}
else -> {
Log.w(TAG, "Unknown uri scheme $uri")
@ -254,9 +254,9 @@ class MediaUploader @Inject constructor(
// .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details.
// Sniff the content of the file to determine the actual type.
if (mimeType != null && (
mimeType.startsWith("audio/", ignoreCase = true) ||
mimeType.startsWith("video/", ignoreCase = true)
)
mimeType.startsWith("audio/", ignoreCase = true) ||
mimeType.startsWith("video/", ignoreCase = true)
)
) {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, media.uri)
@ -264,22 +264,20 @@ class MediaUploader @Inject constructor(
}
val map = MimeTypeMap.getSingleton()
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),
Date().time.toString(),
System.currentTimeMillis(),
randomAlphanumericString(10),
fileExtension
)
val stream = contentResolver.openInputStream(media.uri)
if (mimeType == null) mimeType = "multipart/form-data"
var lastProgress = -1
val fileBody = ProgressRequestBody(
stream!!,
media.mediaSize,
mimeType.toMediaTypeOrNull()!!
val fileBody = media.uri.asRequestBody(
contentResolver,
requireNotNull(mimeType.toMediaTypeOrNull()) { "Invalid Content Type" },
media.mediaSize
) { percentage ->
if (percentage != lastProgress) {
trySend(UploadEvent.ProgressEvent(percentage))

View File

@ -60,7 +60,9 @@ fun showAddPollDialog(
binding.pollChoices.adapter = adapter
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 {
setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
}
@ -75,8 +77,8 @@ fun showAddPollDialog(
}
}
val DAY_SECONDS = 60 * 60 * 24
val desiredDuration = poll?.expiresIn ?: DAY_SECONDS
val secondsInADay = 60 * 60 * 24
val desiredDuration = poll?.expiresIn ?: secondsInADay
val pollDurationId = durations.indexOfLast {
it <= desiredDuration
}
@ -105,5 +107,7 @@ fun showAddPollDialog(
dialog.show()
// 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
)
}

View File

@ -41,8 +41,15 @@ class AddPollOptionsAdapter(
notifyItemInserted(options.size - 1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAddPollOptionBinding> {
val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAddPollOptionBinding> {
val binding = ItemAddPollOptionBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding)
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))

View File

@ -15,7 +15,6 @@
package com.keylesspalace.tusky.components.compose.dialog
import android.app.Dialog
import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
@ -25,8 +24,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import android.widget.LinearLayout
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
@ -36,45 +34,56 @@ import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.DialogImageDescriptionBinding
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.viewBinding
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
class CaptionDialog : DialogFragment() {
private lateinit var listener: Listener
private lateinit var input: EditText
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
private val binding by viewBinding(DialogImageDescriptionBinding::bind)
val binding = DialogImageDescriptionBinding.inflate(layoutInflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
}
input = binding.imageDescriptionText
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.dialog_image_description, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val imageView = binding.imageDescriptionView
imageView.maxZoom = 6f
input.hint = resources.getQuantityString(
binding.imageDescriptionText.hint = resources.getQuantityString(
R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT,
MEDIA_DESCRIPTION_CHARACTER_LIMIT
)
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
input.setText(arguments?.getString(EXISTING_DESCRIPTION_ARG))
binding.imageDescriptionText.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
binding.imageDescriptionText.setText(arguments?.getString(EXISTING_DESCRIPTION_ARG))
savedInstanceState?.getCharSequence(DESCRIPTION_KEY)?.let {
binding.imageDescriptionText.setText(it)
}
binding.cancelButton.setOnClickListener {
dismiss()
}
val localId = arguments?.getInt(LOCAL_ID_ARG) ?: error("Missing localId")
val dialog = AlertDialog.Builder(context)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
listener.onUpdateDescription(localId, input.text.toString())
}
.setNegativeButton(android.R.string.cancel, null)
.create()
binding.okButton.setOnClickListener {
listener.onUpdateDescription(localId, binding.imageDescriptionText.text.toString())
dismiss()
}
isCancelable = true
val window = dialog.window
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
val previewUri = BundleCompat.getParcelable(requireArguments(), PREVIEW_URI_ARG, Uri::class.java) ?: error("Preview Uri is null")
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
Glide.with(this)
.load(previewUri)
@ -90,27 +99,30 @@ class CaptionDialog : DialogFragment() {
) {
imageView.setImageDrawable(resource)
}
})
return dialog
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
imageView.hide()
}
})
}
override fun onStart() {
super.onStart()
dialog?.apply {
window?.setLayout(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(DESCRIPTION_KEY, input.text.toString())
outState.putCharSequence(DESCRIPTION_KEY, binding.imageDescriptionText.text)
super.onSaveInstanceState(outState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
savedInstanceState?.getString(DESCRIPTION_KEY)?.let {
input.setText(it)
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? Listener ?: error("Activity is not ComposeCaptionDialog.Listener")
@ -121,17 +133,14 @@ class CaptionDialog : DialogFragment() {
}
companion object {
fun newInstance(
localId: Int,
existingDescription: String?,
previewUri: Uri
) = CaptionDialog().apply {
arguments = bundleOf(
LOCAL_ID_ARG to localId,
EXISTING_DESCRIPTION_ARG to existingDescription,
PREVIEW_URI_ARG to previewUri
)
}
fun newInstance(localId: Int, existingDescription: String?, previewUri: Uri) =
CaptionDialog().apply {
arguments = bundleOf(
LOCAL_ID_ARG to localId,
EXISTING_DESCRIPTION_ARG to existingDescription,
PREVIEW_URI_ARG to previewUri
)
}
private const val DESCRIPTION_KEY = "description"
private const val EXISTING_DESCRIPTION_ARG = "existing_description"

View File

@ -49,12 +49,23 @@ fun <T> T.makeFocusDialog(
.load(previewUri)
.downsample(DownsampleStrategy.CENTER_INSIDE)
.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
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable?>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
val width = resource!!.intrinsicWidth
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable?>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
val width = resource.intrinsicWidth
val height = resource.intrinsicHeight
dialogBinding.focusIndicator.setImageSize(width, height)

View File

@ -21,7 +21,10 @@ import android.widget.RadioGroup
import com.keylesspalace.tusky.R
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

View File

@ -223,7 +223,8 @@ class ComposeScheduleView
}
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())
}
}

View File

@ -68,7 +68,9 @@ class FocusIndicatorView
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 {
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
return false
@ -112,7 +114,13 @@ class FocusIndicatorView
curtainPath.reset() // Draw a flood fill with a hole cut out of it
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)
canvas.drawPath(curtainPath, curtainPaint)

View File

@ -60,7 +60,10 @@ class TootButton
Status.Visibility.PRIVATE,
Status.Visibility.DIRECT -> {
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 -> {
null

View File

@ -38,7 +38,9 @@ class ConversationAdapter(
}
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)
}
@ -58,15 +60,24 @@ class ConversationAdapter(
companion object {
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
}
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
}
override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? {
override fun getChangePayload(
oldItem: ConversationViewData,
newItem: ConversationViewData
): Any? {
return if (oldItem == newItem) {
// If items are equal - update timestamp only
listOf(StatusBaseViewHolder.Key.KEY_CREATED)

View File

@ -27,6 +27,7 @@ import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.squareup.moshi.JsonClass
import java.util.Date
@Entity(primaryKeys = ["id", "accountId"])
@ -50,6 +51,7 @@ data class ConversationEntity(
}
}
@JsonClass(generateAdapter = true)
data class ConversationAccountEntity(
val id: String,
val localUsername: String,
@ -131,7 +133,7 @@ data class ConversationStatusEntity(
poll = poll,
card = null,
language = language,
filtered = null
filtered = emptyList()
),
isExpanded = expanded,
isShowingContent = showingHiddenContent,
@ -140,21 +142,16 @@ data class ConversationStatusEntity(
}
}
fun TimelineAccount.toEntity() =
ConversationAccountEntity(
id = id,
localUsername = localUsername,
username = username,
displayName = name,
avatar = avatar,
emojis = emojis.orEmpty()
)
fun TimelineAccount.toEntity() = ConversationAccountEntity(
id = id,
localUsername = localUsername,
username = username,
displayName = name,
avatar = avatar,
emojis = emojis.orEmpty()
)
fun Status.toEntity(
expanded: Boolean,
contentShowing: Boolean,
contentCollapsed: Boolean
) =
fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed: Boolean) =
ConversationStatusEntity(
id = id,
url = url,
@ -177,7 +174,7 @@ fun Status.toEntity(
showingHiddenContent = contentShowing,
expanded = expanded,
collapsed = contentCollapsed,
muted = muted ?: false,
muted = muted,
poll = poll,
language = language
)
@ -188,16 +185,15 @@ fun Conversation.toEntity(
expanded: Boolean,
contentShowing: Boolean,
contentCollapsed: Boolean
) =
ConversationEntity(
accountId = accountId,
id = id,
order = order,
accounts = accounts.map { it.toEntity() },
unread = unread,
lastStatus = lastStatus!!.toEntity(
expanded = expanded,
contentShowing = contentShowing,
contentCollapsed = contentCollapsed
)
) = ConversationEntity(
accountId = accountId,
id = id,
order = order,
accounts = accounts.map { it.toEntity() },
unread = unread,
lastStatus = lastStatus!!.toEntity(
expanded = expanded,
contentShowing = contentShowing,
contentCollapsed = contentCollapsed
)
)

View File

@ -27,7 +27,10 @@ class ConversationLoadStateAdapter(
private val retryCallback: () -> Unit
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
override fun onBindViewHolder(holder: BindingHolder<ItemNetworkStateBinding>, loadState: LoadState) {
override fun onBindViewHolder(
holder: BindingHolder<ItemNetworkStateBinding>,
loadState: LoadState
) {
val binding = holder.binding
binding.progressBar.visible(loadState == LoadState.Loading)
binding.retryButton.visible(loadState is LoadState.Error)
@ -47,7 +50,11 @@ class ConversationLoadStateAdapter(
parent: ViewGroup,
loadState: LoadState
): 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)
}
}

View File

@ -29,7 +29,7 @@ data class ConversationViewData(
accountId: Long,
favourited: Boolean = lastStatus.status.favourited,
bookmarked: Boolean = lastStatus.status.bookmarked,
muted: Boolean = lastStatus.status.muted ?: false,
muted: Boolean = lastStatus.status.muted,
poll: Poll? = lastStatus.status.poll,
expanded: Boolean = lastStatus.isExpanded,
collapsed: Boolean = lastStatus.isCollapsed,
@ -57,7 +57,7 @@ data class ConversationViewData(
fun StatusViewData.Concrete.toConversationStatusEntity(
favourited: Boolean = status.favourited,
bookmarked: Boolean = status.bookmarked,
muted: Boolean = status.muted ?: false,
muted: Boolean = status.muted,
poll: Poll? = status.poll,
expanded: Boolean = isExpanded,
collapsed: Boolean = isCollapsed,

View File

@ -78,7 +78,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
if (payloads == null) {
TimelineAccount account = status.getAccount();
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener);
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), status.getSpoilerText(), listener);
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
setUsername(account.getUsername());

View File

@ -40,6 +40,7 @@ import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.account.AccountActivity
@ -54,6 +55,7 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.isAnyLoading
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData
@ -61,12 +63,12 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ConversationsFragment :
SFragment(),
@ -89,7 +91,11 @@ class ConversationsFragment :
private var hideFab = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_timeline, container, false)
}
@ -99,14 +105,14 @@ class ConversationsFragment :
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true),
useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false),
showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true),
useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
confirmFavourites = preferences.getBoolean("confirmFavourites", false),
confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true),
confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false),
@ -128,18 +134,37 @@ class ConversationsFragment :
binding.statusView.hide()
binding.progressBar.hide()
if (loadState.isAnyLoading()) {
lifecycleScope.launch {
eventHub.dispatch(
ConversationsLoadingEvent(
accountManager.activeAccount?.accountId ?: ""
)
)
}
}
if (adapter.itemCount == 0) {
when (loadState.refresh) {
is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
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)
}
}
is LoadState.Error -> {
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 -> {
binding.progressBar.show()
}
@ -223,6 +248,7 @@ class ConversationsFragment :
refreshContent()
true
}
else -> false
}
}
@ -231,11 +257,14 @@ class ConversationsFragment :
binding.recyclerView.setHasFixedSize(true)
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.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry))
binding.recyclerView.adapter =
adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry))
}
private fun refreshContent() {
@ -263,13 +292,15 @@ class ConversationsFragment :
}
}
override val onMoreTranslate: ((translate: Boolean, position: Int) -> Unit)? = null
override fun onMore(view: View, position: Int) {
adapter.peek(position)?.let { conversation ->
val popup = PopupMenu(requireContext(), view)
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)
} else {
popup.menu.removeItem(R.id.status_unmute_conversation)
@ -289,7 +320,11 @@ class ConversationsFragment :
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
adapter.peek(position)?.let { conversation ->
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
viewMedia(
attachmentIndex,
AttachmentViewData.list(conversation.lastStatus.status),
view
)
}
}
@ -361,6 +396,10 @@ class ConversationsFragment :
}
}
override fun onUntranslate(position: Int) {
// not needed
}
private fun deleteConversation(conversation: ConversationViewData) {
AlertDialog.Builder(requireContext())
.setMessage(R.string.dialog_delete_conversation_warning)
@ -377,6 +416,7 @@ class ConversationsFragment :
PrefKeys.FAB_HIDE -> {
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
}
PrefKeys.MEDIA_PREVIEW_ENABLED -> {
val enabled = accountManager.activeAccount!!.mediaPreviewEnabled
val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled

View File

@ -38,7 +38,10 @@ class ConversationsRemoteMediator(
}
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()
if (!conversationsResponse.isSuccessful || conversations == null) {

View File

@ -29,9 +29,9 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource
import javax.inject.Inject
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class ConversationsViewModel @Inject constructor(
private val timelineCases: TimelineCases,
@ -91,7 +91,11 @@ class ConversationsViewModel @Inject constructor(
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
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 ->
val newConversation = conversation.toEntity(
accountId = accountManager.activeAccount!!.id,
@ -155,12 +159,12 @@ class ConversationsViewModel @Inject constructor(
try {
timelineCases.muteConversation(
conversation.lastStatus.id,
!(conversation.lastStatus.status.muted ?: false)
!conversation.lastStatus.status.muted
)
val newConversation = conversation.toEntity(
accountId = accountManager.activeAccount!!.id,
muted = !(conversation.lastStatus.status.muted ?: false)
muted = !conversation.lastStatus.status.muted
)
database.conversationDao().insert(newConversation)

View File

@ -1,15 +1,14 @@
package com.keylesspalace.tusky.components.instancemute
package com.keylesspalace.tusky.components.domainblocks
import android.os.Bundle
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class InstanceListActivity : BaseActivity(), HasAndroidInjector {
class DomainBlocksActivity : BaseActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -28,7 +27,7 @@ class InstanceListActivity : BaseActivity(), HasAndroidInjector {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, InstanceListFragment())
.replace(R.id.fragment_container, DomainBlocksFragment())
.commit()
}

View File

@ -0,0 +1,34 @@
package com.keylesspalace.tusky.components.domainblocks
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import com.keylesspalace.tusky.components.followedtags.FollowedTagsAdapter.Companion.STRING_COMPARATOR
import com.keylesspalace.tusky.databinding.ItemBlockedDomainBinding
import com.keylesspalace.tusky.util.BindingHolder
class DomainBlocksAdapter(
private val onUnmute: (String) -> Unit
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemBlockedDomainBinding> {
val binding = ItemBlockedDomainBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
override fun onBindViewHolder(holder: BindingHolder<ItemBlockedDomainBinding>, position: Int) {
getItem(position)?.let { instance ->
holder.binding.blockedDomain.text = instance
holder.binding.blockedDomainUnblock.setOnClickListener {
onUnmute(instance)
}
}
}
}

View File

@ -0,0 +1,96 @@
package com.keylesspalace.tusky.components.domainblocks
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
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.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val binding by viewBinding(FragmentDomainBlocksBinding::bind)
private val viewModel: DomainBlocksViewModel by viewModels { viewModelFactory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = DomainBlocksAdapter(viewModel::unblock)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.addItemDecoration(
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
)
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiEvents.collect { event ->
showSnackbar(event)
}
}
lifecycleScope.launch {
viewModel.domainPager.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
adapter.addLoadStateListener { loadState ->
binding.progressBar.visible(
loadState.refresh == LoadState.Loading && adapter.itemCount == 0
)
if (loadState.refresh is LoadState.Error) {
binding.recyclerView.hide()
binding.messageView.show()
val errorState = loadState.refresh as LoadState.Error
binding.messageView.setup(errorState.error) { adapter.retry() }
Log.w(TAG, "error loading blocked domains", errorState.error)
} else if (loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0) {
binding.recyclerView.hide()
binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
} else {
binding.recyclerView.show()
binding.messageView.hide()
}
}
}
private fun showSnackbar(event: SnackbarEvent) {
val message = if (event.throwable == null) {
getString(event.message, event.domain)
} else {
Log.w(TAG, event.throwable)
val error = event.throwable.localizedMessage ?: getString(R.string.ui_error_unknown)
getString(event.message, event.domain, error)
}
Snackbar.make(binding.recyclerView, message, Snackbar.LENGTH_LONG)
.setTextMaxLines(5)
.setAction(event.actionText, event.action)
.show()
}
companion object {
private const val TAG = "DomainBlocksFragment"
}
}

View File

@ -0,0 +1,19 @@
package com.keylesspalace.tusky.components.domainblocks
import androidx.paging.PagingSource
import androidx.paging.PagingState
class DomainBlocksPagingSource(
private val domains: List<String>,
private val nextKey: String?
) : PagingSource<String, String>() {
override fun getRefreshKey(state: PagingState<String, String>): String? = null
override suspend fun load(params: LoadParams<String>): LoadResult<String, String> {
return if (params is LoadParams.Refresh) {
LoadResult.Page(domains, null, nextKey)
} else {
LoadResult.Page(emptyList(), null, null)
}
}
}

View File

@ -0,0 +1,57 @@
package com.keylesspalace.tusky.components.domainblocks
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
import retrofit2.HttpException
import retrofit2.Response
@OptIn(ExperimentalPagingApi::class)
class DomainBlocksRemoteMediator(
private val api: MastodonApi,
private val repository: DomainBlocksRepository
) : RemoteMediator<String, String>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<String, String>
): MediatorResult {
return try {
val response = request(loadType)
?: return MediatorResult.Success(endOfPaginationReached = true)
return applyResponse(response)
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
private suspend fun request(loadType: LoadType): Response<List<String>>? {
return when (loadType) {
LoadType.PREPEND -> null
LoadType.APPEND -> api.domainBlocks(maxId = repository.nextKey)
LoadType.REFRESH -> {
repository.nextKey = null
repository.domains.clear()
api.domainBlocks()
}
}
}
private fun applyResponse(response: Response<List<String>>): MediatorResult {
val tags = response.body()
if (!response.isSuccessful || tags == null) {
return MediatorResult.Error(HttpException(response))
}
val links = HttpHeaderLink.parse(response.headers()["Link"])
repository.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
repository.domains.addAll(tags)
repository.invalidate()
return MediatorResult.Success(endOfPaginationReached = repository.nextKey == null)
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2023 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.domainblocks
import androidx.paging.ExperimentalPagingApi
import androidx.paging.InvalidatingPagingSourceFactory
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import at.connyduck.calladapter.networkresult.NetworkResult
import at.connyduck.calladapter.networkresult.onSuccess
import com.keylesspalace.tusky.network.MastodonApi
import javax.inject.Inject
class DomainBlocksRepository @Inject constructor(
private val api: MastodonApi
) {
val domains: MutableList<String> = mutableListOf()
var nextKey: String? = null
private var factory = InvalidatingPagingSourceFactory {
DomainBlocksPagingSource(domains.toList(), nextKey)
}
@OptIn(ExperimentalPagingApi::class)
val domainPager = Pager(
config = PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE),
remoteMediator = DomainBlocksRemoteMediator(api, this),
pagingSourceFactory = factory
).flow
/** Invalidate the active paging source, see [PagingSource.invalidate] */
fun invalidate() {
factory.invalidate()
}
suspend fun block(domain: String): NetworkResult<Unit> {
return api.blockDomain(domain).onSuccess {
domains.add(domain)
factory.invalidate()
}
}
suspend fun unblock(domain: String): NetworkResult<Unit> {
return api.unblockDomain(domain).onSuccess {
domains.remove(domain)
factory.invalidate()
}
}
companion object {
private const val PAGE_SIZE = 20
}
}

View File

@ -0,0 +1,75 @@
package com.keylesspalace.tusky.components.domainblocks
import android.view.View
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure
import com.keylesspalace.tusky.R
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class DomainBlocksViewModel @Inject constructor(
private val repo: DomainBlocksRepository
) : ViewModel() {
val domainPager = repo.domainPager.cachedIn(viewModelScope)
private val _uiEvents = MutableSharedFlow<SnackbarEvent>()
val uiEvents: SharedFlow<SnackbarEvent> = _uiEvents.asSharedFlow()
fun block(domain: String) {
viewModelScope.launch {
repo.block(domain).onFailure { e ->
_uiEvents.emit(
SnackbarEvent(
message = R.string.error_blocking_domain,
domain = domain,
throwable = e,
actionText = R.string.action_retry,
action = { block(domain) }
)
)
}
}
}
fun unblock(domain: String) {
viewModelScope.launch {
repo.unblock(domain).fold({
_uiEvents.emit(
SnackbarEvent(
message = R.string.confirmation_domain_unmuted,
domain = domain,
throwable = null,
actionText = R.string.action_undo,
action = { block(domain) }
)
)
}, { e ->
_uiEvents.emit(
SnackbarEvent(
message = R.string.error_unblocking_domain,
domain = domain,
throwable = e,
actionText = R.string.action_retry,
action = { unblock(domain) }
)
)
})
}
}
}
class SnackbarEvent(
@StringRes val message: Int,
val domain: String,
val throwable: Throwable?,
@StringRes val actionText: Int,
val action: (View) -> Unit
)

View File

@ -29,18 +29,18 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.copyToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.buffer
import okio.sink
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
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(
val context: Context,
@ -101,16 +101,17 @@ class DraftHelper @Inject constructor(
}
}
val attachments: MutableList<DraftAttachment> = mutableListOf()
for (i in mediaUris.indices) {
attachments.add(
DraftAttachment(
uriString = uris[i].toString(),
description = mediaDescriptions[i],
focus = mediaFocus[i],
type = types[i]
val attachments: List<DraftAttachment> = buildList(mediaUris.size) {
for (i in mediaUris.indices) {
add(
DraftAttachment(
uriString = uris[i].toString(),
description = mediaDescriptions[i],
focus = mediaFocus[i],
type = types[i]
)
)
)
}
}
val draft = DraftEntity(
@ -186,10 +187,8 @@ class DraftHelper @Inject constructor(
val response = okHttpClient.newCall(request).execute()
val sink = file.sink().buffer()
response.body?.source()?.use { input ->
sink.use { output ->
file.sink().buffer().use { output ->
response.body?.source()?.use { input ->
output.writeAll(input)
}
}
@ -200,6 +199,10 @@ class DraftHelper @Inject constructor(
} else {
this.copyToFile(contentResolver, file)
}
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
return FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
}
}

View File

@ -35,7 +35,10 @@ class DraftMediaAdapter(
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
override fun areContentsTheSame(
oldItem: DraftAttachment,
newItem: DraftAttachment
): Boolean {
return oldItem == newItem
}
}
@ -75,7 +78,9 @@ class DraftMediaAdapter(
RecyclerView.ViewHolder(imageView) {
init {
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 margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)

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