Compare commits

..

368 Commits

Author SHA1 Message Date
sk
36699e0ab1 change change log 2023-11-15 16:58:49 +01:00
sk22
1a382606d7 Translated using Weblate (German)
Currently translated at 100.0% (417 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-11-15 15:50:50 +00:00
sk22
2522c9c428 Translated using Weblate (German)
Currently translated at 97.6% (407 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-11-15 15:46:34 +00:00
gallegonovato
d1ee12f248 Translated using Weblate (Spanish)
Currently translated at 100.0% (412 of 412 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-11-15 15:46:34 +00:00
SomeTr
d5f9c19fd3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (19 of 19 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-11-15 15:46:34 +00:00
SomeTr
dc700d9e37 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (412 of 412 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-11-15 15:46:34 +00:00
qbane
6eb87149e2 Translated using Weblate (Chinese (Traditional))
Currently translated at 24.0% (96 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-11-15 15:46:34 +00:00
qbane
79a0f8a891 Translated using Weblate (Chinese (Traditional))
Currently translated at 17.7% (71 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-11-15 15:46:34 +00:00
alextecplayz
26d41b006f Translated using Weblate (Romanian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-11-15 15:46:34 +00:00
EndermanCo
60d9dc22d2 Translated using Weblate (Persian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-11-15 15:46:34 +00:00
SomeTr
c0bf78fa7c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-11-15 15:46:34 +00:00
ling0412
f0c2bd0fe6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2023-11-15 15:46:34 +00:00
Arkxv
235cfd1e80 Translated using Weblate (Japanese)
Currently translated at 93.9% (375 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ja/
2023-11-15 15:46:34 +00:00
SomeTr
c96931ba72 Translated using Weblate (Croatian)
Currently translated at 64.4% (257 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/hr/
2023-11-15 15:46:34 +00:00
butterflyoffire
2c654a28d6 Translated using Weblate (French)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-11-15 15:46:33 +00:00
Andrewblasco
72d3fc84f5 Translated using Weblate (Spanish)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-11-15 15:46:33 +00:00
ling0412
3b77604ae6 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (398 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-11-15 15:46:33 +00:00
poesty
4e5f49b37d Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (398 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-11-15 15:46:33 +00:00
sk22
a89c075082 Added translation using Weblate (Esperanto) 2023-11-15 15:46:33 +00:00
sk
99e881bb95 change string name 2023-11-15 16:46:24 +01:00
sk
4c585fc5d0 change confirm string 2023-11-15 16:45:29 +01:00
sk
ae934b5167 add changelog 2023-11-15 16:42:22 +01:00
Jacoco
6b234209c6 Previewing posts on Akkoma (#933)
* Previewing posts on Akkoma

* move preview property into status

* clean up code

* revert imm-related changes

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-15 15:17:29 +01:00
Jacoco
786ce78b08 Translations for Akkoma (#934)
* Translations for Akkoma

* simplify akkoma translation code

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-15 14:32:56 +01:00
sk
cde332684e bump version 2023-11-15 14:07:00 +01:00
sk
d51e06b61f save latest crash log
closes sk22#932
closes sk22#419
2023-11-15 12:06:54 +01:00
sk
0c376d57e7 fix timeline editor messing with non-hashtag tls 2023-11-13 21:31:54 +01:00
sk
ee82772fee weather icons :) 2023-11-13 21:30:06 +01:00
sk
d5b6750abe fix hashtag options showing up for all timelines 2023-11-13 21:17:58 +01:00
sk
70328217c6 add bookmarks and favorites as timelines
closes sk22#908
2023-11-13 21:10:33 +01:00
sk
733fb3f53a re-add pre-releases setting
closes sk22#906
2023-11-13 21:02:06 +01:00
sk
f82eb96a35 increase translate timeout to 1 minute
closes sk22#910
2023-11-13 20:50:43 +01:00
sk
148952b96c update cache on poll vote
closes sk22#924
2023-11-13 20:48:16 +01:00
sk
52928e1577 increase default read timeout
closes sk22#923
2023-11-13 20:43:34 +01:00
sk
cd13777c06 fix report view, allow opening posts in list
closes sk22#928
closes sk22#441
2023-11-13 20:39:27 +01:00
sk
b21e2acdb6 fix metadata items being cut off
closes sk22#921
closes sk22#648
2023-11-13 19:11:04 +01:00
sk
2b65aeb8b4 fix edit note item visible in context menu 2023-11-13 18:48:24 +01:00
Jacoco
a8dcb11094 feature: Display post that's being quoted on Akkoma (#927)
* Displaying Akkoma quote status

* Dummy display items for quote posts

* Only remove quote-inline with RE:

* fix null reference (reply-to instead of quote status)

* fix text bottom padding in quote

* Postprocess status quote

* fix rounded bottom for quoted media

closes sk22#929

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-13 18:46:15 +01:00
sk
aa42873274 use verified/error text colors for diff 2023-11-12 23:01:46 +01:00
FineFindus
8a5b36db96 feat(profile): add note (#918)
* feat(profile): add note

* simplify note code

* adjust spacing

* size and hitbox adjustments, progress

* profile menu item to add note

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-12 22:37:31 +01:00
FineFindus
c85af5502d feat: translate media attachments and poll options (#916)
* feat(status): translate media attachments

* feat(status): translate poll options

* fix(status/translation): do not require all fields

* feat(status/translation): support translating spoiler
2023-11-10 20:44:06 +01:00
FineFindus
06698d3c52 feat: display edit history diff (#922)
* build: add google's diff-match-patch

Copied from 62f2e689f4/java/src/name/fraser/neil/plaintext/diff_match_patch.java

* feat(status/edit-history): display diff for text

Closes https://github.com/sk22/megalodon/issues/789

* fix(status/edit-history): add fake poll id

* code style adjustments

* don't diff if only formatting changed

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-10 20:40:34 +01:00
S1m
2818672cda Fix NullPointerException when receiving push when killed (#914) 2023-11-10 20:15:45 +01:00
S1m
24794f28aa Fix: registering multi account for UnifiedPush (#907)
* Register all account when enabling UnifiedPush
* Register new account to UnifiedPush if enabled
2023-11-10 20:12:27 +01:00
FineFindus
50d1523210 feat: re-add 12 hour option to polls (#917) 2023-11-10 20:10:50 +01:00
sk
613cd2a1ea remove unused playRelease build type 2023-11-10 20:06:18 +01:00
sk
295cb74287 don't show translate for empty decoding 2023-11-10 16:30:46 +01:00
sk
95dd3ff068 fix translation language name 2023-11-10 16:13:43 +01:00
sk
d6aeb753fc allow exclamation/question marks in pronouns 2023-11-10 16:13:14 +01:00
sk
a085744038 fix missing parens on start of pronouns 2023-11-10 15:37:31 +01:00
sk
26391a6f14 hopefully fix string index out of bounds
https://paste.crdroid.net/3FNRe7
2023-11-10 15:37:03 +01:00
sk
4cf734ce9a avoid IllegalArgumentException 2023-11-10 15:23:42 +01:00
sk
c8122aa65b fuck it, production release 2023-10-28 15:09:00 +02:00
sk
ef1584de55 merge upstream strings 2023-10-28 14:50:56 +02:00
sk
3075030b1c add todo 2023-10-27 17:38:54 +02:00
sk
42c56401db fix wrong check for gap item
re sk22#898
2023-10-27 17:34:47 +02:00
sk
a18a4383e5 update gap in database when deleting status
closes sk22#898
2023-10-27 17:19:59 +02:00
sk
375b5b3133 fix gaps not showing time
closes sk22#889
2023-10-27 16:14:16 +02:00
sk
4cab916957 move status list fragment check out of predicate 2023-10-27 16:02:14 +02:00
sk
7902691093 avoid index out of bounds
closes sk22#904
2023-10-27 01:28:27 +02:00
sk
d5d9e20a06 update strings 2023-10-26 18:53:40 +02:00
Codeberg Translate
1eb55428db Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/
2023-10-26 16:39:24 +00:00
reindex
9b82d704f6 Translated using Weblate (Japanese)
Currently translated at 11.1% (2 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ja/
2023-10-26 16:39:24 +00:00
reindex
dbf3d2776e Translated using Weblate (Japanese)
Currently translated at 86.4% (345 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ja/
2023-10-26 16:39:24 +00:00
Espasant3
c077b9d47c Translated using Weblate (Galician)
Currently translated at 98.4% (393 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-10-26 16:39:24 +00:00
kallekn
34157805a1 Translated using Weblate (Finnish)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fi/
2023-10-26 16:39:24 +00:00
AiOO
576da2a3eb Translated using Weblate (Korean)
Currently translated at 99.2% (396 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2023-10-26 16:39:24 +00:00
SomeTr
e99e5b2836 Translated using Weblate (Ukrainian)
Currently translated at 99.7% (398 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-26 16:39:24 +00:00
Oliebol
fe25974958 Translated using Weblate (Dutch)
Currently translated at 91.7% (366 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-10-26 16:39:24 +00:00
Linerly
1a95b4e361 Translated using Weblate (Indonesian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-10-26 16:39:24 +00:00
Choukajohn
35e3d5afc4 Translated using Weblate (French)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-26 16:39:24 +00:00
gallegonovato
cc1e13d1bd Translated using Weblate (Spanish)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-10-26 16:39:24 +00:00
poesty
0b622f8f2a Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (398 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-10-26 16:39:24 +00:00
gallegonovato
9cf7da6419 Translated using Weblate (Spanish)
Currently translated at 100.0% (398 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-10-26 16:39:24 +00:00
sk
812ab7198c revert, i said!
actually, the cached posts limit can stay
2023-10-26 18:38:06 +02:00
sk
e8c9253a76 don't remove gap when removing status
re: sk22#898
2023-10-26 18:32:46 +02:00
sk
c8f633ae3b don't hide gap behind warning
closes sk22#899
2023-10-26 18:23:56 +02:00
sk
af339a833f fix gap time being wrong when next status is own
closes sk22#902
2023-10-26 18:11:54 +02:00
sk
708142e1b8 Revert "refresh data from cache"
This reverts commit 60a998be89.
2023-10-26 17:53:36 +02:00
sk
55f32671c5 Revert "fix timeline breaking when max_id is null"
This reverts commit 289db09770.
2023-10-26 17:52:43 +02:00
sk
289db09770 fix timeline breaking when max_id is null 2023-10-25 14:42:35 +02:00
sk
89d7dfd694 don't globally remove status on refresh
closes sk22#896
2023-10-23 23:55:04 +02:00
sk
60a998be89 refresh data from cache 2023-10-23 01:00:26 +02:00
sk
128e75bc24 fix animatable avatars not playing in notification headers
closes sk22#882
2023-10-22 23:35:09 +02:00
sk
69c60d484c load custom emoji in reply header
closes sk22#886
2023-10-22 23:30:33 +02:00
sk
c70750a508 fix strings 2023-10-22 23:21:47 +02:00
sk
5553ca25a1 Merge remote-tracking branch 'upstream/l10n_master' 2023-10-22 23:10:56 +02:00
sk
ff87829e72 Merge remote-tracking branch 'weblate/main' 2023-10-22 23:09:45 +02:00
sk
02983c76b9 add mutuals follow button state
closes sk22#890
2023-10-22 23:09:35 +02:00
sk
3e28eb2ccf fix polls not updating when behind spoiler
closes sk22#892
2023-10-22 22:39:10 +02:00
sk
3cf23474e3 fix issue with too-wide context menu icons
closes sk22#893
2023-10-22 22:10:25 +02:00
sk
9f0ff2dcd4 fix editing scheduled/drafted polls
closes sk22#894
2023-10-22 21:44:02 +02:00
Eugen Rochko
8ed9fb6276 New translations strings.xml (Chinese Traditional) 2023-10-22 08:53:38 +02:00
SomeTr
df7a0ee490 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-10-21 20:53:12 +00:00
Espasant3
2e360dc275 Translated using Weblate (Galician)
Currently translated at 98.7% (393 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-10-21 20:53:12 +00:00
poesty
8dc88d2fd7 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (397 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-10-21 20:53:12 +00:00
Eugen Rochko
901c70efc3 New translations strings.xml (Icelandic) 2023-10-21 19:24:31 +02:00
Eugen Rochko
3d44e5d2cc New translations strings.xml (Icelandic) 2023-10-21 18:27:37 +02:00
Eugen Rochko
33ea3da84d New translations strings.xml (Basque) 2023-10-21 11:37:37 +02:00
Eugen Rochko
e97203a6e3 New translations strings.xml (Thai) 2023-10-21 09:36:50 +02:00
Eugen Rochko
b3f2987b14 New translations strings.xml (Vietnamese) 2023-10-21 03:54:29 +02:00
Eugen Rochko
c7426453a5 New translations strings.xml (German) 2023-10-21 02:52:53 +02:00
sk
b60c1a5db3 add back old see_new_posts strings 2023-10-21 01:43:03 +02:00
Eugen Rochko
664d5cc4c3 New translations strings.xml (German) 2023-10-21 01:04:51 +02:00
sk
47d1b182ac fix see new posts button design 2023-10-21 01:00:51 +02:00
sk
c33c3d9112 fix compatibility issue 2023-10-21 00:54:38 +02:00
sk
ff99e1023a Revert "use tonal background for see new posts button"
This reverts commit 8009dc0a75.
2023-10-20 16:23:02 +02:00
sk
8009dc0a75 use tonal background for see new posts button 2023-10-20 15:31:06 +02:00
sk
20039ec97e bump version 2023-10-20 13:33:02 +02:00
SomeTr
172d25eeb0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (398 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-20 11:32:21 +00:00
alextecplayz
fa04e00032 Translated using Weblate (Romanian)
Currently translated at 100.0% (398 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-10-20 11:32:21 +00:00
Linerly
3ff442894d Translated using Weblate (Indonesian)
Currently translated at 100.0% (398 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-10-20 11:32:21 +00:00
Choukajohn
edb0823bf9 Translated using Weblate (French)
Currently translated at 100.0% (398 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-20 11:32:21 +00:00
sk
9de83355e1 revert api timeout and increase for search
closes sk22#881
2023-10-20 13:31:27 +02:00
sk
183fb0e8c0 use fluent back button 2023-10-20 12:54:51 +02:00
sk
d624a04e18 fix see new posts button font size 2023-10-20 12:50:34 +02:00
sk
303461d803 take switcher width into account for button 2023-10-20 12:17:12 +02:00
sk
11c7816bb1 make show new posts button less disruptive 2023-10-20 11:43:35 +02:00
sk
b5eae13a16 introduce separate tonal selector backgrounds 2023-10-20 11:43:15 +02:00
sk
59026286a1 don't cache as many posts 2023-10-20 10:46:56 +02:00
sk
ef0fbb26a4 don't return before refreshDone 2023-10-20 10:33:45 +02:00
sk
387b31193f hopefully unfuck removing statuses and reblogs 2023-10-19 21:42:17 +02:00
sk22
95fa547f15 Translated using Weblate (German)
Currently translated at 100.0% (398 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-10-19 15:13:41 +00:00
sk22
3198c9adb1 Translated using Weblate (German)
Currently translated at 99.2% (395 of 398 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-10-19 15:07:29 +00:00
SomeTr
e1af6f4643 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-10-19 15:07:29 +00:00
SomeTr
cc52e491b0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (396 of 396 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-19 15:07:29 +00:00
Choukajohn
b0db0d9f2e Translated using Weblate (French)
Currently translated at 100.0% (396 of 396 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-19 15:07:29 +00:00
sk
d699788a12 fix string 2023-10-19 17:07:18 +02:00
sk
bdbf441e1a Merge remote-tracking branch 'upstream/l10n_master' 2023-10-19 17:01:46 +02:00
sk
0deba2885b Merge remote-tracking branch 'weblate/main' 2023-10-19 17:01:13 +02:00
sk
48b39fd3bc boop version 2023-10-19 16:59:45 +02:00
sk
875b235e6d restart when show replies/boosts changes
closes sk22#879
2023-10-19 16:58:26 +02:00
sk
1711682322 don't display extra text if visibility == local 2023-10-19 16:54:23 +02:00
sk
70f5ec7500 update header layout 2023-10-19 16:54:07 +02:00
sk
d34c73c210 add options for locking account and default vis 2023-10-19 16:49:23 +02:00
sk
50a0586e8a fix content type index and title 2023-10-19 16:02:51 +02:00
Jacoco
1d2ca1e500 fix: hiding discovery on Akkoma + hiding privacy settings (#867)
* Hide privacy settings on Akkoma

* Fix Akkoma skipping discovery

* Make settings order more clear in the code
2023-10-19 15:42:29 +02:00
EndermanCo
34b7123a8d Translated using Weblate (Persian)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-10-19 10:17:38 +00:00
butterflyoffire
ec718ff58e Translated using Weblate (Arabic)
Currently translated at 80.4% (317 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-10-19 10:17:38 +00:00
ihor_ck
20a442e27f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-19 10:17:38 +00:00
SomeTr
47cb74abc9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-19 10:17:37 +00:00
SomeTr
a11e53c89a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-19 10:17:37 +00:00
David Lapshin
d721e8ca46 Translated using Weblate (Russian)
Currently translated at 99.7% (393 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-10-19 10:17:37 +00:00
alextecplayz
23e5dcd030 Translated using Weblate (Romanian)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-10-19 10:17:37 +00:00
Linerly
25dd2cfab8 Translated using Weblate (Indonesian)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-10-19 10:17:37 +00:00
Choukajohn
8537c7a7ae Translated using Weblate (French)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-19 10:17:37 +00:00
gallegonovato
ebd7f9c36c Translated using Weblate (Spanish)
Currently translated at 100.0% (394 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-10-19 10:17:37 +00:00
poesty
49cda3aeb2 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (393 of 394 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-10-19 10:17:37 +00:00
sk
67cbc8aff2 don't load new posts every time we hit the top 2023-10-19 01:06:03 +02:00
sk
5db44cbf9d don't save bidi-safe string in account
also support custom emoji in account switcher
closes sk22#859
2023-10-19 01:01:53 +02:00
sk
95858e3280 remove debug println 2023-10-19 00:44:10 +02:00
sk
36846acbe5 fix null pointer when content language is null 2023-10-19 00:38:06 +02:00
sk
b95d944003 save own posts from timeline before removing them 2023-10-19 00:22:52 +02:00
sk
f52886af74 don't add own boosts to home
closes sk22#875
2023-10-19 00:09:09 +02:00
sk
6cbe406cef load new posts on reload 2023-10-19 00:04:54 +02:00
sk
3097bc7168 oops: put home timeline if result NOT empty 2023-10-18 23:28:44 +02:00
sk
5f7faa69e8 remove existing statuses to move them up 2023-10-18 22:07:10 +02:00
sk
ed24e89a96 don't compare newly fetched posts to created post
closes sk22#866
2023-10-18 21:48:11 +02:00
sk
8ea05c6ebd fix menu grouping 2023-10-18 21:24:59 +02:00
sk
a30fcbbe34 reorder profile menu
closes sk22#873
2023-10-18 21:23:58 +02:00
sk
39f76f9988 always show "boosted" text in reply/boost line
closes sk22#874
2023-10-18 21:21:03 +02:00
sk
5135653cd3 wow!! configurable first fraction 2023-10-18 21:11:48 +02:00
sk
2e4f04cd88 fix divide by zero error 2023-10-18 20:40:23 +02:00
Eugen Rochko
a44e0e036a New translations strings.xml (Persian) 2023-10-18 20:27:30 +02:00
Eugen Rochko
5d54c1bae4 New translations strings.xml (Spanish) 2023-10-18 20:27:29 +02:00
sk
09577074a9 merge settings changes 2023-10-18 20:26:20 +02:00
sk
53f0e2a933 change extra text size 2023-10-18 19:39:12 +02:00
sk
b8dccbbef1 now hopefully fixing header/subtitle extra sizing 2023-10-18 19:23:13 +02:00
Eugen Rochko
2ef17ba051 New translations strings.xml (Spanish) 2023-10-18 19:15:17 +02:00
Eugen Rochko
f63daf3a4e New translations strings.xml (Thai) 2023-10-18 19:15:16 +02:00
Eugen Rochko
52b079be2a New translations strings.xml (Thai) 2023-10-18 18:11:40 +02:00
Eugen Rochko
efeca17106 New translations strings.xml (Chinese Traditional) 2023-10-18 11:34:03 +02:00
Eugen Rochko
6827166c1d New translations strings.xml (Galician) 2023-10-18 08:51:11 +02:00
Eugen Rochko
04483e61e8 New translations strings.xml (Galician) 2023-10-18 07:49:15 +02:00
Eugen Rochko
22fe174922 New translations strings.xml (Icelandic) 2023-10-17 20:07:09 +02:00
Eugen Rochko
f143da3913 New translations strings.xml (Kabyle) 2023-10-17 04:25:08 +02:00
Eugen Rochko
7e9f41c74b New translations strings.xml (Scottish Gaelic) 2023-10-17 04:25:05 +02:00
Eugen Rochko
a1474d0d29 New translations strings.xml (Filipino) 2023-10-17 04:25:03 +02:00
Eugen Rochko
0dce936ad3 New translations strings.xml (Persian) 2023-10-17 04:24:58 +02:00
Eugen Rochko
6ebbbb4c6c New translations strings.xml (Indonesian) 2023-10-17 04:24:57 +02:00
Eugen Rochko
dc6ddbd0ee New translations strings.xml (Portuguese, Brazilian) 2023-10-17 04:24:56 +02:00
Eugen Rochko
86c81d6b53 New translations strings.xml (Galician) 2023-10-17 04:24:55 +02:00
Eugen Rochko
451a92aa36 New translations strings.xml (Vietnamese) 2023-10-17 04:24:53 +02:00
Eugen Rochko
5c42e67e73 New translations strings.xml (Chinese Simplified) 2023-10-17 04:24:52 +02:00
Eugen Rochko
d20d36d964 New translations strings.xml (Turkish) 2023-10-17 04:24:51 +02:00
Eugen Rochko
1a8d46c71e New translations strings.xml (Slovenian) 2023-10-17 04:24:50 +02:00
Eugen Rochko
91da10eca3 New translations strings.xml (Portuguese) 2023-10-17 04:24:49 +02:00
Eugen Rochko
3bb0dcee53 New translations strings.xml (Polish) 2023-10-17 04:24:48 +02:00
Eugen Rochko
d3fd4b200f New translations strings.xml (Norwegian) 2023-10-17 04:24:47 +02:00
Eugen Rochko
fed96864e1 New translations strings.xml (Dutch) 2023-10-17 04:24:46 +02:00
Eugen Rochko
3b351bea27 New translations strings.xml (Korean) 2023-10-17 04:24:45 +02:00
Eugen Rochko
254e01dca1 New translations strings.xml (Armenian) 2023-10-17 04:24:44 +02:00
Eugen Rochko
19158e1d48 New translations strings.xml (Hungarian) 2023-10-17 04:24:43 +02:00
Eugen Rochko
bffb78fccf New translations strings.xml (Basque) 2023-10-17 04:24:41 +02:00
Eugen Rochko
a3800592a2 New translations strings.xml (Greek) 2023-10-17 04:24:39 +02:00
Eugen Rochko
22a498dfc9 New translations strings.xml (German) 2023-10-17 04:24:38 +02:00
Eugen Rochko
9ea96e32bd New translations strings.xml (Danish) 2023-10-17 04:24:37 +02:00
Eugen Rochko
68f51a123e New translations strings.xml (Czech) 2023-10-17 04:24:36 +02:00
Eugen Rochko
43b1b63581 New translations strings.xml (Catalan) 2023-10-17 04:24:35 +02:00
Eugen Rochko
43b4a2c515 New translations strings.xml (Belarusian) 2023-10-17 04:24:34 +02:00
Eugen Rochko
5b9cfdb689 New translations strings.xml (Arabic) 2023-10-17 04:24:33 +02:00
Eugen Rochko
b43cd7103a New translations strings.xml (Spanish) 2023-10-17 04:24:32 +02:00
Eugen Rochko
30e4f6e0f5 New translations strings.xml (French) 2023-10-17 04:24:30 +02:00
Eugen Rochko
1d0ebf889b New translations strings.xml (Chinese Traditional) 2023-10-17 04:24:28 +02:00
Eugen Rochko
7c4f1da485 New translations strings.xml (Finnish) 2023-10-17 04:24:27 +02:00
Eugen Rochko
8163921014 New translations strings.xml (Russian) 2023-10-17 04:24:26 +02:00
Eugen Rochko
993393dd96 New translations strings.xml (Swedish) 2023-10-17 04:24:25 +02:00
Eugen Rochko
82aed43934 New translations strings.xml (Italian) 2023-10-17 04:24:24 +02:00
Eugen Rochko
fa65134c26 New translations strings.xml (Ukrainian) 2023-10-17 04:24:23 +02:00
Eugen Rochko
af4266c739 New translations strings.xml (Japanese) 2023-10-17 04:24:22 +02:00
Eugen Rochko
f72ea2e763 New translations strings.xml (Icelandic) 2023-10-17 04:24:21 +02:00
Eugen Rochko
c5540270a3 New translations strings.xml (Thai) 2023-10-17 04:24:20 +02:00
sk
5840c94395 isolate bi-directional text in display name 2023-10-17 01:03:06 +02:00
sk
92d7ae67ef don't break custom emoji in pronouns 2023-10-17 00:38:27 +02:00
sk
fc09514a4d the pronouns are stored in the balls (extra text)
also, improve max-width behavior
2023-10-17 00:28:00 +02:00
sk
300fa97781 remove comment 2023-10-16 19:36:30 +02:00
sk
e8f0891f3a re-aligning buttons with avatar 2023-10-16 19:36:12 +02:00
sk
f25906a694 Merge remote-tracking branch 'upstream/l10n_master' 2023-10-16 19:28:27 +02:00
sk
e52699bb1c Merge remote-tracking branch 'weblate/main' 2023-10-16 19:27:38 +02:00
SomeTr
712826451f Translated using Weblate (Welsh)
Currently translated at 77.7% (14 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/cy/
2023-10-16 17:27:28 +00:00
ihor_ck
3b3065d8bd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-16 17:27:28 +00:00
alextecplayz
e11fe7d8cf Translated using Weblate (Romanian)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-10-16 17:27:28 +00:00
SomeTr
9a2f7475c9 Translated using Weblate (German)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-10-16 17:27:28 +00:00
sk
6dffb10906 make version go up 2023-10-16 19:27:12 +02:00
sk
71039d6901 the changes i made were wrong (what a surprise) 2023-10-16 19:26:29 +02:00
sk
db18c7a0d0 events for boosts, adapt removeStatus method 2023-10-16 19:14:16 +02:00
sk
7e80ed6af2 fix and refactor removing statuses 2023-10-16 18:28:47 +02:00
sk
bdd2b90581 allow entering full handles instead of domain
closes sk22#646
2023-10-16 01:03:19 +02:00
sk
50d017d8ba fix account context menu
closes sk22#652
2023-10-16 00:44:47 +02:00
sk
98e003437c fix confirm unfollow 2023-10-16 00:11:47 +02:00
sk
d5d06af614 fix keyboard gap in scheduled post list height 2023-10-15 23:59:26 +02:00
sk
1c930ca3bb check for draft before checking alt texts 2023-10-15 23:42:53 +02:00
sk
0f0c1093d4 don't hide draft options when redrafting 2023-10-15 23:35:01 +02:00
sk
2ad5dc5a74 make unfinished attachment dialog make more sense 2023-10-15 23:20:40 +02:00
sk
e02e0865bd fix softlock when re-saving draft
closes sk22#659
2023-10-15 23:05:29 +02:00
sk
59ee1af75d fix border radius
closes sk22#861
2023-10-15 22:53:23 +02:00
sk
c04584dfa6 fix alignments 2023-10-15 21:17:38 +02:00
sk
190a3b5b08 use fluent dismiss button 2023-10-15 21:17:26 +02:00
sk
f5a67e65f0 fix missing content descriptions, button alignment 2023-10-15 21:16:42 +02:00
Eugen Rochko
498078b6e0 New translations strings.xml (Swedish) 2023-10-15 20:37:55 +02:00
sk
de8c289ca7 finally!! fix reblog/-ply line with extra emoji
closes sk22#468
2023-10-15 20:29:15 +02:00
sk
07b205a746 remove fullText emoji helper, fix content description 2023-10-15 20:18:40 +02:00
sk
a7941310bc no more vertical reblog/reply line layout
closes sk22#834
2023-10-15 20:11:03 +02:00
sk
864b6dcdac remove outdated comment 2023-10-15 20:04:39 +02:00
sk
d3744bb397 remove compact reblog line setting 2023-10-15 19:57:22 +02:00
sk
7d1853bc88 fix saving wrong item
closes sk22#863
2023-10-15 19:46:14 +02:00
sk
9f0db755d1 fix text/buttons alignment, margin
closes sk22#864
2023-10-15 19:44:37 +02:00
sk
06819806be "fix" trends loading indefinitely
closes sk22#781
2023-10-15 19:23:57 +02:00
Eugen Rochko
526f5e319b New translations strings.xml (Swedish) 2023-10-15 18:51:35 +02:00
sk
30a4d0efd9 rename vars, compare content status id for reply 2023-10-15 18:10:57 +02:00
Eugen Rochko
d419dba44a New translations strings.xml (Swedish) 2023-10-15 14:26:36 +02:00
Choukajohn
87da6b9b81 Translated using Weblate (French)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-15 01:20:04 +00:00
Linerly
67cc1553da Translated using Weblate (Indonesian)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-10-14 05:22:21 +00:00
sk
9a985aad29 fix deleting boosted posts in profile 2023-10-14 01:47:13 +02:00
Eugen Rochko
fd98159fce New translations strings.xml (Greek) 2023-10-14 00:27:35 +02:00
sk
42fac30e63 wrap into full-width view 2023-10-13 23:50:49 +02:00
sk
18e7f14c16 remove duplicate selectable background 2023-10-13 23:49:54 +02:00
sk
7b263800a6 reduce button margins, realign buttons
closes sk22#
2023-10-13 23:27:49 +02:00
sk
17387a32b2 don't scale buttons with text
re: sk22#787
2023-10-13 23:04:45 +02:00
sk
3fe642c2f2 remove unwanted margin 2023-10-13 22:36:15 +02:00
sk
9225447409 use action bar icons color for toolbar
closes sk22#860
2023-10-13 22:27:53 +02:00
SomeTr
e05dee64b7 Translated using Weblate (Ukrainian)
Currently translated at 99.4% (391 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-13 20:23:08 +00:00
Choukajohn
7f30973b39 Translated using Weblate (French)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-13 20:23:08 +00:00
gallegonovato
25da9bb2d0 Translated using Weblate (Spanish)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-10-13 20:23:08 +00:00
poesty
e17b49e704 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (392 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-10-13 20:23:08 +00:00
sk
db661b56cb fix inconsistent styles, fix non-sp font
closes sk22#862
2023-10-13 22:17:45 +02:00
sk
638e1bf8e9 do show labels per default
This reverts commit a1c81e89e8.
2023-10-13 21:16:52 +02:00
sk
83786171a7 ellipsize tab bar labels 2023-10-13 13:49:54 +02:00
sk
f638a538c1 Merge remote-tracking branch 'upstream/l10n_master' 2023-10-13 13:45:36 +02:00
sk22
11e2316cbc Translated using Weblate (German)
Currently translated at 100.0% (393 of 393 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-10-13 11:45:15 +00:00
ihor_ck
1b372c7dca Translated using Weblate (Ukrainian)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-13 11:43:24 +00:00
ihor_ck
cf4e37fade Translated using Weblate (Ukrainian)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-13 11:43:24 +00:00
SomeTr
f3fc60ac00 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-13 11:43:24 +00:00
butterflyoffire
1026d99025 Translated using Weblate (Arabic)
Currently translated at 81.2% (317 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ar/
2023-10-13 11:43:24 +00:00
ihor_ck
3fb947fbfe Translated using Weblate (Ukrainian)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-10-13 11:43:24 +00:00
David Lapshin
f097506d49 Translated using Weblate (Russian)
Currently translated at 99.7% (389 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-10-13 11:43:24 +00:00
alextecplayz
3df7d74fbf Translated using Weblate (Romanian)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-10-13 11:43:24 +00:00
Oliebol
3fc17eb9c9 Translated using Weblate (Dutch)
Currently translated at 81.0% (316 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-10-13 11:43:24 +00:00
Linerly
67ee74c1a0 Translated using Weblate (Indonesian)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-10-13 11:43:24 +00:00
Espasant3
bcebba8896 Translated using Weblate (Galician)
Currently translated at 97.9% (382 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/gl/
2023-10-13 11:43:24 +00:00
Choukajohn
451b0c1ac5 Translated using Weblate (French)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-13 11:43:24 +00:00
butterflyoffire
5d08b4923b Translated using Weblate (French)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-10-13 11:43:24 +00:00
kallekn
9ba948c721 Translated using Weblate (Finnish)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fi/
2023-10-13 11:43:24 +00:00
gallegonovato
23e7513133 Translated using Weblate (Spanish)
Currently translated at 100.0% (390 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-10-13 11:43:24 +00:00
poesty
aaed8e53c8 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (389 of 390 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-10-13 11:43:24 +00:00
sk
f2754cc5c9 bump version 2023-10-13 13:43:08 +02:00
sk
6a65edd089 fix draft/schedule view for large display sizes
cc @LucasGGamerM
see 77e19b4d6f
2023-10-13 13:39:51 +02:00
LucasGGamerM
4d49b10585 fix(compose): make 'This post will be saved as a draft' view work better on smaller screens
cc: @sk22
2023-10-13 13:27:13 +02:00
sk
86bfab81bd lucas is right
closes sk22#857
2023-10-13 02:27:34 +02:00
sk
50e313cff0 fix gap max_id when post before gap is filtered
hopefully fixes sk22#856
2023-10-13 02:22:03 +02:00
sk
5d07cde6dd fix gap not filtering posts 2023-10-13 01:48:45 +02:00
sk
f84a923102 implement global default color
closes sk22#853
2023-10-13 01:28:59 +02:00
sk
c9eac34ae6 fix gap item alpha not being reset
closes sk22#851
2023-10-13 00:27:35 +02:00
sk
181a0577c8 fix missing gap item when status removed 2023-10-13 00:24:32 +02:00
sk
0426084194 fix changes breaking off-screen bound holders
i hope i didn't break everything lol
2023-10-13 00:14:29 +02:00
Eugen Rochko
197d0caf44 New translations strings.xml (Japanese) 2023-10-11 18:34:49 +02:00
Eugen Rochko
a4a082f76a New translations strings.xml (Japanese) 2023-10-11 17:14:57 +02:00
Eugen Rochko
2528d48010 New translations strings.xml (Icelandic) 2023-10-10 18:10:41 +02:00
Eugen Rochko
5456d71979 New translations strings.xml (Chinese Traditional) 2023-10-10 10:25:33 +02:00
Eugen Rochko
e36aae3cf3 New translations strings.xml (Thai) 2023-10-09 20:31:09 +02:00
Grishka
d6040c0895 Hide the FAB while editing profile 2023-10-09 19:42:54 +02:00
Eugen Rochko
6d12e2dd72 New translations strings.xml (Thai) 2023-10-09 19:20:32 +02:00
sk
ff47e6edba remove unused method 2023-10-09 18:46:52 +02:00
sk
327ceb04d4 fix deleting notifications
closes sk22#677
closes sk22#633
2023-10-09 18:35:27 +02:00
sk
40a34b07de add option to disable underlined links
closes sk22#847
2023-10-09 18:07:19 +02:00
Eugen Rochko
f117249bb5 New translations strings.xml (Thai) 2023-10-09 18:02:31 +02:00
sk
1c90164ece fix broken hashtag links
closes sk22#848
2023-10-09 17:59:22 +02:00
sk
0d5fb250bc migrate theme colors
closes sk22#849
2023-10-09 17:54:54 +02:00
sk
e9bd5a373a remove unused icons and replace some wrong ones 2023-10-09 17:39:18 +02:00
sk
c494d283ba remove duplicate section, replace icons
closes sk22#850
2023-10-09 17:38:55 +02:00
Eugen Rochko
cf1d537367 New translations strings.xml (Vietnamese) 2023-10-09 06:03:28 +02:00
Eugen Rochko
517d13b400 New translations strings.xml (Vietnamese) 2023-10-09 05:07:06 +02:00
Eugen Rochko
fae870c93a New translations strings.xml (Urdu (India)) 2023-10-08 21:13:23 +02:00
Eugen Rochko
f8e00dcc80 New translations strings.xml (Kabyle) 2023-10-08 21:13:22 +02:00
Eugen Rochko
5fdbb597bb New translations strings.xml (Igbo) 2023-10-08 21:13:21 +02:00
Eugen Rochko
d74b286a9d New translations strings.xml (Occitan) 2023-10-08 21:13:20 +02:00
Eugen Rochko
ecb3c521ff New translations strings.xml (Scottish Gaelic) 2023-10-08 21:13:19 +02:00
Eugen Rochko
1d093ce928 New translations strings.xml (Sinhala) 2023-10-08 21:13:18 +02:00
Eugen Rochko
46b711af2e New translations strings.xml (Bosnian) 2023-10-08 21:13:17 +02:00
Eugen Rochko
772e6ddb5d New translations strings.xml (Filipino) 2023-10-08 21:13:16 +02:00
Eugen Rochko
f84e8443d2 New translations strings.xml (Burmese) 2023-10-08 21:13:15 +02:00
Eugen Rochko
250c18ebf1 New translations strings.xml (Hindi) 2023-10-08 21:13:14 +02:00
Eugen Rochko
e3d0f38b79 New translations strings.xml (Croatian) 2023-10-08 21:13:14 +02:00
Eugen Rochko
c512f97783 New translations strings.xml (Bengali) 2023-10-08 21:13:13 +02:00
Eugen Rochko
0594680775 New translations strings.xml (Persian) 2023-10-08 21:13:12 +02:00
Eugen Rochko
f999881f59 New translations strings.xml (Indonesian) 2023-10-08 21:13:11 +02:00
Eugen Rochko
4fe9192ac6 New translations strings.xml (Portuguese, Brazilian) 2023-10-08 21:13:10 +02:00
Eugen Rochko
d936702fa9 New translations strings.xml (Galician) 2023-10-08 21:13:08 +02:00
Eugen Rochko
74e284b0de New translations strings.xml (Vietnamese) 2023-10-08 21:13:07 +02:00
Eugen Rochko
4c42b72ed8 New translations strings.xml (Chinese Simplified) 2023-10-08 21:13:06 +02:00
Eugen Rochko
0e0046df65 New translations strings.xml (Turkish) 2023-10-08 21:13:05 +02:00
Eugen Rochko
c80d1d10c2 New translations strings.xml (Slovenian) 2023-10-08 21:13:05 +02:00
Eugen Rochko
da97971011 New translations strings.xml (Portuguese) 2023-10-08 21:13:04 +02:00
Eugen Rochko
700447dbe7 New translations strings.xml (Polish) 2023-10-08 21:13:03 +02:00
Eugen Rochko
37e7b5ee93 New translations strings.xml (Norwegian) 2023-10-08 21:13:02 +02:00
Eugen Rochko
1265afa93f New translations strings.xml (Dutch) 2023-10-08 21:13:01 +02:00
Eugen Rochko
1e09481b02 New translations strings.xml (Korean) 2023-10-08 21:13:00 +02:00
Eugen Rochko
9996a5a05e New translations strings.xml (Armenian) 2023-10-08 21:12:59 +02:00
Eugen Rochko
f20aac7c81 New translations strings.xml (Hungarian) 2023-10-08 21:12:58 +02:00
Eugen Rochko
98f7b0bacd New translations strings.xml (Hebrew) 2023-10-08 21:12:57 +02:00
Eugen Rochko
3f6d3fb3a2 New translations strings.xml (Irish) 2023-10-08 21:12:56 +02:00
Eugen Rochko
663b49c76b New translations strings.xml (Basque) 2023-10-08 21:12:55 +02:00
Eugen Rochko
16e38f2541 New translations strings.xml (Greek) 2023-10-08 21:12:54 +02:00
Eugen Rochko
842cc55e47 New translations strings.xml (German) 2023-10-08 21:12:53 +02:00
Eugen Rochko
72db099e6f New translations strings.xml (Danish) 2023-10-08 21:12:52 +02:00
Eugen Rochko
be130bc3a7 New translations strings.xml (Czech) 2023-10-08 21:12:51 +02:00
Eugen Rochko
42253336e1 New translations strings.xml (Catalan) 2023-10-08 21:12:50 +02:00
Eugen Rochko
572631e1d7 New translations strings.xml (Belarusian) 2023-10-08 21:12:49 +02:00
Eugen Rochko
723777a800 New translations strings.xml (Arabic) 2023-10-08 21:12:48 +02:00
Eugen Rochko
b825d534c1 New translations strings.xml (Spanish) 2023-10-08 21:12:47 +02:00
Eugen Rochko
b9749620a8 New translations strings.xml (French) 2023-10-08 21:12:46 +02:00
Eugen Rochko
70ea9989aa New translations strings.xml (Romanian) 2023-10-08 21:12:45 +02:00
Eugen Rochko
b3ec9c981c New translations strings.xml (Chinese Traditional) 2023-10-08 21:12:44 +02:00
Eugen Rochko
bf72085abb New translations strings.xml (Finnish) 2023-10-08 21:12:43 +02:00
Eugen Rochko
64dd416b59 New translations strings.xml (Russian) 2023-10-08 21:12:42 +02:00
Eugen Rochko
ab2a920455 New translations strings.xml (Swedish) 2023-10-08 21:12:41 +02:00
Eugen Rochko
7580446d60 New translations strings.xml (Italian) 2023-10-08 21:12:40 +02:00
Eugen Rochko
ade18ac6fc New translations strings.xml (Ukrainian) 2023-10-08 21:12:39 +02:00
Eugen Rochko
005c851d72 New translations strings.xml (Japanese) 2023-10-08 21:12:38 +02:00
Eugen Rochko
0f1d46c765 New translations strings.xml (Icelandic) 2023-10-08 21:12:36 +02:00
Eugen Rochko
21fbb07b1d New translations strings.xml (Thai) 2023-10-08 21:12:36 +02:00
sk
58e0ce3970 add tooltips 2023-10-08 16:36:56 +02:00
sk
139a7d7c98 change settings order 2023-10-08 16:36:48 +02:00
sk
a1c81e89e8 don't show labels per default 2023-10-08 16:36:41 +02:00
sk
a5c197b496 hearts everywhere
closes sk22#846
2023-10-08 13:04:26 +02:00
sk
df49ef9d58 redder red 2023-10-08 12:54:59 +02:00
sk
f747d4c979 toggle label font weight according to m3 2023-10-08 12:49:29 +02:00
sk
98677cd307 on page change listener may also load first page 2023-10-08 11:48:59 +02:00
357 changed files with 7764 additions and 3457 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk" applicationId "org.joinmastodon.android.sk"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 102 versionCode 109
versionName "2.1.6+fork.102" versionName "2.1.6+fork.109"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW'] resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
} }
@@ -33,7 +33,6 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
} }
githubRelease { initWith release } githubRelease { initWith release }
playRelease { initWith release }
fdroidRelease { initWith release } fdroidRelease { initWith release }
} }
compileOptions { compileOptions {

View File

@@ -257,5 +257,9 @@ public class UiUtilsTest {
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount( assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
makeField("pronouns", "-- * (asterisk) --") makeField("pronouns", "-- * (asterisk) --")
)).orElseThrow()); )).orElseThrow());
assertEquals("they/(she?)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
makeField("pronouns", "they/(she?)...")
)).orElseThrow());
} }
} }

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because it is too large Load Diff

View File

@@ -266,7 +266,7 @@ public class AudioPlayerService extends Service{
private void updateNotification(boolean dismissable, boolean removeNotification){ private void updateNotification(boolean dismissable, boolean removeNotification){
Notification.Builder bldr=new Notification.Builder(this) Notification.Builder bldr=new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_ntf_logo) .setSmallIcon(R.drawable.ic_ntf_logo)
.setContentTitle(status.account.displayName) .setContentTitle(status.account.getDisplayName())
.setContentText(HtmlParser.strip(status.content)) .setContentText(HtmlParser.strip(status.content))
.setOngoing(!dismissable) .setOngoing(!dismissable)
.setShowWhen(false) .setShowWhen(false)
@@ -281,7 +281,7 @@ public class AudioPlayerService extends Service{
if(playerReady){ if(playerReady){
boolean isPlaying=player.isPlaying(); boolean isPlaying=player.isPlaying();
bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_pause_24 : R.drawable.ic_play_24), bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_fluent_pause_24_filled : R.drawable.ic_fluent_play_24_filled),
getString(isPlaying ? R.string.pause : R.string.play), getString(isPlaying ? R.string.pause : R.string.play),
PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE)) PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE))
.build()); .build());

View File

@@ -1,17 +1,17 @@
package org.joinmastodon.android; package org.joinmastodon.android;
import static org.joinmastodon.android.api.MastodonAPIController.gson; import static org.joinmastodon.android.api.MastodonAPIController.gson;
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference.MATERIAL3;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
import androidx.annotation.StringRes;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.session.AccountLocalPreferences; import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.ContentType; import org.joinmastodon.android.model.ContentType;
@@ -25,8 +25,6 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import androidx.annotation.StringRes;
public class GlobalUserPreferences{ public class GlobalUserPreferences{
private static final String TAG="GlobalUserPreferences"; private static final String TAG="GlobalUserPreferences";
@@ -53,7 +51,6 @@ public class GlobalUserPreferences{
public static boolean collapseLongPosts; public static boolean collapseLongPosts;
public static boolean spectatorMode; public static boolean spectatorMode;
public static boolean autoHideFab; public static boolean autoHideFab;
public static boolean compactReblogReplyLine;
public static boolean allowRemoteLoading; public static boolean allowRemoteLoading;
public static boolean forwardReportDefault; public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers; public static AutoRevealMode autoRevealEqualSpoilers;
@@ -62,6 +59,9 @@ public class GlobalUserPreferences{
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings; public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
public static boolean overlayMedia; public static boolean overlayMedia;
public static boolean showSuicideHelp; public static boolean showSuicideHelp;
public static boolean underlinedLinks;
public static ColorPreference color;
public static boolean likeIcon;
private static SharedPreferences getPrefs(){ private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE); return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
@@ -111,7 +111,6 @@ public class GlobalUserPreferences{
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true); collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false); spectatorMode=prefs.getBoolean("spectatorMode", false);
autoHideFab=prefs.getBoolean("autoHideFab", true); autoHideFab=prefs.getBoolean("autoHideFab", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true); allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name())); autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true); forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
@@ -122,6 +121,9 @@ public class GlobalUserPreferences{
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true); displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
overlayMedia=prefs.getBoolean("overlayMedia", false); overlayMedia=prefs.getBoolean("overlayMedia", false);
showSuicideHelp=prefs.getBoolean("showSuicideHelp", true); showSuicideHelp=prefs.getBoolean("showSuicideHelp", true);
underlinedLinks=prefs.getBoolean("underlinedLinks", true);
color=ColorPreference.valueOf(prefs.getString("color", MATERIAL3.name()));
likeIcon=prefs.getBoolean("likeIcon", false);
if (prefs.contains("prefixRepliesWithRe")) { if (prefs.contains("prefixRepliesWithRe")) {
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false) prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
@@ -132,8 +134,11 @@ public class GlobalUserPreferences{
.apply(); .apply();
} }
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61(); int migrationLevel=prefs.getInt("migrationLevel", BuildConfig.VERSION_CODE);
if(prefs.getInt("migrationLevel", 0) < 101) migrateToVersion101(); if(migrationLevel < 61)
migrateToUpstreamVersion61();
if(migrationLevel < BuildConfig.VERSION_CODE)
prefs.edit().putInt("migrationLevel", BuildConfig.VERSION_CODE).apply();
} }
public static void save(){ public static void save(){
@@ -163,7 +168,6 @@ public class GlobalUserPreferences{
.putBoolean("collapseLongPosts", collapseLongPosts) .putBoolean("collapseLongPosts", collapseLongPosts)
.putBoolean("spectatorMode", spectatorMode) .putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab) .putBoolean("autoHideFab", autoHideFab)
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
.putBoolean("allowRemoteLoading", allowRemoteLoading) .putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name()) .putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault) .putBoolean("forwardReportDefault", forwardReportDefault)
@@ -174,19 +178,33 @@ public class GlobalUserPreferences{
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings) .putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
.putBoolean("overlayMedia", overlayMedia) .putBoolean("overlayMedia", overlayMedia)
.putBoolean("showSuicideHelp", showSuicideHelp) .putBoolean("showSuicideHelp", showSuicideHelp)
.putBoolean("underlinedLinks", underlinedLinks)
.putString("color", color.name())
.putBoolean("likeIcon", likeIcon)
.apply(); .apply();
} }
private static void migrateToVersion101(){ public enum ThemePreference{
Log.d(TAG, "Migrating preferences to version 101!! (copy current theme to local preferences)"); AUTO,
LIGHT,
AccountSessionManager asm=AccountSessionManager.getInstance(); DARK
for(AccountSession session : asm.getLoggedInAccounts()){
String accountID=session.getID();
AccountLocalPreferences localPrefs=session.getLocalPreferences();
}
} }
public enum AutoRevealMode {
NEVER,
THREADS,
DISCUSSIONS
}
public enum PrefixRepliesMode {
NEVER,
ALWAYS,
TO_OTHERS
}
//region preferences migrations
private static void migrateToUpstreamVersion61(){ private static void migrateToUpstreamVersion61(){
Log.d(TAG, "Migrating preferences to upstream version 61!!"); Log.d(TAG, "Migrating preferences to upstream version 61!!");
@@ -233,25 +251,8 @@ public class GlobalUserPreferences{
localPrefs.save(); localPrefs.save();
} }
prefs.edit().putInt("migrationLevel", 61).apply();
} }
public enum ThemePreference{ //endregion
AUTO,
LIGHT,
DARK
}
public enum AutoRevealMode {
NEVER,
THREADS,
DISCUSSIONS
}
public enum PrefixRepliesMode {
NEVER,
ALWAYS,
TO_OTHERS
}
} }

View File

@@ -31,18 +31,44 @@ import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels; import org.parceler.Parcels;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Instant;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent { public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
private static final String TAG="MainActivity";
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
AccountSession session=getCurrentSession(); AccountSession session=getCurrentSession();
UiUtils.setUserPreferredTheme(this, session); UiUtils.setUserPreferredTheme(this, session);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e)->{
File file=new File(MastodonApp.context.getFilesDir(), "crash.log");
try(FileOutputStream out=new FileOutputStream(file)){
PrintWriter writer=new PrintWriter(out);
writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")");
writer.println(Instant.now().toString());
writer.println();
e.printStackTrace(writer);
writer.flush();
}catch(IOException x){
Log.e(TAG, "Error writing crash.log", x);
}finally{
defaultHandler.uncaughtException(t, e);
}
});
if(savedInstanceState==null){ if(savedInstanceState==null){
restartHomeFragment(); restartHomeFragment();
} }

View File

@@ -14,7 +14,6 @@ import android.content.Intent;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@@ -33,7 +32,6 @@ import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification; import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -212,7 +210,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
if (!GlobalUserPreferences.uniformNotificationIcon) { if (!GlobalUserPreferences.uniformNotificationIcon) {
builder.setSmallIcon(switch (pn.notificationType) { builder.setSmallIcon(switch (pn.notificationType) {
case FAVORITE -> R.drawable.ic_fluent_star_24_filled; case FAVORITE -> GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_filled : R.drawable.ic_fluent_star_24_filled;
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled; case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled; case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled;
case MENTION -> R.drawable.ic_fluent_mention_24_filled; case MENTION -> R.drawable.ic_fluent_mention_24_filled;

View File

@@ -26,6 +26,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -69,7 +70,7 @@ public class CacheController{
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class); Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
status.postprocess(); status.postprocess();
int flags=cursor.getInt(1); int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0); status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0) ? status.id : null;
newMaxID=status.id; newMaxID=status.id;
result.add(status); result.add(status);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
@@ -113,12 +114,14 @@ public class CacheController{
values.put("id", s.id); values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s)); values.put("json", MastodonAPIController.gson.toJson(s));
int flags=0; int flags=0;
if(s.hasGapAfter) if(Objects.equals(s.hasGapAfter, s.id))
flags|=POST_FLAG_GAP_AFTER; flags|=POST_FLAG_GAP_AFTER;
values.put("flags", flags); values.put("flags", flags);
values.put("time", s.createdAt.getEpochSecond()); values.put("time", s.createdAt.getEpochSecond());
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE); db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
if(!clear)
db.delete("home_timeline", "`id` NOT IN (SELECT `id` FROM `home_timeline` ORDER BY `time` DESC LIMIT ?)", new String[]{"1000"});
}); });
} }
@@ -270,6 +273,28 @@ public class CacheController{
public void deleteStatus(String id){ public void deleteStatus(String id){
runOnDbThread((db)->{ runOnDbThread((db)->{
String gapId=null;
int gapFlags=0;
// select to-be-removed and newer row
try(Cursor cursor=db.query("home_timeline", new String[]{"id", "flags"}, "`time`>=(SELECT `time` FROM `home_timeline` WHERE `id`=?)", new String[]{id}, null, null, "`time` ASC", "2")){
boolean hadGapAfter=false;
// always either one or two iterations (only one if there's no newer post)
while(cursor.moveToNext()){
String currentId=cursor.getString(0);
int currentFlags=cursor.getInt(1);
if(currentId.equals(id)){
hadGapAfter=((currentFlags & POST_FLAG_GAP_AFTER)!=0);
}else if(hadGapAfter){
gapFlags=currentFlags|POST_FLAG_GAP_AFTER;
gapId=currentId;
}
}
}
if(gapId!=null){
ContentValues values=new ContentValues();
values.put("flags", gapFlags);
db.update("home_timeline", values, "`id`=?", new String[]{gapId});
}
db.delete("home_timeline", "`id`=?", new String[]{id}); db.delete("home_timeline", "`id`=?", new String[]{id});
}); });
} }

View File

@@ -54,7 +54,7 @@ public class MastodonAPIController{
.create(); .create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController"); private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder() private static OkHttpClient httpClient=new OkHttpClient.Builder()
.readTimeout(5, TimeUnit.MINUTES) .readTimeout(30, TimeUnit.SECONDS)
.build(); .build();
private AccountSession session; private AccountSession session;
@@ -113,13 +113,13 @@ public class MastodonAPIController{
} }
Request hreq=builder.build(); Request hreq=builder.build();
Call call=httpClient.newCall(hreq); OkHttpClient client=req.timeout>0
? httpClient.newBuilder().readTimeout(req.timeout, TimeUnit.MILLISECONDS).build()
: httpClient;
Call call=client.newCall(hreq);
synchronized(req){ synchronized(req){
req.okhttpCall=call; req.okhttpCall=call;
} }
if(req.timeout>0){
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
}
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq); Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);

View File

@@ -153,8 +153,9 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
headers.put(key, value); headers.put(key, value);
} }
protected void setTimeout(long timeout){ public MastodonAPIRequest<T> setTimeout(long timeout){
this.timeout=timeout; this.timeout=timeout;
return this;
} }
protected String getPathPrefix(){ protected String getPathPrefix(){

View File

@@ -7,7 +7,10 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked; import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited; import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged; import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.ReblogDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.StatusPrivacy;
@@ -48,7 +51,7 @@ public class StatusInteractionController{
runningFavoriteRequests.remove(status.id); runningFavoriteRequests.remove(status.id);
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1)); result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
cb.accept(result); cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
} }
@Override @Override
@@ -57,13 +60,13 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context); error.showToast(MastodonApp.context);
status.favourited=!favorited; status.favourited=!favorited;
cb.accept(status); cb.accept(status);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
} }
}) })
.exec(accountID); .exec(accountID);
runningFavoriteRequests.put(status.id, req); runningFavoriteRequests.put(status.id, req);
status.favourited=favorited; status.favourited=favorited;
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
} }
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){ public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
@@ -78,11 +81,15 @@ public class StatusInteractionController{
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Status reblog){ public void onSuccess(Status reblog){
Status result = reblog.getContentStatus(); Status result=reblog.getContentStatus();
runningReblogRequests.remove(status.id); runningReblogRequests.remove(status.id);
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1)); result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
cb.accept(result); cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result)); if(updateCounters){
E.post(new StatusCountersUpdatedEvent(result));
if(reblogged) E.post(new StatusCreatedEvent(reblog, accountID));
else E.post(new ReblogDeletedEvent(status.id, accountID));
}
} }
@Override @Override
@@ -91,13 +98,13 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context); error.showToast(MastodonApp.context);
status.reblogged=!reblogged; status.reblogged=!reblogged;
cb.accept(status); cb.accept(status);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
} }
}) })
.exec(accountID); .exec(accountID);
runningReblogRequests.put(status.id, req); runningReblogRequests.put(status.id, req);
status.reblogged=reblogged; status.reblogged=reblogged;
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
} }
public void setBookmarked(Status status, boolean bookmarked){ public void setBookmarked(Status status, boolean bookmarked){
@@ -118,7 +125,7 @@ public class StatusInteractionController{
public void onSuccess(Status result){ public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id); runningBookmarkRequests.remove(status.id);
cb.accept(result); cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
} }
@Override @Override
@@ -127,12 +134,12 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context); error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked; status.bookmarked=!bookmarked;
cb.accept(status); cb.accept(status);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
} }
}) })
.exec(accountID); .exec(accountID);
runningBookmarkRequests.put(status.id, req); runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked; status.bookmarked=bookmarked;
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status)); if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
} }
} }

View File

@@ -0,0 +1,19 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetPrivateNote extends MastodonAPIRequest<Relationship>{
public SetPrivateNote(String id, String comment){
super(MastodonAPIRequest.HttpMethod.POST, "/accounts/"+id+"/note", Relationship.class);
Request req = new Request(comment);
setRequestBody(req);
}
private static class Request{
public String comment;
public Request(String comment){
this.comment=comment;
}
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.AkkomaTranslation;
public class AkkomaTranslateStatus extends MastodonAPIRequest<AkkomaTranslation>{
public AkkomaTranslateStatus(String id, String lang){
super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toUpperCase(), AkkomaTranslation.class);
}
}

View File

@@ -48,6 +48,8 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public String quoteId; public String quoteId;
public ContentType contentType; public ContentType contentType;
public boolean preview;
public static class Poll{ public static class Poll{
public ArrayList<String> options=new ArrayList<>(); public ArrayList<String> options=new ArrayList<>();
public int expiresIn; public int expiresIn;

View File

@@ -26,6 +26,8 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
s.visibility=StatusPrivacy.PUBLIC; s.visibility=StatusPrivacy.PUBLIC;
s.mentions=Collections.emptyList(); s.mentions=Collections.emptyList();
s.tags=Collections.emptyList(); s.tags=Collections.emptyList();
if (s.poll != null)
s.poll.id="fakeID"+i;
i++; i++;
} }
super.validateAndPostprocessResponse(respObj, httpResponse); super.validateAndPostprocessResponse(respObj, httpResponse);

View File

@@ -10,6 +10,7 @@ import androidx.annotation.StringRes;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.model.ContentType; import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
@@ -43,7 +44,6 @@ public class AccountLocalPreferences{
public boolean emojiReactionsEnabled; public boolean emojiReactionsEnabled;
public ShowEmojiReactions showEmojiReactions; public ShowEmojiReactions showEmojiReactions;
public ColorPreference color; public ColorPreference color;
public boolean likeIcon;
public ArrayList<Emoji> recentCustomEmoji; public ArrayList<Emoji> recentCustomEmoji;
private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType(); private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
@@ -73,8 +73,7 @@ public class AccountLocalPreferences{
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false); keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma()); emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name())); showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name()));
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.MATERIAL3.name())); color=prefs.contains("color") ? ColorPreference.valueOf(prefs.getString("color", null)) : null;
likeIcon=prefs.getBoolean("likeIcon", false);
recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>()); recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>());
} }
@@ -86,6 +85,10 @@ public class AccountLocalPreferences{
prefs.edit().putLong("notificationsPauseTime", time).apply(); prefs.edit().putLong("notificationsPauseTime", time).apply();
} }
public ColorPreference getCurrentColor(){
return color!=null ? color : GlobalUserPreferences.color!=null ? GlobalUserPreferences.color : ColorPreference.MATERIAL3;
}
public void save(){ public void save(){
prefs.edit() prefs.edit()
.putBoolean("interactionCounts", showInteractionCounts) .putBoolean("interactionCounts", showInteractionCounts)
@@ -109,8 +112,7 @@ public class AccountLocalPreferences{
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification) .putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled) .putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
.putString("showEmojiReactions", showEmojiReactions.name()) .putString("showEmojiReactions", showEmojiReactions.name())
.putString("color", color.name()) .putString("color", color!=null ? color.name() : null)
.putBoolean("likeIcon", likeIcon)
.putString("recentCustomEmoji", gson.toJson(recentCustomEmoji)) .putString("recentCustomEmoji", gson.toJson(recentCustomEmoji))
.apply(); .apply();
} }

View File

@@ -218,7 +218,7 @@ public class AccountSession{
public void savePreferencesIfPending(){ public void savePreferencesIfPending(){
if(preferencesNeedSaving){ if(preferencesNeedSaving){
new UpdateAccountCredentialsPreferences(preferences, null, self.discoverable, self.source.indexable) new UpdateAccountCredentialsPreferences(preferences, self.locked, self.discoverable, self.source.indexable)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Account result){ public void onSuccess(Account result){
@@ -260,16 +260,19 @@ public class AccountSession{
} }
private boolean isFilteredType(Status s){ private boolean isFilteredType(Status s){
AccountLocalPreferences localPreferences = getLocalPreferences();
return (!localPreferences.showReplies && s.inReplyToId != null) return (!localPreferences.showReplies && s.inReplyToId != null)
|| (!localPreferences.showBoosts && s.reblog != null); || (!localPreferences.showBoosts && s.reblog != null);
} }
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){ public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
AccountLocalPreferences localPreferences = getLocalPreferences();
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){ if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
Status s=extractor.apply(obj); Status s=extractor.apply(obj);
if(s!=null && s.filtered!=null){ if(s!=null && s.filtered!=null){
localPreferences.serverSideFiltersSupported=true; localPreferences.serverSideFiltersSupported=true;
localPreferences.save(); localPreferences.save();
break;
} }
} }
@@ -279,9 +282,17 @@ public class AccountSession{
if(filterStatusContainingObject(o, extractor, context, profile)){ if(filterStatusContainingObject(o, extractor, context, profile)){
Status s=extractor.apply(o); Status s=extractor.apply(o);
removeUs.add(o); removeUs.add(o);
if(s!=null && s.hasGapAfter && i > 0){ if(s!=null && s.hasGapAfter!=null && i>0){
Status prev=extractor.apply(objects.get(i - 1)); // oops, we're about to remove an item that has a gap after...
if(prev!=null) prev.hasGapAfter=true; // gotta find the previous status that's not also about to be removed
for(int j=i-1; j>=0; j--){
T p=objects.get(j);
Status prev=extractor.apply(objects.get(j));
if(prev!=null && !removeUs.contains(p)){
prev.hasGapAfter=s.hasGapAfter;
break;
}
}
} }
} }
} }
@@ -298,7 +309,7 @@ public class AccountSession{
if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC)) if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC))
return true; return true;
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them // Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
if(localPreferences.serverSideFiltersSupported){ if(getLocalPreferences().serverSideFiltersSupported){
for(FilterResult filter : s.filtered){ for(FilterResult filter : s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE) if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true; return true;

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.api.session; package org.joinmastodon.android.api.session;
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
import android.app.Activity; import android.app.Activity;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.ComponentName; import android.content.ComponentName;
@@ -34,6 +36,7 @@ import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.unifiedpush.android.connector.UnifiedPush;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@@ -101,6 +104,7 @@ public class AccountSessionManager{
} }
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){ public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
Context context = MastodonApp.context;
instances.put(instance.uri, instance); instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo); AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
sessions.put(session.getID(), session); sessions.put(session.getID(), session);
@@ -113,7 +117,14 @@ public class AccountSessionManager{
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri)); MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
updateMoreInstanceInfo(instance, instance.uri); updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){ if (!UnifiedPush.getDistributor(context).isEmpty()) {
UnifiedPush.registerApp(
context,
session.getID(),
new ArrayList<>(),
context.getPackageName()
);
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null); session.getPushSubscriptionManager().registerAccountForPush(null);
} }
maybeUpdateShortcuts(); maybeUpdateShortcuts();

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class ReblogDeletedEvent{
public final String statusID;
public final String accountID;
public ReblogDeletedEvent(String statusID, String accountID){
this.statusID=statusID;
this.accountID=accountID;
}
}

View File

@@ -1,7 +1,5 @@
package org.joinmastodon.android.events; package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusDeletedEvent{ public class ScheduledStatusDeletedEvent{
public final String id; public final String id;
public final String accountID; public final String accountID;

View File

@@ -9,5 +9,6 @@ public class StatusCreatedEvent{
public StatusCreatedEvent(Status status, String accountID){ public StatusCreatedEvent(Status status, String accountID){
this.status=status; this.status=status;
this.accountID=accountID; this.accountID=accountID;
status.fromStatusCreated=true;
} }
} }

View File

@@ -19,12 +19,15 @@ import android.widget.Toolbar;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote; import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus; import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AkkomaTranslation;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
@@ -33,7 +36,6 @@ import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
@@ -48,6 +50,7 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent; import org.joinmastodon.android.utils.ProvidesAssistContent;
@@ -60,6 +63,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -359,12 +363,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
}); });
list.addItemDecoration(new StatusListItemDecoration()); list.addItemDecoration(new StatusListItemDecoration());
list.addItemDecoration(new InsetStatusItemDecoration(this));
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){ ((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
private Rect tmpRect=new Rect(); private Rect tmpRect=new Rect();
@Override @Override
public void getSelectorBounds(View view, Rect outRect){ public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false; if(list!=view.getParent()) return;
int lastIndex = -1, firstIndex = -1; boolean hasDescendant=false, hasAncestor=false, isWarning=false;
int lastIndex=-1, firstIndex=-1;
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){ if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
list.getDecoratedBoundsWithMargins(view, outRect); list.getDecoratedBoundsWithMargins(view, outRect);
}else{ }else{
@@ -469,10 +475,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected void updatePoll(String itemID, Status status, Poll poll){ protected void updatePoll(String itemID, Status status, Poll poll){
status.poll=poll; status.poll=poll;
int firstOptionIndex=-1, footerIndex=-1; int firstOptionIndex=-1, footerIndex=-1;
int spoilerFirstOptionIndex=-1, spoilerFooterIndex=-1;
SpoilerStatusDisplayItem spoilerItem=null;
int i=0; int i=0;
for(StatusDisplayItem item:displayItems){ for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(itemID)){ if(item.parentID.equals(itemID)){
if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){ if(item instanceof SpoilerStatusDisplayItem){
spoilerItem=(SpoilerStatusDisplayItem) item;
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
firstOptionIndex=i; firstOptionIndex=i;
}else if(item instanceof PollFooterStatusDisplayItem){ }else if(item instanceof PollFooterStatusDisplayItem){
footerIndex=i; footerIndex=i;
@@ -485,8 +495,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
throw new IllegalStateException("Can't find all poll items in displayItems"); throw new IllegalStateException("Can't find all poll items in displayItems");
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1); List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
int prevSize=pollItems.size(); int prevSize=pollItems.size();
if(spoilerItem!=null){
spoilerFirstOptionIndex=spoilerItem.contentItems.indexOf(pollItems.get(0));
spoilerFooterIndex=spoilerItem.contentItems.indexOf(pollItems.get(pollItems.size()-1));
}
pollItems.clear(); pollItems.clear();
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems); StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems);
if(spoilerItem!=null){
spoilerItem.contentItems.subList(spoilerFirstOptionIndex, spoilerFooterIndex+1).clear();
spoilerItem.contentItems.addAll(spoilerFirstOptionIndex, pollItems);
}
if(prevSize!=pollItems.size()){ if(prevSize!=pollItems.size()){
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize); adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size()); adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
@@ -553,37 +571,39 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){ public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
Status status=holder.getItem().status; Status status=holder.getItem().status;
toggleSpoiler(status, holder.getItemID()); boolean isForQuote=holder.getItem().isForQuote;
toggleSpoiler(status, isForQuote, holder.getItemID());
} }
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) { public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
Status status = holder.getItem().status; Status status = holder.getItem().status;
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if (mediaGrid != null) {
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
else mediaGrid.hideSensitive();
} else {
// media grid's methods normally change the status' state - we still want to be able
// to do this if the media grid is not bound, tho - so, doing it ourselves here
status.sensitiveRevealed = !status.sensitiveRevealed;
}
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false); if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
if(!status.sensitiveRevealed) mediaGrid.revealSensitive();
else mediaGrid.hideSensitive();
}else{
status.sensitiveRevealed=false;
notifyItemChangedAfter(holder.getItem(), MediaGridStatusDisplayItem.class);
}
} }
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) { public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class); HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(header != null && header.getItem().hasVisibilityToggle) header.animateVisibilityToggle(true); if(header!=null && header.getItem().hasVisibilityToggle) header.animateVisibilityToggle(true);
else notifyItemChangedBefore(holder.getItem(), HeaderStatusDisplayItem.class);
} }
protected void toggleSpoiler(Status status, String itemID){ protected void toggleSpoiler(Status status, boolean isForQuote, String itemID){
status.spoilerRevealed=!status.spoilerRevealed; status.spoilerRevealed=!status.spoilerRevealed;
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
status.sensitiveRevealed = false; status.sensitiveRevealed = false;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class); List<SpoilerStatusDisplayItem.Holder> spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null) SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0);
spoiler.rebind(); if(spoiler!=null) spoiler.rebind();
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class)); else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class);
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem());
int index=displayItems.indexOf(spoilerItem); int index=displayItems.indexOf(spoilerItem);
if(status.spoilerRevealed){ if(status.spoilerRevealed){
@@ -594,39 +614,29 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size()); adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
} }
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); notifyItemChanged(itemID, TextStatusDisplayItem.class);
if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class); HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null) if(header!=null) header.rebind();
header.rebind(); else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
list.invalidateItemDecorations(); list.invalidateItemDecorations();
} }
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) { public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
if (holder.getItem().status.textExpandable != expandable && list != null) { Status s=holder.getItem().status;
holder.getItem().status.textExpandable = expandable; if(s.textExpandable!=expandable && list!=null) {
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class); s.textExpandable=expandable;
if (header != null) header.rebind(); HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(header!=null) header.bindCollapseButton();
} }
} }
public void onToggleExpanded(Status status, String itemID) { public void onToggleExpanded(Status status, String itemID) {
status.textExpanded = !status.textExpanded; status.textExpanded = !status.textExpanded;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); notifyItemChanged(itemID, TextStatusDisplayItem.class);
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class); HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if (text != null) text.rebind(); if(header!=null) header.animateExpandToggle();
if (header != null) header.rebind(); else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
}
public void updateEmojiReactions(Status status, String itemID){
EmojiReactionsStatusDisplayItem.Holder reactions=findHolderOfType(itemID, EmojiReactionsStatusDisplayItem.Holder.class);
if(reactions != null){
reactions.getItem().status.reactions.clear();
reactions.getItem().status.reactions.addAll(status.reactions);
reactions.rebind();
}
} }
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){} public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
@@ -685,9 +695,61 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return null; return null;
} }
/**
* Use this as a fallback if findHolderOfType fails to find the ViewHolder.
* It might still be bound but off-screen and therefore not a child of the RecyclerView -
* resulting in the ViewHolder displaying an outdated state once scrolled back into view.
*/
protected <I extends StatusDisplayItem> int notifyItemChanged(String id, Class<I> type){
boolean encounteredParent=false;
for(int i=0; i<displayItems.size(); i++){
StatusDisplayItem item=displayItems.get(i);
boolean idEquals=id.equals(item.parentID);
if(!encounteredParent && idEquals) encounteredParent=true; // reached top of the parent
else if(encounteredParent && !idEquals) break; // passed by bottom of the parent. man muss ja wissen wann schluss is
if(idEquals && type.isInstance(item)){
adapter.notifyItemChanged(i);
return i;
}
}
return -1;
}
protected <I extends StatusDisplayItem> int notifyItemChangedAfter(StatusDisplayItem afterThis, Class<I> type){
int startIndex=displayItems.indexOf(afterThis);
if(startIndex == -1) throw new IllegalStateException("notifyItemChangedAfter didn't find the passed StatusDisplayItem");
String parentID=afterThis.parentID;
for(int i=startIndex; i<displayItems.size(); i++){
StatusDisplayItem item=displayItems.get(i);
if(!parentID.equals(item.parentID)) break; // didn't find anything
if(type.isInstance(item)){
// found it
adapter.notifyItemChanged(i);
return i;
}
}
return -1;
}
protected <I extends StatusDisplayItem> int notifyItemChangedBefore(StatusDisplayItem beforeThis, Class<I> type){
int startIndex=displayItems.indexOf(beforeThis);
if(startIndex == -1) throw new IllegalStateException("notifyItemChangedBefore didn't find the passed StatusDisplayItem");
String parentID=beforeThis.parentID;
for(int i=startIndex; i>=0; i--){
StatusDisplayItem item=displayItems.get(i);
if(!parentID.equals(item.parentID)) break; // didn't find anything
if(type.isInstance(item)){
// found it
adapter.notifyItemChanged(i);
return i;
}
}
return -1;
}
@Nullable @Nullable
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){ protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){
for(int i=0;i<list.getChildCount();i++){ for(int i=0; i<list.getChildCount(); i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i)); RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder)) if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
return type.cast(holder); return type.cast(holder);
@@ -797,45 +859,78 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
status.translationState=Status.TranslationState.SHOWN; status.translationState=Status.TranslationState.SHOWN;
}else{ }else{
status.translationState=Status.TranslationState.LOADING; status.translationState=Status.TranslationState.LOADING;
new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()) Consumer<Translation> successCallback=(result)->{
.setCallback(new Callback<>(){ status.translation=result;
status.translationState=Status.TranslationState.SHOWN;
updateTranslation(itemID);
};
MastodonAPIRequest<?> req=isInstanceAkkoma()
? new AkkomaTranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
@Override
public void onSuccess(AkkomaTranslation result){
if(getActivity()!=null) successCallback.accept(result.toTranslation());
}
@Override
public void onError(ErrorResponse error){
if(getActivity()!=null) translationCallbackError(status, itemID);
}
})
: new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Translation result){ public void onSuccess(Translation result){
if(getActivity()==null) if(getActivity()!=null) successCallback.accept(result);
return;
status.translation=result;
status.translationState=Status.TranslationState.SHOWN;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
}
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
if(getActivity()==null) if(getActivity()!=null) translationCallbackError(status, itemID);
return;
status.translationState=Status.TranslationState.HIDDEN;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
}
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.translation_failed)
.setPositiveButton(R.string.ok, null)
.show();
} }
}) });
.exec(accountID);
// 1 minute
req.setTimeout(60000).exec(accountID);
} }
} }
} }
updateTranslation(itemID);
}
private void translationCallbackError(Status status, String itemID) {
status.translationState=Status.TranslationState.HIDDEN;
updateTranslation(itemID);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.translation_failed)
.setPositiveButton(R.string.ok, null)
.show();
}
private void updateTranslation(String itemID) {
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){ if(text!=null){
text.updateTranslation(true); text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition()); imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
}else{
notifyItemChanged(itemID, TextStatusDisplayItem.class);
}
if(isInstanceAkkoma())
return;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null){
spoiler.rebind();
}
MediaGridStatusDisplayItem.Holder media=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
if (media!=null) {
media.rebind();
}
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
item.rebind();
}
} }
} }

View File

@@ -29,7 +29,6 @@ import android.text.TextWatcher;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@@ -111,11 +110,11 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -151,7 +150,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public LinearLayout mainLayout; public LinearLayout mainLayout;
private SizeListenerLinearLayout contentView; private SizeListenerLinearLayout contentView;
private TextView selfName, selfUsername, selfExtraText, extraText, pronouns; private TextView selfName, selfUsername, selfExtraText, extraText;
private ImageView selfAvatar; private ImageView selfAvatar;
private Account self; private Account self;
private String instanceDomain; private String instanceDomain;
@@ -201,7 +200,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public Instance instance; public Instance instance;
public Status editingStatus; public Status editingStatus;
private ScheduledStatus scheduledStatus; public ScheduledStatus scheduledStatus;
private boolean redraftStatus; private boolean redraftStatus;
private ContentType contentType; private ContentType contentType;
@@ -215,7 +214,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private BackgroundColorSpan overLimitBG; private BackgroundColorSpan overLimitBG;
private ForegroundColorSpan overLimitFG; private ForegroundColorSpan overLimitFG;
public ComposeFragment(){ public ComposeFragment(){
super(R.layout.toolbar_fragment_with_progressbar); super(R.layout.toolbar_fragment_with_progressbar);
} }
@@ -327,7 +326,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
selfUsername=view.findViewById(R.id.self_username); selfUsername=view.findViewById(R.id.self_username);
selfAvatar=view.findViewById(R.id.self_avatar); selfAvatar=view.findViewById(R.id.self_avatar);
selfExtraText=view.findViewById(R.id.self_extra_text); selfExtraText=view.findViewById(R.id.self_extra_text);
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis); HtmlParser.setTextWithCustomEmoji(selfName, self.getDisplayName(), self.emojis);
selfUsername.setText('@'+self.username+'@'+instanceDomain); selfUsername.setText('@'+self.username+'@'+instanceDomain);
if(self.avatar!=null) if(self.avatar!=null)
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar)); ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
@@ -627,7 +626,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}); });
View originalPost=view.findViewById(R.id.original_post); View originalPost=view.findViewById(R.id.original_post);
extraText=view.findViewById(R.id.extra_text); extraText=view.findViewById(R.id.extra_text);
pronouns=view.findViewById(R.id.pronouns);
originalPost.setVisibility(View.VISIBLE); originalPost.setVisibility(View.VISIBLE);
originalPost.setOnClickListener(v->{ originalPost.setOnClickListener(v->{
Bundle args=new Bundle(); Bundle args=new Bundle();
@@ -667,7 +665,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
moreBtn.setBackground(null); moreBtn.setBackground(null);
TextView name = view.findViewById(R.id.name); TextView name = view.findViewById(R.id.name);
name.setText(HtmlParser.parseCustomEmoji(status.account.displayName, status.account.emojis)); name.setText(HtmlParser.parseCustomEmoji(status.account.getDisplayName(), status.account.emojis));
UiUtils.loadCustomEmojiInTextView(name); UiUtils.loadCustomEmojiInTextView(name);
String time = status==null || status.editedAt==null String time = status==null || status.editedAt==null
@@ -701,7 +699,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16))); .setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
} }
replyText.setText(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.displayName)); replyText.setText(HtmlParser.parseCustomEmoji(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.getDisplayName()), status.account.emojis));
UiUtils.loadCustomEmojiInTextView(replyText);
int visibilityNameRes = switch (status.visibility) { int visibilityNameRes = switch (status.visibility) {
case PUBLIC -> R.string.visibility_public; case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted; case UNLISTED -> R.string.sk_visibility_unlisted;
@@ -709,7 +708,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case DIRECT -> R.string.visibility_private; case DIRECT -> R.string.visibility_private;
case LOCAL -> R.string.sk_local_only; case LOCAL -> R.string.sk_local_only;
}; };
replyText.setContentDescription(getString(R.string.in_reply_to, status.account.displayName) + ", " + getString(visibilityNameRes)); replyText.setContentDescription(getString(R.string.in_reply_to, status.account.getDisplayName()) + ", " + getString(visibilityNameRes));
replyText.setOnClickListener(v->{ replyText.setOnClickListener(v->{
scrollView.smoothScrollTo(0, 0); scrollView.smoothScrollTo(0, 0);
}); });
@@ -737,7 +736,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id))) || (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
&& !status.spoilerText.startsWith("re: ") ? "re: " : ""; && !status.spoilerText.startsWith("re: ") ? "re: " : "";
spoilerEdit.setText(prefix + replyTo.spoilerText); spoilerEdit.setText(prefix + status.spoilerText);
spoilerBtn.setSelected(true); spoilerBtn.setSelected(true);
} }
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language); if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
@@ -798,6 +797,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
} }
@SuppressLint("ClickableViewAccessibility")
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu); inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu);
@@ -807,40 +807,58 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
actionItem.setActionView(wrap); actionItem.setActionView(wrap);
actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
draftsBtn = wrap.findViewById(R.id.drafts_btn); draftsBtn=wrap.findViewById(R.id.drafts_btn);
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn); draftOptionsPopup=new PopupMenu(getContext(), draftsBtn);
draftOptionsPopup.inflate(R.menu.compose_more); draftOptionsPopup.inflate(R.menu.compose_more);
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft); Menu draftOptionsMenu=draftOptionsPopup.getMenu();
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft); draftMenuItem=draftOptionsMenu.findItem(R.id.draft);
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule); undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft);
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule); scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule);
unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule);
draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma());
draftOptionsPopup.setOnMenuItemClickListener(i->{ draftOptionsPopup.setOnMenuItemClickListener(i->{
int id = i.getItemId(); int id=i.getItemId();
if (id == R.id.draft) updateScheduledAt(getDraftInstant()); if(id==R.id.draft) updateScheduledAt(getDraftInstant());
else if (id == R.id.schedule) pickScheduledDateTime(); else if(id==R.id.schedule) pickScheduledDateTime();
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null); else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null);
else navigateToUnsentPosts(); else if(id==R.id.drafts) navigateToUnsentPosts();
else if(id==R.id.preview) publish(true);
return true; return true;
}); });
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup); UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
publishButton = wrap.findViewById(R.id.publish_btn); publishButton=wrap.findViewById(R.id.publish_btn);
languageButton = wrap.findViewById(R.id.language_btn); languageButton=wrap.findViewById(R.id.language_btn);
languageButton.setOnClickListener(v->showLanguageAlert()); languageButton.setOnClickListener(v->showLanguageAlert());
languageButton.setOnLongClickListener(v->{ languageButton.setOnLongClickListener(v->{
languageButton.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if(!getLocalPrefs().bottomEncoding){ if(!getLocalPrefs().bottomEncoding){
getLocalPrefs().bottomEncoding=true; getLocalPrefs().bottomEncoding=true;
getLocalPrefs().save(); getLocalPrefs().save();
} }
return false; return false;
}); });
publishButton.post(()->publishButton.setMinimumWidth(publishButton.getWidth()));
publishButton.setOnClickListener(v -> { publishButton.setOnClickListener(v->{
if(GlobalUserPreferences.altTextReminders && editingStatus==null) Consumer<Boolean> draftCheckComplete=(isDraft)->{
checkAltTextsAndPublish(); if(GlobalUserPreferences.altTextReminders && !isDraft) checkAltTextsAndPublish();
else else publish();
publish(); };
boolean isAlreadyDraft=scheduledAt!=null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT);
if(editingStatus!=null && scheduledAt!=null && isAlreadyDraft) {
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_save_draft)
.setMessage(R.string.sk_save_draft_message)
.setPositiveButton(R.string.save, (d, w)->draftCheckComplete.accept(isAlreadyDraft))
.setNegativeButton(R.string.publish, (d, w)->{
updateScheduledAt(null);
draftCheckComplete.accept(false);
})
.show();
}else{
draftCheckComplete.accept(isAlreadyDraft);
}
}); });
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show()); draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener()); draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
@@ -852,8 +870,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
? languageResolver.fromOrFallback(prefs.postingDefaultLanguage) ? languageResolver.fromOrFallback(prefs.postingDefaultLanguage)
: languageResolver.getDefault()); : languageResolver.getDefault());
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE); if(isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) { if(isInstancePixelfed() || (editingStatus!=null && !redraftStatus)) {
// editing an already published post // editing an already published post
draftsBtn.setVisibility(View.GONE); draftsBtn.setVisibility(View.GONE);
} }
@@ -977,7 +995,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override @Override
protected int getNavigationIconDrawableResource(){ protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24; return R.drawable.ic_fluent_dismiss_24_regular;
} }
@Override @Override
@@ -1036,6 +1054,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void publish(){ private void publish(){
publish(false);
}
private void publish(boolean preview){
sendingOverlay=new View(getActivity()); sendingOverlay=new View(getActivity());
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams(); WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
@@ -1049,23 +1071,22 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
publishButton.setEnabled(false); publishButton.setEnabled(false);
V.setVisibilityAnimated(sendProgress, View.VISIBLE); V.setVisibilityAnimated(sendProgress, View.VISIBLE);
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError); mediaViewController.saveAltTextsBeforePublishing(
()->actuallyPublish(preview),
this::handlePublishError);
} }
private void actuallyPublish(){ private void actuallyPublish(boolean preview){
actuallyPublish(false);
}
private void actuallyPublish(boolean force){
String text=mainEditText.getText().toString(); String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request(); CreateStatus.Request req=new CreateStatus.Request();
if ("bottom".equals(postLang.encoding)) { if("bottom".equals(postLang.encoding)){
text = new StatusTextEncoder(Bottom::encode).encode(text); text=new StatusTextEncoder(Bottom::encode).encode(text);
req.spoilerText = "bottom-encoded emoji spam"; req.spoilerText="bottom-encoded emoji spam";
} }
if (localOnly && if(localOnly &&
AccountSessionManager.get(accountID).getLocalPreferences().glitchInstance && AccountSessionManager.get(accountID).getLocalPreferences().glitchInstance &&
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) { !GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()){
text += " " + GLITCH_LOCAL_ONLY_SUFFIX; text+=" "+GLITCH_LOCAL_ONLY_SUFFIX;
} }
req.status=text; req.status=text;
req.localOnly=localOnly; req.localOnly=localOnly;
@@ -1073,25 +1094,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
req.sensitive=sensitive; req.sensitive=sensitive;
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType; req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
req.scheduledAt=scheduledAt; req.scheduledAt=scheduledAt;
req.preview=preview;
if(!mediaViewController.isEmpty()){ if(!mediaViewController.isEmpty()){
req.mediaIds=mediaViewController.getAttachmentIDs(); req.mediaIds=mediaViewController.getAttachmentIDs();
if(editingStatus != null){ if(editingStatus != null){
req.mediaAttributes=mediaViewController.getAttachmentAttributes(); req.mediaAttributes=mediaViewController.getAttachmentAttributes();
} }
} }
// ask whether to publish now when editing an existing draft
if (!force && editingStatus != null && scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_save_draft)
.setMessage(R.string.sk_save_draft_message)
.setPositiveButton(R.string.save, (d, w) -> actuallyPublish(true))
.setNegativeButton(R.string.publish, (d, w) -> {
updateScheduledAt(null);
actuallyPublish();
})
.show();
return;
}
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){ if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id; req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
} }
@@ -1113,7 +1122,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Callback<Status> resCallback=new Callback<>(){ Callback<Status> resCallback=new Callback<>(){
@Override @Override
public void onSuccess(Status result){ public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> { if(preview){
openPreview(result);
return;
}
maybeDeleteScheduledPost(()->{
wm.removeView(sendingOverlay); wm.removeView(sendingOverlay);
sendingOverlay=null; sendingOverlay=null;
if(editingStatus==null || redraftStatus){ if(editingStatus==null || redraftStatus){
@@ -1135,10 +1149,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
E.post(new StatusUpdatedEvent(editedStatus)); E.post(new StatusUpdatedEvent(editedStatus));
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) { if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()){
Nav.finish(ComposeFragment.this); Nav.finish(ComposeFragment.this);
} }
if (getArguments().getBoolean("navigateToStatus", false)) { if(getArguments().getBoolean("navigateToStatus", false)){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result)); args.putParcelable("status", Parcels.wrap(result));
@@ -1154,11 +1168,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
}; };
if(editingStatus!=null && !redraftStatus){ if(editingStatus!=null && !redraftStatus && !preview){
new EditStatus(req, editingStatus.id) new EditStatus(req, editingStatus.id)
.setCallback(resCallback) .setCallback(resCallback)
.exec(accountID); .exec(accountID);
}else if(req.scheduledAt == null){ }else if(req.scheduledAt == null || preview){
new CreateStatus(req, uuid) new CreateStatus(req, uuid)
.setCallback(resCallback) .setCallback(resCallback)
.exec(accountID); .exec(accountID);
@@ -1211,6 +1225,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
} }
private void openPreview(Status result){
result.preview=true;
wm.removeView(sendingOverlay);
sendingOverlay=null;
publishButton.setEnabled(true);
V.setVisibilityAnimated(sendProgress, View.GONE);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null){
args.putParcelable("inReplyTo", Parcels.wrap(replyTo));
args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo.account));
}
Nav.go(getActivity(), ThreadFragment.class, args);
}
private void updateRecentLanguages() { private void updateRecentLanguages() {
if (postLang == null || postLang.language == null) return; if (postLang == null || postLang.language == null) return;
String language = postLang.language.getLanguage(); String language = postLang.language.getLanguage();
@@ -1286,20 +1319,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void confirmDiscardDraftAndFinish(){ private void confirmDiscardDraftAndFinish(){
boolean attachmentsPending = mediaViewController.areAnyAttachmentsNotDone(); boolean attachmentsPending=mediaViewController.areAnyAttachmentsNotDone();
if (attachmentsPending) new M3AlertDialogBuilder(getActivity()) if(attachmentsPending) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_unfinished_attachments) .setTitle(R.string.sk_unfinished_attachments)
.setMessage(R.string.sk_unfinished_attachments_message) .setMessage(R.string.sk_unfinished_attachments_message)
.setPositiveButton(R.string.edit, (d, w) -> {}) .setPositiveButton(R.string.ok, (d, w)->{})
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this)) .setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
.show(); .show();
else new M3AlertDialogBuilder(getActivity()) else new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft) .setTitle(editingStatus!=null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> { .setPositiveButton(R.string.save, (d, w)->{
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt); updateScheduledAt(scheduledAt==null ? getDraftInstant() : scheduledAt);
publish(); publish();
}) })
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this)) .setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
.show(); .show();
} }
@@ -1456,8 +1489,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void updateHeaders() { private void updateHeaders() {
UiUtils.setExtraTextInfo(getContext(), selfExtraText, null, false, false, localOnly || statusVisibility==StatusPrivacy.LOCAL, null); UiUtils.setExtraTextInfo(getContext(), selfExtraText, false, false, localOnly, null);
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, pronouns, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account); if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
} }
private void buildVisibilityPopup(View v){ private void buildVisibilityPopup(View v){
@@ -1529,6 +1562,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
contentTypePopup.setOnMenuItemClickListener(i->{ contentTypePopup.setOnMenuItemClickListener(i->{
uuid=null;
int index=i.getItemId(); int index=i.getItemId();
contentType=ContentType.values()[index]; contentType=ContentType.values()[index];
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal()); btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());

View File

@@ -63,7 +63,8 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
attachmentID=getArguments().getString("attachment"); attachmentID=getArguments().getString("attachment");
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark); themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
ColorPalette.palettes.get(AccountSessionManager.get(accountID).getLocalPreferences().color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK); ColorPalette.palettes.get(AccountSessionManager.get(accountID).getLocalPreferences().getCurrentColor())
.apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
setTitle(R.string.add_alt_text); setTitle(R.string.add_alt_text);
} }

View File

@@ -60,191 +60,191 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop { public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
private String accountID; private String accountID;
private TimelinesAdapter adapter; private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper; private final ItemTouchHelper itemTouchHelper;
private Menu optionsMenu; private Menu optionsMenu;
private boolean updated; private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>(); private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
private final List<ListTimeline> listTimelines = new ArrayList<>(); private final List<ListTimeline> listTimelines=new ArrayList<>();
private final List<Hashtag> hashtags = new ArrayList<>(); private final List<Hashtag> hashtags=new ArrayList<>();
private MenuItem addHashtagItem; private MenuItem addHashtagItem;
public EditTimelinesFragment() { public EditTimelinesFragment(){
super(10); super(10);
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ; ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback();
itemTouchHelper = new ItemTouchHelper(itemTouchCallback); itemTouchHelper=new ItemTouchHelper(itemTouchCallback);
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setHasOptionsMenu(true); setHasOptionsMenu(true);
setTitle(R.string.sk_timelines); setTitle(R.string.sk_timelines);
accountID = getArguments().getString("account"); accountID=getArguments().getString("account");
new GetLists().setCallback(new Callback<>() { new GetLists().setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<ListTimeline> result) { public void onSuccess(List<ListTimeline> result){
listTimelines.addAll(result); listTimelines.addAll(result);
updateOptionsMenu(); updateOptionsMenu();
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error){
error.showToast(getContext()); error.showToast(getContext());
} }
}).exec(accountID); }).exec(accountID);
new GetFollowedHashtags().setCallback(new Callback<>() { new GetFollowedHashtags().setCallback(new Callback<>(){
@Override @Override
public void onSuccess(HeaderPaginationList<Hashtag> result) { public void onSuccess(HeaderPaginationList<Hashtag> result){
hashtags.addAll(result); hashtags.addAll(result);
updateOptionsMenu(); updateOptionsMenu();
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error){
error.showToast(getContext()); error.showToast(getContext());
} }
}).exec(accountID); }).exec(accountID);
} }
@Override @Override
protected void onShown(){ protected void onShown(){
super.onShown(); super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData(); if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
} }
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState) { public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
itemTouchHelper.attachToRecyclerView(list); itemTouchHelper.attachToRecyclerView(list);
refreshLayout.setEnabled(false); refreshLayout.setEnabled(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16)); list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
} }
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
this.optionsMenu = menu; this.optionsMenu=menu;
updateOptionsMenu(); updateOptionsMenu();
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item){
if (item.getItemId() == R.id.menu_back) { if(item.getItemId()==R.id.menu_back){
updateOptionsMenu(); updateOptionsMenu();
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0); optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
return true; return true;
} }
TimelineDefinition tl = timelineByMenuItem.get(item); TimelineDefinition tl=timelineByMenuItem.get(item);
if (tl != null) { if(tl!=null){
addTimeline(tl); addTimeline(tl);
} else if (item == addHashtagItem) { }else if(item==addHashtagItem){
makeTimelineEditor(null, (hashtag) -> { makeTimelineEditor(null, (hashtag)->{
if (hashtag != null) addTimeline(hashtag); if(hashtag!=null) addTimeline(hashtag);
}, null); }, null);
} }
return true; return true;
} }
private void addTimeline(TimelineDefinition tl) { private void addTimeline(TimelineDefinition tl){
data.add(tl.copy()); data.add(tl.copy());
adapter.notifyItemInserted(data.size()); adapter.notifyItemInserted(data.size());
saveTimelines(); saveTimelines();
updateOptionsMenu(); updateOptionsMenu();
} }
private void addTimelineToOptions(TimelineDefinition tl, Menu menu) { private void addTimelineToOptions(TimelineDefinition tl, Menu menu){
if (data.contains(tl)) return; if(data.contains(tl)) return;
MenuItem item = addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes); MenuItem item=addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes);
timelineByMenuItem.put(item, tl); timelineByMenuItem.put(item, tl);
} }
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) { private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name); MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
item.setIcon(icon); item.setIcon(icon);
return item; return item;
} }
private void updateOptionsMenu() { private void updateOptionsMenu(){
if(getActivity()==null) return; if(getActivity()==null) return;
optionsMenu.clear(); optionsMenu.clear();
timelineByMenuItem.clear(); timelineByMenuItem.clear();
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add); SubMenu menu=optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular); menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline); SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular); timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list); SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular); listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag); SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular); hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
makeBackItem(timelinesMenu); makeBackItem(timelinesMenu);
makeBackItem(listsMenu); makeBackItem(listsMenu);
makeBackItem(hashtagsMenu); makeBackItem(hashtagsMenu);
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu)); TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu)); listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular); addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu)); hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0); timelinesMenu.getItem().setVisible(timelinesMenu.size()>0);
listsMenu.getItem().setVisible(listsMenu.size() > 0); listsMenu.getItem().setVisible(listsMenu.size()>0);
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0); hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0);
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline); UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
} }
private void saveTimelines() { private void saveTimelines(){
updated=true; updated=true;
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences(); AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE); if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
prefs.timelines=data; prefs.timelines=data;
prefs.save(); prefs.save();
} }
private void removeTimeline(int position) { private void removeTimeline(int position){
data.remove(position); data.remove(position);
adapter.notifyItemRemoved(position); adapter.notifyItemRemoved(position);
saveTimelines(); saveTimelines();
updateOptionsMenu(); updateOptionsMenu();
} }
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines); onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
updateOptionsMenu(); updateOptionsMenu();
} }
@Override @Override
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() { protected RecyclerView.Adapter<TimelineViewHolder> getAdapter(){
return adapter = new TimelinesAdapter(); return adapter=new TimelinesAdapter();
} }
@Override @Override
public void scrollToTop() { public void scrollToTop(){
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override @Override
public void onDestroy() { public void onDestroy(){
super.onDestroy(); super.onDestroy();
if (updated) UiUtils.restartApp(); if(updated) UiUtils.restartApp();
} }
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) { private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags){
if (tags == null || tags.isEmpty()) return false; if(tags==null || tags.isEmpty()) return false;
editText.setText(tags); editText.setText(tags);
editText.chipifyAllUnterminatedTokens(); editText.chipifyAllUnterminatedTokens();
return true; return true;
} }
private NachoTextView prepareChipTextView(NachoTextView nacho) { private NachoTextView prepareChipTextView(NachoTextView nacho){
//Ill Be Back //Ill Be Back
nacho.setChipTerminators( nacho.setChipTerminators(
Map.of( Map.of(
@@ -254,223 +254,228 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
';', BEHAVIOR_CHIPIFY_ALL ';', BEHAVIOR_CHIPIFY_ALL
) )
); );
nacho.enableEditChipOnTouch(true, true); nacho.enableEditChipOnTouch(true, true);
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens()); nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens());
return nacho; return nacho;
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) { protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove){
Context ctx = getContext(); Context ctx=getContext();
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false); View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
View divider = view.findViewById(R.id.divider); View divider=view.findViewById(R.id.divider);
Button advancedBtn = view.findViewById(R.id.advanced); Button advancedBtn=view.findViewById(R.id.advanced);
EditText editText = view.findViewById(R.id.input); EditText editText=view.findViewById(R.id.input);
if (item != null) editText.setText(item.getCustomTitle()); if(item!=null) editText.setText(item.getCustomTitle());
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag)); editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap); LinearLayout tagWrap=view.findViewById(R.id.tag_wrap);
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG; boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG;
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE); advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE);
advancedBtn.setOnClickListener(l -> { advancedBtn.setOnClickListener(l->{
advancedBtn.setSelected(!advancedBtn.isSelected()); advancedBtn.setSelected(!advancedBtn.isSelected());
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show); advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE); divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE); tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
UiUtils.beginLayoutTransition((ViewGroup) view); UiUtils.beginLayoutTransition((ViewGroup) view);
}); });
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch); Switch localOnlySwitch=view.findViewById(R.id.local_only_switch);
view.findViewById(R.id.local_only) view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
EditText tagMain = view.findViewById(R.id.tag_main); EditText tagMain=view.findViewById(R.id.tag_main);
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any)); NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any));
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all)); NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all));
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none)); NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none));
if (item != null) {
tagMain.setText(item.getHashtagName()); if(item!=null && hashtagOptionsAvailable){
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle()); tagMain.setText(item.getHashtagName());
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced; boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced; hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced; hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
if (item.isHashtagLocalOnly()) { hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
localOnlySwitch.setChecked(true); if(item.isHashtagLocalOnly()){
hasAdvanced = true; localOnlySwitch.setChecked(true);
} hasAdvanced=true;
if (hasAdvanced) { }
advancedBtn.setSelected(true); if(hasAdvanced){
advancedBtn.setText(R.string.sk_advanced_options_hide); advancedBtn.setSelected(true);
advancedBtn.setText(R.string.sk_advanced_options_hide);
tagWrap.setVisibility(View.VISIBLE); tagWrap.setVisibility(View.VISIBLE);
divider.setVisibility(View.VISIBLE); divider.setVisibility(View.VISIBLE);
} }
} }
ImageButton btn = view.findViewById(R.id.button); ImageButton btn=view.findViewById(R.id.button);
PopupMenu popup = new PopupMenu(ctx, btn); PopupMenu popup=new PopupMenu(ctx, btn);
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG; TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
btn.setImageResource(currentIcon.iconRes); btn.setImageResource(currentIcon.iconRes);
btn.setTag(currentIcon.ordinal()); btn.setTag(currentIcon.ordinal());
btn.setContentDescription(ctx.getString(currentIcon.nameRes)); btn.setContentDescription(ctx.getString(currentIcon.nameRes));
btn.setOnTouchListener(popup.getDragToOpenListener()); btn.setOnTouchListener(popup.getDragToOpenListener());
btn.setOnClickListener(l -> popup.show()); btn.setOnClickListener(l->popup.show());
Menu menu = popup.getMenu(); Menu menu=popup.getMenu();
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG; TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes); menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
if (!currentIcon.equals(defaultIcon)) { if(!currentIcon.equals(defaultIcon)){
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes); menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
} }
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) { for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue; if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue;
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes); menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
} }
UiUtils.enablePopupMenuIcons(ctx, popup); UiUtils.enablePopupMenuIcons(ctx, popup);
popup.setOnMenuItemClickListener(menuItem -> { popup.setOnMenuItemClickListener(menuItem->{
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()]; TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()];
btn.setImageResource(icon.iconRes); btn.setImageResource(icon.iconRes);
btn.setTag(menuItem.getItemId()); btn.setTag(menuItem.getItemId());
btn.setContentDescription(ctx.getString(icon.nameRes)); btn.setContentDescription(ctx.getString(icon.nameRes));
return true; return true;
}); });
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx) AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx)
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline) .setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
.setView(view) .setView(view)
.setPositiveButton(R.string.save, (d, which) -> { .setPositiveButton(R.string.save, (d, which)->{
tagsAny.chipifyAllUnterminatedTokens(); String name=editText.getText().toString().trim();
tagsAll.chipifyAllUnterminatedTokens();
tagsNone.chipifyAllUnterminatedTokens();
String name = editText.getText().toString().trim();
String mainHashtag = tagMain.getText().toString().trim();
if (TextUtils.isEmpty(mainHashtag)) {
mainHashtag = name;
name = null;
}
if (TextUtils.isEmpty(mainHashtag) && (item != null && item.getType() == TimelineDefinition.TimelineType.HASHTAG)) {
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
onSave.accept(null);
return;
}
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name); String mainHashtag=tagMain.getText().toString().trim();
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()]; if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){
tl.setIcon(icon); tagsAny.chipifyAllUnterminatedTokens();
tl.setTitle(name); tagsAll.chipifyAllUnterminatedTokens();
tl.setTagOptions( tagsNone.chipifyAllUnterminatedTokens();
mainHashtag, if(TextUtils.isEmpty(mainHashtag)){
tagsAny.getChipValues(), mainHashtag=name;
tagsAll.getChipValues(), name=null;
tagsNone.getChipValues(), }
localOnlySwitch.isChecked() if(TextUtils.isEmpty(mainHashtag) && (item!=null && item.getType()==TimelineDefinition.TimelineType.HASHTAG)){
); Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
onSave.accept(tl); onSave.accept(null);
}) return;
.setNegativeButton(R.string.cancel, (d, which) -> {}); }
}
if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run()); TimelineDefinition tl=item!=null ? item : TimelineDefinition.ofHashtag(name);
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[(int) btn.getTag()];
tl.setIcon(icon);
tl.setTitle(name);
if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){
tl.setTagOptions(
mainHashtag,
tagsAny.getChipValues(),
tagsAll.getChipValues(),
tagsNone.getChipValues(),
localOnlySwitch.isChecked()
);
}
onSave.accept(tl);
})
.setNegativeButton(R.string.cancel, (d, which)->{});
builder.show(); if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run());
btn.requestFocus();
}
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{ builder.show();
@NonNull btn.requestFocus();
@Override }
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new TimelineViewHolder();
}
@Override private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) { @NonNull
holder.bind(data.get(position)); @Override
} public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new TimelineViewHolder();
}
@Override @Override
public int getItemCount() { public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){
return data.size(); holder.bind(data.get(position));
} }
}
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{ @Override
private final TextView title; public int getItemCount(){
private final ImageView dragger; return data.size();
}
}
public TimelineViewHolder(){ private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
super(getActivity(), R.layout.item_text, list); private final TextView title;
title=findViewById(R.id.title); private final ImageView dragger;
dragger=findViewById(R.id.dragger_thingy);
}
@SuppressLint("ClickableViewAccessibility") public TimelineViewHolder(){
@Override super(getActivity(), R.layout.item_text, list);
public void onBind(TimelineDefinition item) { title=findViewById(R.id.title);
title.setText(item.getTitle(getContext())); dragger=findViewById(R.id.dragger_thingy);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null); }
dragger.setVisibility(View.VISIBLE);
dragger.setOnTouchListener((View v, MotionEvent event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(this);
return true;
}
return false;
});
}
private void onSave(TimelineDefinition tl) { @SuppressLint("ClickableViewAccessibility")
saveTimelines(); @Override
rebind(); public void onBind(TimelineDefinition item){
} title.setText(item.getTitle(getContext()));
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
dragger.setVisibility(View.VISIBLE);
dragger.setOnTouchListener((View v, MotionEvent event)->{
if(event.getAction()==MotionEvent.ACTION_DOWN){
itemTouchHelper.startDrag(this);
return true;
}
return false;
});
}
private void onRemove() { private void onSave(TimelineDefinition tl){
removeTimeline(getAbsoluteAdapterPosition()); saveTimelines();
} rebind();
}
@SuppressLint("ClickableViewAccessibility") private void onRemove(){
@Override removeTimeline(getAbsoluteAdapterPosition());
public void onClick() { }
makeTimelineEditor(item, this::onSave, this::onRemove);
}
}
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback { @SuppressLint("ClickableViewAccessibility")
public ItemTouchHelperCallback() { @Override
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT); public void onClick(){
} makeTimelineEditor(item, this::onSave, this::onRemove);
}
}
@Override private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { public ItemTouchHelperCallback(){
int fromPosition = viewHolder.getAbsoluteAdapterPosition(); super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
int toPosition = target.getAbsoluteAdapterPosition(); }
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
return false;
} else {
Collections.swap(data, fromPosition, toPosition);
adapter.notifyItemMoved(fromPosition, toPosition);
saveTimelines();
return true;
}
}
@Override @Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) { public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) { int fromPosition=viewHolder.getAbsoluteAdapterPosition();
viewHolder.itemView.animate().alpha(0.65f); int toPosition=target.getAbsoluteAdapterPosition();
} if(Math.max(fromPosition, toPosition)>=data.size() || Math.min(fromPosition, toPosition)<0){
} return false;
}else{
Collections.swap(data, fromPosition, toPosition);
adapter.notifyItemMoved(fromPosition, toPosition);
saveTimelines();
return true;
}
}
@Override @Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.clearView(recyclerView, viewHolder); if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){
viewHolder.itemView.animate().alpha(1f); viewHolder.itemView.animate().alpha(0.65f);
} }
}
@Override @Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
int position = viewHolder.getAbsoluteAdapterPosition(); super.clearView(recyclerView, viewHolder);
removeTimeline(position); viewHolder.itemView.animate().alpha(1f);
} }
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
int position=viewHolder.getAbsoluteAdapterPosition();
removeTimeline(position);
}
}
} }

View File

@@ -20,7 +20,6 @@ import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
@@ -271,7 +270,7 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id); relationship=relationships.get(item.account.id);
UiUtils.setExtraTextInfo(getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account); UiUtils.setExtraTextInfo(getContext(), null, true, false, false, item.account);
if(relationship==null || !relationship.followedBy){ if(relationship==null || !relationship.followedBy){
actionWrap.setVisibility(View.GONE); actionWrap.setVisibility(View.GONE);
@@ -366,9 +365,9 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(account.emojis.isEmpty()){ if(account.emojis.isEmpty()){
parsedName=account.displayName; parsedName= account.getDisplayName();
}else{ }else{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
} }
} }

View File

@@ -32,7 +32,6 @@ import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent; import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.fragments.discover.DiscoverFragment; import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.AccountSwitcherSheet;
@@ -71,14 +70,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private TextView notificationsBadge; private TextView notificationsBadge;
private String accountID; private String accountID;
private boolean isAkkoma;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name); setTitle(R.string.sk_app_name);
isAkkoma = getInstance().map(Instance::isAkkoma).orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true); setRetainInstance(true);
@@ -89,7 +86,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
homeTabFragment=new HomeTabFragment(); homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args); homeTabFragment.setArguments(args);
args=new Bundle(args); args=new Bundle(args);
args.putBoolean("disableDiscover", isAkkoma);
args.putBoolean("noAutoLoad", true); args.putBoolean("noAutoLoad", true);
discoverFragment=new DiscoverFragment(); discoverFragment=new DiscoverFragment();
discoverFragment.setArguments(args); discoverFragment.setArguments(args);
@@ -296,7 +292,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(tab==R.id.tab_profile){ if(tab==R.id.tab_profile){
ArrayList<String> options=new ArrayList<>(); ArrayList<String> options=new ArrayList<>();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")"); options.add(session.self.getDisplayName()+"\n("+session.self.username+"@"+session.domain+")");
} }
new AccountSwitcherSheet(getActivity(), this).show(); new AccountSwitcherSheet(getActivity(), this).show();
return true; return true;

View File

@@ -233,21 +233,25 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
ViewTreeObserver vto = getToolbar().getViewTreeObserver(); ViewTreeObserver vto = getToolbar().getViewTreeObserver();
if (vto.isAlive()) { if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(() -> { vto.addOnGlobalLayoutListener(()->{
Toolbar t = getToolbar(); Toolbar t=getToolbar();
if (t == null) return; if(t==null) return;
int toolbarWidth = t.getWidth(); int toolbarWidth=t.getWidth();
if (toolbarWidth == 0) return; if(toolbarWidth==0) return;
int toolbarFrameWidth = toolbarFrame.getWidth(); int toolbarFrameWidth=toolbarFrame.getWidth();
int padding = toolbarWidth - toolbarFrameWidth; int actionsWidth=toolbarWidth-toolbarFrameWidth;
FrameLayout parent = ((FrameLayout) toolbarShowNewPostsBtn.getParent()); // margin (4) + padding (12) + icon (24) + margin (8) + chevron (16) + padding (12)
if (padding == parent.getPaddingStart()) return; int switcherWidth=V.dp(76);
FrameLayout parent=((FrameLayout) toolbarShowNewPostsBtn.getParent());
if(actionsWidth==parent.getPaddingStart()) return;
int paddingMax=Math.max(actionsWidth, switcherWidth);
int paddingEnd=(Math.max(0, switcherWidth-actionsWidth));
// toolbar frame goes from screen edge to beginning of right-aligned option buttons. // toolbar frame goes from screen edge to beginning of right-aligned option buttons.
// centering button by applying the same space on the left // centering button by applying the same space on the left
parent.setPaddingRelative(padding, 0, 0, 0); parent.setPaddingRelative(paddingMax, 0, paddingEnd, 0);
toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth - padding * 2); toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth-paddingMax*2);
switcher.setPivotX(V.dp(28)); // padding + half of icon switcher.setPivotX(V.dp(28)); // padding + half of icon
switcher.setPivotY(switcher.getHeight() / 2f); switcher.setPivotY(switcher.getHeight() / 2f);
@@ -400,9 +404,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
addListsToOverflowMenu(); addListsToOverflowMenu();
addHashtagsToOverflowMenu(); addHashtagsToOverflowMenu();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !UiUtils.isEMUI()) { if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
m.setGroupDividerEnabled(true); m.setGroupDividerEnabled(true);
}
} }
@Override @Override

View File

@@ -61,7 +61,7 @@ public class HomeTimelineFragment extends StatusListFragment {
maxID=result.maxID; maxID=result.maxID;
AccountSessionManager.get(accountID).filterStatuses(result.items, getFilterContext()); AccountSessionManager.get(accountID).filterStatuses(result.items, getFilterContext());
onDataLoaded(result.items, !empty); onDataLoaded(result.items, !empty);
if(result.isFromCache()) if(result.isFromCache() && GlobalUserPreferences.loadNewPosts)
loadNewPosts(); loadNewPosts();
} }
}); });
@@ -74,7 +74,7 @@ public class HomeTimelineFragment extends StatusListFragment {
list.addOnScrollListener(new RecyclerView.OnScrollListener(){ list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override @Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(parent != null && parent.isNewPostsBtnShown() && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){ if(parent!=null && parent.isNewPostsBtnShown() && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
parent.hideNewPostsButton(); parent.hideNewPostsButton();
} }
} }
@@ -87,7 +87,7 @@ public class HomeTimelineFragment extends StatusListFragment {
if(!getArguments().getBoolean("noAutoLoad")){ if(!getArguments().getBoolean("noAutoLoad")){
if(!loaded && !dataLoading){ if(!loaded && !dataLoading){
loadData(); loadData();
}else if(!dataLoading){ }else if(!dataLoading && GlobalUserPreferences.loadNewPosts){
loadNewPosts(); loadNewPosts();
} }
} }
@@ -117,48 +117,54 @@ public class HomeTimelineFragment extends StatusListFragment {
} }
public void onStatusCreated(Status status){ public void onStatusCreated(Status status){
if(status.reblog!=null) return;
prependItems(Collections.singletonList(status), true); prependItems(Collections.singletonList(status), true);
} }
private void loadNewPosts(){ private void loadNewPosts(){
if (!GlobalUserPreferences.loadNewPosts) return;
dataLoading=true; dataLoading=true;
// we only care about the data that was actually retrieved from the timeline api since
// user-created statuses are probably in the wrong position
List<Status> dataFromTimeline=data.stream().filter(s->!s.fromStatusCreated).collect(Collectors.toList());
// The idea here is that we request the timeline such that if there are fewer than `limit` posts, // The idea here is that we request the timeline such that if there are fewer than `limit` posts,
// we'll get the currently topmost post as last in the response. This way we know there's no gap // we'll get the currently topmost post as last in the response. This way we know there's no gap
// between the existing and newly loaded parts of the timeline. // between the existing and newly loaded parts of the timeline.
String sinceID=data.size()>1 ? data.get(1).id : "1"; String sinceID=dataFromTimeline.size()>1 ? dataFromTimeline.get(1).id : "1";
currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility) currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
currentRequest=null; currentRequest=null;
dataLoading=false; dataLoading=false;
refreshDone();
if(result.isEmpty() || getActivity()==null) if(result.isEmpty() || getActivity()==null)
return; return;
Status last=result.get(result.size()-1); Status last=result.get(result.size()-1);
List<Status> toAdd; List<Status> toAdd;
if(!data.isEmpty() && last.id.equals(data.get(0).id)){ // This part intersects with the existing one if(!dataFromTimeline.isEmpty() && last.id.equals(dataFromTimeline.get(0).id)){ // This part intersects with the existing one
toAdd=new ArrayList<>(result.subList(0, result.size()-1)); // Remove the already known last post toAdd=new ArrayList<>(result.subList(0, result.size()-1)); // Remove the already known last post
}else{ }else{
result.get(result.size()-1).hasGapAfter=true; last.hasGapAfter=last.id;
toAdd=result; toAdd=result;
} }
if(!toAdd.isEmpty())
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(new ArrayList<>(toAdd), false);
// removing statuses that come up as duplicates (hopefully only posts and boosts that were locally created
// and thus were already prepended to the timeline earlier)
List<String> existingIds=data.stream().map(Status::getID).collect(Collectors.toList()); List<String> existingIds=data.stream().map(Status::getID).collect(Collectors.toList());
toAdd.removeIf(s->existingIds.contains(s.getID())); toAdd.removeIf(s->existingIds.contains(s.getID()));
List<Status> toAddUnfiltered=new ArrayList<>(toAdd);
AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext()); AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
if(!toAdd.isEmpty()){ if(!toAdd.isEmpty()){
prependItems(toAdd, true); prependItems(toAdd, true);
if(parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton(); if(parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
} }
if(toAddUnfiltered.isEmpty())
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAddUnfiltered, false);
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
currentRequest=null; currentRequest=null;
dataLoading=false; dataLoading=false;
refreshDone();
} }
}) })
.exec(accountID); .exec(accountID);
@@ -176,10 +182,10 @@ public class HomeTimelineFragment extends StatusListFragment {
gap.loading=true; gap.loading=true;
dataLoading=true; dataLoading=true;
String maxID = null; String maxID=null;
String minID = null; String minID=null;
if (downwards) { if (downwards) {
maxID = item.getItemID(); maxID=item.getItem().getMaxID();
} else { } else {
int gapPos=displayItems.indexOf(gap); int gapPos=displayItems.indexOf(gap);
StatusDisplayItem nextItem=displayItems.get(gapPos + 1); StatusDisplayItem nextItem=displayItems.get(gapPos + 1);
@@ -196,15 +202,17 @@ public class HomeTimelineFragment extends StatusListFragment {
int gapPos=displayItems.indexOf(gap); int gapPos=displayItems.indexOf(gap);
if(gapPos==-1) if(gapPos==-1)
return; return;
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
if(result.isEmpty()){ if(result.isEmpty()){
displayItems.remove(gapPos); displayItems.remove(gapPos);
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos); adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
Status gapStatus=getStatusByID(gap.parentID); Status gapStatus=getStatusByID(gap.parentID);
if(gapStatus!=null){ if(gapStatus!=null){
gapStatus.hasGapAfter=false; gapStatus.hasGapAfter=null;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false); AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
} }
}else{ }else{
// TODO: refactor this code. it's too long. incomprehensible, even
if(downwards) { if(downwards) {
Set<String> idsBelowGap=new HashSet<>(); Set<String> idsBelowGap=new HashSet<>();
boolean belowGap=false; boolean belowGap=false;
@@ -214,7 +222,7 @@ public class HomeTimelineFragment extends StatusListFragment {
idsBelowGap.add(s.id); idsBelowGap.add(s.id);
}else if(s.id.equals(gap.parentID)){ }else if(s.id.equals(gap.parentID)){
belowGap=true; belowGap=true;
s.hasGapAfter=false; s.hasGapAfter=null;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false); AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
}else{ }else{
gapPostIndex++; gapPostIndex++;
@@ -227,7 +235,8 @@ public class HomeTimelineFragment extends StatusListFragment {
break; break;
} }
if(endIndex==result.size()){ if(endIndex==result.size()){
result.get(result.size()-1).hasGapAfter=true; Status last=result.get(result.size()-1);
last.hasGapAfter=last.id;
}else{ }else{
result=result.subList(0, endIndex); result=result.subList(0, endIndex);
} }
@@ -272,7 +281,7 @@ public class HomeTimelineFragment extends StatusListFragment {
.filter(s->Objects.equals(s.id, gap.parentID)) .filter(s->Objects.equals(s.id, gap.parentID))
.findFirst(); .findFirst();
if (gapStatus.isPresent()) { if (gapStatus.isPresent()) {
gapStatus.get().hasGapAfter=false; gapStatus.get().hasGapAfter=null;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus.get()), false); AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus.get()), false);
} }
targetList.clear(); targetList.clear();
@@ -323,7 +332,7 @@ public class HomeTimelineFragment extends StatusListFragment {
currentRequest=null; currentRequest=null;
dataLoading=false; dataLoading=false;
} }
if (parent != null) parent.hideNewPostsButton(); if(parent!=null) parent.hideNewPostsButton();
super.onRefresh(); super.onRefresh();
} }

View File

@@ -174,7 +174,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
list.addItemDecoration(new RecyclerView.ItemDecoration(){ list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(); private Paint paint=new Paint();
private Rect tmpRect=new Rect(); private Rect tmpRect=new Rect();
@@ -238,7 +237,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
continue; continue;
Status contentStatus=ntf.status.getContentStatus(); Status contentStatus=ntf.status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){ if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(ntf.id, ntf.status, ev.poll); updatePoll(ntf.id, contentStatus, ev.poll);
} }
} }
} }

View File

@@ -21,8 +21,11 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher;
import android.transition.ChangeBounds; import android.transition.ChangeBounds;
import android.transition.Fade; import android.transition.Fade;
import android.transition.TransitionManager; import android.transition.TransitionManager;
@@ -56,6 +59,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.SetPrivateNote;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials; import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -145,7 +149,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private SwipeRefreshLayout refreshLayout; private SwipeRefreshLayout refreshLayout;
private View followersBtn, followingBtn; private View followersBtn, followingBtn;
private EditText nameEdit, bioEdit; private EditText nameEdit, bioEdit;
private ProgressBar actionProgress, notifyProgress; private ProgressBar actionProgress, notifyProgress, noteSaveProgress;
private FrameLayout[] tabViews; private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator; private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView; private TextView followsYouView;
@@ -186,6 +190,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback()); private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private ListImageLoaderWrapper imgLoader; private ListImageLoaderWrapper imgLoader;
// profile note
private FrameLayout noteWrap;
private ImageButton noteSaveBtn;
private EditText noteEdit;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -257,6 +266,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEditWrap=content.findViewById(R.id.bio_edit_wrap); bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
actionProgress=content.findViewById(R.id.action_progress); actionProgress=content.findViewById(R.id.action_progress);
notifyProgress=content.findViewById(R.id.notify_progress); notifyProgress=content.findViewById(R.id.notify_progress);
noteSaveProgress=content.findViewById(R.id.note_save_progress);
fab=content.findViewById(R.id.fab); fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you); followsYouView=content.findViewById(R.id.follows_you);
countersLayout=content.findViewById(R.id.profile_counters); countersLayout=content.findViewById(R.id.profile_counters);
@@ -271,6 +281,51 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatar.setOutlineProvider(OutlineProviders.roundedRect(24)); avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true); avatar.setClipToOutline(true);
noteEdit=content.findViewById(R.id.note_edit);
noteWrap=content.findViewById(R.id.note_edit_wrap);
noteSaveBtn=content.findViewById(R.id.note_save_btn);
noteSaveBtn.setOnClickListener((v->{
savePrivateNote(noteEdit.getText().toString());
InputMethodManager imm=(InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
noteEdit.clearFocus();
noteSaveBtn.clearFocus();
}));
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus){
hideFab();
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
}else if(!noteSaveBtn.hasFocus()){
showFab();
hideNoteSaveBtnIfNotDirty();
}
});
noteEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
if(relationship!=null && noteSaveBtn.getVisibility()!=View.VISIBLE && !s.toString().equals(relationship.note))
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
}
@Override
public void afterTextChanged(Editable s){}
});
noteSaveBtn.setOnFocusChangeListener((v, hasFocus)->{
if(!hasFocus && !noteEdit.hasFocus()){
showFab();
hideNoteSaveBtnIfNotDirty();
}
});
FrameLayout sizeWrapper=new FrameLayout(getActivity()){ FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
@@ -435,6 +490,46 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return sizeWrapper; return sizeWrapper;
} }
private void hideNoteSaveBtnIfNotDirty(){
if(noteEdit.getText().toString().equals(relationship.note)){
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
}
}
private void showPrivateNote(){
noteWrap.setVisibility(View.VISIBLE);
noteEdit.setText(relationship.note);
}
private void hidePrivateNote(){
noteWrap.setVisibility(View.GONE);
noteEdit.setText(null);
}
private void savePrivateNote(String note){
if(note!=null && note.equals(relationship.note)){
updateRelationship();
invalidateOptionsMenu();
return;
}
V.setVisibilityAnimated(noteSaveProgress, View.VISIBLE);
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship result) {
updateRelationship(result);
invalidateOptionsMenu();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
}
}).exec(accountID);
}
private void onAccountLoaded(Account result) { private void onAccountLoaded(Account result) {
account=result; account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account); isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
@@ -621,14 +716,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private void bindHeaderView(){ private void bindHeaderView(){
setTitle(account.displayName); setTitle(account.getDisplayName());
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount)); setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest( ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() : TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() :
GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic,
V.dp(100), V.dp(100))); V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000)); ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName); SpannableStringBuilder ssb=new SpannableStringBuilder(account.getDisplayName());
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames) if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(ssb, account.emojis); HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb); name.setText(ssb);
@@ -755,20 +850,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1; boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account); MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
openWithAccounts.setVisible(hasMultipleAccounts); openWithAccounts.setVisible(hasMultipleAccounts);
SubMenu accountsMenu = openWithAccounts.getSubMenu(); SubMenu accountsMenu=openWithAccounts.getSubMenu();
if (hasMultipleAccounts) { if(hasMultipleAccounts){
accountsMenu.clear(); accountsMenu.clear();
UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL( UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL(
getActivity(), s.getID(), account.url, false getActivity(), s.getID(), account.url, false
)); ));
} }
menu.findItem(R.id.share).setTitle(R.string.share_user);
if(isOwnProfile) { if(isOwnProfile) {
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false); if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
return; return;
} }
MenuItem mute = menu.findItem(R.id.mute); menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
MenuItem mute=menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername())); mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular); mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), mute); UiUtils.insetPopupMenuIcon(getContext(), mute);
@@ -776,19 +872,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername())); menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
menu.findItem(R.id.manage_user_lists).setVisible(relationship.following); menu.findItem(R.id.manage_user_lists).setVisible(relationship.following);
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following); menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
if (relationship.following) { if (relationship.following) {
MenuItem hideBoosts = menu.findItem(R.id.hide_boosts);
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername())); hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular); hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts); UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername())); hideBoosts.setVisible(true);
} else { } else {
menu.findItem(R.id.hide_boosts).setVisible(false); hideBoosts.setVisible(false);
} }
if(!account.isLocal()) MenuItem blockDomain=menu.findItem(R.id.block_domain);
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain())); if(!account.isLocal()){
else blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
menu.findItem(R.id.block_domain).setVisible(false); blockDomain.setVisible(true);
}else{
blockDomain.setVisible(false);
}
menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty())
? R.string.sk_add_note : R.string.sk_delete_note);
} }
@Override @Override
@@ -800,11 +901,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
intent.putExtra(Intent.EXTRA_TEXT, account.url); intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle())); startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.mute){ }else if(id==R.id.mute){
confirmToggleMuted(); UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){ }else if(id==R.id.block){
confirmToggleBlocked(); UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}else if(id==R.id.soft_block){ }else if(id==R.id.soft_block){
confirmSoftBlockUser(); UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
}else if(id==R.id.report){ }else if(id==R.id.report){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
@@ -870,6 +971,26 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else if(id==R.id.save){ }else if(id==R.id.save){
if(isInEditMode) if(isInEditMode)
saveAndExitEditMode(); saveAndExitEditMode();
}else if(id==R.id.edit_note){
if(noteWrap.getVisibility()==View.GONE){
showPrivateNote();
UiUtils.beginLayoutTransition(scrollableContent);
noteEdit.requestFocus();
noteEdit.postDelayed(()->{
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.showSoftInput(noteEdit, 0);
}, 100);
}else if(relationship.note.isEmpty()){
hidePrivateNote();
UiUtils.beginLayoutTransition(scrollableContent);
}else{
new M3AlertDialogBuilder(getActivity())
.setMessage(getContext().getString(R.string.sk_private_note_confirm_delete, account.getDisplayUsername()))
.setPositiveButton(R.string.delete, (dlg, btn)->savePrivateNote(null))
.setNegativeButton(R.string.cancel, null)
.show();
}
invalidateOptionsMenu();
} }
return true; return true;
} }
@@ -895,15 +1016,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void updateRelationship(){ private void updateRelationship(){
if(getActivity()==null) return; if(getActivity()==null) return;
if(relationship.note!=null && !relationship.note.isEmpty()) showPrivateNote();
else hidePrivateNote();
invalidateOptionsMenu(); invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE); actionButton.setVisibility(View.VISIBLE);
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE); notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton); UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
actionProgress.setIndeterminateTintList(actionButton.getTextColors()); actionProgress.setIndeterminateTintList(actionButton.getTextColors());
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors()); notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
noteSaveProgress.setIndeterminateTintList(noteEdit.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE); followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying); notifyButton.setSelected(relationship.notifying);
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username)); notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
UiUtils.beginLayoutTransition(scrollableContent);
} }
public ImageButton getFab() { public ImageButton getFab() {
@@ -1101,7 +1229,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateMetadataHeight(); updateMetadataHeight();
Toolbar toolbar=getToolbar(); Toolbar toolbar=getToolbar();
Drawable close=getToolbarContext().getDrawable(R.drawable.ic_baseline_close_24).mutate(); Drawable close=getToolbarContext().getDrawable(R.drawable.ic_fluent_dismiss_24_regular).mutate();
close.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant)); close.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
toolbar.setNavigationIcon(close); toolbar.setNavigationIcon(close);
toolbar.setNavigationContentDescription(R.string.discard); toolbar.setNavigationContentDescription(R.string.discard);
@@ -1177,6 +1305,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
imm.hideSoftInputFromWindow(content.getWindowToken(), 0); imm.hideSoftInputFromWindow(content.getWindowToken(), 0);
V.setVisibilityAnimated(fab, View.VISIBLE); V.setVisibilityAnimated(fab, View.VISIBLE);
bindHeaderView(); bindHeaderView();
V.setVisibilityAnimated(fab, View.VISIBLE);
} }
private void saveAndExitEditMode(){ private void saveAndExitEditMode(){
@@ -1206,18 +1335,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
.exec(accountID); .exec(accountID);
} }
private void confirmToggleMuted(){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}
private void confirmToggleBlocked(){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}
private void confirmSoftBlockUser(){
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
}
private void updateRelationship(Relationship r){ private void updateRelationship(Relationship r){
relationship=r; relationship=r;
updateRelationship(); updateRelationship();
@@ -1484,7 +1601,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
title.setText(item.parsedName); title.setText(item.parsedName);
value.setText(item.parsedValue); value.setText(item.parsedValue);
if(item.verifiedAt!=null){ if(item.verifiedAt!=null){
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63; int textColor=UiUtils.getThemeColor(getContext(), R.attr.colorM3Success);
value.setTextColor(textColor); value.setTextColor(textColor);
value.setLinkTextColor(textColor); value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate(); Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate();

View File

@@ -119,6 +119,15 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
} }
} }
@Override
protected void onShown(){
super.onShown();
// because, for some reason, when navigating back from compose fragment,
// match_parent would otherwise be incorrect (leaving a gap for the keyboard
// where there is none)
list.post(list::requestLayout);
}
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count) currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count)

View File

@@ -12,18 +12,22 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import name.fraser.neil.plaintext.diff_match_patch;
public class StatusEditHistoryFragment extends StatusListFragment{ public class StatusEditHistoryFragment extends StatusListFragment{
private String id, url; private String id, url;
@@ -58,7 +62,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); List<StatusDisplayItem> items=new ArrayList<>();
int idx=data.indexOf(s); int idx=data.indexOf(s);
if(idx>=0){ if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -83,8 +87,11 @@ public class StatusEditHistoryFragment extends StatusListFragment{
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class); EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
Status prev=data.get(idx+1); Status prev=data.get(idx+1);
if(!Objects.equals(s.content, prev.content)){ // if only formatting was changed, don't even try to create a diff text
if(!Objects.equals(HtmlParser.text(s.content), HtmlParser.text(prev.content))){
changes.add(StatusEditChangeType.TEXT_CHANGED); changes.add(StatusEditChangeType.TEXT_CHANGED);
//update status content to display a diffs
s.content=createDiffText(prev.content, s.content);
} }
if(!Objects.equals(s.spoilerText, prev.spoilerText)){ if(!Objects.equals(s.spoilerText, prev.spoilerText)){
if(s.spoilerText==null){ if(s.spoilerText==null){
@@ -147,15 +154,10 @@ public class StatusEditHistoryFragment extends StatusListFragment{
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null, s)); items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null, s));
items.add(1, new DummyStatusDisplayItem(s.id, this)); items.add(1, new DummyStatusDisplayItem(s.id, this));
} }
items.addAll(StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER|StatusDisplayItem.FLAG_INSET|StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS));
return items; return items;
} }
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
@Override @Override
public boolean isItemEnabled(String id){ public boolean isItemEnabled(String id){
return false; return false;
@@ -170,4 +172,28 @@ public class StatusEditHistoryFragment extends StatusListFragment{
public Uri getWebUri(Uri.Builder base) { public Uri getWebUri(Uri.Builder base) {
return Uri.parse(url); return Uri.parse(url);
} }
private String createDiffText(String original, String modified) {
diff_match_patch dmp=new diff_match_patch();
LinkedList<diff_match_patch.Diff> diffs=dmp.diff_main(original, modified);
dmp.diff_cleanupSemantic(diffs);
StringBuilder stringBuilder=new StringBuilder();
for(diff_match_patch.Diff diff : diffs){
switch(diff.operation){
case DELETE->{
stringBuilder.append("<edit-diff-delete>");
stringBuilder.append(diff.text);
stringBuilder.append("</edit-diff-delete>");
}
case INSERT->{
stringBuilder.append("<edit-diff-insert>");
stringBuilder.append(diff.text);
stringBuilder.append("</edit-diff-insert>");
}
default->stringBuilder.append(diff.text);
}
}
return stringBuilder.toString();
}
} }

View File

@@ -9,10 +9,12 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.session.AccountLocalPreferences; import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent; import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.ReblogDeletedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
@@ -23,12 +25,18 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -76,8 +84,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
@Override @Override
public void onItemClick(String id){ public void onItemClick(String id){
Status status=getContentStatusByID(id); Status status=getContentStatusByID(id);
if(status==null) if(status==null || status.preview) return;
return;
status.filterRevealed=true; status.filterRevealed=true;
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
@@ -142,12 +149,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
} }
} }
protected Status getContentStatusByID(String id){ public Status getContentStatusByID(String id){
Status s=getStatusByID(id); Status s=getStatusByID(id);
return s==null ? null : s.getContentStatus(); return s==null ? null : s.getContentStatus();
} }
protected Status getStatusByID(String id){ public Status getStatusByID(String id){
for(Status s:data){ for(Status s:data){
if(s.id.equals(id)){ if(s.id.equals(id)){
return s; return s;
@@ -174,41 +181,73 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
} }
} }
protected void removeStatus(Status status){ private boolean removeStatusDisplayItems(String parentID, int firstIndex, int ancestorFirstIndex, int ancestorLastIndex){
data.remove(status);
preloadedData.remove(status);
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
for(int i=0;i<displayItems.size();i++){
StatusDisplayItem item = displayItems.get(i);
if(status.id.equals(item.parentID)){
index=i;
break;
}
if (item.parentID.equals(status.inReplyToId)) {
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
ancestorLastIndex = i;
}
}
// did we find an ancestor that is also the status' neighbor? // did we find an ancestor that is also the status' neighbor?
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) { if(ancestorFirstIndex>=0 && ancestorLastIndex==firstIndex-1){
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) { // update ancestor to have no descendant anymore
StatusDisplayItem item = displayItems.get(i); displayItems.subList(ancestorFirstIndex, ancestorLastIndex+1).forEach(i->i.hasDescendantNeighbor=false);
// update ancestor to have no descendant anymore adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex-ancestorFirstIndex+1);
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
}
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
} }
if(index==-1) if(firstIndex==-1) return false;
return; int lastIndex=firstIndex;
int lastIndex; while(lastIndex<displayItems.size()){
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){ StatusDisplayItem item=displayItems.get(lastIndex);
if(!displayItems.get(lastIndex).parentID.equals(status.id)) if(!item.parentID.equals(parentID) || item instanceof GapStatusDisplayItem) break;
break; lastIndex++;
} }
displayItems.subList(index, lastIndex).clear(); int count=lastIndex-firstIndex;
adapter.notifyItemRangeRemoved(index, lastIndex-index); if(count<1) return false;
displayItems.subList(firstIndex, lastIndex).clear();
adapter.notifyItemRangeRemoved(firstIndex, count);
return true;
}
protected void removeStatus(Status status){
final AccountSessionManager asm=AccountSessionManager.getInstance();
final CacheController cache=AccountSessionManager.get(accountID).getCacheController();
final boolean unReblogging=status.reblog!=null && asm.isSelf(accountID, status.account);
final Predicate<Status> isToBeRemovedReblog=item->item!=null && item.reblog!=null
&& item.reblog.id.equals(status.reblog.id)
&& asm.isSelf(accountID, item.account);
final BiPredicate<String, Supplier<String>> isToBeRemovedContent=(parentId, contentIdSupplier)->
parentId.equals(status.id) || contentIdSupplier.get().equals(status.id);
int ancestorFirstIndex=-1, ancestorLastIndex=-1;
for(int i=0;i<displayItems.size();i++){
StatusDisplayItem item=displayItems.get(i);
// we found a status that the to-be-removed status replies to!
// storing indices to maybe update its display items
if(item.parentID.equals(status.inReplyToId)){
if(ancestorFirstIndex==-1) ancestorFirstIndex=i;
ancestorLastIndex=i;
}
// if we're un-reblogging, we compare the reblogged status's id with the current status's
if(unReblogging
? isToBeRemovedReblog.test(getStatusByID(item.parentID))
: isToBeRemovedContent.test(item.parentID, item::getContentStatusID)){
// if statuses are removed from index i, the next iteration should be on the same index again
if(removeStatusDisplayItems(item.parentID, i, ancestorFirstIndex, ancestorLastIndex)) i--;
// resetting in case we find another occurrence of the same status that also has ancestors
// (we won't - unless the timeline is being especially weird)
ancestorFirstIndex=-1; ancestorLastIndex=-1;
}
}
Consumer<List<Status>> removeStatusFromData=(list)->{
Iterator<Status> it=list.iterator();
while(it.hasNext()){
Status s=it.next();
if(unReblogging
? isToBeRemovedReblog.test(s)
: isToBeRemovedContent.test(s.id, s::getContentStatusID)){
it.remove();
cache.deleteStatus(s.id);
}
}
};
removeStatusFromData.accept(data);
removeStatusFromData.accept(preloadedData);
} }
@Override @Override
@@ -277,6 +316,22 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
removeStatus(status); removeStatus(status);
} }
@Subscribe
public void onReblogDeleted(ReblogDeletedEvent ev){
AccountSessionManager asm=AccountSessionManager.getInstance();
if(!ev.accountID.equals(accountID))
return;
for(Status item : data){
boolean itemIsOwnReblog=item.reblog!=null
&& item.getContentStatusID().equals(ev.statusID)
&& asm.isSelf(accountID, item.account);
if(itemIsOwnReblog){
removeStatus(item);
break;
}
}
}
@Subscribe @Subscribe
public void onStatusCreated(StatusCreatedEvent ev){ public void onStatusCreated(StatusCreatedEvent ev){
if(!ev.accountID.equals(accountID)) if(!ev.accountID.equals(accountID))
@@ -297,6 +352,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
Status contentStatus=status.getContentStatus(); Status contentStatus=status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){ if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(status.id, contentStatus, ev.poll); updatePoll(status.id, contentStatus, ev.poll);
AccountSessionManager.get(accountID).getCacheController().updateStatus(contentStatus);
} }
} }
} }

View File

@@ -50,21 +50,25 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent { public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus, updatedStatus; protected Status mainStatus, updatedStatus, replyTo;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>(); private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
private StatusContext result; private StatusContext result;
protected boolean contextInitiallyRendered, transitionFinished; protected boolean contextInitiallyRendered, transitionFinished, preview;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mainStatus=Parcels.unwrap(getArguments().getParcelable("status")); mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount")); Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
refreshing=contextInitiallyRendered=getArguments().getBoolean("refresh", false);
if(inReplyToAccount!=null) if(inReplyToAccount!=null)
knownAccounts.put(inReplyToAccount.id, inReplyToAccount); knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
data.add(mainStatus); data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus)); onAppendItems(Collections.singletonList(mainStatus));
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis)); preview=mainStatus.preview;
if(preview) setRefreshEnabled(false);
setTitle(preview ? getString(R.string.sk_post_preview) : HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis));
transitionFinished = getArguments().getBoolean("noTransition", false); transitionFinished = getArguments().getBoolean("noTransition", false);
} }
@@ -129,11 +133,21 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
if (refreshing) loadMainStatus(); if(preview && replyTo==null){
currentRequest=new GetStatusContext(mainStatus.id) result=new StatusContext();
result.descendants=Collections.emptyList();
result.ancestors=Collections.emptyList();
return;
}
if(refreshing && !preview) loadMainStatus();
currentRequest=new GetStatusContext(preview ? replyTo.id : mainStatus.id)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(StatusContext result){ public void onSuccess(StatusContext result){
if(preview){
result.descendants=Collections.emptyList();
result.ancestors.add(replyTo);
}
ThreadFragment.this.result = result; ThreadFragment.this.result = result;
maybeApplyContext(); maybeApplyContext();
} }

View File

@@ -4,6 +4,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
@@ -65,6 +66,8 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
} }
@Override @Override
@@ -258,7 +261,7 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id); relationship=relationships.get(item.account.id);
UiUtils.setExtraTextInfo(getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account); UiUtils.setExtraTextInfo(getContext(), null, true, false, false, item.account);
if(relationship==null){ if(relationship==null){
actionWrap.setVisibility(View.GONE); actionWrap.setVisibility(View.GONE);
@@ -327,9 +330,9 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(account.emojis.isEmpty()){ if(account.emojis.isEmpty()){
parsedName=account.displayName; parsedName= account.getDisplayName();
}else{ }else{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
} }
} }

View File

@@ -13,8 +13,10 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.IsOnTop; import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SimpleViewHolder;
@@ -96,8 +98,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){ pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override @Override
public void onPageSelected(int position){ public void onPageSelected(int position){
if(position==0)
return;
Fragment _page=getFragmentForPage(position); Fragment _page=getFragmentForPage(position);
if(_page instanceof BaseRecyclerFragment<?> page){ if(_page instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading()) if(!page.loaded && !page.isDataLoading())
@@ -157,7 +157,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
} }
}); });
disableDiscover=getArguments().getBoolean("disableDiscover"); disableDiscover=AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false);
searchView=view.findViewById(R.id.search_fragment); searchView=view.findViewById(R.id.search_fragment);
if(searchFragment==null){ if(searchFragment==null){
searchFragment=new SearchFragment(); searchFragment=new SearchFragment();

View File

@@ -2,8 +2,8 @@ package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
@@ -14,14 +14,12 @@ import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card; import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.viewmodel.CardViewModel; import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -32,11 +30,9 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper; import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate; import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
@@ -60,6 +56,8 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID); bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
} }
@Override @Override

View File

@@ -36,6 +36,7 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult>{ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@@ -142,7 +143,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}*/ }*/
int offset=_offset; int offset=_offset;
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count) currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
.setCallback(new Callback<>(){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(SearchResults result){ public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>(); ArrayList<SearchResult> results=new ArrayList<>();
@@ -165,16 +166,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
unfilteredResults=results; unfilteredResults=results;
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty()); onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
} }
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
}
}) })
.setTimeout(180000) // 3 minutes (searches can take a long time)
.exec(accountID); .exec(accountID);
} }

View File

@@ -115,7 +115,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
onDataLoaded(results.stream().map(sr->{ onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true); SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
if(sr.type==SearchResult.Type.HASHTAG){ if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr); vm.hashtagItem.setOnClick(i->openHashtag(sr));
} }
return vm; return vm;
}).collect(Collectors.toList()), false); }).collect(Collectors.toList()), false);
@@ -132,7 +132,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
.map(sr->{ .map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false); SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
if(sr.type==SearchResult.Type.HASHTAG){ if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr); vm.hashtagItem.setOnClick(i->openHashtag(sr));
} }
return vm; return vm;
}) })
@@ -429,11 +429,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
wrapSuicideDialog(()->deliverResult(currentQuery, null)); wrapSuicideDialog(()->deliverResult(currentQuery, null));
} }
private void onOpenURLClick(){ private void onOpenURLClick(ListItem<?> item_){
UiUtils.openURL(getContext(), accountID, searchViewHelper.getQuery(), false); UiUtils.openURL(getContext(), accountID, searchViewHelper.getQuery(), false);
} }
private void onGoToHashtagClick(){ private void onGoToHashtagClick(ListItem<?> item_){
wrapSuicideDialog(()->{ wrapSuicideDialog(()->{
String q=searchViewHelper.getQuery(); String q=searchViewHelper.getQuery();
if(q.startsWith("#")) if(q.startsWith("#"))
@@ -442,7 +442,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
}); });
} }
private void onGoToAccountClick(){ private void onGoToAccountClick(ListItem<?> item_){
String q=searchViewHelper.getQuery(); String q=searchViewHelper.getQuery();
if(!q.startsWith("@")){ if(!q.startsWith("@")){
q="@"+q; q="@"+q;
@@ -459,11 +459,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
}).ifPresent(progress -> progress.wrapProgress((Activity) getContext(), R.string.loading, true)); }).ifPresent(progress -> progress.wrapProgress((Activity) getContext(), R.string.loading, true));
} }
private void onGoToStatusSearchClick(){ private void onGoToStatusSearchClick(ListItem<?> item_){
wrapSuicideDialog(()->deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS)); wrapSuicideDialog(()->deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS));
} }
private void onGoToAccountSearchClick(){ private void onGoToAccountSearchClick(ListItem<?> item_){
wrapSuicideDialog(()->deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT)); wrapSuicideDialog(()->deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT));
} }

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover; package org.joinmastodon.android.fragments.discover;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -9,8 +10,6 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags; import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
import org.joinmastodon.android.fragments.ScrollableToTop; import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView; import org.joinmastodon.android.ui.views.HashtagChartView;
@@ -34,6 +33,8 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
accountID=getArguments().getString("account"); accountID=getArguments().getString("account");
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
} }
@Override @Override

View File

@@ -83,9 +83,10 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
@Override @Override
protected void updateFilteredList(){ protected void updateFilteredList(){
boolean addFakeInstance = currentSearchQuery.length()>0 && currentSearchQuery.matches("^\\S+\\.[^\\.]+$"); String query=getCurrentSearchQuery();
boolean addFakeInstance=query.length()>0 && query.matches("^\\S+\\.[^\\.]+$");
if(addFakeInstance){ if(addFakeInstance){
fakeInstance.domain=fakeInstance.normalizedDomain=currentSearchQuery; fakeInstance.domain=fakeInstance.normalizedDomain=query;
fakeInstance.description=getString(R.string.loading_instance); fakeInstance.description=getString(R.string.loading_instance);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){ if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){ if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){
@@ -99,12 +100,12 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
} }
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData); ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear(); filteredData.clear();
if(currentSearchQuery.length()>0){ if(query.length()>0){
boolean foundExactMatch=false; boolean foundExactMatch=false;
for(CatalogInstance inst:data){ for(CatalogInstance inst:data){
if(inst.normalizedDomain.contains(currentSearchQuery)){ if(inst.normalizedDomain.contains(query)){
filteredData.add(inst); filteredData.add(inst);
if(inst.normalizedDomain.equals(currentSearchQuery)) if(inst.normalizedDomain.equals(query))
foundExactMatch=true; foundExactMatch=true;
} }
} }

View File

@@ -93,10 +93,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim(); currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
updateFilteredList(); updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer); searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery)); Instance instance=instancesCache.get(normalizeInstanceDomain(getCurrentSearchQuery()));
if(instance==null){ if(instance==null){
showProgressDialog(); showProgressDialog();
loadInstanceInfo(currentSearchQuery, false); loadInstanceInfo(getCurrentSearchQuery(), false);
}else{ }else{
proceedWithAuthOrSignup(instance); proceedWithAuthOrSignup(instance);
} }
@@ -106,7 +106,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
protected void onSearchChangedDebounced(){ protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim(); currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
updateFilteredList(); updateFilteredList();
loadInstanceInfo(currentSearchQuery, false); loadInstanceInfo(getCurrentSearchQuery(), false);
} }
protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){ protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
@@ -126,9 +126,16 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
instanceProgressDialog.show(); instanceProgressDialog.show();
} }
protected String getCurrentSearchQuery(){
String[] parts=currentSearchQuery.split("@");
return parts.length>0 ? parts[parts.length-1] : "";
}
protected String normalizeInstanceDomain(String _domain){ protected String normalizeInstanceDomain(String _domain){
if(TextUtils.isEmpty(_domain)) if(TextUtils.isEmpty(_domain))
return null; return null;
String[] parts=_domain.split("@");
_domain=parts[parts.length - 1];
if(_domain.contains(":")){ if(_domain.contains(":")){
try{ try{
_domain=Uri.parse(_domain).getAuthority(); _domain=Uri.parse(_domain).getAuthority();
@@ -198,7 +205,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
instanceProgressDialog=null; instanceProgressDialog=null;
proceedWithAuthOrSignup(result); proceedWithAuthOrSignup(result);
} }
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){ if(Objects.equals(domain, getCurrentSearchQuery()) || Objects.equals(getCurrentSearchQuery(), redirects.get(domain)) || Objects.equals(getCurrentSearchQuery(), redirectsInverse.get(domain))){
boolean found=false; boolean found=false;
for(CatalogInstance ci:filteredData){ for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance){ if(ci.domain.equals(domain) && ci!=fakeInstance){

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;

View File

@@ -1,8 +1,6 @@
package org.joinmastodon.android.fragments.report; package org.joinmastodon.android.fragments.report;
import android.app.Activity; import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@@ -25,6 +23,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -97,15 +96,14 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
.exec(accountID); .exec(accountID);
} }
@Override public void onToggleItem(String id){
public void onItemClick(String id){
if(selectedIDs.contains(id)) if(selectedIDs.contains(id))
selectedIDs.remove(id); selectedIDs.remove(id);
else else
selectedIDs.add(id); selectedIDs.add(id);
CheckableHeaderStatusDisplayItem.Holder holder=findHolderOfType(id, CheckableHeaderStatusDisplayItem.Holder.class); CheckableHeaderStatusDisplayItem.Holder holder=findHolderOfType(id, CheckableHeaderStatusDisplayItem.Holder.class);
if(holder!=null) if(holder!=null) holder.rebind();
holder.rebind(); else notifyItemChanged(id, CheckableHeaderStatusDisplayItem.class);
} }
@Override @Override
@@ -121,13 +119,20 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder) if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder)
return; return;
outRect.left=V.dp(40); boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
if(isRTL) outRect.right=V.dp(40);
else outRect.left=V.dp(40);
if(holder instanceof AudioStatusDisplayItem.Holder){ if(holder instanceof AudioStatusDisplayItem.Holder){
outRect.bottom=V.dp(16); outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){ }else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
outRect.bottom=V.dp(16); outRect.bottom=V.dp(8);
outRect.left+=V.dp(16); if(isRTL){
outRect.right=V.dp(16); outRect.right+=V.dp(16);
outRect.left=V.dp(16);
}else{
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
}
} }
} }
}); });
@@ -155,9 +160,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
return adapter; return adapter;
} }
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
}
private void onButtonClick(View v){ private void onButtonClick(View v){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
@@ -201,7 +203,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override @Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){ protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN); List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
items.add(new DummyStatusDisplayItem(s.getID(), this));
return items;
} }
@Override @Override

View File

@@ -34,7 +34,6 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ReportDoneFragment extends MastodonToolbarFragment{ public class ReportDoneFragment extends MastodonToolbarFragment{
@@ -227,7 +226,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
@Override @Override
protected int getNavigationIconDrawableResource(){ protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24; return R.drawable.ic_fluent_dismiss_24_regular;
} }
@Override @Override

View File

@@ -204,14 +204,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
float off=paint.getStrokeWidth()/2f; float off=paint.getStrokeWidth()/2f;
c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), paint); c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), paint);
} }
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder<?>){
outRect.left=outRect.right=V.dp(16);
}
}
}); });
} }
} }

View File

@@ -73,7 +73,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick), durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick), wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick), contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem)) cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, this::toggleCheckableItem)
)); ));
if(filter!=null){ if(filter!=null){
@@ -113,7 +113,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
return 1; return 1;
} }
private void onDurationClick(){ private void onDurationClick(ListItem<Void> item_){
int[] durationOptions={ int[] durationOptions={
1800, 1800,
3600, 3600,
@@ -182,21 +182,21 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
alert.setOnDismissListener(dialog->callback.accept(null)); alert.setOnDismissListener(dialog->callback.accept(null));
} }
private void onWordsClick(){ private void onWordsClick(ListItem<Void> item){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new))); args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this); Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
} }
private void onContextClick(){ private void onContextClick(ListItem<Void> item){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putSerializable("context", context); args.putSerializable("context", context);
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this); Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
} }
private void onDeleteClick(){ private void onDeleteClick(ListItem<Void> item_){
AlertDialog alert=new M3AlertDialogBuilder(getActivity()) AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(getString(R.string.settings_delete_filter_title, filter.title)) .setTitle(getString(R.string.settings_delete_filter_title, filter.title))
.setMessage(R.string.settings_delete_filter_confirmation) .setMessage(R.string.settings_delete_filter_confirmation)

View File

@@ -22,10 +22,9 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
setTitle(R.string.settings_filter_context); setTitle(R.string.settings_filter_context);
context=(EnumSet<FilterContext>) getArguments().getSerializable("context"); context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{ onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null); CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), this::toggleCheckableItem);
item.parentObject=c; item.parentObject=c;
item.isEnabled=true; item.isEnabled=true;
item.onClick=()->toggleCheckableItem(item);
return item; return item;
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
} }

View File

@@ -1,11 +1,6 @@
package org.joinmastodon.android.fragments.settings; package org.joinmastodon.android.fragments.settings;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.IntEvaluator;
import android.animation.ObjectAnimator;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
@@ -27,6 +22,7 @@ import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.ActionModeHelper;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout; import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
@@ -37,7 +33,6 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
@@ -60,7 +55,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
FilterKeyword word=Parcels.unwrap(p); FilterKeyword word=Parcels.unwrap(p);
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word); ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
item.isEnabled=true; item.isEnabled=true;
item.onClick=()->onWordClick(item); item.setOnClick(this::onWordClick);
return item; return item;
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
setHasOptionsMenu(true); setHasOptionsMenu(true);
@@ -97,7 +92,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab); fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_add_24px); fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
fab.setContentDescription(getString(R.string.add_muted_word)); fab.setContentDescription(getString(R.string.add_muted_word));
fab.setOnClickListener(v->onFabClick()); fab.setOnClickListener(v->onFabClick());
} }
@@ -114,7 +109,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.settings_filter_words, menu); inflater.inflate(R.menu.selectable_list, menu);
} }
@Override @Override
@@ -174,7 +169,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
w.keyword=input; w.keyword=input;
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w); ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
item.isEnabled=true; item.isEnabled=true;
item.onClick=()->onWordClick(item); item.setOnClick(this::onWordClick);
data.add(item); data.add(item);
itemsAdapter.notifyItemInserted(data.size()-1); itemsAdapter.notifyItemInserted(data.size()-1);
}else{ }else{
@@ -228,29 +223,15 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
return; return;
V.setVisibilityAnimated(fab, View.GONE); V.setVisibilityAnimated(fab, View.GONE);
actionMode=getActivity().startActionMode(new ActionMode.Callback(){ actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
@Override @Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){ public boolean onCreateActionMode(ActionMode mode, Menu menu){
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", elevationOnScrollListener.getCurrentStatusBarColor(), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.start();
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
return true; return true;
} }
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){ public boolean onPrepareActionMode(ActionMode mode, Menu menu){
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu); mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
for(int i=0;i<menu.size();i++){
Drawable icon=menu.getItem(i).getIcon().mutate();
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnPrimary));
menu.getItem(i).setIcon(icon);
}
deleteItem=menu.findItem(R.id.delete); deleteItem=menu.findItem(R.id.delete);
return true; return true;
} }
@@ -266,21 +247,6 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
@Override @Override
public void onDestroyActionMode(ActionMode mode){ public void onDestroyActionMode(ActionMode mode){
leaveSelectionMode(true); leaveSelectionMode(true);
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
getActivity().getWindow().setStatusBarColor(0);
}
});
anim.start();
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
} }
}); });
@@ -289,7 +255,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
ListItem<FilterKeyword> item=data.get(i); ListItem<FilterKeyword> item=data.get(i);
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null); CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
newItem.isEnabled=true; newItem.isEnabled=true;
newItem.onClick=()->onSelectionModeWordClick(newItem); newItem.setOnClick(this::onSelectionModeWordClick);
newItem.parentObject=item.parentObject; newItem.parentObject=item.parentObject;
if(selectAll) if(selectAll)
selectedItems.add(newItem); selectedItems.add(newItem);
@@ -313,7 +279,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
ListItem<FilterKeyword> item=data.get(i); ListItem<FilterKeyword> item=data.get(i);
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null); ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
newItem.isEnabled=true; newItem.isEnabled=true;
newItem.onClick=()->onWordClick(newItem); newItem.setOnClick(this::onWordClick);
newItem.parentObject=item.parentObject; newItem.parentObject=item.parentObject;
data.set(i, newItem); data.set(i, newItem);
} }

View File

@@ -2,21 +2,39 @@ package org.joinmastodon.android.fragments.settings;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ImageCache; import me.grishka.appkit.imageloader.ImageCache;
@@ -25,24 +43,49 @@ import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
private ListItem<Void> mediaCacheItem; private static final String TAG="SettingsAboutAppFragment";
private ListItem<Void> mediaCacheItem, copyCrashLogItem;
private CheckableListItem<Void> enablePreReleasesItem;
private AccountSession session;
private boolean timelineCacheCleared=false;
private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log");
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setTitle(getString(R.string.about_app, getString(R.string.sk_app_name))); setTitle(getString(R.string.about_app, getString(R.string.sk_app_name)));
AccountSession s=AccountSessionManager.get(accountID); session=AccountSessionManager.get(accountID);
onDataLoaded(List.of(
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, ()->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag))), String lastModified=crashLogFile.exists()
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))), ? DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT).withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(crashLogFile.lastModified()))
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")), : getString(R.string.sk_settings_crash_log_unavailable);
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true), List<ListItem<Void>> items=new ArrayList<>(List.of(
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick) new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, i->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag))),
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))),
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick),
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick),
copyCrashLogItem=new ListItem<>(getString(R.string.sk_settings_copy_crash_log), lastModified, 0, this::onCopyCrashLog)
)); ));
if(GithubSelfUpdater.needSelfUpdating()){
items.add(enablePreReleasesItem=new CheckableListItem<>(R.string.sk_updater_enable_pre_releases, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enablePreReleases, i->toggleCheckableItem(enablePreReleasesItem)));
}
copyCrashLogItem.isEnabled=crashLogFile.exists();
onDataLoaded(items);
updateMediaCacheItem(); updateMediaCacheItem();
} }
@Override
protected void onHidden(){
super.onHidden();
GlobalUserPreferences.enablePreReleases=enablePreReleasesItem.checked;
GlobalUserPreferences.save();
if(timelineCacheCleared) getActivity().recreate();
}
@Override @Override
protected void doLoadData(int offset, int count){} protected void doLoadData(int offset, int count){}
@@ -63,7 +106,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
return adapter; return adapter;
} }
private void onClearMediaCacheClick(){ private void onClearMediaCacheClick(ListItem<?> item){
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
Activity activity=getActivity(); Activity activity=getActivity();
ImageCache.getInstance(getActivity()).clear(); ImageCache.getInstance(getActivity()).clear();
@@ -74,10 +117,29 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
}); });
} }
private void onClearTimelineCacheClick(ListItem<?> item){
session.getCacheController().putHomeTimeline(List.of(), true);
Toast.makeText(getContext(), R.string.sk_timeline_cache_cleared, Toast.LENGTH_SHORT).show();
timelineCacheCleared=true;
}
private void updateMediaCacheItem(){ private void updateMediaCacheItem(){
long size=ImageCache.getInstance(getActivity()).getDiskCache().size(); long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false); mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
mediaCacheItem.isEnabled=size>0; mediaCacheItem.isEnabled=size>0;
rebindItem(mediaCacheItem); rebindItem(mediaCacheItem);
} }
private void onCopyCrashLog(ListItem<?> item){
if(!crashLogFile.exists()) return;
try(InputStream is=new FileInputStream(crashLogFile)){
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
StringBuilder sb=new StringBuilder();
String line;
while ((line=reader.readLine())!=null) sb.append(line).append("\n");
UiUtils.copyText(list, sb.toString());
} catch(IOException e){
Log.e(TAG, "Error reading crash log", e);
}
}
} }

View File

@@ -46,20 +46,20 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
List<ListItem<Void>> items = new ArrayList<>(List.of( List<ListItem<Void>> items = new ArrayList<>(List.of(
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick), languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick),
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_fluent_image_alt_text_24_regular, ()->toggleCheckableItem(altTextItem)), altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_fluent_image_alt_text_24_regular, i->toggleCheckableItem(altTextItem)),
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_fluent_gif_24_regular, ()->toggleCheckableItem(playGifsItem)), playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_fluent_gif_24_regular, i->toggleCheckableItem(playGifsItem)),
overlayMediaItem=new CheckableListItem<>(R.string.sk_settings_continues_playback, R.string.sk_settings_continues_playback_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.overlayMedia, R.drawable.ic_fluent_play_circle_hint_24_regular, ()->toggleCheckableItem(overlayMediaItem)), overlayMediaItem=new CheckableListItem<>(R.string.sk_settings_continues_playback, R.string.sk_settings_continues_playback_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.overlayMedia, R.drawable.ic_fluent_play_circle_hint_24_regular, i->toggleCheckableItem(overlayMediaItem)),
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_fluent_link_24_regular, ()->toggleCheckableItem(customTabsItem)), customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_fluent_link_24_regular, i->toggleCheckableItem(customTabsItem)),
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_fluent_person_delete_24_regular, ()->toggleCheckableItem(confirmUnfollowItem)), confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_fluent_person_delete_24_regular, i->toggleCheckableItem(confirmUnfollowItem)),
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(confirmBoostItem)), confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_fluent_arrow_repeat_all_24_regular, i->toggleCheckableItem(confirmBoostItem)),
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_fluent_delete_24_regular, ()->toggleCheckableItem(confirmDeleteItem)), confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_fluent_delete_24_regular, i->toggleCheckableItem(confirmDeleteItem)),
prefixRepliesItem=new ListItem<>(R.string.sk_settings_prefix_reply_cw_with_re, getPrefixWithRepliesString(), R.drawable.ic_fluent_arrow_reply_24_regular, this::onPrefixRepliesClick), prefixRepliesItem=new ListItem<>(R.string.sk_settings_prefix_reply_cw_with_re, getPrefixWithRepliesString(), R.drawable.ic_fluent_arrow_reply_24_regular, this::onPrefixRepliesClick),
forwardReportsItem=new CheckableListItem<>(R.string.sk_settings_forward_report_default, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.forwardReportDefault, R.drawable.ic_fluent_arrow_forward_24_regular, ()->toggleCheckableItem(forwardReportsItem)), forwardReportsItem=new CheckableListItem<>(R.string.sk_settings_forward_report_default, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.forwardReportDefault, R.drawable.ic_fluent_arrow_forward_24_regular, i->toggleCheckableItem(forwardReportsItem)),
loadNewPostsItem=new CheckableListItem<>(R.string.sk_settings_load_new_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.loadNewPosts, R.drawable.ic_fluent_arrow_sync_24_regular, this::onLoadNewPostsClick), loadNewPostsItem=new CheckableListItem<>(R.string.sk_settings_load_new_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.loadNewPosts, R.drawable.ic_fluent_arrow_sync_24_regular, i->onLoadNewPostsClick()),
seeNewPostsBtnItem=new CheckableListItem<>(R.string.sk_settings_see_new_posts_button, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNewPostsButton, R.drawable.ic_fluent_arrow_up_24_regular, ()->toggleCheckableItem(seeNewPostsBtnItem)), seeNewPostsBtnItem=new CheckableListItem<>(R.string.sk_settings_see_new_posts_button, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNewPostsButton, R.drawable.ic_fluent_arrow_up_24_regular, i->toggleCheckableItem(seeNewPostsBtnItem)),
remoteLoadingItem=new CheckableListItem<>(R.string.sk_settings_allow_remote_loading, R.string.sk_settings_allow_remote_loading_explanation, CheckableListItem.Style.SWITCH, GlobalUserPreferences.allowRemoteLoading, R.drawable.ic_fluent_communication_24_regular, ()->toggleCheckableItem(remoteLoadingItem), true), remoteLoadingItem=new CheckableListItem<>(R.string.sk_settings_allow_remote_loading, R.string.sk_settings_allow_remote_loading_explanation, CheckableListItem.Style.SWITCH, GlobalUserPreferences.allowRemoteLoading, R.drawable.ic_fluent_communication_24_regular, i->toggleCheckableItem(remoteLoadingItem), true),
showBoostsItem=new CheckableListItem<>(R.string.sk_settings_show_boosts, 0, CheckableListItem.Style.SWITCH, lp.showBoosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(showBoostsItem)), showBoostsItem=new CheckableListItem<>(R.string.sk_settings_show_boosts, 0, CheckableListItem.Style.SWITCH, lp.showBoosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, i->toggleCheckableItem(showBoostsItem)),
showRepliesItem=new CheckableListItem<>(R.string.sk_settings_show_replies, 0, CheckableListItem.Style.SWITCH, lp.showReplies, R.drawable.ic_fluent_arrow_reply_24_regular, ()->toggleCheckableItem(showRepliesItem)) showRepliesItem=new CheckableListItem<>(R.string.sk_settings_show_replies, 0, CheckableListItem.Style.SWITCH, lp.showReplies, R.drawable.ic_fluent_arrow_reply_24_regular, i->toggleCheckableItem(showRepliesItem))
)); ));
if(isInstanceAkkoma()) items.add( if(isInstanceAkkoma()) items.add(
@@ -93,7 +93,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
@Override @Override
protected void doLoadData(int offset, int count){} protected void doLoadData(int offset, int count){}
private void onDefaultLanguageClick(){ private void onDefaultLanguageClick(ListItem<?> item){
if (languageResolver == null) return; if (languageResolver == null) return;
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(postLanguage), null, languageResolver); ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(postLanguage), null, languageResolver);
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
@@ -112,14 +112,14 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
.show(); .show();
} }
private void onPrefixRepliesClick(){ private void onPrefixRepliesClick(ListItem<?> item){
int selected=GlobalUserPreferences.prefixReplies.ordinal(); int selected=GlobalUserPreferences.prefixReplies.ordinal();
int[] newSelected={selected}; int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_prefix_reply_cw_with_re) .setTitle(R.string.sk_settings_prefix_reply_cw_with_re)
.setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_prefix_replies_never, R.string.sk_settings_prefix_replies_always, R.string.sk_settings_prefix_replies_to_others).mapToObj(this::getString).toArray(String[]::new), .setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_prefix_replies_never, R.string.sk_settings_prefix_replies_always, R.string.sk_settings_prefix_replies_to_others).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item) selected, (dlg, which)->newSelected[0]=which)
.setPositiveButton(R.string.ok, (dlg, item)->{ .setPositiveButton(R.string.ok, (dlg, which)->{
GlobalUserPreferences.prefixReplies=GlobalUserPreferences.PrefixRepliesMode.values()[newSelected[0]]; GlobalUserPreferences.prefixReplies=GlobalUserPreferences.PrefixRepliesMode.values()[newSelected[0]];
prefixRepliesItem.subtitleRes=getPrefixWithRepliesString(); prefixRepliesItem.subtitleRes=getPrefixWithRepliesString();
rebindItem(prefixRepliesItem); rebindItem(prefixRepliesItem);
@@ -128,7 +128,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
.show(); .show();
} }
private void onReplyVisibilityClick(){ private void onReplyVisibilityClick(ListItem<?> item){
AccountLocalPreferences lp=getLocalPrefs(); AccountLocalPreferences lp=getLocalPrefs();
int selected=lp.timelineReplyVisibility==null ? 2 : switch(lp.timelineReplyVisibility){ int selected=lp.timelineReplyVisibility==null ? 2 : switch(lp.timelineReplyVisibility){
case "following" -> 0; case "following" -> 0;
@@ -139,8 +139,8 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_prefix_reply_cw_with_re) .setTitle(R.string.sk_settings_prefix_reply_cw_with_re)
.setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_reply_visibility_following, R.string.sk_settings_reply_visibility_self, R.string.sk_settings_reply_visibility_all).mapToObj(this::getString).toArray(String[]::new), .setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_reply_visibility_following, R.string.sk_settings_reply_visibility_self, R.string.sk_settings_reply_visibility_all).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item) selected, (dlg, which)->newSelected[0]=which)
.setPositiveButton(R.string.ok, (dlg, item)->{ .setPositiveButton(R.string.ok, (dlg, which)->{
lp.timelineReplyVisibility=switch(newSelected[0]){ lp.timelineReplyVisibility=switch(newSelected[0]){
case 0 -> "following"; case 0 -> "following";
case 1 -> "self"; case 1 -> "self";
@@ -167,7 +167,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
GlobalUserPreferences.overlayMedia=overlayMediaItem.checked; GlobalUserPreferences.overlayMedia=overlayMediaItem.checked;
GlobalUserPreferences.useCustomTabs=customTabsItem.checked; GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
GlobalUserPreferences.altTextReminders=altTextItem.checked; GlobalUserPreferences.altTextReminders=altTextItem.checked;
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked; GlobalUserPreferences.confirmUnfollow=confirmUnfollowItem.checked;
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked; GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked; GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
GlobalUserPreferences.forwardReportDefault=forwardReportsItem.checked; GlobalUserPreferences.forwardReportDefault=forwardReportsItem.checked;
@@ -176,6 +176,8 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
GlobalUserPreferences.allowRemoteLoading=remoteLoadingItem.checked; GlobalUserPreferences.allowRemoteLoading=remoteLoadingItem.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();
AccountLocalPreferences lp=getLocalPrefs(); AccountLocalPreferences lp=getLocalPrefs();
boolean restartPlease=lp.showBoosts!=showBoostsItem.checked
|| lp.showReplies!=showRepliesItem.checked;
lp.showBoosts=showBoostsItem.checked; lp.showBoosts=showBoostsItem.checked;
lp.showReplies=showRepliesItem.checked; lp.showReplies=showRepliesItem.checked;
lp.save(); lp.save();
@@ -186,6 +188,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
s.preferences.postingDefaultLanguage=newPostLanguage.language.getLanguage(); s.preferences.postingDefaultLanguage=newPostLanguage.language.getLanguage();
s.savePreferencesLater(); s.savePreferencesLater();
} }
if(restartPlease) getActivity().recreate();
} }
@Override @Override

View File

@@ -39,7 +39,7 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
@Override @Override
protected void doLoadData(int offset, int count){} protected void doLoadData(int offset, int count){}
private void onTestEmailConfirmClick(){ private void onTestEmailConfirmClick(ListItem<?> item){
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID); AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false; sess.activated=false;
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis()); sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
@@ -49,18 +49,18 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args); Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
} }
private void onForceSelfUpdateClick(){ private void onForceSelfUpdateClick(ListItem<?> item){
GithubSelfUpdater.forceUpdate=true; GithubSelfUpdater.forceUpdate=true;
GithubSelfUpdater.getInstance().maybeCheckForUpdates(); GithubSelfUpdater.getInstance().maybeCheckForUpdates();
restartUI(); restartUI();
} }
private void onResetUpdaterClick(){ private void onResetUpdaterClick(ListItem<?> item){
GithubSelfUpdater.getInstance().reset(); GithubSelfUpdater.getInstance().reset();
restartUI(); restartUI();
} }
private void onResetDiscoverBannersClick(){ private void onResetDiscoverBannersClick(ListItem<?> item){
DiscoverInfoBannerHelper.reset(); DiscoverInfoBannerHelper.reset();
restartUI(); restartUI();
} }

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.settings; package org.joinmastodon.android.fragments.settings;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.os.Build; import android.os.Build;
@@ -28,6 +29,8 @@ import org.joinmastodon.android.ui.views.TextInputFrameLayout;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
@@ -38,7 +41,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> revealCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem; private CheckableListItem<Void> revealCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
// MEGALODON // MEGALODON
private CheckableListItem<Void> trueBlackModeItem, marqueeItem, disableSwipeItem, reduceMotionItem, altIndicatorItem, noAltIndicatorItem, collapsePostsItem, spectatorModeItem, hideFabItem, translateOpenedItem, disablePillItem, showNavigationLabelsItem, likeIconItem; private CheckableListItem<Void> trueBlackModeItem, marqueeItem, disableSwipeItem, reduceMotionItem, altIndicatorItem, noAltIndicatorItem, collapsePostsItem, spectatorModeItem, hideFabItem, translateOpenedItem, disablePillItem, showNavigationLabelsItem, likeIconItem, underlinedLinksItem;
private ListItem<Void> colorItem, publishTextItem, autoRevealCWsItem; private ListItem<Void> colorItem, publishTextItem, autoRevealCWsItem;
private CheckableListItem<Void> pronounsInUserListingsItem, pronounsInTimelinesItem, pronounsInThreadsItem; private CheckableListItem<Void> pronounsInUserListingsItem, pronounsInTimelinesItem, pronounsInThreadsItem;
@@ -52,29 +55,30 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
lp=s.getLocalPreferences(); lp=s.getLocalPreferences();
onDataLoaded(List.of( onDataLoaded(List.of(
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_fluent_weather_moon_24_regular, this::onAppearanceClick), themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_fluent_weather_moon_24_regular, this::onAppearanceClick),
colorItem=new ListItem<>(R.string.sk_settings_color_palette, getColorPaletteValue(), R.drawable.ic_fluent_color_24_regular, this::onColorClick), colorItem=new ListItem<>(getString(R.string.sk_settings_color_palette), getColorPaletteValue(), R.drawable.ic_fluent_color_24_regular, this::onColorClick),
trueBlackModeItem=new CheckableListItem<>(R.string.sk_settings_true_black, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.trueBlackTheme, R.drawable.ic_fluent_dark_theme_24_regular, this::onTrueBlackModeClick, true), trueBlackModeItem=new CheckableListItem<>(R.string.sk_settings_true_black, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.trueBlackTheme, R.drawable.ic_fluent_dark_theme_24_regular, i->onTrueBlackModeClick(), true),
publishTextItem=new ListItem<>(getString(R.string.sk_settings_publish_button_text), getPublishButtonText(), R.drawable.ic_fluent_send_24_regular, this::onPublishTextClick), publishTextItem=new ListItem<>(getString(R.string.sk_settings_publish_button_text), getPublishButtonText(), R.drawable.ic_fluent_send_24_regular, this::onPublishTextClick),
autoRevealCWsItem=new ListItem<>(R.string.sk_settings_auto_reveal_equal_spoilers, getAutoRevealSpoilersText(), R.drawable.ic_fluent_eye_24_regular, this::onAutoRevealSpoilersClick), autoRevealCWsItem=new ListItem<>(R.string.sk_settings_auto_reveal_equal_spoilers, getAutoRevealSpoilersText(), R.drawable.ic_fluent_eye_24_regular, this::onAutoRevealSpoilersClick),
revealCWsItem=new CheckableListItem<>(R.string.sk_settings_always_reveal_content_warnings, 0, CheckableListItem.Style.SWITCH, lp.revealCWs, R.drawable.ic_fluent_chat_warning_24_regular, ()->toggleCheckableItem(revealCWsItem)), revealCWsItem=new CheckableListItem<>(R.string.sk_settings_always_reveal_content_warnings, 0, CheckableListItem.Style.SWITCH, lp.revealCWs, R.drawable.ic_fluent_chat_warning_24_regular, i->toggleCheckableItem(revealCWsItem)),
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_fluent_flag_24_regular, ()->toggleCheckableItem(hideSensitiveMediaItem)), hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_fluent_flag_24_regular, i->toggleCheckableItem(hideSensitiveMediaItem)),
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_fluent_number_row_24_regular, ()->toggleCheckableItem(interactionCountsItem)), interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_fluent_number_row_24_regular, i->toggleCheckableItem(interactionCountsItem)),
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_fluent_emoji_24_regular, ()->toggleCheckableItem(emojiInNamesItem)), emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_fluent_emoji_24_regular, i->toggleCheckableItem(emojiInNamesItem)),
marqueeItem=new CheckableListItem<>(R.string.sk_settings_enable_marquee, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.toolbarMarquee, R.drawable.ic_fluent_text_more_24_regular, ()->toggleCheckableItem(marqueeItem)), marqueeItem=new CheckableListItem<>(R.string.sk_settings_enable_marquee, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.toolbarMarquee, R.drawable.ic_fluent_text_more_24_regular, i->toggleCheckableItem(marqueeItem)),
reduceMotionItem=new CheckableListItem<>(R.string.sk_settings_reduce_motion, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.reduceMotion, R.drawable.ic_fluent_star_emphasis_24_regular, ()->toggleCheckableItem(reduceMotionItem)), reduceMotionItem=new CheckableListItem<>(R.string.sk_settings_reduce_motion, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.reduceMotion, R.drawable.ic_fluent_star_emphasis_24_regular, i->toggleCheckableItem(reduceMotionItem)),
disableSwipeItem=new CheckableListItem<>(R.string.sk_settings_tabs_disable_swipe, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableSwipe, R.drawable.ic_fluent_swipe_right_24_regular, ()->toggleCheckableItem(disableSwipeItem)), disableSwipeItem=new CheckableListItem<>(R.string.sk_settings_tabs_disable_swipe, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableSwipe, R.drawable.ic_fluent_swipe_right_24_regular, i->toggleCheckableItem(disableSwipeItem)),
altIndicatorItem=new CheckableListItem<>(R.string.sk_settings_show_alt_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showAltIndicator, R.drawable.ic_fluent_scan_text_24_regular, ()->toggleCheckableItem(altIndicatorItem)), altIndicatorItem=new CheckableListItem<>(R.string.sk_settings_show_alt_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showAltIndicator, R.drawable.ic_fluent_scan_text_24_regular, i->toggleCheckableItem(altIndicatorItem)),
noAltIndicatorItem=new CheckableListItem<>(R.string.sk_settings_show_no_alt_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNoAltIndicator, R.drawable.ic_fluent_important_24_regular, ()->toggleCheckableItem(noAltIndicatorItem)), noAltIndicatorItem=new CheckableListItem<>(R.string.sk_settings_show_no_alt_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNoAltIndicator, R.drawable.ic_fluent_important_24_regular, i->toggleCheckableItem(noAltIndicatorItem)),
collapsePostsItem=new CheckableListItem<>(R.string.sk_settings_collapse_long_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.collapseLongPosts, R.drawable.ic_fluent_chevron_down_24_regular, ()->toggleCheckableItem(collapsePostsItem)), collapsePostsItem=new CheckableListItem<>(R.string.sk_settings_collapse_long_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.collapseLongPosts, R.drawable.ic_fluent_chevron_down_24_regular, i->toggleCheckableItem(collapsePostsItem)),
spectatorModeItem=new CheckableListItem<>(R.string.sk_settings_hide_interaction, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.spectatorMode, R.drawable.ic_fluent_star_off_24_regular, ()->toggleCheckableItem(spectatorModeItem)), spectatorModeItem=new CheckableListItem<>(R.string.sk_settings_hide_interaction, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.spectatorMode, R.drawable.ic_fluent_star_off_24_regular, i->toggleCheckableItem(spectatorModeItem)),
hideFabItem=new CheckableListItem<>(R.string.sk_settings_hide_fab, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.autoHideFab, R.drawable.ic_fluent_edit_24_regular, ()->toggleCheckableItem(hideFabItem)), hideFabItem=new CheckableListItem<>(R.string.sk_settings_hide_fab, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.autoHideFab, R.drawable.ic_fluent_edit_24_regular, i->toggleCheckableItem(hideFabItem)),
translateOpenedItem=new CheckableListItem<>(R.string.sk_settings_translate_only_opened, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.translateButtonOpenedOnly, R.drawable.ic_fluent_translate_24_regular, ()->toggleCheckableItem(translateOpenedItem)), translateOpenedItem=new CheckableListItem<>(R.string.sk_settings_translate_only_opened, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.translateButtonOpenedOnly, R.drawable.ic_fluent_translate_24_regular, i->toggleCheckableItem(translateOpenedItem)),
disablePillItem=new CheckableListItem<>(R.string.sk_disable_pill_shaped_active_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableM3PillActiveIndicator, R.drawable.ic_fluent_pill_24_regular, ()->toggleCheckableItem(disablePillItem)), likeIconItem=new CheckableListItem<>(R.string.sk_settings_like_icon, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.likeIcon, R.drawable.ic_fluent_heart_24_regular, i->toggleCheckableItem(likeIconItem)),
likeIconItem=new CheckableListItem<>(R.string.sk_settings_like_icon, 0, CheckableListItem.Style.SWITCH, lp.likeIcon, R.drawable.ic_fluent_heart_24_regular, ()->toggleCheckableItem(likeIconItem)), underlinedLinksItem=new CheckableListItem<>(R.string.sk_settings_underlined_links, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.underlinedLinks, R.drawable.ic_fluent_text_underline_24_regular, i->toggleCheckableItem(underlinedLinksItem)),
showNavigationLabelsItem=new CheckableListItem<>(R.string.sk_settings_show_labels_in_navigation_bar, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNavigationLabels, R.drawable.ic_fluent_tag_24_regular, ()->toggleCheckableItem(showNavigationLabelsItem), true), disablePillItem=new CheckableListItem<>(R.string.sk_disable_pill_shaped_active_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableM3PillActiveIndicator, R.drawable.ic_fluent_pill_24_regular, i->toggleCheckableItem(disablePillItem)),
pronounsInTimelinesItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_timelines, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInTimelines, 0, ()->toggleCheckableItem(pronounsInTimelinesItem)), showNavigationLabelsItem=new CheckableListItem<>(R.string.sk_settings_show_labels_in_navigation_bar, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNavigationLabels, R.drawable.ic_fluent_tag_24_regular, i->toggleCheckableItem(showNavigationLabelsItem), true),
pronounsInThreadsItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_threads, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInThreads, 0, ()->toggleCheckableItem(pronounsInThreadsItem)), pronounsInTimelinesItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_timelines, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInTimelines, 0, i->toggleCheckableItem(pronounsInTimelinesItem)),
pronounsInUserListingsItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_user_listings, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInUserListings, 0, ()->toggleCheckableItem(pronounsInUserListingsItem)) pronounsInThreadsItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_threads, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInThreads, 0, i->toggleCheckableItem(pronounsInThreadsItem)),
pronounsInUserListingsItem=new CheckableListItem<>(R.string.sk_settings_display_pronouns_in_user_listings, 0, CheckableListItem.Style.CHECKBOX, GlobalUserPreferences.displayPronounsInUserListings, 0, i->toggleCheckableItem(pronounsInUserListingsItem))
)); ));
trueBlackModeItem.checkedChangeListener=checked->onTrueBlackModeClick(); trueBlackModeItem.checkedChangeListener=checked->onTrueBlackModeClick();
} }
@@ -96,16 +100,14 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
protected void onHidden(){ protected void onHidden(){
super.onHidden(); super.onHidden();
boolean restartPlease= boolean restartPlease=GlobalUserPreferences.disableM3PillActiveIndicator!=disablePillItem.checked
GlobalUserPreferences.disableM3PillActiveIndicator!=disablePillItem.checked || || GlobalUserPreferences.showNavigationLabels!=showNavigationLabelsItem.checked
GlobalUserPreferences.showNavigationLabels!=showNavigationLabelsItem.checked || || GlobalUserPreferences.likeIcon!=likeIconItem.checked;
lp.likeIcon!=likeIconItem.checked;
lp.revealCWs=revealCWsItem.checked; lp.revealCWs=revealCWsItem.checked;
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked; lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
lp.showInteractionCounts=interactionCountsItem.checked; lp.showInteractionCounts=interactionCountsItem.checked;
lp.customEmojiInNames=emojiInNamesItem.checked; lp.customEmojiInNames=emojiInNamesItem.checked;
lp.likeIcon=likeIconItem.checked;
lp.save(); lp.save();
GlobalUserPreferences.toolbarMarquee=marqueeItem.checked; GlobalUserPreferences.toolbarMarquee=marqueeItem.checked;
GlobalUserPreferences.reduceMotion=reduceMotionItem.checked; GlobalUserPreferences.reduceMotion=reduceMotionItem.checked;
@@ -116,6 +118,8 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
GlobalUserPreferences.spectatorMode=spectatorModeItem.checked; GlobalUserPreferences.spectatorMode=spectatorModeItem.checked;
GlobalUserPreferences.autoHideFab=hideFabItem.checked; GlobalUserPreferences.autoHideFab=hideFabItem.checked;
GlobalUserPreferences.translateButtonOpenedOnly=translateOpenedItem.checked; GlobalUserPreferences.translateButtonOpenedOnly=translateOpenedItem.checked;
GlobalUserPreferences.likeIcon=likeIconItem.checked;
GlobalUserPreferences.underlinedLinks=underlinedLinksItem.checked;
GlobalUserPreferences.disableM3PillActiveIndicator=disablePillItem.checked; GlobalUserPreferences.disableM3PillActiveIndicator=disablePillItem.checked;
GlobalUserPreferences.showNavigationLabels=showNavigationLabelsItem.checked; GlobalUserPreferences.showNavigationLabels=showNavigationLabelsItem.checked;
GlobalUserPreferences.displayPronounsInTimelines=pronounsInTimelinesItem.checked; GlobalUserPreferences.displayPronounsInTimelines=pronounsInTimelinesItem.checked;
@@ -134,17 +138,11 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
}; };
} }
private @StringRes int getColorPaletteValue(){ private String getColorPaletteValue(){
return switch(AccountSessionManager.get(accountID).getLocalPreferences().color){ ColorPreference color=AccountSessionManager.get(accountID).getLocalPreferences().color;
case MATERIAL3 -> R.string.sk_color_palette_material3; return color==null
case PINK -> R.string.sk_color_palette_pink; ? getString(R.string.sk_settings_color_palette_default, getString(GlobalUserPreferences.color.getName()))
case PURPLE -> R.string.sk_color_palette_purple; : getString(color.getName());
case GREEN -> R.string.sk_color_palette_green;
case BLUE -> R.string.sk_color_palette_blue;
case BROWN -> R.string.sk_color_palette_brown;
case RED -> R.string.sk_color_palette_red;
case YELLOW -> R.string.sk_color_palette_yellow;
};
} }
private String getPublishButtonText() { private String getPublishButtonText() {
@@ -168,7 +166,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
maybeApplyNewThemeRightNow(null, null, prev); maybeApplyNewThemeRightNow(null, null, prev);
} }
private void onAppearanceClick(){ private void onAppearanceClick(ListItem<?> item_){
int selected=switch(GlobalUserPreferences.theme){ int selected=switch(GlobalUserPreferences.theme){
case LIGHT -> 0; case LIGHT -> 0;
case DARK -> 1; case DARK -> 1;
@@ -199,30 +197,44 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
.show(); .show();
} }
private void onColorClick(){ private void onColorClick(ListItem<?> item_){
int selected=lp.color.ordinal(); boolean multiple=AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
int indexOffset=multiple ? 1 : 0;
int selected=lp.color==null ? 0 : lp.color.ordinal() + indexOffset;
int[] newSelected={selected}; int[] newSelected={selected};
String[] names=Arrays.stream(ColorPreference.values()).map(ColorPreference::getName).map(this::getString).toArray(String[]::new); List<String> items=Arrays.stream(ColorPreference.values()).map(ColorPreference::getName).map(this::getString).collect(Collectors.toList());
new M3AlertDialogBuilder(getActivity()) if(multiple)
items.add(0, getString(R.string.sk_settings_color_palette_default, items.get(GlobalUserPreferences.color.ordinal())));
Consumer<Boolean> save=(asDefault)->{
boolean defaultSelected=multiple && newSelected[0]==0;
ColorPreference pref=defaultSelected ? null : ColorPreference.values()[newSelected[0]-indexOffset];
if(pref!=lp.color){
ColorPreference prev=lp.color;
lp.color=asDefault ? null : pref;
lp.save();
if((asDefault || !multiple) && pref!=null){
GlobalUserPreferences.color=pref;
GlobalUserPreferences.save();
}
colorItem.subtitle=getColorPaletteValue();
rebindItem(colorItem);
if(prev==null && pref!=null) restartActivityToApplyNewTheme();
else maybeApplyNewThemeRightNow(null, prev, null);
}
};
AlertDialog.Builder alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_color_palette) .setTitle(R.string.sk_settings_color_palette)
.setSingleChoiceItems(names, .setSingleChoiceItems(items.stream().toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item) selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{ .setPositiveButton(R.string.ok, (dlg, item)->save.accept(false))
ColorPreference pref=ColorPreference.values()[newSelected[0]]; .setNegativeButton(R.string.cancel, null);
if(pref!=lp.color){ if(multiple) alert.setNeutralButton(R.string.sk_set_as_default, (dlg, item)->save.accept(true));
ColorPreference prev=lp.color; alert.show();
lp.color=pref;
GlobalUserPreferences.save();
colorItem.subtitleRes=getColorPaletteValue();
rebindItem(colorItem);
maybeApplyNewThemeRightNow(null, prev, null);
}
})
.setNegativeButton(R.string.cancel, null)
.show();
} }
private void onPublishTextClick(){ private void onPublishTextClick(ListItem<?> item_){
TextInputFrameLayout input = new TextInputFrameLayout( TextInputFrameLayout input = new TextInputFrameLayout(
getContext(), getContext(),
getString(R.string.publish), getString(R.string.publish),
@@ -245,7 +257,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
.show(); .show();
} }
private void onAutoRevealSpoilersClick(){ private void onAutoRevealSpoilersClick(ListItem<?> item_){
int selected=GlobalUserPreferences.autoRevealEqualSpoilers.ordinal(); int selected=GlobalUserPreferences.autoRevealEqualSpoilers.ordinal();
int[] newSelected={selected}; int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
@@ -264,14 +276,14 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prevTheme, ColorPreference prevColor, Boolean prevTrueBlack){ private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prevTheme, ColorPreference prevColor, Boolean prevTrueBlack){
if(prevTheme==null) prevTheme=GlobalUserPreferences.theme; if(prevTheme==null) prevTheme=GlobalUserPreferences.theme;
if(prevTrueBlack==null) prevTrueBlack=GlobalUserPreferences.trueBlackTheme; if(prevTrueBlack==null) prevTrueBlack=GlobalUserPreferences.trueBlackTheme;
if(prevColor==null) prevColor=lp.color; if(prevColor==null) prevColor=lp.getCurrentColor();
boolean isCurrentDark=prevTheme==GlobalUserPreferences.ThemePreference.DARK || boolean isCurrentDark=prevTheme==GlobalUserPreferences.ThemePreference.DARK ||
(prevTheme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive()); (prevTheme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK || boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive()); (GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
boolean isNewBlack=GlobalUserPreferences.trueBlackTheme; boolean isNewBlack=GlobalUserPreferences.trueBlackTheme;
if(isCurrentDark!=isNewDark || prevColor!=lp.color || (isNewDark && prevTrueBlack!=isNewBlack)){ if(isCurrentDark!=isNewDark || prevColor!=lp.getCurrentColor() || (isNewDark && prevTrueBlack!=isNewBlack)){
restartActivityToApplyNewTheme(); restartActivityToApplyNewTheme();
} }
} }

View File

@@ -55,7 +55,7 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(super.getAdapter()); adapter.addAdapter(super.getAdapter());
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList( adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick) new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_fluent_add_24_regular, this::onAddFilterClick)
))); )));
return adapter; return adapter;
} }
@@ -67,16 +67,14 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
Nav.go(getActivity(), EditFilterFragment.class, args); Nav.go(getActivity(), EditFilterFragment.class, args);
} }
private void onAddFilterClick(){ private void onAddFilterClick(ListItem<?> item){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.go(getActivity(), EditFilterFragment.class, args); Nav.go(getActivity(), EditFilterFragment.class, args);
} }
private ListItem<Filter> makeListItem(Filter f){ private ListItem<Filter> makeListItem(Filter f){
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f); ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), this::onFilterClick, f);
item.onClick=()->onFilterClick(item);
item.isEnabled=true;
return item; return item;
} }

View File

@@ -19,6 +19,7 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
@@ -36,15 +37,15 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
lp=s.getLocalPreferences(); lp=s.getLocalPreferences();
onDataLoaded(List.of( onDataLoaded(List.of(
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_fluent_server_24_regular, this::onServerClick), new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_fluent_server_24_regular, this::onServerClick),
new ListItem<>(R.string.sk_settings_profile, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/profile")), new ListItem<>(R.string.sk_settings_profile, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/profile")),
new ListItem<>(R.string.sk_settings_posting, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/preferences/other")), new ListItem<>(R.string.sk_settings_posting, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/preferences/other")),
new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit"), 0, true), new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit"), 0, true),
contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, this::onContentTypeClick), contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, i->onContentTypeClick()),
defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true), defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true),
emojiReactionsItem=new CheckableListItem<>(R.string.sk_settings_emoji_reactions, R.string.sk_settings_emoji_reactions_explanation, CheckableListItem.Style.SWITCH, lp.emojiReactionsEnabled, R.drawable.ic_fluent_emoji_laugh_24_regular, this::onEmojiReactionsClick), emojiReactionsItem=new CheckableListItem<>(R.string.sk_settings_emoji_reactions, R.string.sk_settings_emoji_reactions_explanation, CheckableListItem.Style.SWITCH, lp.emojiReactionsEnabled, R.drawable.ic_fluent_emoji_laugh_24_regular, i->onEmojiReactionsClick()),
showEmojiReactionsItem=new ListItem<>(R.string.sk_settings_show_emoji_reactions, getShowEmojiReactionsString(), R.drawable.ic_fluent_emoji_24_regular, this::onShowEmojiReactionsClick, 0, true), showEmojiReactionsItem=new ListItem<>(R.string.sk_settings_show_emoji_reactions, getShowEmojiReactionsString(), R.drawable.ic_fluent_emoji_24_regular, this::onShowEmojiReactionsClick, 0, true),
localOnlyItem=new CheckableListItem<>(R.string.sk_settings_support_local_only, R.string.sk_settings_local_only_explanation, CheckableListItem.Style.SWITCH, lp.localOnlySupported, R.drawable.ic_fluent_eye_24_regular, this::onLocalOnlyClick), localOnlyItem=new CheckableListItem<>(R.string.sk_settings_support_local_only, R.string.sk_settings_local_only_explanation, CheckableListItem.Style.SWITCH, lp.localOnlySupported, R.drawable.ic_fluent_eye_24_regular, i->onLocalOnlyClick()),
glitchModeItem=new CheckableListItem<>(R.string.sk_settings_glitch_instance, R.string.sk_settings_glitch_mode_explanation, CheckableListItem.Style.SWITCH, lp.glitchInstance, R.drawable.ic_fluent_eye_24_filled, ()->toggleCheckableItem(glitchModeItem)) glitchModeItem=new CheckableListItem<>(R.string.sk_settings_glitch_instance, R.string.sk_settings_glitch_mode_explanation, CheckableListItem.Style.SWITCH, lp.glitchInstance, R.drawable.ic_fluent_eye_24_filled, i->toggleCheckableItem(glitchModeItem))
)); ));
contentTypesItem.checkedChangeListener=checked->onContentTypeClick(); contentTypesItem.checkedChangeListener=checked->onContentTypeClick();
defaultContentTypeItem.isEnabled=contentTypesItem.checked; defaultContentTypeItem.isEnabled=contentTypesItem.checked;
@@ -68,7 +69,7 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
E.post(new StatusDisplaySettingsChangedEvent(accountID)); E.post(new StatusDisplaySettingsChangedEvent(accountID));
} }
private void onServerClick(){ private void onServerClick(ListItem<?> item){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
Nav.go(getActivity(), SettingsServerFragment.class, args); Nav.go(getActivity(), SettingsServerFragment.class, args);
@@ -87,23 +88,23 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
defaultContentTypeItem.subtitleRes=lp.defaultContentType.getName(); defaultContentTypeItem.subtitleRes=lp.defaultContentType.getName();
} }
private void onDefaultContentTypeClick(){ private void onDefaultContentTypeClick(ListItem<?> item_){
int selected=lp.defaultContentType.ordinal(); List<ContentType> supportedContentTypes=Arrays.stream(ContentType.values())
int[] newSelected={selected};
ContentType[] supportedContentTypes=Arrays.stream(ContentType.values())
.filter(t->t.supportedByInstance(getInstance().orElse(null))) .filter(t->t.supportedByInstance(getInstance().orElse(null)))
.toArray(ContentType[]::new); .collect(Collectors.toList());
String[] names=Arrays.stream(supportedContentTypes) int selected=supportedContentTypes.indexOf(lp.defaultContentType);
int[] newSelected={selected};
String[] names=supportedContentTypes.stream()
.map(ContentType::getName) .map(ContentType::getName)
.map(this::getString) .map(this::getString)
.toArray(String[]::new); .toArray(String[]::new);
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme) .setTitle(R.string.sk_settings_default_content_type)
.setSingleChoiceItems(names, .setSingleChoiceItems(names,
selected, (dlg, item)->newSelected[0]=item) selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{ .setPositiveButton(R.string.ok, (dlg, item)->{
ContentType type=supportedContentTypes[newSelected[0]]; ContentType type=supportedContentTypes.get(newSelected[0]);
lp.defaultContentType=type; lp.defaultContentType=type;
defaultContentTypeItem.subtitleRes=type.getName(); defaultContentTypeItem.subtitleRes=type.getName();
rebindItem(defaultContentTypeItem); rebindItem(defaultContentTypeItem);
@@ -112,7 +113,7 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
.show(); .show();
} }
private void onShowEmojiReactionsClick(){ private void onShowEmojiReactionsClick(ListItem<?> item_){
int selected=lp.showEmojiReactions.ordinal(); int selected=lp.showEmojiReactions.ordinal();
int[] newSelected={selected}; int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())

View File

@@ -18,6 +18,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent; import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.viewmodel.ListItem; import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -49,26 +50,27 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
account = AccountSessionManager.get(accountID); account=AccountSessionManager.get(accountID);
setTitle(R.string.settings); setTitle(R.string.settings);
setSubtitle(account.getFullUsername()); setSubtitle(account.getFullUsername());
onDataLoaded(List.of( onDataLoaded(List.of(
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_fluent_settings_24_regular, this::onBehaviorClick), new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_fluent_settings_24_regular, this::onBehaviorClick),
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_fluent_color_24_regular, this::onDisplayClick), new ListItem<>(R.string.settings_display, 0, R.drawable.ic_fluent_color_24_regular, this::onDisplayClick),
new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick), new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_fluent_shield_24_regular, this::onPrivacyClick),
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_fluent_alert_24_regular, this::onNotificationsClick), new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_fluent_alert_24_regular, this::onNotificationsClick),
new ListItem<>(R.string.sk_settings_instance, 0, R.drawable.ic_fluent_server_24_regular, this::onInstanceClick), new ListItem<>(R.string.sk_settings_instance, 0, R.drawable.ic_fluent_server_24_regular, this::onInstanceClick),
new ListItem<>(getString(R.string.about_app, getString(R.string.sk_app_name)), null, R.drawable.ic_fluent_info_24_regular, this::onAboutClick, null, 0, true), new ListItem<>(getString(R.string.about_app, getString(R.string.sk_app_name)), null, R.drawable.ic_fluent_info_24_regular, this::onAboutClick, null, 0, true),
new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_fluent_person_swap_24_regular, this::onManageAccountsClick),
new ListItem<>(R.string.log_out, 0, R.drawable.ic_fluent_sign_out_24_regular, this::onLogOutClick, R.attr.colorM3Error, false) new ListItem<>(R.string.log_out, 0, R.drawable.ic_fluent_sign_out_24_regular, this::onLogOutClick, R.attr.colorM3Error, false)
)); ));
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(account.domain); Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if (!instance.isAkkoma()) if(!instance.isAkkoma()){
data.add(2, new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_fluent_filter_24_regular, this::onFiltersClick)); data.add(3, new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_fluent_filter_24_regular, this::onFiltersClick));
}
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){ if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_fluent_wrench_screwdriver_24_regular, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true)); data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_fluent_wrench_screwdriver_24_regular, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
} }
AccountSession session=AccountSessionManager.get(accountID); AccountSession session=AccountSessionManager.get(accountID);
@@ -129,35 +131,39 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
return args; return args;
} }
private void onBehaviorClick(){ private void onBehaviorClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
} }
private void onDisplayClick(){ private void onDisplayClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
} }
private void onPrivacyClick(){ private void onPrivacyClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
} }
private void onFiltersClick(){ private void onFiltersClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
} }
private void onNotificationsClick(){ private void onNotificationsClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
} }
private void onInstanceClick(){ private void onInstanceClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsInstanceFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsInstanceFragment.class, makeFragmentArgs());
} }
private void onAboutClick(){ private void onAboutClick(ListItem<?> item_){
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs()); Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
} }
private void onLogOutClick(){ private void onManageAccountsClick(ListItem<?> item){
new AccountSwitcherSheet(getActivity(), null).show();
}
private void onLogOutClick(ListItem<?> item_){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername())) .setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))

View File

@@ -27,7 +27,6 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter; import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.unifiedpush.android.connector.RegistrationDialogContent;
import org.unifiedpush.android.connector.UnifiedPush; import org.unifiedpush.android.connector.UnifiedPush;
import java.time.Instant; import java.time.Instant;
@@ -73,21 +72,21 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
useUnifiedPush=!getDistributor(getContext()).isEmpty(); useUnifiedPush=!getDistributor(getContext()).isEmpty();
onDataLoaded(List.of( onDataLoaded(List.of(
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, ()->onPauseNotificationsClick(false)), pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, i->onPauseNotificationsClick(false)),
policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_fluent_people_24_regular, this::onNotificationsPolicyClick, 0, true), policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_fluent_people_24_regular, this::onNotificationsPolicyClick, 0, true),
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, R.drawable.ic_fluent_mention_24_regular, ()->toggleCheckableItem(mentionsItem)), mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, R.drawable.ic_fluent_mention_24_regular, i->toggleCheckableItem(mentionsItem)),
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(boostsItem)), boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, i->toggleCheckableItem(boostsItem)),
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, R.drawable.ic_fluent_star_24_regular, ()->toggleCheckableItem(favoritesItem)), favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, R.drawable.ic_fluent_star_24_regular, i->toggleCheckableItem(favoritesItem)),
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, R.drawable.ic_fluent_person_add_24_regular, ()->toggleCheckableItem(followersItem)), followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, R.drawable.ic_fluent_person_add_24_regular, i->toggleCheckableItem(followersItem)),
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, R.drawable.ic_fluent_poll_24_regular, ()->toggleCheckableItem(pollsItem)), pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, R.drawable.ic_fluent_poll_24_regular, i->toggleCheckableItem(pollsItem)),
updateItem=new CheckableListItem<>(R.string.sk_notification_type_update, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.update, R.drawable.ic_fluent_history_24_regular, ()->toggleCheckableItem(updateItem)), updateItem=new CheckableListItem<>(R.string.sk_notification_type_update, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.update, R.drawable.ic_fluent_history_24_regular, i->toggleCheckableItem(updateItem)),
postsItem=new CheckableListItem<>(R.string.sk_notification_type_posts, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.status, R.drawable.ic_fluent_chat_24_regular, ()->toggleCheckableItem(postsItem), true), postsItem=new CheckableListItem<>(R.string.sk_notification_type_posts, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.status, R.drawable.ic_fluent_chat_24_regular, i->toggleCheckableItem(postsItem), true),
uniformIconItem=new CheckableListItem<>(R.string.sk_settings_uniform_icon_for_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.uniformNotificationIcon, R.drawable.ic_ntf_logo, ()->toggleCheckableItem(uniformIconItem)), uniformIconItem=new CheckableListItem<>(R.string.sk_settings_uniform_icon_for_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.uniformNotificationIcon, R.drawable.ic_ntf_logo, i->toggleCheckableItem(uniformIconItem)),
deleteItem=new CheckableListItem<>(R.string.sk_settings_enable_delete_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enableDeleteNotifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, ()->toggleCheckableItem(deleteItem)), deleteItem=new CheckableListItem<>(R.string.sk_settings_enable_delete_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enableDeleteNotifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, i->toggleCheckableItem(deleteItem)),
onlyLatestItem=new CheckableListItem<>(R.string.sk_settings_single_notification, 0, CheckableListItem.Style.SWITCH, lp.keepOnlyLatestNotification, R.drawable.ic_fluent_convert_range_24_regular, ()->toggleCheckableItem(onlyLatestItem), true), onlyLatestItem=new CheckableListItem<>(R.string.sk_settings_single_notification, 0, CheckableListItem.Style.SWITCH, lp.keepOnlyLatestNotification, R.drawable.ic_fluent_convert_range_24_regular, i->toggleCheckableItem(onlyLatestItem), true),
unifiedPushItem=new CheckableListItem<>(R.string.sk_settings_unifiedpush, 0, CheckableListItem.Style.SWITCH, useUnifiedPush, R.drawable.ic_fluent_alert_arrow_up_24_regular, this::onUnifiedPush, true) unifiedPushItem=new CheckableListItem<>(R.string.sk_settings_unifiedpush, 0, CheckableListItem.Style.SWITCH, useUnifiedPush, R.drawable.ic_fluent_alert_arrow_up_24_regular, i->onUnifiedPushClick(), true)
)); ));
//only enable when distributors, who can receive notifications, are available //only enable when distributors, who can receive notifications, are available
@@ -98,7 +97,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem, updateItem, postsItem); typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem, updateItem, postsItem);
pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true); pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
unifiedPushItem.checkedChangeListener=checked->onUnifiedPush(); unifiedPushItem.checkedChangeListener=checked->onUnifiedPushClick();
updatePolicyItem(null); updatePolicyItem(null);
updatePauseItem(); updatePauseItem();
} }
@@ -254,7 +253,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} }
private void onNotificationsPolicyClick(){ private void onNotificationsPolicyClick(ListItem<?> item_){
String[] items=Stream.of( String[] items=Stream.of(
R.string.notifications_policy_anyone, R.string.notifications_policy_anyone,
R.string.notifications_policy_followed, R.string.notifications_policy_followed,
@@ -328,20 +327,22 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
} }
} }
private void onUnifiedPush(){ private void onUnifiedPushClick(){
if(getDistributor(getContext()).isEmpty()){ if(getDistributor(getContext()).isEmpty()){
List<String> distributors = UnifiedPush.getDistributors(getContext(), new ArrayList<>()); List<String> distributors = UnifiedPush.getDistributors(getContext(), new ArrayList<>());
showUnifiedPushRegisterDialog(distributors); showUnifiedPushRegisterDialog(distributors);
return; return;
} }
UnifiedPush.unregisterApp( for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()) {
getContext(), UnifiedPush.unregisterApp(
accountID getContext(),
); accountSession.getID()
);
//re-register to fcm //re-register to fcm
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().registerAccountForPush(getPushSubscription()); accountSession.getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
}
unifiedPushItem.toggle(); unifiedPushItem.toggle();
rebindItem(unifiedPushItem); rebindItem(unifiedPushItem);
} }
@@ -351,12 +352,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
(dialog, which)->{ (dialog, which)->{
String userDistrib = distributors.get(which); String userDistrib = distributors.get(which);
UnifiedPush.saveDistributor(getContext(), userDistrib); UnifiedPush.saveDistributor(getContext(), userDistrib);
UnifiedPush.registerApp( for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
getContext(), UnifiedPush.registerApp(
accountID, getContext(),
new ArrayList<>(), accountSession.getID(),
getContext().getPackageName() new ArrayList<>(),
); getContext().getPackageName()
);
}
unifiedPushItem.toggle(); unifiedPushItem.toggle();
rebindItem(unifiedPushItem); rebindItem(unifiedPushItem);
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show(); }).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();

View File

@@ -2,40 +2,98 @@ package org.joinmastodon.android.fragments.settings;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.StringRes;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.viewmodel.CheckableListItem; import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> discoverableItem, indexableItem; private CheckableListItem<Void> discoverableItem, indexableItem, lockedItem;
private ListItem<Void> privacyItem;
private StatusPrivacy privacy=null;
private Instance instance;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setTitle(R.string.settings_privacy); setTitle(R.string.settings_privacy);
Account self=AccountSessionManager.get(accountID).self; AccountSession session=AccountSessionManager.get(accountID);
Account self=session.self;
instance=AccountSessionManager.getInstance().getInstanceInfo(session.domain);
privacy=self.source.privacy;
onDataLoaded(List.of( onDataLoaded(List.of(
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_24px, ()->toggleCheckableItem(discoverableItem)), privacyItem=new ListItem<>(R.string.sk_settings_default_visibility, getPrivacyString(privacy), R.drawable.ic_fluent_eye_24_regular, this::onPrivacyClick, 0, true),
indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_search_24px, ()->toggleCheckableItem(indexableItem)) lockedItem=new CheckableListItem<>(R.string.sk_settings_lock_account, 0, CheckableListItem.Style.SWITCH, self.locked, R.drawable.ic_fluent_person_available_24_regular, i->toggleCheckableItem(lockedItem))
)); ));
if(self.source.indexable==null)
indexableItem.isEnabled=false; if(!instance.isAkkoma()){
data.addAll(List.of(
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_fluent_thumb_like_dislike_24_regular, i->toggleCheckableItem(discoverableItem)),
indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_fluent_search_24_regular, i->toggleCheckableItem(indexableItem))
));
if(self.source.indexable==null)
indexableItem.isEnabled=false;
}
} }
@Override @Override
protected void doLoadData(int offset, int count){} protected void doLoadData(int offset, int count){}
private @StringRes int getPrivacyString(StatusPrivacy p){
if(p==null) return R.string.visibility_public;
return switch(p){
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
case LOCAL -> R.string.sk_local_only;
};
}
private void onPrivacyClick(ListItem<?> item_){
Account self=AccountSessionManager.get(accountID).self;
List<StatusPrivacy> options=new ArrayList<>(List.of(StatusPrivacy.PUBLIC, StatusPrivacy.UNLISTED, StatusPrivacy.PRIVATE, StatusPrivacy.DIRECT));
if(instance.isAkkoma()) options.add(StatusPrivacy.LOCAL);
int selected=options.indexOf(self.source.privacy);
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_default_visibility)
.setSingleChoiceItems(options.stream().map(this::getPrivacyString).map(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
privacy=options.get(newSelected[0]);
privacyItem.subtitleRes=getPrivacyString(privacy);
rebindItem(privacyItem);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@Override @Override
public void onPause(){ public void onPause(){
super.onPause(); super.onPause();
Account self=AccountSessionManager.get(accountID).self; AccountSession s=AccountSessionManager.get(accountID);
if(self.discoverable!=discoverableItem.checked || (self.source.indexable!=null && self.source.indexable!=indexableItem.checked)){ Account self=s.self;
self.discoverable=discoverableItem.checked; boolean savePlease=self.locked!=lockedItem.checked
self.source.indexable=indexableItem.checked; || self.source.privacy!=privacy
AccountSessionManager.get(accountID).savePreferencesLater(); || (discoverableItem!=null && self.discoverable!=discoverableItem.checked)
|| (indexableItem!=null && self.source.indexable!=null && self.source.indexable!=indexableItem.checked);
if(savePlease){
if(discoverableItem!=null) self.discoverable=discoverableItem.checked;
if(indexableItem!=null) self.source.indexable=indexableItem.checked;
self.locked=lockedItem.checked;
s.preferences.postingDefaultVisibility=privacy;
s.savePreferencesLater();
} }
} }
} }

View File

@@ -145,7 +145,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
if(!TextUtils.isEmpty(instance.email)){ if(!TextUtils.isEmpty(instance.email)){
needDivider=true; needDivider=true;
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout); SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_fluent_mail_24_regular, ()->{}); ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_fluent_mail_24_regular, i->{});
holder.bind(item); holder.bind(item);
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground)); holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
holder.itemView.setOnClickListener(v->openAdminEmail()); holder.itemView.setOnClickListener(v->openAdminEmail());

View File

@@ -6,8 +6,6 @@ import android.text.TextUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.parceler.Parcel; import org.parceler.Parcel;
import java.time.Instant; import java.time.Instant;
@@ -15,9 +13,6 @@ import java.time.LocalDate;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
/** /**
* Represents a user of Mastodon and their associated profile. * Represents a user of Mastodon and their associated profile.
*/ */
@@ -163,26 +158,26 @@ public class Account extends BaseModel implements Searchable{
if(fields!=null){ if(fields!=null){
for(AccountField f:fields) for(AccountField f:fields)
f.postprocess(); f.postprocess();
} else { }else{
fields = Collections.emptyList(); fields=Collections.emptyList();
} }
if(emojis!=null){ if(emojis!=null){
for(Emoji e:emojis) for(Emoji e:emojis)
e.postprocess(); e.postprocess();
} else { }else{
emojis = Collections.emptyList(); emojis=Collections.emptyList();
} }
if(moved!=null) if(moved!=null)
moved.postprocess(); moved.postprocess();
if(fqn == null) fqn = getFullyQualifiedName(); if(fqn==null) fqn=getFullyQualifiedName();
if(id == null) id = ""; if(id==null) id="";
if(username == null) username = ""; if(username==null) username="";
if(TextUtils.isEmpty(displayName)) if(TextUtils.isEmpty(displayName))
displayName = !TextUtils.isEmpty(username) ? username : ""; displayName=!TextUtils.isEmpty(username) ? username : "";
if(acct == null) acct = ""; if(acct==null) acct="";
if(url == null) url = ""; if(url==null) url="";
if(note == null) note = ""; if(note==null) note="";
if(avatar == null) avatar = ""; if(avatar==null) avatar="";
} }
public boolean isLocal(){ public boolean isLocal(){
@@ -212,6 +207,10 @@ public class Account extends BaseModel implements Searchable{
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL(); return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
} }
public String getDisplayName(){
return '\u2068'+displayName+'\u2069';
}
@Override @Override
public String toString(){ public String toString(){
return "Account{"+ return "Account{"+

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.model;
public class AkkomaTranslation extends BaseModel{
public String text;
public String detectedLanguage;
public Translation toTranslation() {
Translation translation=new Translation();
translation.content=text;
translation.detectedSourceLanguage=detectedLanguage;
translation.provider="Akkoma";
return translation;
}
}

View File

@@ -52,7 +52,7 @@ public class Announcement extends BaseModel implements DisplayItemsParent {
public Status toStatus() { public Status toStatus() {
Status s=Status.ofFake(id, content, publishedAt); Status s=Status.ofFake(id, content, publishedAt);
s.createdAt=startsAt != null ? startsAt : publishedAt; s.createdAt=startsAt != null ? startsAt : publishedAt;
s.reactions=reactions; s.reactions=reactions;
if(updatedAt != null) s.editedAt=updatedAt; if(updatedAt != null) s.editedAt=updatedAt;
return s; return s;

View File

@@ -161,11 +161,15 @@ public class Instance extends BaseModel{
case BUBBLE_TIMELINE -> pleromaFeatures case BUBBLE_TIMELINE -> pleromaFeatures
.map(f -> f.contains("bubble_timeline")) .map(f -> f.contains("bubble_timeline"))
.orElse(false); .orElse(false);
case MACHINE_TRANSLATION -> pleromaFeatures
.map(f -> f.contains("akkoma:machine_translation"))
.orElse(false);
}; };
} }
public enum Feature { public enum Feature {
BUBBLE_TIMELINE BUBBLE_TIMELINE,
MACHINE_TRANSLATION
} }
@Parcel @Parcel

View File

@@ -6,6 +6,7 @@ import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel; import org.parceler.Parcel;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -27,10 +28,10 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
@Override @Override
public void postprocess() throws ObjectValidationException { public void postprocess() throws ObjectValidationException {
super.postprocess(); super.postprocess();
if (mediaAttachments == null) mediaAttachments = List.of(); if(mediaAttachments==null) mediaAttachments=List.of();
for(Attachment a:mediaAttachments) for(Attachment a:mediaAttachments)
a.postprocess(); a.postprocess();
if (params != null) params.postprocess(); if(params!=null) params.postprocess();
} }
@Parcel @Parcel
@@ -53,7 +54,7 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
@Override @Override
public void postprocess() throws ObjectValidationException { public void postprocess() throws ObjectValidationException {
super.postprocess(); super.postprocess();
if (poll != null) poll.postprocess(); if(poll!=null) poll.postprocess();
} }
} }
@@ -67,25 +68,26 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public boolean hideTotals; public boolean hideTotals;
public Poll toPoll() { public Poll toPoll() {
Poll p = new Poll(); Poll p=new Poll();
p.voted = true; p.voted=true;
p.emojis = List.of(); p.emojis=List.of();
p.ownVotes = List.of(); p.ownVotes=List.of();
p.multiple = multiple; p.multiple=multiple;
p.options = options.stream().map(Option::new).collect(Collectors.toList()); p.options=options.stream().map(Option::new).collect(Collectors.toList());
p.expiresAt=Instant.now().plus(Integer.parseInt(expiresIn)+1, ChronoUnit.SECONDS);
return p; return p;
} }
} }
public Status toStatus() { public Status toStatus() {
Status s = Status.ofFake(id, params.text, scheduledAt); Status s=Status.ofFake(id, params.text, scheduledAt);
s.mediaAttachments = mediaAttachments; s.mediaAttachments=mediaAttachments;
s.inReplyToId = params.inReplyToId > 0 ? "" + params.inReplyToId : null; s.inReplyToId=params.inReplyToId>0 ? ""+params.inReplyToId : null;
s.spoilerText = params.spoilerText; s.spoilerText=params.spoilerText;
s.visibility = params.visibility; s.visibility=params.visibility;
s.language = params.language; s.language=params.language;
s.sensitive = params.sensitive; s.sensitive=params.sensitive;
if (params.poll != null) s.poll = params.poll.toPoll(); if(params.poll!=null) s.poll=params.poll.toPoll();
return s; return s;
} }
} }

View File

@@ -4,6 +4,7 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
import static org.joinmastodon.android.api.MastodonAPIController.gsonWithoutDeserializer; import static org.joinmastodon.android.api.MastodonAPIController.gsonWithoutDeserializer;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.api.RequiredField;
@@ -26,22 +27,13 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent; import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.utils.StatusTextEncoder; import org.joinmastodon.android.utils.StatusTextEncoder;
import org.parceler.Parcel;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Parcel @Parcel
@@ -100,10 +92,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public transient boolean spoilerRevealed; public transient boolean spoilerRevealed;
public transient boolean sensitiveRevealed; public transient boolean sensitiveRevealed;
public transient boolean textExpanded, textExpandable; public transient boolean textExpanded, textExpandable;
public transient boolean hasGapAfter; public transient String hasGapAfter;
private transient String strippedText; private transient String strippedText;
public transient TranslationState translationState=TranslationState.HIDDEN; public transient TranslationState translationState=TranslationState.HIDDEN;
public transient Translation translation; public transient Translation translation;
public transient boolean fromStatusCreated;
public transient boolean preview;
public Status(){} public Status(){}
@@ -131,6 +125,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
if(filtered!=null) if(filtered!=null)
for(FilterResult fr:filtered) for(FilterResult fr:filtered)
fr.postprocess(); fr.postprocess();
if(quote!=null)
quote.postprocess();
spoilerRevealed=!hasSpoiler(); spoilerRevealed=!hasSpoiler();
if(!spoilerRevealed) sensitive=true; if(!spoilerRevealed) sensitive=true;
@@ -203,6 +199,10 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
return reblog!=null ? reblog : this; return reblog!=null ? reblog : this;
} }
public String getContentStatusID(){
return getContentStatus().id;
}
public String getStrippedText(){ public String getStrippedText(){
if(strippedText==null) if(strippedText==null)
strippedText=HtmlParser.strip(content); strippedText=HtmlParser.strip(content);
@@ -221,16 +221,17 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48"); public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48");
public boolean isEligibleForTranslation(AccountSession session){ public boolean isEligibleForTranslation(AccountSession session){
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain); Instance instanceInfo=AccountSessionManager.getInstance().getInstanceInfo(session.domain);
boolean translateEnabled = instanceInfo != null && boolean translateEnabled=instanceInfo!=null && (
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && (instanceInfo.v2!=null && instanceInfo.v2.configuration.translation!=null && instanceInfo.v2.configuration.translation.enabled) ||
instanceInfo.v2.configuration.translation.enabled; (instanceInfo.isAkkoma() && instanceInfo.hasFeature(Instance.Feature.MACHINE_TRANSLATION))
);
try { try {
String bottomText = BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find() Pair<String, List<String>> decoded=BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find()
? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN) ? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN)
: null; : null;
if(bottomText==null || bottomText.length()==0 || bottomText.equals("\u0005")) bottomText=null; String bottomText=decoded==null || decoded.second.stream().allMatch(s->s.trim().isEmpty()) ? null : decoded.first;
if(bottomText!=null){ if(bottomText!=null){
translation=new Translation(); translation=new Translation();
translation.content=bottomText; translation.content=bottomText;
@@ -267,7 +268,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
s.visibility=StatusPrivacy.PUBLIC; s.visibility=StatusPrivacy.PUBLIC;
s.reactions=List.of(); s.reactions=List.of();
s.mentions=List.of(); s.mentions=List.of();
s.tags =List.of(); s.tags=List.of();
s.emojis=List.of(); s.emojis=List.of();
s.filtered=List.of(); s.filtered=List.of();
return s; return s;

View File

@@ -12,6 +12,8 @@ import androidx.annotation.StringRes;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BookmarkedStatusListFragment;
import org.joinmastodon.android.fragments.FavoritedStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment; import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment; import org.joinmastodon.android.fragments.ListTimelineFragment;
@@ -138,6 +140,8 @@ public class TimelineDefinition {
case LIST -> listTitle; case LIST -> listTitle;
case HASHTAG -> hashtagName; case HASHTAG -> hashtagName;
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble); case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
case BOOKMARKS -> ctx.getString(R.string.bookmarks);
case FAVORITES -> ctx.getString(R.string.your_favorites);
}; };
} }
@@ -150,6 +154,8 @@ public class TimelineDefinition {
case LIST -> listIsExclusive ? Icon.EXCLUSIVE_LIST : Icon.LIST; case LIST -> listIsExclusive ? Icon.EXCLUSIVE_LIST : Icon.LIST;
case HASHTAG -> Icon.HASHTAG; case HASHTAG -> Icon.HASHTAG;
case BUBBLE -> Icon.BUBBLE; case BUBBLE -> Icon.BUBBLE;
case BOOKMARKS -> Icon.BOOKMARKS;
case FAVORITES -> Icon.FAVORITES;
}; };
} }
@@ -162,6 +168,8 @@ public class TimelineDefinition {
case HASHTAG -> new HashtagTimelineFragment(); case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment(); case POST_NOTIFICATIONS -> new NotificationsListFragment();
case BUBBLE -> new BubbleTimelineFragment(); case BUBBLE -> new BubbleTimelineFragment();
case BOOKMARKS -> new BookmarkedStatusListFragment();
case FAVORITES -> new FavoritedStatusListFragment();
}; };
} }
@@ -228,7 +236,19 @@ public class TimelineDefinition {
return args; return args;
} }
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, BUBBLE } public enum TimelineType {
HOME,
LOCAL,
FEDERATED,
POST_NOTIFICATIONS,
LIST,
HASHTAG,
BUBBLE,
// not really timelines, but some people want it, so,,
BOOKMARKS,
FAVORITES
}
public enum Icon { public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart), HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
@@ -293,6 +313,13 @@ public class TimelineDefinition {
DOCTOR(R.drawable.ic_fluent_doctor_24_regular, R.string.sk_icon_doctor), DOCTOR(R.drawable.ic_fluent_doctor_24_regular, R.string.sk_icon_doctor),
DIAMOND(R.drawable.ic_fluent_premium_24_regular, R.string.sk_icon_diamond), DIAMOND(R.drawable.ic_fluent_premium_24_regular, R.string.sk_icon_diamond),
UMBRELLA(R.drawable.ic_fluent_umbrella_24_regular, R.string.sk_icon_umbrella), UMBRELLA(R.drawable.ic_fluent_umbrella_24_regular, R.string.sk_icon_umbrella),
WATER(R.drawable.ic_fluent_water_24_regular, R.string.sk_icon_water),
SUN(R.drawable.ic_fluent_weather_sunny_24_regular, R.string.sk_icon_sun),
SUNSET(R.drawable.ic_fluent_weather_sunny_low_24_regular, R.string.sk_icon_sunset),
CLOUD(R.drawable.ic_fluent_cloud_24_regular, R.string.sk_icon_cloud),
THUNDERSTORM(R.drawable.ic_fluent_weather_thunderstorm_24_regular, R.string.sk_icon_thunderstorm),
RAIN(R.drawable.ic_fluent_weather_rain_24_regular, R.string.sk_icon_rain),
SNOWFLAKE(R.drawable.ic_fluent_weather_snowflake_24_regular, R.string.sk_icon_snowflake),
HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true), HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true),
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true), LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
@@ -301,7 +328,9 @@ public class TimelineDefinition {
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true), LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
EXCLUSIVE_LIST(R.drawable.ic_fluent_rss_24_regular, R.string.sk_exclusive_list, true), EXCLUSIVE_LIST(R.drawable.ic_fluent_rss_24_regular, R.string.sk_exclusive_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true), HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true); BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true),
BOOKMARKS(R.drawable.ic_fluent_bookmark_multiple_24_regular, R.string.bookmarks, true),
FAVORITES(R.drawable.ic_fluent_star_24_regular, R.string.your_favorites, true);
public final int iconRes, nameRes; public final int iconRes, nameRes;
public final boolean hidden; public final boolean hidden;
@@ -321,6 +350,8 @@ public class TimelineDefinition {
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL); public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED); public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS); public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final TimelineDefinition BOOKMARKS_TIMELINE = new TimelineDefinition(TimelineType.BOOKMARKS);
public static final TimelineDefinition FAVORITES_TIMELINE = new TimelineDefinition(TimelineType.FAVORITES);
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) { public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
@Override @Override
public boolean isCompatible(AccountSession session) { public boolean isCompatible(AccountSession session) {
@@ -365,6 +396,8 @@ public class TimelineDefinition {
LOCAL_TIMELINE, LOCAL_TIMELINE,
FEDERATED_TIMELINE, FEDERATED_TIMELINE,
POSTS_TIMELINE, POSTS_TIMELINE,
BUBBLE_TIMELINE BUBBLE_TIMELINE,
BOOKMARKS_TIMELINE,
FAVORITES_TIMELINE
); );
} }

View File

@@ -1,10 +1,30 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import org.joinmastodon.android.api.AllFieldsAreRequired;
@AllFieldsAreRequired import org.joinmastodon.android.api.RequiredField;
public class Translation extends BaseModel{ public class Translation extends BaseModel{
@RequiredField
public String content; public String content;
@RequiredField
public String detectedSourceLanguage; public String detectedSourceLanguage;
@RequiredField
public String provider; public String provider;
public String spoilerText;
public MediaAttachment[] mediaAttachments;
public PollTranslation poll;
public static class MediaAttachment {
public String id;
public String description;
}
public static class PollTranslation {
public String id;
public PollOption[] options;
}
public static class PollOption {
public String title;
}
} }

View File

@@ -33,9 +33,9 @@ public class AccountViewModel{
V.dp(50), V.dp(50)); V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper(); emojiHelper=new CustomEmojiHelper();
if(session.getLocalPreferences().customEmojiInNames) if(session.getLocalPreferences().customEmojiInNames)
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
else else
parsedName=account.displayName; parsedName=account.getDisplayName();
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID); parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName); SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
ssb.append(parsedBio); ssb.append(parsedBio);

View File

@@ -9,42 +9,42 @@ public class CheckableListItem<T> extends ListItem<T>{
public boolean checked; public boolean checked;
public Consumer<Boolean> checkedChangeListener; public Consumer<Boolean> checkedChangeListener;
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject, boolean dividerAfter){ public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, T parentObject, boolean dividerAfter){
super(title, subtitle, iconRes, onClick, parentObject, 0, dividerAfter); super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
this.style=style; this.style=style;
this.checked=checked; this.checked=checked;
} }
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick){ public CheckableListItem(String title, String subtitle, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick){
this(title, subtitle, style, checked, 0, onClick, null, false); this(title, subtitle, style, checked, 0, onClick, null, false);
} }
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick, T parentObject){ public CheckableListItem(String title, String subtitle, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick, T parentObject){
this(title, subtitle, style, checked, 0, onClick, parentObject, false); this(title, subtitle, style, checked, 0, onClick, parentObject, false);
} }
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick){ public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick){
this(title, subtitle, style, checked, iconRes, onClick, null, false); this(title, subtitle, style, checked, iconRes, onClick, null, false);
} }
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject){ public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, T parentObject){
this(title, subtitle, style, checked, iconRes, onClick, parentObject, false); this(title, subtitle, style, checked, iconRes, onClick, parentObject, false);
} }
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick){ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick){
this(titleRes, subtitleRes, style, checked, 0, onClick, false); this(titleRes, subtitleRes, style, checked, 0, onClick, false);
} }
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick, boolean dividerAfter){ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick, boolean dividerAfter){
this(titleRes, subtitleRes, style, checked, 0, onClick, dividerAfter); this(titleRes, subtitleRes, style, checked, 0, onClick, dividerAfter);
} }
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick){ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick){
this(titleRes, subtitleRes, style, checked, iconRes, onClick, false); this(titleRes, subtitleRes, style, checked, iconRes, onClick, false);
} }
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick, boolean dividerAfter){ public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, boolean dividerAfter){
super(titleRes, subtitleRes, iconRes, onClick, 0, dividerAfter); super(titleRes, subtitleRes, iconRes, (Consumer<ListItem<T>>)(Object)onClick, 0, dividerAfter);
this.style=style; this.style=style;
this.checked=checked; this.checked=checked;
} }

View File

@@ -2,6 +2,8 @@ package org.joinmastodon.android.model.viewmodel;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import java.util.function.Consumer;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
@@ -16,11 +18,11 @@ public class ListItem<T>{
public int iconRes; public int iconRes;
public int colorOverrideAttr; public int colorOverrideAttr;
public boolean dividerAfter; public boolean dividerAfter;
public Runnable onClick; private Consumer<ListItem<T>> onClick;
public boolean isEnabled=true; public boolean isEnabled=true;
public T parentObject; public T parentObject;
public ListItem(String title, String subtitle, int iconRes, Runnable onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){ public ListItem(String title, String subtitle, int iconRes, Consumer<ListItem<T>> onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
this.title=title; this.title=title;
this.subtitle=subtitle; this.subtitle=subtitle;
this.iconRes=iconRes; this.iconRes=iconRes;
@@ -32,45 +34,41 @@ public class ListItem<T>{
isEnabled=false; isEnabled=false;
} }
public ListItem(String title, String subtitle, Runnable onClick){ public ListItem(String title, String subtitle, Consumer<ListItem<T>> onClick){
this(title, subtitle, 0, onClick, null, 0, false); this(title, subtitle, 0, onClick, null, 0, false);
} }
public ListItem(String title, String subtitle, Runnable onClick, T parentObject){ public ListItem(String title, String subtitle, Consumer<ListItem<T>> onClick, T parentObject){
this(title, subtitle, 0, onClick, parentObject, 0, false); this(title, subtitle, 0, onClick, parentObject, 0, false);
} }
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick){ public ListItem(String title, String subtitle, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick){
this(title, subtitle, iconRes, onClick, null, 0, false); this(title, subtitle, iconRes, onClick, null, 0, false);
} }
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick, boolean dividerAfter){ public ListItem(String title, String subtitle, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, T parentObject){
this(title, subtitle, iconRes, onClick, null, 0, dividerAfter);
}
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick, T parentObject){
this(title, subtitle, iconRes, onClick, parentObject, 0, false); this(title, subtitle, iconRes, onClick, parentObject, 0, false);
} }
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick){ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Consumer<ListItem<T>> onClick){
this(null, null, 0, onClick, null, 0, false); this(null, null, 0, onClick, null, 0, false);
this.titleRes=titleRes; this.titleRes=titleRes;
this.subtitleRes=subtitleRes; this.subtitleRes=subtitleRes;
} }
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
this(null, null, 0, onClick, null, colorOverrideAttr, dividerAfter); this(null, null, 0, onClick, null, colorOverrideAttr, dividerAfter);
this.titleRes=titleRes; this.titleRes=titleRes;
this.subtitleRes=subtitleRes; this.subtitleRes=subtitleRes;
} }
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick){ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick){
this(null, null, iconRes, onClick, null, 0, false); this(null, null, iconRes, onClick, null, 0, false);
this.titleRes=titleRes; this.titleRes=titleRes;
this.subtitleRes=subtitleRes; this.subtitleRes=subtitleRes;
} }
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){ public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter); this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
this.titleRes=titleRes; this.titleRes=titleRes;
this.subtitleRes=subtitleRes; this.subtitleRes=subtitleRes;
@@ -79,4 +77,13 @@ public class ListItem<T>{
public int getItemViewType(){ public int getItemViewType(){
return colorOverrideAttr==0 ? R.id.list_item_simple : R.id.list_item_simple_tinted; return colorOverrideAttr==0 ? R.id.list_item_simple : R.id.list_item_simple_tinted;
} }
public void performClick(){
if(onClick!=null)
onClick.accept(this);
}
public <I extends ListItem<T>> void setOnClick(Consumer<I> onClick){
this.onClick=(Consumer<ListItem<T>>) onClick;
}
} }

View File

@@ -8,7 +8,7 @@ import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.text.SpannableStringBuilder;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
@@ -27,6 +27,8 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment; import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment; import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment; import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout; import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
@@ -40,7 +42,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@@ -282,7 +283,7 @@ public class AccountSwitcherSheet extends BottomSheet{
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@Override @Override
public void onBind(AccountSession item){ public void onBind(AccountSession item){
name.setText(item.self.displayName); HtmlParser.setTextWithCustomEmoji(name, item.self.getDisplayName(), item.self.emojis);
username.setText(item.getFullUsername()); username.setText(item.getFullUsername());
radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE); radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE); extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);

View File

@@ -71,7 +71,7 @@ public class SearchViewHelper{
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
clearSearchButton=new ImageButton(context); clearSearchButton=new ImageButton(context);
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24); clearSearchButton.setImageResource(R.drawable.ic_fluent_dismiss_24_regular);
clearSearchButton.setContentDescription(context.getString(R.string.clear)); clearSearchButton.setContentDescription(context.getString(R.string.clear));
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant))); clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
clearSearchButton.setBackground(UiUtils.getThemeDrawable(toolbarContext, android.R.attr.actionBarItemBackground)); clearSearchButton.setBackground(UiUtils.getThemeDrawable(toolbarContext, android.R.attr.actionBarItemBackground));

View File

@@ -26,9 +26,7 @@ import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton; import org.joinmastodon.android.ui.views.ProgressBarButton;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -53,9 +51,9 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), parentFragment.getAccountID()); parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), parentFragment.getAccountID());
if(account.emojis.isEmpty()){ if(account.emojis.isEmpty()){
parsedName=account.displayName; parsedName=account.getDisplayName();
}else{ }else{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
} }
} }
@@ -149,7 +147,7 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE); followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=item.parentFragment.getRelationship(item.account.id); relationship=item.parentFragment.getRelationship(item.account.id);
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account); UiUtils.setExtraTextInfo(item.parentFragment.getContext(), null,true, false, false, item.account);
if(item.notification.type==Notification.Type.FOLLOW_REQUEST && (relationship==null || !relationship.followedBy)){ if(item.notification.type==Notification.Type.FOLLOW_REQUEST && (relationship==null || !relationship.followedBy)){
actionWrap.setVisibility(View.GONE); actionWrap.setVisibility(View.GONE);

View File

@@ -32,7 +32,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class AudioStatusDisplayItem extends StatusDisplayItem{ public class AudioStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final Attachment attachment; public final Attachment attachment;
private final ImageLoaderRequest imageRequest; private final ImageLoaderRequest imageRequest;

View File

@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity; import android.app.Activity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.CheckBox; import android.widget.CheckBox;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.report.ReportAddPostsChoiceFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout; import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
@@ -34,8 +34,16 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
public Holder(Activity activity, ViewGroup parent){ public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_header_checkable, parent); super(activity, R.layout.display_item_header_checkable, parent);
checkbox=findViewById(R.id.checkbox); checkbox=findViewById(R.id.checkbox);
view=(CheckableRelativeLayout) itemView; view=findViewById(R.id.checkbox_wrap);
checkbox.setBackground(new CheckBox(activity).getButtonDrawable()); checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
view.setOnClickListener(this::onToggle);
view.setAccessibilityDelegate(new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(CheckBox.class.getName());
}
});
} }
@Override @Override
@@ -46,6 +54,12 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
} }
} }
private void onToggle(View v){
if(item.parentFragment instanceof ReportAddPostsChoiceFragment reportFragment){
reportFragment.onToggleItem(item.parentID);
}
}
public void setIsChecked(Predicate<Holder> isChecked){ public void setIsChecked(Predicate<Holder> isChecked){
this.isChecked=isChecked; this.isChecked=isChecked;
} }

View File

@@ -58,7 +58,6 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem { public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
public final Status status;
private final Drawable placeholder; private final Drawable placeholder;
private final boolean hideEmpty, forAnnouncement, playGifs; private final boolean hideEmpty, forAnnouncement, playGifs;
private final String accountID; private final String accountID;

View File

@@ -13,6 +13,7 @@ import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -35,7 +36,6 @@ import androidx.annotation.PluralsRes;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final String accountID; public final String accountID;
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
@@ -76,9 +76,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void onBind(ExtendedFooterStatusDisplayItem item){ public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status; Status s=item.status;
AccountSession session=AccountSessionManager.get(item.accountID); favorites.setCompoundDrawablesRelativeWithIntrinsicBounds(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular, 0, 0, 0);
boolean like=session!=null && session.getLocalPreferences().likeIcon;
favorites.setCompoundDrawablesRelativeWithIntrinsicBounds(like ? R.drawable.ic_fluent_heart_20_regular : R.drawable.ic_fluent_star_20_regular, 0, 0, 0);
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount)); favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount)); reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
reblogs.setVisibility(s.visibility != StatusPrivacy.DIRECT ? View.VISIBLE : View.GONE); reblogs.setVisibility(s.visibility != StatusPrivacy.DIRECT ? View.VISIBLE : View.GONE);
@@ -131,6 +129,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
} }
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){ private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
if(item.status.preview) return;
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID()); args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status)); args.putParcelable("status", Parcels.wrap(item.status));
@@ -138,6 +137,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
} }
private void startEditHistoryFragment(){ private void startEditHistoryFragment(){
if(item.status.preview) return;
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID()); args.putString("account", item.parentFragment.getAccountID());
args.putString("id", item.status.id); args.putString("id", item.status.id);

View File

@@ -1,24 +1,17 @@
package org.joinmastodon.android.ui.displayitems; package org.joinmastodon.android.ui.displayitems;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
public class FileStatusDisplayItem extends StatusDisplayItem{ public class FileStatusDisplayItem extends StatusDisplayItem{
private final Attachment attachment; private final Attachment attachment;

View File

@@ -38,7 +38,6 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class FooterStatusDisplayItem extends StatusDisplayItem{ public class FooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private final String accountID; private final String accountID;
public boolean hideCounts; public boolean hideCounts;
@@ -133,13 +132,10 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor && boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
!nextIsWarning; !nextIsWarning;
AccountSession session=AccountSessionManager.get(item.accountID);
boolean like=session!=null && session.getLocalPreferences().likeIcon;
ColorStateList color=item.parentFragment.getResources().getColorStateList( ColorStateList color=item.parentFragment.getResources().getColorStateList(
like ? R.color.like_icon : R.color.favorite_icon, item.parentFragment.getContext().getTheme() GlobalUserPreferences.likeIcon ? R.color.like_icon : R.color.favorite_icon, item.parentFragment.getContext().getTheme()
); );
favIcon.setImageResource(like ? R.drawable.ic_fluent_heart_24_selector : R.drawable.ic_fluent_star_24_selector); favIcon.setImageResource(GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_selector : R.drawable.ic_fluent_star_24_selector);
favIcon.setImageTintList(color); favIcon.setImageTintList(color);
favorites.setTextColor(color); favorites.setTextColor(color);
@@ -162,6 +158,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private boolean onButtonTouch(View v, MotionEvent event){ private boolean onButtonTouch(View v, MotionEvent event){
if(item.status.preview) return false;
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame && boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled()); parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
int action = event.getAction(); int action = event.getAction();
@@ -175,7 +172,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} else if (action == MotionEvent.ACTION_DOWN) { } else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false; longClickPerformed = false;
touchingView = v; touchingView = v;
v.setPivotX(V.sp(28));
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start(); v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true; if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout()); v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
@@ -185,6 +181,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private void onReplyClick(View v){ private void onReplyClick(View v){
if(item.status.preview) return;
UiUtils.opacityIn(v); UiUtils.opacityIn(v);
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", item.accountID); args.putString("account", item.accountID);
@@ -193,6 +190,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private boolean onReplyLongClick(View v) { private boolean onReplyLongClick(View v) {
if(item.status.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> { UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
Bundle args=new Bundle(); Bundle args=new Bundle();
@@ -208,6 +206,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private void onBoostClick(View v){ private void onBoostClick(View v){
if(item.status.preview) return;
if (GlobalUserPreferences.confirmBoost) { if (GlobalUserPreferences.confirmBoost) {
UiUtils.opacityIn(v); UiUtils.opacityIn(v);
onBoostLongClick(v); onBoostLongClick(v);
@@ -223,6 +222,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private boolean onBoostLongClick(View v){ private boolean onBoostLongClick(View v){
if(item.status.preview) return false;
Context ctx = itemView.getContext(); Context ctx = itemView.getContext();
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null); View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create(); Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
@@ -305,6 +305,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private void onFavoriteClick(View v){ private void onFavoriteClick(View v){
if(item.status.preview) return;
favorite.setSelected(!item.status.favourited); favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{ AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
UiUtils.opacityIn(v); UiUtils.opacityIn(v);
@@ -313,6 +314,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private boolean onFavoriteLongClick(View v) { private boolean onFavoriteLongClick(View v) {
if(item.status.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(), UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status, item.accountID, item.status,
@@ -321,12 +323,13 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
R.string.sk_favorite_as, R.string.sk_favorite_as,
R.string.sk_favorited_as, R.string.sk_favorited_as,
R.string.sk_already_favorited, R.string.sk_already_favorited,
R.drawable.ic_fluent_star_28_regular GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_28_regular : R.drawable.ic_fluent_star_28_regular
); );
return true; return true;
} }
private void onBookmarkClick(View v){ private void onBookmarkClick(View v){
if(item.status.preview) return;
bookmark.setSelected(!item.status.bookmarked); bookmark.setSelected(!item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{ AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
UiUtils.opacityIn(v); UiUtils.opacityIn(v);
@@ -334,6 +337,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private boolean onBookmarkLongClick(View v) { private boolean onBookmarkLongClick(View v) {
if(item.status.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(), UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status, item.accountID, item.status,
@@ -348,6 +352,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private void onShareClick(View v){ private void onShareClick(View v){
if(item.status.preview) return;
UiUtils.opacityIn(v); UiUtils.opacityIn(v);
Intent intent=new Intent(Intent.ACTION_SEND); Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain"); intent.setType("text/plain");
@@ -356,6 +361,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} }
private boolean onShareLongClick(View v){ private boolean onShareLongClick(View v){
if(item.status.preview) return false;
UiUtils.copyText(v, item.status.url); UiUtils.copyText(v, item.status.url);
return true; return true;
} }

View File

@@ -8,6 +8,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable; import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -18,13 +19,16 @@ import me.grishka.appkit.utils.V;
public class GapStatusDisplayItem extends StatusDisplayItem{ public class GapStatusDisplayItem extends StatusDisplayItem{
public boolean loading; public boolean loading;
private Status status;
public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){ public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){
super(parentID, parentFragment); super(parentID, parentFragment);
this.status=status; this.status=status;
} }
public String getMaxID(){
return status.hasGapAfter;
}
@Override @Override
public Type getType(){ public Type getType(){
return Type.GAP; return Type.GAP;
@@ -54,13 +58,19 @@ public class GapStatusDisplayItem extends StatusDisplayItem{
if(!item.loading){ if(!item.loading){
progressBottom.setVisibility(View.GONE); progressBottom.setVisibility(View.GONE);
progressTop.setVisibility(View.GONE); progressTop.setVisibility(View.GONE);
textTop.setAlpha(1);
textBottom.setAlpha(1);
} }
top.setClickable(!item.loading); top.setClickable(!item.loading);
bottom.setClickable(!item.loading); bottom.setClickable(!item.loading);
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null); Status next=!(item.parentFragment instanceof StatusListFragment) ? null : getNextVisibleDisplayItem(i->{
Instant dateBelow=next instanceof HeaderStatusDisplayItem h ? h.status.createdAt Status s=((StatusListFragment) item.parentFragment).getStatusByID(i.parentID);
: next instanceof ReblogOrReplyLineStatusDisplayItem l ? l.status.createdAt return s!=null && !s.fromStatusCreated;
: null; })
.map(i->((StatusListFragment) item.parentFragment).getStatusByID(i.parentID))
.orElse(null);
bottom.setVisibility(next==null ? View.GONE : View.VISIBLE);
Instant dateBelow=next!=null ? next.createdAt : null;
String text=dateBelow!=null && item.status.createdAt!=null && dateBelow.isBefore(item.status.createdAt) String text=dateBelow!=null && item.status.createdAt!=null && dateBelow.isBefore(item.status.createdAt)
? UiUtils.formatPeriodBetween(item.parentFragment.getContext(), dateBelow, item.status.createdAt) ? UiUtils.formatPeriodBetween(item.parentFragment.getContext(), dateBelow, item.status.createdAt)
: null; : null;

View File

@@ -76,7 +76,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private String accountID; private String accountID;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private SpannableStringBuilder parsedName; private SpannableStringBuilder parsedName;
public final Status status;
public boolean hasVisibilityToggle; public boolean hasVisibilityToggle;
boolean needBottomPadding; boolean needBottomPadding;
private CharSequence extraText; private CharSequence extraText;
@@ -96,7 +95,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic,
V.dp(50), V.dp(50)); V.dp(50), V.dp(50));
this.accountID=accountID; this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName); parsedName=new SpannableStringBuilder(user.getDisplayName());
this.status=status; this.status=status;
this.notification=notification; this.notification=notification;
this.scheduledStatus=scheduledStatus; this.scheduledStatus=scheduledStatus;
@@ -137,7 +136,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
} }
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{ public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView name, time, username, extraText, pronouns; private final TextView name, time, username, extraText;
private final View collapseBtn, timeUsernameSeparator; private final View collapseBtn, timeUsernameSeparator;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, markAsRead, collapseBtnIcon, botIcon; private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, markAsRead, collapseBtnIcon, botIcon;
private final PopupMenu optionsMenu; private final PopupMenu optionsMenu;
@@ -164,7 +163,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
collapseBtn=findViewById(R.id.collapse_btn); collapseBtn=findViewById(R.id.collapse_btn);
collapseBtnIcon=findViewById(R.id.collapse_btn_icon); collapseBtnIcon=findViewById(R.id.collapse_btn_icon);
extraText=findViewById(R.id.extra_text); extraText=findViewById(R.id.extra_text);
pronouns=findViewById(R.id.pronouns);
avatar.setOnClickListener(this::onAvaClick); avatar.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true); avatar.setClipToOutline(true);
@@ -209,7 +207,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}else if(item.scheduledStatus!=null){ }else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text); args.putString("sourceText", item.status.text);
args.putString("sourceSpoiler", item.status.spoilerText); args.putString("sourceSpoiler", item.status.spoilerText);
args.putBoolean("redraftStatus", true);
args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus)); args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else{ }else{
@@ -219,14 +216,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public void onSuccess(GetStatusSourceText.Response result){ public void onSuccess(GetStatusSourceText.Response result){
args.putString("sourceText", result.text); args.putString("sourceText", result.text);
args.putString("sourceSpoiler", result.spoilerText); args.putString("sourceSpoiler", result.spoilerText);
if (result.contentType != null) { if(result.contentType!=null){
args.putString("sourceContentType", result.contentType.name()); args.putString("sourceContentType", result.contentType.name());
} }
if (redraft) { if(redraft){
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{ UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}, true); }, true);
} else { }else{
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
} }
} }
@@ -243,7 +240,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
if (item.scheduledStatus != null) { if (item.scheduledStatus != null) {
UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{}); UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{});
} else { } else {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{}); UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{}, false);
} }
}else if(id==R.id.pin || id==R.id.unpin) { }else if(id==R.id.pin || id==R.id.unpin) {
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{}); UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
@@ -333,7 +330,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
if(TextUtils.isEmpty(item.extraText)){ if(TextUtils.isEmpty(item.extraText)){
if (item.status != null) { if (item.status != null) {
boolean displayPronouns=item.parentFragment instanceof ThreadFragment ? GlobalUserPreferences.displayPronounsInThreads : GlobalUserPreferences.displayPronounsInTimelines; boolean displayPronouns=item.parentFragment instanceof ThreadFragment ? GlobalUserPreferences.displayPronounsInThreads : GlobalUserPreferences.displayPronounsInTimelines;
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, pronouns, displayPronouns, item.status.visibility==StatusPrivacy.DIRECT, item.status.localOnly || item.status.visibility==StatusPrivacy.LOCAL, item.status.account); UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, displayPronouns, item.status.visibility==StatusPrivacy.DIRECT, item.status.localOnly || item.status.visibility==StatusPrivacy.LOCAL, item.status.account);
} }
}else{ }else{
extraText.setVisibility(View.VISIBLE); extraText.setVisibility(View.VISIBLE);
@@ -377,21 +374,32 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
markAsRead.setVisibility(View.GONE); markAsRead.setVisibility(View.GONE);
} }
if (item.status == null || !item.status.textExpandable) { bindCollapseButton();
collapseBtn.setVisibility(View.GONE);
} else {
String collapseText = item.parentFragment.getString(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
collapseBtn.setVisibility(item.status.textExpandable ? View.VISIBLE : View.GONE);
collapseBtn.setContentDescription(collapseText);
if (GlobalUserPreferences.reduceMotion) collapseBtnIcon.setScaleY(item.status.textExpanded ? -1 : 1);
else collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
}
itemView.setPaddingRelative(itemView.getPaddingStart(), itemView.getPaddingTop(), itemView.setPaddingRelative(itemView.getPaddingStart(), itemView.getPaddingTop(),
item.inset ? V.dp(10) : V.dp(4), itemView.getPaddingBottom()); item.inset ? V.dp(10) : V.dp(4), itemView.getPaddingBottom());
} }
public void bindCollapseButton(){
boolean expandable=item.status!=null && item.status.textExpandable;
collapseBtn.setVisibility(expandable ? View.VISIBLE : View.GONE);
if(expandable) {
bindCollapseButtonText();
collapseBtnIcon.setScaleY(item.status.textExpanded ? -1 : 1);
}
}
private void bindCollapseButtonText(){
String collapseText = item.parentFragment.getString(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
collapseBtn.setContentDescription(collapseText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
}
public void animateExpandToggle(){
bindCollapseButtonText();
collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
}
public void animateVisibilityToggle(boolean visible){ public void animateVisibilityToggle(boolean visible){
visibility.animate() visibility.animate()
.alpha(visible ? 1 : 0) .alpha(visible ? 1 : 0)
@@ -437,6 +445,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
} }
private void onMoreClick(View v){ private void onMoreClick(View v){
if(item.status.preview) return;
updateOptionsMenu(); updateOptionsMenu();
optionsMenu.show(); optionsMenu.show();
if(relationship==null && currentRelationshipRequest==null){ if(relationship==null && currentRelationshipRequest==null){
@@ -537,7 +546,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular); follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
manageUserLists.setVisible(relationship != null && relationship.following); manageUserLists.setVisible(relationship != null && relationship.following);
manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, username)); manageUserLists.setTitle(item.parentFragment.getString(R.string.sk_lists_with_user, username));
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow); // ic_fluent_person_add_24_regular actually has a width of 25dp -.-
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow, following ? 0 : V.dp(-1));
} }
workaroundChangingMenuItemWidths(menu, username); workaroundChangingMenuItemWidths(menu, username);

View File

@@ -24,7 +24,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class LinkCardStatusDisplayItem extends StatusDisplayItem{ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
private final Status status;
private final UrlImageLoaderRequest imgRequest; private final UrlImageLoaderRequest imgRequest;
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){ public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){

View File

@@ -12,6 +12,7 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -26,6 +27,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PhotoLayoutHelper; import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable; import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
@@ -38,7 +40,12 @@ import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool; import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -52,8 +59,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private PhotoLayoutHelper.TiledLayoutResult tiledLayout; private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool; private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments; private final List<Attachment> attachments;
private final Map<String, Pair<String, String>> translatedAttachments = new HashMap<>();
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>(); private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public String sensitiveTitle; public String sensitiveTitle;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){ public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
@@ -189,6 +196,25 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
c.btnsWrap.setAlpha(1f); c.btnsWrap.setAlpha(1f);
} }
controllers.add(c); controllers.add(c);
if (item.status.translation != null){
if(item.status.translationState==Status.TranslationState.SHOWN){
if(!item.translatedAttachments.containsKey(att.id)){
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();
translatedAttachment.ifPresent(mediaAttachment->{
item.translatedAttachments.put(mediaAttachment.id, new Pair<>(att.description, mediaAttachment.description));
att.description=mediaAttachment.description;
});
}else{
//SAFETY: must be non-null, as we check if the map contains the attachment before
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).second;
}
}else{
if (item.translatedAttachments.containsKey(att.id)) {
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).first;
}
}
}
c.bind(att, item.status); c.bind(att, item.status);
i++; i++;
} }
@@ -382,7 +408,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
} }
public MediaAttachmentViewController getViewController(int index){ public MediaAttachmentViewController getViewController(int index){
return controllers.get(index); return index<controllers.size() ? controllers.get(index) : null;
} }
public void setClipChildren(boolean clip){ public void setClipChildren(boolean clip){

View File

@@ -1,15 +1,14 @@
package org.joinmastodon.android.ui.displayitems; package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.MastodonApp.context; import static org.joinmastodon.android.MastodonApp.context;
import static org.joinmastodon.android.model.Notification.Type.PLEROMA_EMOJI_REACTION;
import static org.joinmastodon.android.ui.utils.UiUtils.generateFormattedString; import static org.joinmastodon.android.ui.utils.UiUtils.generateFormattedString;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.TypedValue; import android.util.TypedValue;
@@ -23,6 +22,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
@@ -62,7 +62,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
TextUtils.isEmpty(notification.account.avatar) ? session.getDefaultAvatarUrl() : TextUtils.isEmpty(notification.account.avatar) ? session.getDefaultAvatarUrl() :
GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic,
V.dp(50), V.dp(50)); V.dp(50), V.dp(50));
SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName); SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.getDisplayName());
HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis); HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis);
String str = parentFragment.getString(switch(notification.type){ String str = parentFragment.getString(switch(notification.type){
case FOLLOW -> R.string.user_followed_you; case FOLLOW -> R.string.user_followed_you;
@@ -113,7 +113,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
} }
public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{ public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final ImageView icon, avatar; private final ImageView icon, avatar, deleteNotification;
private final TextView text, timestamp; private final TextView text, timestamp;
private final int selectableItemBackground; private final int selectableItemBackground;
@@ -123,9 +123,15 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
avatar=findViewById(R.id.avatar); avatar=findViewById(R.id.avatar);
text=findViewById(R.id.text); text=findViewById(R.id.text);
timestamp=findViewById(R.id.timestamp); timestamp=findViewById(R.id.timestamp);
deleteNotification=findViewById(R.id.delete_notification);
avatar.setOutlineProvider(OutlineProviders.roundedRect(8)); avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
avatar.setClipToOutline(true); avatar.setClipToOutline(true);
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
if (item.parentFragment instanceof NotificationsListFragment fragment) {
fragment.removeNotification(item.notification);
}
}));
itemView.setOnClickListener(this::onItemClick); itemView.setOnClickListener(this::onItemClick);
TypedValue outValue = new TypedValue(); TypedValue outValue = new TypedValue();
@@ -141,6 +147,8 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
item.emojiHelper.setImageDrawable(index-1, image); item.emojiHelper.setImageDrawable(index-1, image);
text.invalidate(); text.invalidate();
} }
if(image instanceof Animatable)
((Animatable) image).start();
} }
@Override @Override
@@ -158,7 +166,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
timestamp.setText(item.timestamp); timestamp.setText(item.timestamp);
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE); avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
icon.setImageResource(switch(item.notification.type){ icon.setImageResource(switch(item.notification.type){
case FAVORITE -> R.drawable.ic_fluent_star_24_filled; case FAVORITE -> GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_filled : R.drawable.ic_fluent_star_24_filled;
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled; case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_fluent_person_add_24_filled; case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_fluent_person_add_24_filled;
case POLL -> R.drawable.ic_fluent_poll_24_filled; case POLL -> R.drawable.ic_fluent_poll_24_filled;
@@ -169,15 +177,18 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
default -> throw new IllegalStateException("Unexpected value: "+item.notification.type); default -> throw new IllegalStateException("Unexpected value: "+item.notification.type);
}); });
icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.type){ icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.type){
case FAVORITE -> R.attr.colorFavorite; case FAVORITE -> GlobalUserPreferences.likeIcon ? R.attr.colorLike : R.attr.colorFavorite;
case REBLOG -> R.attr.colorBoost; case REBLOG -> R.attr.colorBoost;
case POLL -> R.attr.colorPoll; case POLL -> R.attr.colorPoll;
default -> android.R.attr.colorAccent; default -> android.R.attr.colorAccent;
}))); })));
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification != null ? View.VISIBLE : View.GONE);
itemView.setBackgroundResource(item.notification.type != Notification.Type.POLL itemView.setBackgroundResource(item.notification.type != Notification.Type.POLL
&& item.notification.type != Notification.Type.REPORT ? && item.notification.type != Notification.Type.REPORT ?
selectableItemBackground : 0); selectableItemBackground : 0);
itemView.setClickable(item.notification.type != Notification.Type.POLL); itemView.setClickable(item.notification.type != Notification.Type.POLL);
itemView.setPaddingRelative(itemView.getPaddingStart(), itemView.getPaddingTop(),
GlobalUserPreferences.enableDeleteNotifications ? V.dp(4) : V.dp(16), itemView.getPaddingBottom());
} }
public void onItemClick(View v) { public void onItemClick(View v) {

View File

@@ -11,6 +11,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
@@ -23,6 +24,7 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public class PollOptionStatusDisplayItem extends StatusDisplayItem{ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private CharSequence text; private CharSequence text;
private CharSequence translatedText;
public final Poll.Option option; public final Poll.Option option;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private boolean showResults; private boolean showResults;
@@ -30,12 +32,15 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private boolean isMostVoted; private boolean isMostVoted;
private final int optionIndex; private final int optionIndex;
public final Poll poll; public final Poll poll;
public final Status status;
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment){
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment, Status status){
super(parentID, parentFragment); super(parentID, parentFragment);
this.optionIndex=optionIndex; this.optionIndex=optionIndex;
option=poll.options.get(optionIndex); option=poll.options.get(optionIndex);
this.poll=poll; this.poll=poll;
this.status=status;
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis); text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
emojiHelper.setText(text); emojiHelper.setText(text);
showResults=poll.isExpired() || poll.voted; showResults=poll.isExpired() || poll.voted;
@@ -84,7 +89,14 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void onBind(PollOptionStatusDisplayItem item){ public void onBind(PollOptionStatusDisplayItem item){
text.setText(item.text); if (item.status.translation != null && item.status.translationState == Status.TranslationState.SHOWN) {
if(item.translatedText==null){
item.translatedText=item.status.translation.poll.options[item.optionIndex].title;
}
text.setText(item.translatedText);
} else {
text.setText(item.text);
}
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE); percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
itemView.setClickable(!item.showResults); itemView.setClickable(!item.showResults);
icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ? icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ?

View File

@@ -10,10 +10,8 @@ import android.text.SpannableStringBuilder;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
@@ -40,12 +38,11 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private StatusPrivacy visibility; private StatusPrivacy visibility;
@DrawableRes @DrawableRes
private int iconEnd; private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), fullTextEmojiHelper; private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private View.OnClickListener handleClick; private View.OnClickListener handleClick;
public boolean needBottomPadding; public boolean needBottomPadding;
ReblogOrReplyLineStatusDisplayItem extra; ReblogOrReplyLineStatusDisplayItem extra;
CharSequence fullText; CharSequence fullText;
Status status;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) { public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status); this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);
@@ -58,21 +55,13 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
HtmlParser.parseCustomEmoji(ssb, emojis); HtmlParser.parseCustomEmoji(ssb, emojis);
this.text=ssb; this.text=ssb;
emojiHelper.setText(ssb); emojiHelper.setText(ssb);
this.fullText=fullText;
this.icon=icon; this.icon=icon;
this.status=status; this.status=status;
this.handleClick=handleClick; this.handleClick=handleClick;
TypedValue outValue = new TypedValue(); TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
updateVisibility(visibility); updateVisibility(visibility);
if (fullText != null) {
fullTextEmojiHelper = new CustomEmojiHelper();
SpannableStringBuilder fullTextSsb = new SpannableStringBuilder(fullText);
HtmlParser.parseCustomEmoji(fullTextSsb, emojis);
this.fullText=fullTextSsb;
fullTextEmojiHelper.setText(fullTextSsb);
}
} }
public void updateVisibility(StatusPrivacy visibility) { public void updateVisibility(StatusPrivacy visibility) {
@@ -92,34 +81,28 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public int getImageCount(){ public int getImageCount(){
return emojiHelper.getImageCount(); return emojiHelper.getImageCount() + (extra!=null ? extra.emojiHelper.getImageCount() : 0);
} }
@Override @Override
public ImageLoaderRequest getImageRequest(int index){ public ImageLoaderRequest getImageRequest(int index){
return emojiHelper.getImageRequest(index); int firstHelperCount=emojiHelper.getImageCount();
CustomEmojiHelper helper=index<firstHelperCount ? emojiHelper : extra.emojiHelper;
return helper.getImageRequest(firstHelperCount>0 ? index%firstHelperCount : index);
} }
public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{ public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView text, extraText; private final TextView text, extraText;
private final View separator; private final View separator;
private final ViewGroup parent;
public Holder(Activity activity, ViewGroup parent){ public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_reblog_or_reply_line, parent); super(activity, R.layout.display_item_reblog_or_reply_line, parent);
this.parent = parent;
text=findViewById(R.id.text); text=findViewById(R.id.text);
extraText=findViewById(R.id.extra_text); extraText=findViewById(R.id.extra_text);
separator=findViewById(R.id.separator); separator=findViewById(R.id.separator);
if (GlobalUserPreferences.compactReblogReplyLine) {
parent.addOnLayoutChangeListener((v, l, t, right, b, ol, ot, oldRight, ob) -> {
if (right != oldRight) layoutLine();
});
}
} }
private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) { private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) {
if (item.fullText != null) text.setContentDescription(item.fullText);
text.setText(item.text); text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0); text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
text.setOnClickListener(item.handleClick); text.setOnClickListener(item.handleClick);
@@ -133,7 +116,10 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
case LOCAL -> R.string.sk_local_only; case LOCAL -> R.string.sk_local_only;
default -> 0; default -> 0;
} : 0; } : 0;
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")"); String visibilityDescription=visibilityText!=0 ? " (" + ctx.getString(visibilityText) + ")" : null;
text.setContentDescription(item.fullText==null && visibilityDescription==null ? null :
(item.fullText!=null ? item.fullText : item.text)
+ (visibilityDescription!=null ? visibilityDescription : ""));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(text); UiUtils.fixCompoundDrawableTintOnAndroid6(text);
text.setCompoundDrawableTintList(text.getTextColors()); text.setCompoundDrawableTintList(text.getTextColors());
@@ -146,31 +132,15 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
extraText.setVisibility(item.extra == null ? View.GONE : View.VISIBLE); extraText.setVisibility(item.extra == null ? View.GONE : View.VISIBLE);
separator.setVisibility(item.extra == null ? View.GONE : View.VISIBLE); separator.setVisibility(item.extra == null ? View.GONE : View.VISIBLE);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0); itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
layoutLine();
}
private void layoutLine() {
// layout line only if above header, compact and has extra
if (!GlobalUserPreferences.compactReblogReplyLine || item.extra == null) return;
itemView.measure(
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.UNSPECIFIED);
boolean isVertical = ((LinearLayout) itemView).getOrientation() == LinearLayout.VERTICAL;
extraText.setPaddingRelative(extraText.getPaddingStart(), item.extra != null && isVertical ? 0 : V.dp(16), extraText.getPaddingEnd(), extraText.getPaddingBottom());
separator.setVisibility(item.extra != null && !isVertical ? View.VISIBLE : View.GONE);
((LinearLayout) itemView).removeView(extraText);
if (isVertical) ((LinearLayout) itemView).addView(extraText);
else ((LinearLayout) itemView).addView(extraText, 0);
text.setText(isVertical ? item.fullText : item.text);
if (item.extra != null) {
extraText.setText(isVertical ? item.extra.fullText : item.extra.text);
}
} }
@Override @Override
public void setImage(int index, Drawable image){ public void setImage(int index, Drawable image){
item.emojiHelper.setImageDrawable(index, image); int firstHelperCount=item.emojiHelper.getImageCount();
CustomEmojiHelper helper=index<firstHelperCount ? item.emojiHelper : item.extra.emojiHelper;
helper.setImageDrawable(firstHelperCount>0 ? index%firstHelperCount : index, image);
text.invalidate(); text.invalidate();
extraText.invalidate();
} }
@Override @Override

View File

@@ -24,9 +24,9 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public class SpoilerStatusDisplayItem extends StatusDisplayItem{ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>(); public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
private final CharSequence parsedTitle; private final CharSequence parsedTitle;
private CharSequence translatedTitle;
private final CustomEmojiHelper emojiHelper; private final CustomEmojiHelper emojiHelper;
private final Type type; private final Type type;
private final int attachmentCount; private final int attachmentCount;
@@ -90,7 +90,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
@Override @Override
public void onBind(SpoilerStatusDisplayItem item){ public void onBind(SpoilerStatusDisplayItem item){
title.setText(item.parsedTitle); if(item.status.translationState==Status.TranslationState.SHOWN){
if(item.translatedTitle==null){
item.translatedTitle=item.status.translation.spoilerText;
}
title.setText(item.translatedTitle);
}else{
title.setText(item.parsedTitle);
}
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.sk_spoiler_show); action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.sk_spoiler_show);
itemView.setPadding( itemView.setPadding(
itemView.getPaddingLeft(), itemView.getPaddingLeft(),

View File

@@ -13,7 +13,8 @@ import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.joinmastodon.android.GlobalUserPreferences; import androidx.annotation.NonNull;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences; import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -22,14 +23,16 @@ import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTabFragment; import org.joinmastodon.android.fragments.HomeTabFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment; import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult; import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -37,13 +40,14 @@ import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder; import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
@@ -54,13 +58,15 @@ import me.grishka.appkit.views.UsableRecyclerView;
public abstract class StatusDisplayItem{ public abstract class StatusDisplayItem{
public final String parentID; public final String parentID;
public final BaseStatusListFragment<?> parentFragment; public final BaseStatusListFragment<?> parentFragment;
public Status status;
public boolean inset; public boolean inset;
public int index; public int index;
public boolean public boolean
hasDescendantNeighbor = false, hasDescendantNeighbor=false,
hasAncestoringNeighbor = false, hasAncestoringNeighbor=false,
isMainStatus = true, isMainStatus=true,
isDirectDescendant = false; isDirectDescendant=false,
isForQuote=false;
public static final int FLAG_INSET=1; public static final int FLAG_INSET=1;
public static final int FLAG_NO_FOOTER=1 << 1; public static final int FLAG_NO_FOOTER=1 << 1;
@@ -69,7 +75,8 @@ public abstract class StatusDisplayItem{
public static final int FLAG_NO_HEADER=1 << 4; public static final int FLAG_NO_HEADER=1 << 4;
public static final int FLAG_NO_TRANSLATE=1 << 5; public static final int FLAG_NO_TRANSLATE=1 << 5;
public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6; public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6;
public static final int FLAG_IS_FOR_QUOTE=1 << 7;
public void setAncestryInfo( public void setAncestryInfo(
boolean hasDescendantNeighbor, boolean hasDescendantNeighbor,
boolean hasAncestoringNeighbor, boolean hasAncestoringNeighbor,
@@ -87,6 +94,16 @@ public abstract class StatusDisplayItem{
this.parentFragment=parentFragment; this.parentFragment=parentFragment;
} }
@NonNull
public String getContentStatusID(){
if(parentFragment instanceof StatusListFragment slf){
Status s=slf.getContentStatusByID(parentID);
return s!=null ? s.id : parentID;
}else{
return parentID;
}
}
public abstract Type getType(); public abstract Type getType();
public int getImageCount(){ public int getImageCount(){
@@ -128,11 +145,11 @@ public abstract class StatusDisplayItem{
String parentID = parent.getID(); String parentID = parent.getID();
String text = threadReply ? fragment.getString(R.string.sk_show_thread) String text = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply) : account == null ? fragment.getString(R.string.sk_in_reply)
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName : status.reblog != null ? account.getDisplayName()
: fragment.getString(R.string.in_reply_to, account.displayName); : fragment.getString(R.string.in_reply_to, account.getDisplayName());
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread) String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
: account == null ? fragment.getString(R.string.sk_in_reply) : account == null ? fragment.getString(R.string.sk_in_reply)
: fragment.getString(R.string.in_reply_to, account.displayName); : fragment.getString(R.string.in_reply_to, account.getDisplayName());
return new ReblogOrReplyLineStatusDisplayItem( return new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis, parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText, status R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText, status
@@ -145,11 +162,11 @@ public abstract class StatusDisplayItem{
Status statusForContent=status.getContentStatus(); Status statusForContent=status.getContentStatus();
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null; ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus s ? s : null;
HeaderStatusDisplayItem header=null; HeaderStatusDisplayItem header=null;
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts; boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
if((flags & FLAG_NO_HEADER)==0){ if((flags & FLAG_NO_HEADER)==0){
ReblogOrReplyLineStatusDisplayItem replyLine = null; ReblogOrReplyLineStatusDisplayItem replyLine = null;
boolean threadReply = statusForContent.inReplyToAccountId != null && boolean threadReply = statusForContent.inReplyToAccountId != null &&
@@ -162,12 +179,11 @@ public abstract class StatusDisplayItem{
if(status.reblog!=null){ if(status.reblog!=null){
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account); boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName); String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName());
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{ items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account)); args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args); Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, fullText, status)); }, null, status));
} else if (!(status.tags.isEmpty() || } else if (!(status.tags.isEmpty() ||
fragment instanceof HashtagTimelineFragment || fragment instanceof HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment fragment instanceof ListTimelineFragment
@@ -191,7 +207,7 @@ public abstract class StatusDisplayItem{
.map(ReblogOrReplyLineStatusDisplayItem.class::cast) .map(ReblogOrReplyLineStatusDisplayItem.class::cast)
.findFirst(); .findFirst();
if (primaryLine.isPresent() && GlobalUserPreferences.compactReblogReplyLine) { if (primaryLine.isPresent()) {
primaryLine.get().extra = replyLine; primaryLine.get().extra = replyLine;
} else { } else {
items.add(replyLine); items.add(replyLine);
@@ -201,7 +217,7 @@ public abstract class StatusDisplayItem{
if((flags & FLAG_CHECKABLE)!=0) if((flags & FLAG_CHECKABLE)!=0)
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null)); items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
else else
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, null, scheduledStatus)); items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus));
} }
LegacyFilter applyingFilter=null; LegacyFilter applyingFilter=null;
@@ -219,27 +235,28 @@ public abstract class StatusDisplayItem{
if(statusForContent.hasSpoiler()){ if(statusForContent.hasSpoiler()){
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true; if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER); SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
if((flags & FLAG_IS_FOR_QUOTE)!=0){
for(StatusDisplayItem item:spoilerItem.contentItems){
item.isForQuote=true;
}
}
items.add(spoilerItem); items.add(spoilerItem);
contentItems=spoilerItem.contentItems; contentItems=spoilerItem.contentItems;
}else{ }else{
contentItems=items; contentItems=items;
} }
if (statusForContent.quote != null) { if(statusForContent.quote!=null) {
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">"); int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
if (!hasQuoteInlineTag) { if (quoteInlineIndex!=-1)
String quoteUrl = statusForContent.quote.url; statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
} }
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText); boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
if(!TextUtils.isEmpty(statusForContent.content)){ if(!TextUtils.isEmpty(statusForContent.content)){
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID); SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
contentItems.add(text); contentItems.add(text);
}else if(!hasSpoiler && header!=null){ }else if(!hasSpoiler && header!=null){
header.needBottomPadding=true; header.needBottomPadding=true;
@@ -257,9 +274,11 @@ public abstract class StatusDisplayItem{
} }
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent); MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0) if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden); mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia) statusForContent.sensitiveRevealed=false;
statusForContent.sensitive=true;
} else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
statusForContent.sensitiveRevealed=true; statusForContent.sensitiveRevealed=true;
contentItems.add(mediaGrid); contentItems.add(mediaGrid);
} }
@@ -272,17 +291,23 @@ public abstract class StatusDisplayItem{
} }
} }
if(statusForContent.poll!=null){ if(statusForContent.poll!=null){
buildPollItems(parentID, fragment, statusForContent.poll, contentItems); buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
} }
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){ if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null){
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent)); contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
} }
if(statusForContent.quote!=null && !(parentObject instanceof Notification)){
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE));
}
if(contentItems!=items && statusForContent.spoilerRevealed){ if(contentItems!=items && statusForContent.spoilerRevealed){
items.addAll(contentItems); items.addAll(contentItems);
} }
AccountLocalPreferences lp=fragment.getLocalPrefs(); AccountLocalPreferences lp=fragment.getLocalPrefs();
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && lp.emojiReactionsEnabled && if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment)){ (lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
statusForContent.reactions!=null){
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id); boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus; boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false)); items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
@@ -292,38 +317,55 @@ public abstract class StatusDisplayItem{
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID); footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
footer.hideCounts=hideCounts; footer.hideCounts=hideCounts;
items.add(footer); items.add(footer);
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment, status));
} }
int i=1;
boolean inset=(flags & FLAG_INSET)!=0; boolean inset=(flags & FLAG_INSET)!=0;
boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0;
// add inset dummy so last content item doesn't clip out of inset bounds // add inset dummy so last content item doesn't clip out of inset bounds
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0){ if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){
items.add(new DummyStatusDisplayItem(parentID, fragment)); items.add(new DummyStatusDisplayItem(parentID, fragment));
// in case we ever need the dummy to display a margin for the media grid again: // in case we ever need the dummy to display a margin for the media grid again:
// (i forgot why we apparently don't need this anymore) // (i forgot why we apparently don't need this anymore)
// !contentItems.isEmpty() && contentItems // !contentItems.isEmpty() && contentItems
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem)); // .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
} }
GapStatusDisplayItem gap=null;
if((flags & FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment))
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
int i=1;
for(StatusDisplayItem item:items){ for(StatusDisplayItem item:items){
item.inset=inset; if(inset)
item.inset=true;
if(isForQuote){
item.status=statusForContent;
item.isForQuote=true;
}
item.index=i++; item.index=i++;
} }
if(items!=contentItems && !statusForContent.spoilerRevealed){ if(items!=contentItems && !statusForContent.spoilerRevealed){
for(StatusDisplayItem item:contentItems){ for(StatusDisplayItem item:contentItems){
item.inset=inset; if(inset)
item.inset=true;
if(isForQuote){
item.status=statusForContent;
item.isForQuote=true;
}
item.index=i++; item.index=i++;
} }
} }
return applyingFilter==null ? items : List<StatusDisplayItem> nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items;
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items, applyingFilter))); WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null :
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter);
return applyingFilter==null ? items : new ArrayList<>(gap!=null
? List.of(warning, gap)
: Collections.singletonList(warning)
);
} }
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){ public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> items){
int i=0; int i=0;
for(Poll.Option opt:poll.options){ for(Poll.Option opt:poll.options){
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment)); items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment, status));
i++; i++;
} }
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll)); items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
@@ -356,12 +398,15 @@ public abstract class StatusDisplayItem{
} }
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{ public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
private Context context;
public Holder(View itemView){ public Holder(View itemView){
super(itemView); super(itemView);
} }
public Holder(Context context, int layout, ViewGroup parent){ public Holder(Context context, int layout, ViewGroup parent){
super(context, layout, parent); super(context, layout, parent);
this.context=context;
} }
public String getItemID(){ public String getItemID(){
@@ -370,16 +415,28 @@ public abstract class StatusDisplayItem{
@Override @Override
public void onClick(){ public void onClick(){
if(item.isForQuote){
item.status.filterRevealed=true;
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status.clone()));
args.putBoolean("refresh", true);
Nav.go((Activity) context, ThreadFragment.class, args);
return;
}
item.parentFragment.onItemClick(item.parentID); item.parentFragment.onItemClick(item.parentID);
} }
public Optional<StatusDisplayItem> getNextVisibleDisplayItem(){ public Optional<StatusDisplayItem> getNextVisibleDisplayItem(){
return getNextVisibleDisplayItem(null);
}
public Optional<StatusDisplayItem> getNextVisibleDisplayItem(Predicate<StatusDisplayItem> predicate){
Optional<StatusDisplayItem> next=getNextDisplayItem(); Optional<StatusDisplayItem> next=getNextDisplayItem();
for(int offset=1; next.isPresent(); next=getDisplayItemOffset(++offset)){ for(int offset=1; next.isPresent(); next=getDisplayItemOffset(++offset)){
if(!next.map(n-> boolean isHidden=next.map(n->(n instanceof EmojiReactionsStatusDisplayItem e && e.isHidden())
(n instanceof EmojiReactionsStatusDisplayItem e && e.isHidden()) || || (n instanceof DummyStatusDisplayItem)).orElse(false);
(n instanceof DummyStatusDisplayItem) if(!isHidden && (predicate==null || predicate.test(next.get()))) return next;
).orElse(false)) return next;
} }
return Optional.empty(); return Optional.empty();
} }
@@ -399,13 +456,13 @@ public abstract class StatusDisplayItem{
public boolean isLastDisplayItemForStatus(){ public boolean isLastDisplayItemForStatus(){
return getNextVisibleDisplayItem() return getNextVisibleDisplayItem()
.map(n->!n.parentID.equals(item.parentID)) .map(next->!next.parentID.equals(item.parentID) || item.inset && !next.inset)
.orElse(true); .orElse(true);
} }
@Override @Override
public boolean isEnabled(){ public boolean isEnabled(){
return item.parentFragment.isItemEnabled(item.parentID); return item.parentFragment.isItemEnabled(item.parentID) || item.isForQuote;
} }
} }
} }

View File

@@ -39,7 +39,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public boolean textSelectable; public boolean textSelectable;
public boolean reduceTopPadding; public boolean reduceTopPadding;
public boolean disableTranslate; public boolean disableTranslate;
public final Status status;
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){ public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
super(parentID, parentFragment); super(parentID, parentFragment);
@@ -113,7 +112,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setText(item.text); text.setText(item.text);
} }
text.setTextIsSelectable(false); text.setTextIsSelectable(false);
if(item.textSelectable) itemView.post(() -> text.setTextIsSelectable(true)); if(item.textSelectable && !item.isForQuote) itemView.post(() -> text.setTextIsSelectable(true));
text.setInvalidateOnEveryFrame(false); text.setInvalidateOnEveryFrame(false);
itemView.setClickable(false); itemView.setClickable(false);
itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom()); itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom());
@@ -124,8 +123,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null); StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
if(next!=null && !next.parentID.equals(item.parentID)) next=null; if(next!=null && !next.parentID.equals(item.parentID)) next=null;
int bottomPadding=next instanceof FooterStatusDisplayItem ? V.dp(6) int bottomPadding=item.inset ? V.dp(12)
: item.inset ? V.dp(12) : next instanceof FooterStatusDisplayItem ? V.dp(6)
: (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0 : (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0
: V.dp(12); : V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding); itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
@@ -192,7 +191,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void updateTranslation(boolean updateText){ public void updateTranslation(boolean updateText){
if(item.status==null) if(item.status==null)
return; return;
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()); boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()) && !item.isForQuote;
if(translationFooter==null && translateEnabled){ if(translationFooter==null && translateEnabled){
translationFooter=translationFooterStub.inflate(); translationFooter=translationFooterStub.inflate();
translationInfo=findViewById(R.id.translation_info_text); translationInfo=findViewById(R.id.translation_info_text);
@@ -210,9 +209,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
Translation existingTrans=item.status.getContentStatus().translation; Translation existingTrans=item.status.getContentStatus().translation;
String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null; String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null;
String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language; String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language;
String displayLang=Locale.forLanguageTag(lang != null ? lang Locale locale=lang!=null ? Locale.forLanguageTag(lang) : null;
: AccountSessionManager.get(item.parentFragment.getAccountID()).preferences.postingDefaultLanguage).getDisplayLanguage(); String displayLang=locale==null || locale.getDisplayLanguage().isBlank() ? lang : locale.getDisplayLanguage();
translationButton.setText(item.parentFragment.getString(R.string.translate_post, !displayLang.isBlank() ? displayLang : lang)); translationButton.setText(displayLang!=null
? item.parentFragment.getString(R.string.translate_post, displayLang)
: item.parentFragment.getString(R.string.sk_translate_post));
translationButton.setClickable(true); translationButton.setClickable(true);
translationButton.animate().alpha(1).setDuration(100).start(); translationButton.animate().alpha(1).setDuration(100).start();
translationInfo.setVisibility(View.GONE); translationInfo.setVisibility(View.GONE);

View File

@@ -14,7 +14,6 @@ import java.util.List;
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public boolean loading; public boolean loading;
public final Status status;
public List<StatusDisplayItem> filteredItems; public List<StatusDisplayItem> filteredItems;
public LegacyFilter applyingFilter; public LegacyFilter applyingFilter;

View File

@@ -564,7 +564,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(player==null || !player.isPlaying()) if(player==null || !player.isPlaying())
return; return;
player.pause(); player.pause();
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24); videoPlayPauseButton.setImageResource(R.drawable.ic_fluent_play_24_filled);
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play)); videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
stopUpdatingVideoPosition(); stopUpdatingVideoPosition();
windowView.removeCallbacks(uiAutoHider); windowView.removeCallbacks(uiAutoHider);
@@ -575,7 +575,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(player==null || player.isPlaying()) if(player==null || player.isPlaying())
return; return;
player.start(); player.start();
videoPlayPauseButton.setImageResource(R.drawable.ic_pause_24); videoPlayPauseButton.setImageResource(R.drawable.ic_fluent_pause_24_filled);
videoPlayPauseButton.setContentDescription(activity.getString(R.string.pause)); videoPlayPauseButton.setContentDescription(activity.getString(R.string.pause));
startUpdatingVideoPosition(player); startUpdatingVideoPosition(player);
} }
@@ -940,7 +940,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
@Override @Override
public void onCompletion(MediaPlayer mp){ public void onCompletion(MediaPlayer mp){
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24); videoPlayPauseButton.setImageResource(R.drawable.ic_fluent_play_24_filled);
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play)); videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
stopUpdatingVideoPosition(); stopUpdatingVideoPosition();
if(!uiVisible) if(!uiVisible)

View File

@@ -0,0 +1,26 @@
package org.joinmastodon.android.ui.text;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
public class DiffRemovedSpan extends CharacterStyle {
private final String text;
private final int color;
public DiffRemovedSpan(String text, int color){
this.text=text;
this.color=color;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setStrikeThruText(true);
tp.setColor(color);
}
public String getText() {
return text;
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.ui.text; package org.joinmastodon.android.ui.text;
import android.content.Context; import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
@@ -69,6 +70,10 @@ public class HtmlParser{
private HtmlParser(){} private HtmlParser(){}
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
return parse(source, emojis, mentions, tags, accountID, null);
}
/** /**
* Parse HTML and custom emoji into a spanned string for display. * Parse HTML and custom emoji into a spanned string for display.
* Supported tags: <ul> * Supported tags: <ul>
@@ -81,7 +86,7 @@ public class HtmlParser{
* @param emojis Custom emojis that are present in source as <code>:code:</code> * @param emojis Custom emojis that are present in source as <code>:code:</code>
* @return a spanned string * @return a spanned string
*/ */
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){ public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Context context){
class SpanInfo{ class SpanInfo{
public Object span; public Object span;
public int start; public int start;
@@ -106,6 +111,9 @@ public class HtmlParser{
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity())); Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
final SpannableStringBuilder ssb=new SpannableStringBuilder(); final SpannableStringBuilder ssb=new SpannableStringBuilder();
int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success);
int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error);
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
private final ArrayList<SpanInfo> openSpans=new ArrayList<>(); private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
@@ -171,6 +179,9 @@ public class HtmlParser{
} }
case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el)); case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el));
case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el)); case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el));
// fake elements for the edit history diff view
case "edit-diff-insert" -> openSpans.add(new SpanInfo(new ForegroundColorSpan(colorInsert), ssb.length(), el));
case "edit-diff-delete" -> openSpans.add(new SpanInfo(new DiffRemovedSpan(el.text(), colorDelete), ssb.length(), el));
} }
} }
} }
@@ -210,6 +221,7 @@ public class HtmlParser{
} }
public static void parseCustomEmoji(SpannableStringBuilder ssb, List<Emoji> emojis){ public static void parseCustomEmoji(SpannableStringBuilder ssb, List<Emoji> emojis){
if(emojis==null) return;
Map<String, Emoji> emojiByCode = Map<String, Emoji> emojiByCode =
emojis.stream() emojis.stream()
.collect( .collect(

View File

@@ -5,6 +5,7 @@ import android.text.TextPaint;
import android.text.style.CharacterStyle; import android.text.style.CharacterStyle;
import android.view.View; import android.view.View;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -34,7 +35,7 @@ public class LinkSpan extends CharacterStyle {
@Override @Override
public void updateDrawState(TextPaint tp) { public void updateDrawState(TextPaint tp) {
tp.setColor(color=tp.linkColor); tp.setColor(color=tp.linkColor);
tp.setUnderlineText(true); tp.setUnderlineText(GlobalUserPreferences.underlinedLinks);
} }
public void onClick(Context context){ public void onClick(Context context){
@@ -45,7 +46,7 @@ public class LinkSpan extends CharacterStyle {
if(linkObject instanceof Hashtag ht) if(linkObject instanceof Hashtag ht)
UiUtils.openHashtagTimeline(context, accountID, ht); UiUtils.openHashtagTimeline(context, accountID, ht);
else else
UiUtils.openHashtagTimeline(context, accountID, text); UiUtils.openHashtagTimeline(context, accountID, link);
} }
case CUSTOM -> listener.onLinkClick(this); case CUSTOM -> listener.onLinkClick(this);
} }

View File

@@ -0,0 +1,88 @@
package org.joinmastodon.android.ui.utils;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.IntEvaluator;
import android.animation.ObjectAnimator;
import android.graphics.drawable.Drawable;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import org.joinmastodon.android.R;
import java.util.function.IntSupplier;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.fragments.AppKitFragment;
public class ActionModeHelper{
public static ActionMode startActionMode(AppKitFragment fragment, IntSupplier statusBarColorSupplier, ActionMode.Callback callback){
FragmentStackActivity activity=(FragmentStackActivity) fragment.getActivity();
return activity.startActionMode(new ActionMode.Callback(){
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){
if(!callback.onCreateActionMode(mode, menu))
return false;
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", statusBarColorSupplier.getAsInt(), UiUtils.getThemeColor(activity, R.attr.colorM3Primary));
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.start();
activity.invalidateSystemBarColors(fragment);
View fakeView=new View(activity);
// mode.setCustomView(fakeView);
// int buttonID=activity.getResources().getIdentifier("action_mode_close_button", "id", "android");
// View btn=activity.getWindow().getDecorView().findViewById(buttonID);
// if(btn!=null){
// ((ViewGroup.MarginLayoutParams)btn.getLayoutParams()).setMarginEnd(0);
// }
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
if(!callback.onPrepareActionMode(mode, menu))
return false;
for(int i=0;i<menu.size();i++){
Drawable icon=menu.getItem(i).getIcon();
if(icon!=null){
icon=icon.mutate();
icon.setTint(UiUtils.getThemeColor(activity, R.attr.colorM3OnPrimary));
menu.getItem(i).setIcon(icon);
}
}
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
return callback.onActionItemClicked(mode, item);
}
@Override
public void onDestroyActionMode(ActionMode mode){
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", UiUtils.getThemeColor(activity, R.attr.colorM3Primary), statusBarColorSupplier.getAsInt());
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
activity.getWindow().setStatusBarColor(0);
}
});
anim.start();
activity.invalidateSystemBarColors(fragment);
callback.onDestroyActionMode(mode);
}
});
}
}

View File

@@ -68,6 +68,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.StatusInteractionController;
@@ -479,7 +480,7 @@ public class UiUtils {
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback) { public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title), showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName), activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.getDisplayName()),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_person_prohibited_28_regular, R.drawable.ic_fluent_person_prohibited_28_regular,
() -> { () -> {
@@ -507,7 +508,7 @@ public class UiUtils {
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback) { public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, showConfirmationAlert(activity,
activity.getString(R.string.sk_remove_follower), activity.getString(R.string.sk_remove_follower),
activity.getString(R.string.sk_remove_follower_confirm, account.displayName), activity.getString(R.string.sk_remove_follower_confirm, account.getDisplayName()),
activity.getString(R.string.sk_do_remove_follower), activity.getString(R.string.sk_do_remove_follower),
R.drawable.ic_fluent_person_delete_24_regular, R.drawable.ic_fluent_person_delete_24_regular,
() -> new SetAccountBlocked(account.id, true).setCallback(new Callback<>() { () -> new SetAccountBlocked(account.id, true).setCallback(new Callback<>() {
@@ -565,7 +566,7 @@ public class UiUtils {
params.setMargins(0, V.dp(-12), 0, 0); params.setMargins(0, V.dp(-12), 0, 0);
durationView.setLayoutParams(params); durationView.setLayoutParams(params);
Button button=durationView.findViewById(R.id.button); Button button=durationView.findViewById(R.id.button);
((TextView) durationView.findViewById(R.id.message)).setText(context.getString(R.string.confirm_mute, account.displayName)); ((TextView) durationView.findViewById(R.id.message)).setText(context.getString(R.string.confirm_mute, account.getDisplayName()));
AtomicReference<Duration> muteDuration=new AtomicReference<>(Duration.ZERO); AtomicReference<Duration> muteDuration=new AtomicReference<>(Duration.ZERO);
@@ -599,7 +600,7 @@ public class UiUtils {
new M3AlertDialogBuilder(context) new M3AlertDialogBuilder(context)
.setTitle(context.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title)) .setTitle(context.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title))
.setMessage(currentlyMuted ? context.getString(R.string.confirm_unmute, account.displayName) : null) .setMessage(currentlyMuted ? context.getString(R.string.confirm_unmute, account.getDisplayName()) : null)
.setView(currentlyMuted ? null : durationView) .setView(currentlyMuted ? null : durationView)
.setPositiveButton(context.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), (dlg, i)->{ .setPositiveButton(context.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), (dlg, i)->{
new SetAccountMuted(account.id, !currentlyMuted, muteDuration.get().getSeconds()) new SetAccountMuted(account.id, !currentlyMuted, muteDuration.get().getSeconds())
@@ -625,23 +626,22 @@ public class UiUtils {
.show(); .show();
} }
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback) {
confirmDeletePost(activity, accountID, status, resultCallback, false);
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft) { public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft) {
Status s=status.getContentStatus();
showConfirmationAlert(activity, showConfirmationAlert(activity,
forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title, forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title,
forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete, forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete,
forRedraft ? R.string.sk_delete_and_redraft : R.string.delete, forRedraft ? R.string.sk_delete_and_redraft : R.string.delete,
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular, forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus(status.id) () -> new DeleteStatus(s.id)
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Status result) { public void onSuccess(Status result) {
resultCallback.accept(result); resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id); E.post(new StatusDeletedEvent(s.id, accountID));
E.post(new StatusDeletedEvent(status.id, accountID)); if(status!=s){
E.post(new StatusDeletedEvent(status.id, accountID));
}
} }
@Override @Override
@@ -779,8 +779,8 @@ public class UiUtils {
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow); button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
styleRes=R.style.Widget_Mastodon_M3_Button_Filled; styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}else{ }else{
button.setText(R.string.button_following); button.setText(relationship.followedBy ? R.string.sk_button_mutuals : R.string.button_following);
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal; styleRes=relationship.followedBy ? R.style.Widget_Mastodon_M3_Button_Tonal_Outlined : R.style.Widget_Mastodon_M3_Button_Tonal;
} }
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background}); TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
@@ -797,25 +797,32 @@ public class UiUtils {
} else if (relationship.muting) { } else if (relationship.muting) {
confirmToggleMuteUser(activity, accountID, account, true, resultCallback); confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
} else { } else {
progressCallback.accept(true); Runnable action=()->{
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true, false) progressCallback.accept(true);
.setCallback(new Callback<>() { new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
@Override .setCallback(new Callback<>(){
public void onSuccess(Relationship result) { @Override
resultCallback.accept(result); public void onSuccess(Relationship result){
progressCallback.accept(false); resultCallback.accept(result);
if(!result.following && !result.requested){ progressCallback.accept(false);
E.post(new RemoveAccountPostsEvent(accountID, account.id, true)); if(!result.following && !result.requested){
E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
}
} }
}
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error){
error.showToast(activity); error.showToast(activity);
progressCallback.accept(false); progressCallback.accept(false);
} }
}) })
.exec(accountID); .exec(accountID);
};
if(relationship.following && GlobalUserPreferences.confirmUnfollow){
showConfirmationAlert(activity, null, activity.getString(R.string.unfollow_confirmation, account.getDisplayUsername()), activity.getString(R.string.unfollow), R.drawable.ic_fluent_person_delete_24_regular, action);
}else{
action.run();
}
} }
} }
@@ -899,17 +906,26 @@ public class UiUtils {
} }
public static void insetPopupMenuIcon(Context context, MenuItem item) { public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary)); insetPopupMenuIcon(context, item, 0);
insetPopupMenuIcon(item, iconTint);
} }
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) { public static void insetPopupMenuIcon(Context context, MenuItem item, int addWidth) {
Drawable icon = item.getIcon().mutate(); ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
if (Build.VERSION.SDK_INT >= 26) item.setIconTintList(iconTint); insetPopupMenuIcon(item, iconTint, addWidth);
}
/**
* @param addWidth set if icon is too wide/narrow. if icon is 25dp in width, set to -1dp
*/
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint, int addWidth) {
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint); else icon.setTintList(iconTint);
icon = new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0); int pad=V.dp(8);
boolean rtl=icon.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
icon=new InsetDrawable(icon, rtl ? pad+addWidth : pad, 0, rtl ? pad : addWidth+pad, 0);
item.setIcon(icon); item.setIcon(icon);
SpannableStringBuilder ssb = new SpannableStringBuilder(item.getTitle()); SpannableStringBuilder ssb = new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb); item.setTitle(ssb);
} }
@@ -943,7 +959,7 @@ public class UiUtils {
if (subMenu != null) enableMenuIcons(context, subMenu, exclude); if (subMenu != null) enableMenuIcons(context, subMenu, exclude);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId()))
continue; continue;
insetPopupMenuIcon(item, iconTint); insetPopupMenuIcon(item, iconTint, 0);
} }
} }
@@ -973,8 +989,8 @@ public class UiUtils {
default -> R.style.Theme_Mastodon_AutoLightDark; default -> R.style.Theme_Mastodon_AutoLightDark;
}); });
AccountLocalPreferences prefs=session != null ? session.getLocalPreferences() : null; AccountLocalPreferences prefs=session!=null ? session.getLocalPreferences() : null;
AccountLocalPreferences.ColorPreference color=prefs != null ? prefs.color : AccountLocalPreferences.ColorPreference.MATERIAL3; AccountLocalPreferences.ColorPreference color=prefs!=null ? prefs.getCurrentColor() : AccountLocalPreferences.ColorPreference.MATERIAL3;
ColorPalette palette = ColorPalette.palettes.get(color); ColorPalette palette = ColorPalette.palettes.get(color);
if (palette != null) palette.apply(context, theme); if (palette != null) palette.apply(context, theme);
@@ -1100,23 +1116,21 @@ public class UiUtils {
return back; return back;
} }
public static boolean setExtraTextInfo(Context ctx, @Nullable TextView extraText, @Nullable TextView pronouns, boolean displayPronouns, boolean mentionedOnly, boolean localOnly, @Nullable Account account) { public static boolean setExtraTextInfo(Context ctx, @Nullable TextView extraText, boolean displayPronouns, boolean mentionedOnly, boolean localOnly, @Nullable Account account) {
List<String> extraParts = extraText!=null && (localOnly || mentionedOnly) ? new ArrayList<>() : null; List<String> extraParts=new ArrayList<>();
Optional<String> p=pronouns==null || !displayPronouns ? Optional.empty() : extractPronouns(ctx, account); Optional<String> p=!displayPronouns ? Optional.empty() : extractPronouns(ctx, account);
if(p.isPresent()) {
HtmlParser.setTextWithCustomEmoji(pronouns, p.get(), account.emojis);
pronouns.setVisibility(View.VISIBLE);
}else if(pronouns!=null){
pronouns.setVisibility(View.GONE);
}
if(localOnly) if(localOnly)
extraParts.add(ctx.getString(R.string.sk_inline_local_only)); extraParts.add(ctx.getString(R.string.sk_inline_local_only));
if(mentionedOnly) if(mentionedOnly)
extraParts.add(ctx.getString(R.string.sk_inline_direct)); extraParts.add(ctx.getString(R.string.sk_inline_direct));
if(extraText!=null && extraParts!=null && !extraParts.isEmpty()) { if(p.isPresent() && extraParts.isEmpty())
String sepp = ctx.getString(R.string.sk_separator); extraParts.add(p.get());
String text = String.join(" " + sepp + " ", extraParts);
if(account == null) extraText.setText(text); if(extraText!=null && !extraParts.isEmpty()) {
String sepp=ctx.getString(R.string.sk_separator);
String text=String.join(" " + sepp + " ", extraParts);
if(account==null) extraText.setText(text);
else HtmlParser.setTextWithCustomEmoji(extraText, text, account.emojis); else HtmlParser.setTextWithCustomEmoji(extraText, text, account.emojis);
extraText.setVisibility(View.VISIBLE); extraText.setVisibility(View.VISIBLE);
return true; return true;
@@ -1669,12 +1683,14 @@ public class UiUtils {
"pronouns.page/" "pronouns.page/"
}; };
private static final Pattern trimPronouns=Pattern.compile("[^\\w*]*([\\w*].*[\\w*]|[\\w*])\\W*"); private static final String PRONOUN_CHARS="\\w*¿¡!?";
private static final Pattern trimPronouns=
Pattern.compile("[^"+PRONOUN_CHARS+"]*(["+PRONOUN_CHARS+"].*["+PRONOUN_CHARS+"]|["+PRONOUN_CHARS+"])\\W*");
private static String extractPronounsFromField(String localizedPronouns, AccountField field) { private static String extractPronounsFromField(String localizedPronouns, AccountField field) {
if(!field.name.toLowerCase().contains(localizedPronouns) && if(!field.name.toLowerCase().contains(localizedPronouns) &&
!field.name.toLowerCase().contains("pronouns")) return null; !field.name.toLowerCase().contains("pronouns")) return null;
String text=HtmlParser.text(field.value); String text=HtmlParser.text(field.value);
if(field.value.toLowerCase().contains("https://")){ if(text.toLowerCase().contains("https://")){
for(String pronounUrl : pronounsUrls){ for(String pronounUrl : pronounsUrls){
int index=text.indexOf(pronounUrl); int index=text.indexOf(pronounUrl);
int beginPronouns=index+pronounUrl.length(); int beginPronouns=index+pronounUrl.length();
@@ -1692,14 +1708,24 @@ public class UiUtils {
Matcher matcher=trimPronouns.matcher(text); Matcher matcher=trimPronouns.matcher(text);
if(!matcher.find()) return null; if(!matcher.find()) return null;
String matched=matcher.group(1); String pronouns=matcher.group(1);
// crude fix to allow for pronouns like "it(/she)"
int missingClosingParens=0; // crude fix to allow for pronouns like "it(/she)" or "(de) sie/ihr"
for(char c : matched.toCharArray()){ int missingParens=0, missingBrackets=0;
if(c=='(') missingClosingParens++; for(char c : pronouns.toCharArray()){
if(c==')') missingClosingParens--; if(c=='(') missingParens++;
else if(c=='[') missingBrackets++;
else if(c==')') missingParens--;
else if(c==']') missingBrackets--;
} }
return matched+")".repeat(Math.max(0, missingClosingParens)); if(missingParens > 0) pronouns+=")".repeat(missingParens);
else if(missingParens < 0) pronouns="(".repeat(missingParens*-1)+pronouns;
if(missingBrackets > 0) pronouns+="]".repeat(missingBrackets);
else if(missingBrackets < 0) pronouns="[".repeat(missingBrackets*-1)+pronouns;
// if ends with an un-closed custom emoji
if(pronouns.matches("^.*\\s+:[a-zA-Z_]+$")) pronouns+=':';
return pronouns;
} }
// https://stackoverflow.com/questions/9475589/how-to-get-string-from-different-locales-in-android // https://stackoverflow.com/questions/9475589/how-to-get-string-from-different-locales-in-android
@@ -1711,7 +1737,7 @@ public class UiUtils {
} }
public static Optional<String> extractPronouns(Context context, @Nullable Account account) { public static Optional<String> extractPronouns(Context context, @Nullable Account account) {
if (account == null) return Optional.empty(); if (account==null || account.fields==null) return Optional.empty();
String localizedPronouns=context.getString(R.string.sk_pronouns_label).toLowerCase(); String localizedPronouns=context.getString(R.string.sk_pronouns_label).toLowerCase();
// higher = worse. the lowest number wins. also i'm sorry for writing this // higher = worse. the lowest number wins. also i'm sorry for writing this

View File

@@ -4,11 +4,12 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@@ -19,6 +20,7 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.HorizontalScrollView; import android.widget.HorizontalScrollView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
@@ -27,8 +29,8 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable; import androidx.annotation.StringRes;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -51,11 +53,8 @@ import org.parceler.Parcel;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -204,6 +203,15 @@ public class ComposeMediaViewController{
} }
} }
private void updateButton(ImageButton btn, @DrawableRes int drawableId, @StringRes int labelId){
btn.setImageResource(drawableId);
String label=fragment.getContext().getString(labelId);
btn.setContentDescription(label);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
btn.setTooltipText(label);
}
}
private View createMediaAttachmentView(DraftMediaAttachment draft){ private View createMediaAttachmentView(DraftMediaAttachment draft){
View thumb=fragment.getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false); View thumb=fragment.getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
ImageView img=thumb.findViewById(R.id.thumb); ImageView img=thumb.findViewById(R.id.thumb);
@@ -231,12 +239,11 @@ public class ComposeMediaViewController{
draft.removeButton.setOnClickListener(this::onRemoveMediaAttachmentClick); draft.removeButton.setOnClickListener(this::onRemoveMediaAttachmentClick);
draft.editButton.setTag(draft); draft.editButton.setTag(draft);
thumb.setOutlineProvider(OutlineProviders.roundedRect(12)); thumb.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
thumb.setClipToOutline(true); thumb.setClipToOutline(true);
img.setOutlineProvider(OutlineProviders.roundedRect(12)); img.setOutlineProvider(OutlineProviders.roundedRect(12));
img.setClipToOutline(true); img.setClipToOutline(true);
thumb.setBackgroundColor(UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3Surface));
thumb.setOnLongClickListener(v->{ thumb.setOnLongClickListener(v->{
if(!v.hasTransientState() && attachments.size()>1){ if(!v.hasTransientState() && attachments.size()>1){
attachmentsView.startDragging(v); attachmentsView.startDragging(v);
@@ -266,11 +273,11 @@ public class ComposeMediaViewController{
draft.subtitleView.setText(subtitleRes); draft.subtitleView.setText(subtitleRes);
} }
draft.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, 0)); draft.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, 0));
draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24); updateButton(draft.removeButton, R.drawable.ic_fluent_dismiss_24_regular, R.string.delete);
if(draft.state==AttachmentUploadState.ERROR){ if(draft.state==AttachmentUploadState.ERROR){
draft.titleView.setText(R.string.upload_failed); draft.titleView.setText(R.string.upload_failed);
draft.editButton.setImageResource(R.drawable.ic_fluent_arrow_counterclockwise_24_regular); updateButton(draft.removeButton, R.drawable.ic_fluent_arrow_counterclockwise_24_regular, R.string.retry);
draft.editButton.setOnClickListener(this::onRetryOrCancelMediaUploadClick); draft.editButton.setOnClickListener(this::onRetryOrCancelMediaUploadClick);
draft.progressBar.setVisibility(View.GONE); draft.progressBar.setVisibility(View.GONE);
draft.setUseErrorColors(true); draft.setUseErrorColors(true);
@@ -280,7 +287,7 @@ public class ComposeMediaViewController{
draft.editButton.setOnClickListener(this::onEditMediaDescriptionClick); draft.editButton.setOnClickListener(this::onEditMediaDescriptionClick);
}else{ }else{
draft.editButton.setVisibility(View.GONE); draft.editButton.setVisibility(View.GONE);
draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24); updateButton(draft.removeButton, R.drawable.ic_fluent_dismiss_24_regular, R.string.delete);
if(draft.state==AttachmentUploadState.PROCESSING){ if(draft.state==AttachmentUploadState.PROCESSING){
draft.titleView.setText(R.string.upload_processing); draft.titleView.setText(R.string.upload_processing);
}else{ }else{
@@ -374,7 +381,7 @@ public class ComposeMediaViewController{
// attachment.retryButton.setContentDescription(fragment.getString(R.string.retry_upload)); // attachment.retryButton.setContentDescription(fragment.getString(R.string.retry_upload));
V.setVisibilityAnimated(attachment.editButton, View.VISIBLE); V.setVisibilityAnimated(attachment.editButton, View.VISIBLE);
attachment.editButton.setImageResource(R.drawable.ic_fluent_arrow_counterclockwise_24_regular); updateButton(attachment.editButton, R.drawable.ic_fluent_arrow_counterclockwise_24_regular, R.string.retry);
attachment.editButton.setOnClickListener(ComposeMediaViewController.this::onRetryOrCancelMediaUploadClick); attachment.editButton.setOnClickListener(ComposeMediaViewController.this::onRetryOrCancelMediaUploadClick);
attachment.setUseErrorColors(true); attachment.setUseErrorColors(true);
V.setVisibilityAnimated(attachment.progressBar, View.GONE); V.setVisibilityAnimated(attachment.progressBar, View.GONE);
@@ -478,8 +485,8 @@ public class ComposeMediaViewController{
throw new IllegalStateException("Unexpected state "+attachment.state); throw new IllegalStateException("Unexpected state "+attachment.state);
attachment.uploadRequest=null; attachment.uploadRequest=null;
attachment.state=AttachmentUploadState.DONE; attachment.state=AttachmentUploadState.DONE;
attachment.editButton.setImageResource(R.drawable.ic_fluent_edit_24_regular); updateButton(attachment.editButton, R.drawable.ic_fluent_edit_24_regular, R.string.sk_edit_alt_text);
attachment.removeButton.setImageResource(R.drawable.ic_fluent_delete_24_regular); updateButton(attachment.removeButton, R.drawable.ic_fluent_dismiss_24_regular, R.string.delete);
attachment.editButton.setOnClickListener(this::onEditMediaDescriptionClick); attachment.editButton.setOnClickListener(this::onEditMediaDescriptionClick);
V.setVisibilityAnimated(attachment.progressBar, View.GONE); V.setVisibilityAnimated(attachment.progressBar, View.GONE);
V.setVisibilityAnimated(attachment.editButton, View.VISIBLE); V.setVisibilityAnimated(attachment.editButton, View.VISIBLE);
@@ -708,18 +715,21 @@ public class ComposeMediaViewController{
if(errorTransitionAnimator!=null) if(errorTransitionAnimator!=null)
errorTransitionAnimator.cancel(); errorTransitionAnimator.cancel();
AnimatorSet set=new AnimatorSet(); AnimatorSet set=new AnimatorSet();
int color1, color2, color3; int defaultBg=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Surface);
int errorBg=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3ErrorContainer);
int color2, color3;
if(use){ if(use){
color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3ErrorContainer);
color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Error); color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Error);
color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnErrorContainer); color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnErrorContainer);
}else{ }else{
color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Surface);
color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurface); color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurface);
color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant); color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant);
} }
GradientDrawable bg=(GradientDrawable) view.getBackground().mutate();
ValueAnimator bgAnim=ValueAnimator.ofArgb(use ? defaultBg : errorBg, use ? errorBg : defaultBg);
bgAnim.addUpdateListener(anim->bg.setColor((Integer) anim.getAnimatedValue()));
set.playTogether( set.playTogether(
ObjectAnimator.ofArgb(view, "backgroundColor", ((ColorDrawable)view.getBackground()).getColor(), color1), bgAnim,
ObjectAnimator.ofArgb(titleView, "textColor", titleView.getCurrentTextColor(), color2), ObjectAnimator.ofArgb(titleView, "textColor", titleView.getCurrentTextColor(), color2),
ObjectAnimator.ofArgb(subtitleView, "textColor", subtitleView.getCurrentTextColor(), color3), ObjectAnimator.ofArgb(subtitleView, "textColor", subtitleView.getCurrentTextColor(), color3),
ObjectAnimator.ofArgb(removeButton.getDrawable(), "tint", subtitleView.getCurrentTextColor(), color3) ObjectAnimator.ofArgb(removeButton.getDrawable(), "tint", subtitleView.getCurrentTextColor(), color3)

View File

@@ -42,6 +42,7 @@ public class ComposePollViewController{
30*60, 30*60,
3600, 3600,
6*3600, 6*3600,
12*3600,
24*3600, 24*3600,
3*24*3600, 3*24*3600,
7*24*3600, 7*24*3600,
@@ -134,7 +135,10 @@ public class ComposePollViewController{
DraftPollOption opt=createDraftPollOption(false); DraftPollOption opt=createDraftPollOption(false);
opt.edit.setText(eopt.title); opt.edit.setText(eopt.title);
} }
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond(); if(fragment.scheduledStatus!=null && fragment.scheduledStatus.params.poll!=null)
pollDuration=Integer.parseInt(fragment.scheduledStatus.params.poll.expiresIn);
else if(fragment.editingStatus.poll.expiresAt!=null)
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond();
updatePollOptionHints(); updatePollOptionHints();
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration)); pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
pollIsMultipleChoice=fragment.editingStatus.poll.multiple; pollIsMultipleChoice=fragment.editingStatus.poll.multiple;

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