Compare commits

..

284 Commits

Author SHA1 Message Date
sk
3e3ed050ba bump version 2023-01-09 17:18:43 +01:00
sk
84179bc207 implement announcements
closes #127
2023-01-09 17:16:55 +01:00
sk
9dc795ded7 don't connect to fascists 2023-01-09 12:25:48 +01:00
sk
9c733d65b2 tweak header text margin 2023-01-09 11:24:02 +01:00
sk
4d49890b1e hide keyboard when navigating
closes #227
2023-01-09 11:15:14 +01:00
sk
f78c6978cf scale text according to system
closes #244
2023-01-09 11:11:00 +01:00
sk
b58a3d89e8 fix wrong pin icon
closes #230
2023-01-09 10:47:12 +01:00
sk
cd0cfba7c0 click "replying to" to scroll up
closes #241
2023-01-09 10:44:51 +01:00
sk
713c95d597 tweak reply animation
closes #237
2023-01-09 10:38:30 +01:00
sk
15534ad42e don't add subject if blank
closes #240
2023-01-09 10:30:49 +01:00
Thiago 'Jedi' Abreu
32bb3fac69 Filter all status, even if filters are empty (#255) 2023-01-09 10:23:14 +01:00
sk
e604da4ff4 Merge remote-tracking branch 'upstream/master' 2023-01-09 10:22:25 +01:00
Gregory K
60517b00f3 Merge pull request #491 from mishnz/master
Second fix for MIME64 inconsistency in serverKey.
2023-01-02 14:23:23 +03:00
mishnz
faf5e8e82b Merge branch 'master' of https://github.com/mishnz/mastodon-android 2023-01-03 00:16:12 +13:00
mishnz
7264982761 serverKey assignment was missing, corrected. 2023-01-03 00:15:45 +13:00
mishnz
fedf74258f Merge branch 'mastodon:master' into master 2023-01-03 00:06:53 +13:00
mishnz
def4960be6 Second fix for MIME64 inconsistency in serverKey.
The previous fix https://github.com/mastodon/mastodon-android/pull/486 would break any connections to any instances using WEB_SAFE MIME64 encoding on the serverKey, which actually appears to be the usual case.
This update reverts to the previous logic, but also converts standard MIME64 characters ('/' and '+') to their WEB_SAFE equivalents.
This ensures the standard case of WEB_SAFE BASE64 serverKeys and the anomolous case of DEFAULT BASE64 keys both work.
2023-01-02 23:56:52 +13:00
Eugen Rochko
525cc69c70 Merge pull request #486 from mishnz/master
The Mastodon server does not currently use URL_SAFE encoding on its s…
2023-01-01 10:01:17 +01:00
mishnz
7ed1b164b5 The Mastodon server does not currently use URL_SAFE encoding on its serverKey. Using URL_SAFE in this client means the client will crash for any server that uses a key that generates a Mime64 string containing a "+" or "/". This change removes the URL_SAFE logic. See: https://github.com/mastodon/mastodon-android/issues/483 2023-01-01 18:50:09 +13:00
sk
0a9c31fb09 update changelog 2022-12-31 03:13:06 +01:00
sk
5ed6f97846 update changelog 2022-12-31 03:08:08 +01:00
sk
7dc195606c fix empty space when no text 2022-12-31 03:07:14 +01:00
sk
b2377a3353 tweak scroll height/timing 2022-12-31 02:54:33 +01:00
sk
0fdae0c775 Revert "match navigation bar color with toolbar"
This reverts commit 43d334259b.
2022-12-31 02:50:18 +01:00
sk
113bbd960f move settings items 2022-12-31 02:40:49 +01:00
sk
a48e09e77b bump version 2022-12-31 02:02:38 +01:00
sk
3c3f759d9a update readme 2022-12-31 02:02:00 +01:00
sk
5f0986d03b add option to reduce motion 2022-12-31 02:00:40 +01:00
sk
579794d7e0 peek original post before scrolling 2022-12-31 01:52:07 +01:00
sk
4956543eac update changelog 2022-12-31 01:37:39 +01:00
sk
87043f19bc Merge remote-tracking branch 'weblate/main' 2022-12-31 01:35:07 +01:00
sk
375f8ceb27 display original post when replying
closes sk22#193
2022-12-31 01:33:37 +01:00
sk
496ad6a442 change interact with strings 2022-12-30 23:08:04 +01:00
ihor_ck
83a09c4af2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (12 of 12 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2022-12-30 21:54:40 +00:00
gallegonovato
fac6985d01 Translated using Weblate (Spanish)
Currently translated at 100.0% (12 of 12 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-30 21:54:40 +00:00
Linerly
43670ba62b Translated using Weblate (Indonesian)
Currently translated at 100.0% (12 of 12 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2022-12-30 21:54:40 +00:00
Choukajohn
ef511349f8 Translated using Weblate (French)
Currently translated at 100.0% (12 of 12 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2022-12-30 21:54:40 +00:00
ihor_ck
eea0199a21 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (121 of 121 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2022-12-30 21:54:40 +00:00
Linerly
7e0f02ecc7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (121 of 121 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-30 21:54:40 +00:00
Choukajohn
7b26a65ea0 Translated using Weblate (French)
Currently translated at 100.0% (121 of 121 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-30 21:54:40 +00:00
gallegonovato
c5b92d7162 Translated using Weblate (Spanish)
Currently translated at 100.0% (121 of 121 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-30 21:54:40 +00:00
bubblineyuri
f8387f0a81 Translated using Weblate (German)
Currently translated at 100.0% (12 of 12 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2022-12-30 21:54:40 +00:00
bubblineyuri
8486326b00 Translated using Weblate (German)
Currently translated at 100.0% (121 of 121 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-30 21:54:40 +00:00
edxkl
8f64747e75 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2022-12-30 21:54:40 +00:00
ihor_ck
f75efdaa03 Translated using Weblate (Ukrainian)
Currently translated at 96.6% (115 of 119 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2022-12-30 21:54:40 +00:00
Linerly
ee8d9d0c07 Translated using Weblate (Indonesian)
Currently translated at 100.0% (119 of 119 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-30 21:54:40 +00:00
ling0412
6791adef46 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (119 of 119 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2022-12-30 21:54:40 +00:00
irure
80bcb84acf Translated using Weblate (Basque)
Currently translated at 18.1% (2 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/eu/
2022-12-30 21:54:40 +00:00
AiOO
7d6b0f9ca8 Translated using Weblate (Korean)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2022-12-30 21:54:39 +00:00
nkufideal
594f6d4fc5 Translated using Weblate (Russian)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2022-12-30 21:54:39 +00:00
nkufideal
e40acb9bf6 Translated using Weblate (Russian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2022-12-30 21:54:39 +00:00
itslameni
e372871108 Translated using Weblate (Russian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2022-12-30 21:54:39 +00:00
edxkl
1c01469a3e Translated using Weblate (Portuguese (Brazil))
Currently translated at 80.3% (82 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2022-12-30 21:54:39 +00:00
irure
90c2e45be2 Translated using Weblate (Basque)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/eu/
2022-12-30 21:54:39 +00:00
AiOO
1a783d4faf Translated using Weblate (Korean)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2022-12-30 21:54:39 +00:00
edxkl
d2944983a4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2022-12-30 21:54:39 +00:00
ihor_ck
26459eecb0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2022-12-30 21:54:39 +00:00
Linerly
90dea16222 Translated using Weblate (Indonesian)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-30 21:54:39 +00:00
Choukajohn
8897a326b3 Translated using Weblate (French)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-30 21:54:39 +00:00
gallegonovato
f3e69ddef1 Translated using Weblate (Spanish)
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-30 21:54:39 +00:00
ling0412
8ecaa2c4d1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (102 of 102 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2022-12-30 21:54:39 +00:00
sk
3a607a81f6 add compose fab to drafts list 2022-12-30 22:54:07 +01:00
sk
4b113e4a82 handle recursive compose/drafts list 2022-12-30 22:34:41 +01:00
sk
3e0e0c1484 move and refine scheduling and drafting 2022-12-30 21:48:59 +01:00
sk
676e166459 move compose action to layout file 2022-12-30 19:24:30 +01:00
sk
f0a704b93b schedule/draft in overflow menu 2022-12-29 20:24:37 +01:00
sk
cfc528df9a Merge remote-tracking branch 'origin/main' 2022-12-29 20:12:31 +01:00
sk
9231ea1446 offer to save when scheduledAt changed
closes sk22#218
2022-12-29 20:12:08 +01:00
sk22
f2f9138435 Update README.md 2022-12-29 18:39:32 +01:00
sk
624700497b update changelog 2022-12-29 18:28:24 +01:00
sk
5794a64da6 use rules fragment in settings 2022-12-29 18:27:37 +01:00
sk
7e6e9d3dcd move translation availability
closes sk22#213
2022-12-29 18:14:26 +01:00
sk
07634edfa3 don't finish compose fragment 2022-12-29 18:14:00 +01:00
sk
4a03fbfbf0 update changelog, bump version 2022-12-29 18:01:50 +01:00
sk
000cdb08ec change fab styles
closes sk22#211
2022-12-29 18:01:22 +01:00
sk
e0521b3c95 add mention with quasi-quoting 2022-12-29 17:14:38 +01:00
sk22
9547be89e1 Drafts and scheduled posts (#217)
closes #100
closes #59 

* enable saving as draft (scheduled)
* add scheduled posts list
* fix NoSuchMethodError
* editable drafts/scheduled posts
* ui for drafts
* use instants between 9999-01-01 and 9999-12-31
* use save and draft strings
* map scheduled status params to status
* implement scheduling posts
* improve save/discard draft dialog
* persist scheduled date in state
* add unsent posts button to toolbar
* clean up imports
2022-12-29 16:53:18 +01:00
sk
40d44269fc Merge branch 'main' of github.com:sk22/megalodon 2022-12-29 16:46:14 +01:00
sk
3bf0903453 fix toolbar inset not applying in true black 2022-12-29 16:46:10 +01:00
Thiago 'Jedi' Abreu
c3abf8c05c Better handling of filter expiration date (#212)
* Better handling of filter expiration date
* Simplify Thread and Home Timeline filtering
2022-12-29 13:32:22 +01:00
Thiago 'Jedi' Abreu
6220ce6780 Implement 4.0 filters with hide action (#202)
* adding new "filtered" field for status
* respect "hide" filter action on status
* handling expire date for filter

closes #161
2022-12-28 14:49:45 +01:00
sk
6107698a76 optionally forward report
closes #205
2022-12-28 14:45:20 +01:00
sk
a3c3fec9b4 add icon indicating the existence of replies
closes #207
2022-12-28 14:13:49 +01:00
sk
f3a9b19104 replace mute icon 2022-12-28 01:58:44 +01:00
sk
d9ed6f600b change animation duration 2022-12-28 01:39:06 +01:00
sk
8a6d86727c remove unused menu item 2022-12-28 01:35:53 +01:00
sk
d9abf82918 fix null pointer on loading profile 2022-12-28 01:35:04 +01:00
sk
3cd9020ee0 use short usernames in popup menus
improves upon mastodon#148
2022-12-28 00:58:57 +01:00
sk
6d22a4d014 icons. icons everywhere 2022-12-28 00:53:06 +01:00
sk
ccad5d40ec fix akkoma crash on edit 2022-12-27 22:00:19 +01:00
ihor_ck
5b7b022d9f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2022-12-27 20:46:35 +00:00
Andrewblasco
387139b5d3 Translated using Weblate (Spanish)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-27 20:46:35 +00:00
edxkl
0ae10d5fbe Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2022-12-27 20:46:35 +00:00
ihor_ck
d53397d8f8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2022-12-27 20:46:35 +00:00
sk
b1006d2a14 rename string 2022-12-27 21:46:21 +01:00
sk
c5cf318cdd Merge remote-tracking branch 'weblate/main' 2022-12-27 21:45:01 +01:00
sk
671338c16a different icon for delete notifs setting 2022-12-27 21:41:08 +01:00
sk
bbaa70e396 add option to use uniform notif icon 2022-12-27 21:39:41 +01:00
sk
536d6cf63e revert broken color attribute for notif icon
closes #190
2022-12-27 21:30:35 +01:00
sk
b564a297ab fix #200 2022-12-27 21:20:20 +01:00
sk
6d34ae1a50 long-click to compose from other account 2022-12-27 21:02:05 +01:00
sk
0d2457f39e restore accidentally deleted functionality 2022-12-27 20:43:29 +01:00
sk
420505328c fix default visibility crashing akkoma session
closes #196
2022-12-27 20:36:06 +01:00
sk
0e60d71006 refine long-clicks
and disable long-click for multiple accounts when logged in to single account
2022-12-27 20:30:58 +01:00
sk
0f1456819b interact as other accounts
closes #199
2022-12-27 19:53:01 +01:00
sk
3af89a6175 update readme 2022-12-27 13:32:23 +01:00
gallegonovato
2fac871493 Translated using Weblate (Spanish)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-26 09:34:47 +00:00
Pointifurry
0166133b61 Translated using Weblate (Spanish)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-26 09:34:46 +00:00
Linerly
a9e0911796 Translated using Weblate (Indonesian)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2022-12-26 09:34:46 +00:00
ling0412
213f341257 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2022-12-26 09:34:46 +00:00
Choukajohn
01cc2a2c67 Translated using Weblate (French)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2022-12-26 09:34:46 +00:00
nitrogenez
6c7a040c02 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2022-12-26 09:34:46 +00:00
itslameni
4e40944b26 Translated using Weblate (Russian)
Currently translated at 96.7% (88 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2022-12-26 09:34:46 +00:00
goliv
af859438b4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 74.7% (68 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2022-12-26 09:34:46 +00:00
Linerly
7ef80e87b8 Translated using Weblate (Indonesian)
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-26 09:34:45 +00:00
Choukajohn
f8e873cd78 Translated using Weblate (French)
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-26 09:34:45 +00:00
Pointifurry
5cc97f7cf1 Translated using Weblate (Spanish)
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-26 09:34:45 +00:00
ling0412
44c56331fd Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2022-12-26 09:34:45 +00:00
sk22
1272c3962e Translated using Weblate (German)
Currently translated at 100.0% (91 of 91 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-26 09:34:45 +00:00
sk
e2799fcdff fix metadata markdown 2022-12-23 23:58:59 +01:00
sk
8e4f402e21 update changelogs 2022-12-23 23:48:21 +01:00
sk
10d2949647 remove unused method 2022-12-23 23:43:19 +01:00
sk
30ef23308b bump version 2022-12-23 23:41:46 +01:00
sk
564132ee82 update changelog 2022-12-23 23:40:50 +01:00
sk
1e527e87df fix default reblog visibility 2022-12-23 23:39:51 +01:00
sk
9692a04275 Merge remote-tracking branch 'weblate/main' 2022-12-23 23:36:48 +01:00
Linerly
6ece8eb0c1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2022-12-23 22:18:14 +00:00
Choukajohn
1f33237e8a Translated using Weblate (French)
Currently translated at 100.0% (11 of 11 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2022-12-23 22:18:14 +00:00
Linerly
9209508aac Translated using Weblate (Indonesian)
Currently translated at 100.0% (89 of 89 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-23 22:18:14 +00:00
Choukajohn
f750e6ff7e Translated using Weblate (French)
Currently translated at 100.0% (89 of 89 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-23 22:18:14 +00:00
sk
f2eecf774b disable block domain from post 2022-12-23 22:59:06 +01:00
sk
98de4e75e0 implement open with other account as submenu 2022-12-23 22:56:18 +01:00
sk
459e32caf8 fix instance info v2 never getting saved 2022-12-23 21:49:21 +01:00
sk
c4ad325e5c improve settings items 2022-12-23 21:35:16 +01:00
sk
f55bd6d6cd truncate publish button 2022-12-23 21:16:36 +01:00
sk
f1ce700c93 add option to open post with other account
closes #182
2022-12-23 21:11:22 +01:00
nitrogenez
c18e1e8456 Translated using Weblate (Ukrainian)
Currently translated at 90.0% (9 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2022-12-23 19:22:28 +00:00
ling0412
1fcd1924c3 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2022-12-23 19:22:28 +00:00
Choukajohn
a1767f0425 Translated using Weblate (French)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2022-12-23 19:22:28 +00:00
nitrogenez
44d95ca25f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (86 of 86 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2022-12-23 19:22:28 +00:00
Linerly
7432540c29 Translated using Weblate (Indonesian)
Currently translated at 100.0% (86 of 86 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-23 19:22:28 +00:00
Choukajohn
c289f58351 Translated using Weblate (French)
Currently translated at 100.0% (86 of 86 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-23 19:22:28 +00:00
gallegonovato
711e041ff5 Translated using Weblate (Spanish)
Currently translated at 100.0% (86 of 86 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-23 19:22:28 +00:00
ling0412
0ac3534585 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (86 of 86 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2022-12-23 19:22:27 +00:00
benjaminwolkchen
57821e0860 Translated using Weblate (German)
Currently translated at 98.8% (85 of 86 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-23 19:22:27 +00:00
sk
0dae798c9a add option to open post with other account
closes #182
2022-12-23 20:17:54 +01:00
sk
732e780fd9 Merge remote-tracking branch 'weblate/main' 2022-12-23 18:54:20 +01:00
sk
5577124b7a use popup background color for dialogs 2022-12-23 18:38:16 +01:00
sk
81b82c75a2 add copy link item and change copyText
use haptic feedback instead of vibrator
2022-12-23 18:10:08 +01:00
sk
b32e322749 implement long-click to copy links
closes sk22#84
2022-12-23 17:51:06 +01:00
sk
3031cc4561 remove selectable background for username
closes sk22#187
2022-12-23 17:22:17 +01:00
sk
d60abc648c only show boost visibility for own posts 2022-12-23 17:11:29 +01:00
sk
a9a0233bb3 Merge branch 'clickable-boost-reply-line' 2022-12-23 16:52:04 +01:00
sk
f6fa9e5122 remove selectable background 2022-12-23 16:51:13 +01:00
sk
29b4f7c91a update changelog 2022-12-22 16:51:50 +01:00
sk
0a9ee57233 implement followed hashtags list
closes sk22/megalodon#162
2022-12-22 16:51:43 +01:00
AiOO
94b69a9c1c Translated using Weblate (Korean)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2022-12-22 14:59:36 +00:00
gallegonovato
f0209dd1cc Translated using Weblate (Spanish)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-22 14:59:36 +00:00
Linerly
366e432c18 Translated using Weblate (Indonesian)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2022-12-22 14:59:36 +00:00
Christian Elbrianno
0075c0e779 Translated using Weblate (Indonesian)
Currently translated at 100.0% (83 of 83 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-22 14:59:36 +00:00
Linerly
d99dfd4185 Translated using Weblate (Indonesian)
Currently translated at 100.0% (83 of 83 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-22 14:59:36 +00:00
Choukajohn
0309b3ad25 Translated using Weblate (French)
Currently translated at 100.0% (83 of 83 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-22 14:59:36 +00:00
gallegonovato
7a85532b73 Translated using Weblate (Spanish)
Currently translated at 100.0% (83 of 83 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-22 14:59:36 +00:00
AiOO
7299d947f7 Translated using Weblate (Korean)
Currently translated at 100.0% (83 of 83 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2022-12-22 14:59:35 +00:00
braydofficial
adec7b28f1 Translated using Weblate (German)
Currently translated at 100.0% (83 of 83 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-22 14:59:35 +00:00
sk
c2563da056 Merge branch 'feature/open-urls-in-app' 2022-12-22 15:35:53 +01:00
sk
64b9e53916 add missing fedi url format 2022-12-22 15:35:26 +01:00
sk
86ab6f7b3d implement setting boost visibility 2022-12-22 15:03:42 +01:00
sk
ed02733524 disable boost button if disabled
closes #180
2022-12-22 11:27:24 +01:00
sk
a99af63f34 Merge branch 'feature/open-urls-in-app' 2022-12-22 11:14:03 +01:00
sk
1d900a66fe try to open link previews in app 2022-12-22 11:13:50 +01:00
sk
f29acc217d update changelog 2022-12-22 01:37:52 +01:00
sk
f60164f5c5 bump version 2022-12-22 01:30:41 +01:00
sk
578cf1f00d update readme 2022-12-22 01:29:28 +01:00
sk
bca3bc6b4a Merge branch 'feature/more-html-tags' 2022-12-22 01:28:21 +01:00
sk
352c813544 smaller space between paragraphs 2022-12-22 01:27:55 +01:00
sk
7d876bddc7 preserve whitespaces in html 2022-12-22 01:25:43 +01:00
sk
43d334259b match navigation bar color with toolbar 2022-12-22 01:09:46 +01:00
sk
750579b1c2 use color attributes instead of fixed colors 2022-12-22 00:57:12 +01:00
sk
d69221d85b fix media upload colors 2022-12-22 00:50:23 +01:00
sk
a1c80e92cd reset state on boost long click 2022-12-22 00:47:44 +01:00
sk
ce1a0d66f1 update changelog 2022-12-22 00:14:59 +01:00
sk
f28057d620 update changelog 2022-12-22 00:08:37 +01:00
sk
8f6dc7e6d2 update changelog 2022-12-22 00:05:19 +01:00
sk
b25a237c20 add long-click to "quote" 2022-12-22 00:01:51 +01:00
sk
ccb0b59e17 Merge branch 'feature/open-urls-in-app' 2022-12-21 23:50:14 +01:00
sk
14658a2d70 only perform fedi lookup if looks like fedi url 2022-12-21 23:45:27 +01:00
sk
5f26878c06 resolve fediverse links in app
closes sk22#177
closes sk22#96
2022-12-21 23:29:16 +01:00
sk
df78ae59fe Merge remote-tracking branch 'weblate/main' 2022-12-21 22:39:19 +01:00
Choukajohn
47924dfb61 Translated using Weblate (French)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2022-12-21 21:37:59 +00:00
Choukajohn
3eb442e0f6 Translated using Weblate (French)
Currently translated at 100.0% (82 of 82 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-21 21:37:59 +00:00
sk
b309fc09f6 tweak item header and notification deletion 2022-12-21 22:37:30 +01:00
gallegonovato
a00fe0485b Translated using Weblate (Spanish)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-21 19:21:44 +00:00
sk22
a60f65857b Translated using Weblate (Spanish)
Currently translated at 98.7% (81 of 82 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-21 19:21:44 +00:00
gallegonovato
fe584b58b0 Translated using Weblate (Spanish)
Currently translated at 98.7% (81 of 82 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-21 19:21:44 +00:00
sk22
91ad2bf66b Translated using Weblate (German)
Currently translated at 100.0% (82 of 82 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-21 19:21:44 +00:00
sk22
01988a9435 Translated using Weblate (English)
Currently translated at 100.0% (82 of 82 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/en/
2022-12-21 19:21:44 +00:00
sk
51bc8e1b2c use instance domain if title blank 2022-12-21 19:57:31 +01:00
sk
b28c684bd3 fix missing text in notification 2022-12-21 19:54:15 +01:00
sk
42b2cde1e2 use UiUtils.getThemeColor 2022-12-21 19:48:44 +01:00
sk
199a7b816c Merge remote-tracking branch 'weblate/main' 2022-12-21 18:20:08 +01:00
sk22
2b8fc6764e Translated using Weblate (German)
Currently translated at 100.0% (80 of 80 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-21 17:18:57 +00:00
sk22
37f93cda4d Translated using Weblate (English)
Currently translated at 100.0% (80 of 80 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/en/
2022-12-21 17:18:57 +00:00
sk
4114f5b3c2 update changelog 2022-12-21 18:18:03 +01:00
Florian Obernberger
14d45eb759 Custom icons for different types of notifications (#178)
* Add filled variant of the person_add icon
* Add mention icon
* Add custom icons for different notification types

Co-authored-by: sk22 <sk22@mailbox.org>
2022-12-21 18:13:08 +01:00
sk
0dfa9d2c2c Merge branch 'use-fluent-mention-icon' 2022-12-21 18:09:41 +01:00
sk
6fce18ffe8 replace custom symbol with fluent mention symbol 2022-12-21 17:59:38 +01:00
sk
956c56c494 add translation availability info text 2022-12-21 17:54:28 +01:00
gallegonovato
75e7c6a9eb Translated using Weblate (Spanish)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2022-12-21 16:18:19 +00:00
Choukajohn
a7fb66d269 Translated using Weblate (French)
Currently translated at 100.0% (10 of 10 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fr/
2022-12-21 16:18:19 +00:00
Choukajohn
5f48802357 Translated using Weblate (French)
Currently translated at 100.0% (70 of 70 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-21 16:18:19 +00:00
gallegonovato
a27488c8da Translated using Weblate (Spanish)
Currently translated at 100.0% (70 of 70 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-21 16:18:19 +00:00
sk
b6dbd7512c no ellipsis for settings items 2022-12-21 16:37:40 +01:00
sk
e41386082c add toggle for hiding translate button 2022-12-21 16:35:47 +01:00
sk
ae385c139b animate translate button opacity 2022-12-21 16:23:09 +01:00
LucasGGamerM
dd2f213a4f lower opacity of button while translating 2022-12-21 16:18:28 +01:00
sk
35f92a6e91 fix icons not showing in RTL layout 2022-12-21 16:12:03 +01:00
sk
ab5648a5d6 make toot button customizable 2022-12-21 16:07:47 +01:00
sk
8a9b59c66a generalize color picker settings item 2022-12-21 14:49:38 +01:00
sk
a6e0c877b5 use heart for donation link 2022-12-21 14:36:23 +01:00
sk
de5a623a00 update changelog 2022-12-21 14:34:27 +01:00
sk
b0f9ce081f implement deleting notifications 2022-12-21 14:25:19 +01:00
sk
e17b6e83a4 new light grays 2022-12-21 11:40:07 +01:00
sk
96f4322f74 reuse old brownish gray and move colors 2022-12-21 10:45:40 +01:00
sk
780f59d666 new grays 2022-12-21 10:19:59 +01:00
sk
8c5db2eef5 Merge remote-tracking branch 'upstream/master' 2022-12-21 09:41:29 +01:00
sk
5ab004b87e fix wrong default visibility in popup
closes sk22#174
2022-12-21 09:41:03 +01:00
sk
1b001bdd4f Merge branch 'fix-wrong-visibility-on-edit' 2022-12-21 09:29:57 +01:00
Gregory K
a5fa44213d Merge pull request #474 from sk22/fix-wrong-visibility-on-edit
Only load default status visibility if not editing
2022-12-21 11:25:14 +03:00
sk
68e9d9d91c only load default visibility if not editing
fix mastodon#306
2022-12-21 09:24:09 +01:00
sk
a14783a275 fix action bar buttons color
closes sk22#175
2022-12-21 09:14:36 +01:00
sk
3ce58d2edf move button text/background to styles 2022-12-20 16:06:28 +01:00
sk
523efac4fb make separator color brighter
closes #159
2022-12-20 15:51:00 +01:00
sk
d352aca9cc add toolbar background color 2022-12-20 15:44:37 +01:00
sk
ae6afab01b Merge branch 'feature/animate-buttons' 2022-12-20 15:19:02 +01:00
sk
efea405b83 fire counter updated event for content status
see mastodon#467
closes sk22#173
2022-12-20 15:17:43 +01:00
sk
ad9262cf0f fix counter updates for preloaded data
re: mastodon#467
see fb5289372d
2022-12-20 14:58:27 +01:00
sk
636c268e46 add drag to open to visibility button 2022-12-20 12:58:18 +01:00
sk
06a61f0374 update fastlane descriptions 2022-12-20 12:50:47 +01:00
sk
4ba7763de5 fix fastlane configs 2022-12-20 12:49:01 +01:00
sk
b486542e7b update strings 2022-12-20 12:43:56 +01:00
sk22
f53ce6cbf0 Translated using Weblate (German)
Currently translated at 100.0% (70 of 70 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2022-12-20 11:34:43 +00:00
AiOO
b10ff655f1 Translated using Weblate (Korean)
Currently translated at 100.0% (8 of 8 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ko/
2022-12-20 11:34:43 +00:00
jonta
bbc27dbeea Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (8 of 8 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2022-12-20 11:34:43 +00:00
jonta
dadb929afd Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (62 of 62 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2022-12-20 11:34:43 +00:00
gallegonovato
c0f397cdd5 Translated using Weblate (Spanish)
Currently translated at 100.0% (62 of 62 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2022-12-20 11:34:43 +00:00
itslameni
b597cf6e18 Translated using Weblate (Russian)
Currently translated at 37.5% (3 of 8 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2022-12-20 11:34:43 +00:00
Linerly
e3bbeb2022 Translated using Weblate (Indonesian)
Currently translated at 100.0% (8 of 8 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/id/
2022-12-20 11:34:43 +00:00
Radiquum
cad9fc8977 Translated using Weblate (Russian)
Currently translated at 100.0% (62 of 62 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2022-12-20 11:34:43 +00:00
Linerly
2f191c1f71 Translated using Weblate (Indonesian)
Currently translated at 100.0% (62 of 62 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2022-12-20 11:34:43 +00:00
Choukajohn
dc0debce1a Translated using Weblate (French)
Currently translated at 100.0% (62 of 62 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2022-12-20 11:34:43 +00:00
AiOO
d96b6f0100 Translated using Weblate (Korean)
Currently translated at 100.0% (62 of 62 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ko/
2022-12-20 11:34:42 +00:00
sk
16f1901976 update strings 2022-12-20 12:34:32 +01:00
sk
61d9e4d531 update german 2022-12-20 12:25:59 +01:00
sk
aec4479e19 rename color theme to color palette 2022-12-20 12:23:59 +01:00
sk
ec7235c03a add settings links
closes #163
2022-12-20 12:12:45 +01:00
sk
f3db153ea9 check if server supports translation earlier
closes #172
2022-12-20 11:19:50 +01:00
sk
e1c258478f fix italian 2022-12-20 10:38:54 +01:00
sk
1f2e691eb1 Merge remote-tracking branch 'upstream/master' 2022-12-20 10:27:54 +01:00
sk
be9a3a9975 fix error in readme 2022-12-20 10:27:11 +01:00
sk
8d17ac6f28 simplify themes 2022-12-20 10:26:03 +01:00
Grishka
d9df150cf8 Fix #472 2022-12-20 12:14:56 +03:00
sk
667a4aab1a Merge branch 'main' of github.com:sk22/megalodon 2022-12-19 19:06:04 +01:00
sk
862ffd8172 Merge remote-tracking branch 'upstream/master' 2022-12-19 19:05:36 +01:00
sk
eb1dd954ee merge miui fix for copying text 2022-12-19 19:03:11 +01:00
Grishka
6f2e5a63d7 This is officially the first Xiaomi workaround in this app 🎉
#469
2022-12-19 20:27:56 +03:00
sk
5bf78c5cd2 use primary color for update notification
closes #169
2022-12-19 18:27:03 +01:00
LucasGGamerM
277361a562 merge moshidon's red theme
closes sk22#160
2022-12-19 17:58:16 +01:00
sk
6697d3d0d5 update changelog 2022-12-19 16:38:00 +01:00
sk
7202aae712 Merge branch 'feature/animate-buttons' 2022-12-19 16:34:25 +01:00
sk
6d2c6748f7 set pivot point once 2022-12-19 16:34:16 +01:00
sk
6c98c9ccf2 fix broken long click 2022-12-19 16:33:30 +01:00
sk
afc25ec8b3 Merge branch 'feature/animate-buttons' 2022-12-19 16:06:24 +01:00
sk
6c9553ba65 tweak animations 2022-12-19 16:05:30 +01:00
sk
0fd5ea4689 fix counter updates for preloaded data
re: mastodon#467
see fb5289372d
2022-12-19 16:00:46 +01:00
sk
8441208058 fix bug in bookmark button
closes #167
2022-12-19 15:59:15 +01:00
sk
aa34298771 Merge branch 'feature/animate-buttons' 2022-12-19 15:43:18 +01:00
sk
ccacd88f80 tweak animations 2022-12-19 15:42:32 +01:00
sk
1f20b21fc8 add option to disable swiping
closes sk22#165
2022-12-19 15:31:38 +01:00
sk
ae7152aca7 Merge branch 'feature/long-press-copy-url' 2022-12-19 15:15:03 +01:00
sk
ba36347f03 copy post URL on long click 2022-12-19 15:14:54 +01:00
sk
c0c5e83f31 move copy text to UiUtils 2022-12-19 15:12:37 +01:00
sk
9c05ff2f7c restore toast message for android 13+ 2022-12-19 15:08:13 +01:00
sk
0ada05bf3a add vibration when copying username 2022-12-19 15:06:17 +01:00
275 changed files with 5242 additions and 2251 deletions

View File

@@ -8,21 +8,18 @@
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
&nbsp;
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk"><img height="50" alt="Get it on F-Droid" src="img/f-droid-badge.png"></a>
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly wont ever be implemented, such as the federated timeline, unlisted posting and an image description viewer.
---
## Key features
### **Unlisted posting**
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Local”, “Community” and “Posts”).**
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).**
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in peoples Home timelines, but only if they follow you or someone they follow reposted/replied to your post.
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in peoples Home timelines, but only if they follow you or someone they follow reblogged/replied to your post.
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
@@ -34,6 +31,12 @@ Despite being one of the main features of federated social media, the Federated
Thats one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
### Draft and schedule posts
**Allows for preparing a post and scheduling it to send it automatically at a specific time.**
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
### **Image description viewer**
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
@@ -48,21 +51,29 @@ On the Fediverse, its quite common for people to pin posts they want others t
## Installation
### From app stores
### IzzyOnDroid
* **[Izzy's F-Droid repository](https://apt.izzysoft.de/fdroid/repo)**: [apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
[apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:
`https://apt.izzysoft.de/fdroid/repo`
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
* **[Google Play Store](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)**: [play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:
* **[F-Droid.org](https://f-droid.org)?** Not yet, sorry!
[`https://apt.izzysoft.de/fdroid/repo`](https://apt.izzysoft.de/fdroid/repo)
If you want, you can help me figure out if something's missing in the [Issue #47: F-Droid.org](https://github.com/sk22/megalodon/issues/47)
### Google Play Store
### Directly from GitHub
[play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
### F-Droid
**[F-Droid.org?](https://f-droid.org)** Not yet, sorry!
If you want, you can help me figure out if something's missing in the [Issue #47: F-Droid.org](https://github.com/sk22/megalodon/issues/47)
### Direct
Press the download button to download the APK. Open the downloaded file on your Android device to install it. Megalodon will automatically notify you about new updates inside the app.
@@ -120,7 +131,7 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Implement a bookmark button and list](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Add settings to hide replies and reblogs from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
@@ -130,11 +141,14 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
* [Add push notification setting for post notifications](https://github.com/sk22/megalodon/commit/b190480d7739be47f23543d9e7644660f9b4b4ee)
* [Add option to allow voting for multiple options on polls](https://github.com/sk22/megalodon/commit/5b28468efd49387b4f8b83f142f3adf3104ca60c)
* [Add translate function](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/translate-button)
* [Add language selector](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/language-selector)
* [Implement deleting notifications](https://github.com/sk22/megalodon/commit/b0f9ce081f69f29ad59658fc00ca41372cd2677d) (disabled by default)
* [Long-click boost button to "quote" a post](https://github.com/sk22/megalodon/commit/b25a237c20c6a924ed4d9b357999867c3a32b32b)
* [Draft and schedule posts](https://github.com/sk22/megalodon/pull/217)
* [Display original post when replying](https://github.com/sk22/megalodon/commit/375f8ceb2747705fedf43686681cc0e0b812f899)
### Behavior
@@ -152,6 +166,11 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Make inline emoji search case-insensitive and don't only search from start of emoji names](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:better-inline-emoji-search) ([Pull request](https://github.com/mastodon/mastodon-android/pull/445))
* [Include subject line when sharing e.g. a website to Megalodon](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:external-share-include-subject)
* [Improve semantics for voting on polls (radio buttons and checkboxes)](https://github.com/sk22/megalodon/commit/6fd58c96827cb1d2da329cebdc170a1425dd18d7)
* [Copy post URL when long-pressing share button](https://github.com/sk22/megalodon/commit/ba36347f03278763ecec617b1ce57ba89db7be72)
* [Add option to disable swiping between tabs](https://github.com/sk22/megalodon/commit/1f20b21fc84bf006c1ec14bd2229cbfad5215ec8)
* [Resolve Fediverse links in the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/open-urls-in-app)
* [Preserve whitespaces in HTML](https://github.com/sk22/megalodon/commit/7d876bddc7a07d98f0fecbf62b13bdb9fcce3412)
* [Long-click to copy links](https://github.com/sk22/megalodon/commit/b32e32274923a94742a9926ef38785f746d41405)
### Visual
@@ -163,8 +182,9 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Custom "megalodon" text logo](https://github.com/sk22/megalodon/commit/563afd487ca5c608cfbb00fa3909d3c27384acc0) by [@LucasGGamerM](https://github.com/LucasGGamerM)
* [Custom login screen](https://github.com/sk22/megalodon/commit/9bbf8c4618dbe13accaeb3b5482bf3fe88cac4c0)
* [More distinct filled boost icon](https://github.com/sk22/megalodon/commits/more-distinct-filled-boost-icon)
* [Material You color theme] by [@LucasGGamerM](https://github.com/LucasGGamerM)
* Material You color theme by [@LucasGGamerM](https://github.com/LucasGGamerM)
* [Animations for interaction buttons](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/animate-buttons)
* [Dedicated icons for different notification types](https://github.com/sk22/megalodon/pull/178) by [@florian-obernberger](https://github.com/florian-obernberger)
## Building

3
fix-metadata-markdown-lists.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
find metadata -name '*.txt' -exec sed -Ei 's/^[–—─•·*]\s+/- /' {} \;

BIN
img/izzy-badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -9,8 +9,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 61
versionName "1.1.5+fork.61"
versionCode 66
versionName "1.1.5+fork.66"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}

View File

@@ -4,7 +4,8 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

View File

@@ -0,0 +1,39 @@
# lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
# https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
# This list contains domains of toxic mastodon instances
# Last-Modified: 1672044500
# gab - a neonazi social network
gab.ai
gab.com
gab.protohype.net
# consequence-free speech
social.unzensiert.to
freeatlantis.com
# reactionary bigotry and hatespeech against magrinalized groups
poa.st
freespeechextremist.com
rdrama.cc
outpoa.st
anime.website
gameliberty.club
social.byoblu.com
yggdrasil.social
smuglo.li
dogeposting.social
unsafe.space
freezepeach.xyz
# + CSAM
rojogato.com
# antivaxxer shitposting & fearmongering
shadowsocial.org
# Kiwifarms
kiwifarms.net
kiwifarms.cc
kiwifarms.is
kiwifarms.pleroma.net
1 # lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
2 # https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
3 # This list contains domains of toxic mastodon instances
4 # Last-Modified: 1672044500
5 # gab - a neonazi social network
6 gab.ai
7 gab.com
8 gab.protohype.net
9 # consequence-free speech
10 social.unzensiert.to
11 freeatlantis.com
12 # reactionary bigotry and hatespeech against magrinalized groups
13 poa.st
14 freespeechextremist.com
15 rdrama.cc
16 outpoa.st
17 anime.website
18 gameliberty.club
19 social.byoblu.com
20 yggdrasil.social
21 smuglo.li
22 dogeposting.social
23 unsafe.space
24 freezepeach.xyz
25 # + CSAM
26 rojogato.com
27 # antivaxxer shitposting & fearmongering
28 shadowsocial.org
29 # Kiwifarms
30 kiwifarms.net
31 kiwifarms.cc
32 kiwifarms.is
33 kiwifarms.pleroma.net

View File

@@ -12,7 +12,6 @@ import android.widget.Toast;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
@@ -36,13 +35,10 @@ public class ExternalShareActivity extends FragmentStackActivity{
openComposeFragment(sessions.get(0).getID());
}else{
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
new M3AlertDialogBuilder(this)
.setItems(sessions.stream().map(as->"@"+as.self.username+"@"+as.domain).toArray(String[]::new), (dialog, which)->{
openComposeFragment(sessions.get(which).getID());
})
.setTitle(R.string.choose_account)
.setOnCancelListener(dialog -> finish())
.show();
UiUtils.pickAccount(this, null, R.string.choose_account, 0,
session -> openComposeFragment(session.getID()),
b -> b.setOnCancelListener(d -> finish())
);
}
}
}
@@ -52,7 +48,11 @@ public class ExternalShareActivity extends FragmentStackActivity{
Intent intent=getIntent();
StringBuilder builder=new StringBuilder();
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) builder.append(intent.getStringExtra(Intent.EXTRA_SUBJECT)).append("\n\n");
String subject = "";
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (!subject.isBlank()) builder.append(subject).append("\n\n");
}
if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
String text=builder.toString();
List<Uri> mediaUris;
@@ -80,6 +80,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
args.putString("account", accountID);
if(!TextUtils.isEmpty(text))
args.putString("prefilledText", text);
if(!subject.isBlank())
args.putInt("selectionEnd", subject.length());
if(mediaUris!=null && !mediaUris.isEmpty())
args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris));
Fragment fragment=new ComposeFragment();

View File

@@ -5,8 +5,6 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
@@ -26,7 +24,13 @@ public class GlobalUserPreferences{
public static boolean showInteractionCounts;
public static boolean alwaysExpandContentWarnings;
public static boolean disableMarquee;
public static boolean disableSwipe;
public static boolean voteButtonForSingleChoice;
public static boolean enableDeleteNotifications;
public static boolean translateButtonOpenedOnly;
public static boolean uniformNotificationIcon;
public static boolean reduceMotion;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
@@ -54,7 +58,13 @@ public class GlobalUserPreferences{
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
disableSwipe=prefs.getBoolean("disableSwipe", false);
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
reduceMotion=prefs.getBoolean("reduceMotion", false);
publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>());
@@ -78,6 +88,12 @@ public class GlobalUserPreferences{
.putBoolean("showInteractionCounts", showInteractionCounts)
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
.putBoolean("disableMarquee", disableMarquee)
.putBoolean("disableSwipe", disableSwipe)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly)
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
.putBoolean("reduceMotion", reduceMotion)
.putString("publishButtonText", publishButtonText)
.putInt("theme", theme.ordinal())
.putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages))
@@ -92,7 +108,6 @@ public class GlobalUserPreferences{
BLUE,
BROWN,
RED,
ORANGE,
YELLOW
}

View File

@@ -8,9 +8,7 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -144,6 +142,15 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
.setColor(context.getColor(R.color.primary_700));
if (!GlobalUserPreferences.uniformNotificationIcon) switch (pn.notificationType) {
case FAVORITE -> builder.setSmallIcon(R.drawable.ic_fluent_star_24_filled);
case REBLOG -> builder.setSmallIcon(R.drawable.ic_fluent_arrow_repeat_all_24_filled);
case FOLLOW -> builder.setSmallIcon(R.drawable.ic_fluent_person_add_24_filled);
case MENTION -> builder.setSmallIcon(R.drawable.ic_fluent_mention_24_filled);
case POLL -> builder.setSmallIcon(R.drawable.ic_fluent_poll_24_filled);
}
if(avatar!=null){
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
}

View File

@@ -12,11 +12,14 @@ import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.time.Instant;
import java.time.LocalDate;
@@ -47,9 +50,22 @@ public class MastodonAPIController{
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
private AccountSession session;
private static List<String> badDomains = new ArrayList<>();
static{
thread.start();
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(
MastodonApp.context.getAssets().open("blocks.tsv")
));
String line;
while ((line = reader.readLine()) != null) {
if (line.isBlank() || line.startsWith("#")) continue;
badDomains.add(line.toLowerCase().trim());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public MastodonAPIController(@Nullable AccountSession session){
@@ -57,8 +73,11 @@ public class MastodonAPIController{
}
public <T> void submitRequest(final MastodonAPIRequest<T> req){
final String host = req.getURL().getHost().toLowerCase();
final boolean isBad = badDomains.stream().anyMatch(host::endsWith);
thread.postRunnable(()->{
try{
if (isBad) throw new IllegalArgumentException();
if(req.canceled)
return;
Request.Builder builder=new Request.Builder()

View File

@@ -20,6 +20,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import androidx.annotation.CallSuper;
import androidx.annotation.StringRes;
@@ -101,9 +102,14 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null);
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
progressDialog=new ProgressDialog(activity);
progressDialog.setMessage(activity.getString(message));
progressDialog.setCancelable(cancelable);
if (transform != null) transform.accept(progressDialog);
if(cancelable){
progressDialog.setOnCancelListener(dialog->cancel());
}

View File

@@ -162,6 +162,8 @@ public class PushSubscriptionManager{
@Override
public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);

View File

@@ -9,6 +9,7 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.util.HashMap;
import java.util.function.Consumer;
@@ -18,12 +19,18 @@ import me.grishka.appkit.api.ErrorResponse;
public class StatusInteractionController{
private final String accountID;
private final boolean updateCounters;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
public StatusInteractionController(String accountID){
public StatusInteractionController(String accountID, boolean updateCounters) {
this.accountID=accountID;
this.updateCounters=updateCounters;
}
public StatusInteractionController(String accountID){
this(accountID, true);
}
public void setFavorited(Status status, boolean favorited, Consumer<Status> cb){
@@ -41,7 +48,7 @@ public class StatusInteractionController{
runningFavoriteRequests.remove(status.id);
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
cb.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -50,16 +57,16 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context);
status.favourited=!favorited;
cb.accept(status);
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningFavoriteRequests.put(status.id, req);
status.favourited=favorited;
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged, Consumer<Status> cb){
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
@@ -67,14 +74,15 @@ public class StatusInteractionController{
if(current!=null){
current.cancel();
}
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged)
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged, visibility)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
public void onSuccess(Status reblog){
Status result = reblog.getContentStatus();
runningReblogRequests.remove(status.id);
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
cb.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -83,13 +91,13 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context);
status.reblogged=!reblogged;
cb.accept(status);
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningReblogRequests.put(status.id, req);
status.reblogged=reblogged;
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
public void setBookmarked(Status status, boolean bookmarked){
@@ -110,7 +118,7 @@ public class StatusInteractionController{
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
cb.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -119,12 +127,12 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
cb.accept(status);
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.announcements;
import org.joinmastodon.android.api.MastodonAPIRequest;
public class DismissAnnouncement extends MastodonAPIRequest<Object>{
public DismissAnnouncement(String id){
super(HttpMethod.POST, "/announcements/" + id + "/dismiss", Object.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.api.requests.announcements;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Announcement;
import java.util.List;
public class GetAnnouncements extends MastodonAPIRequest<List<Announcement>> {
public GetAnnouncements(boolean withDismissed) {
super(MastodonAPIRequest.HttpMethod.GET, "/announcements", new TypeToken<>(){});
addQueryParameter("with_dismissed", withDismissed ? "true" : "false");
}
}

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.EnumSet;
import java.util.List;
public class DismissNotification extends MastodonAPIRequest<Object>{
public DismissNotification(String id){
super(HttpMethod.POST, "/notifications/" + (id != null ? id + "/dismiss" : "clear"), Object.class);
setRequestBody(new Object());
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
@@ -9,12 +10,29 @@ import java.util.ArrayList;
import java.util.List;
public class CreateStatus extends MastodonAPIRequest<Status>{
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
public static Instant getDraftInstant() {
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
// yes, this is a weird implementation for something that hardly matters
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
}
public CreateStatus(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", Status.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
public static class Scheduled extends MastodonAPIRequest<ScheduledStatus>{
public Scheduled(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", ScheduledStatus.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
}
public static class Request{
public String status;
public List<String> mediaIds;

View File

@@ -7,4 +7,10 @@ public class DeleteStatus extends MastodonAPIRequest<Status>{
public DeleteStatus(String id){
super(HttpMethod.DELETE, "/statuses/"+id, Status.class);
}
public static class Scheduled extends MastodonAPIRequest<Object> {
public Scheduled(String id) {
super(HttpMethod.DELETE, "/scheduled_statuses/"+id, Object.class);
}
}
}

View File

@@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.ScheduledStatus;
public class GetScheduledStatuses extends HeaderPaginationRequest<ScheduledStatus>{
public GetScheduledStatuses(String maxID, int limit){
super(HttpMethod.GET, "/scheduled_statuses", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@@ -2,10 +2,17 @@ package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
public class SetStatusReblogged extends MastodonAPIRequest<Status>{
public SetStatusReblogged(String id, boolean reblogged){
public SetStatusReblogged(String id, boolean reblogged, StatusPrivacy visibility){
super(HttpMethod.POST, "/statuses/"+id+"/"+(reblogged ? "reblog" : "unreblog"), Status.class);
setRequestBody(new Object());
Request req = new Request();
req.visibility = visibility;
setRequestBody(req);
}
public static class Request {
public StatusPrivacy visibility;
}
}

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.api.requests.tags;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Hashtag;
import java.util.List;
public class GetFollowedHashtags extends HeaderPaginationRequest<Hashtag> {
public GetFollowedHashtags(String maxID, String minID, int limit, String sinceID){
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
}

View File

@@ -32,7 +32,7 @@ public class AccountSession{
public Preferences preferences;
public AccountActivationInfo activationInfo;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
@@ -52,6 +52,10 @@ public class AccountSession{
return domain+"_"+self.id;
}
public String getFullUsername() {
return "@"+self.username+"@"+domain;
}
public MastodonAPIController getApiController(){
if(apiController==null)
apiController=new MastodonAPIController(this);
@@ -64,6 +68,12 @@ public class AccountSession{
return statusInteractionController;
}
public StatusInteractionController getRemoteStatusInteractionController(){
if(remoteStatusInteractionController==null)
remoteStatusInteractionController=new StatusInteractionController(getID(), false);
return remoteStatusInteractionController;
}
public CacheController getCacheController(){
if(cacheController==null)
cacheController=new CacheController(getID());

View File

@@ -13,8 +13,6 @@ import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
@@ -108,7 +106,7 @@ public class AccountSessionManager{
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
updateInstanceEmojis(instance, instance.uri);
updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
@@ -327,12 +325,7 @@ public class AccountSessionManager{
@Override
public void onSuccess(Instance instance){
instances.put(domain, instance);
updateInstanceEmojis(instance, domain);
try {
if (Integer.parseInt(instance.version.split("\\.")[0]) >= 4) {
updateInstanceInfoV2(domain);
}
} catch (Exception ignored) {}
updateMoreInstanceInfo(instance, domain);
}
@Override
@@ -343,17 +336,19 @@ public class AccountSessionManager{
.execNoAuth(domain);
}
public void updateInstanceInfoV2(String domain) {
public void updateMoreInstanceInfo(Instance instance, String domain) {
new GetInstance.V2().setCallback(new Callback<>() {
@Override
public void onSuccess(Instance.V2 v2) {
Instance instanceInfo = instances.get(domain);
if (instanceInfo != null) instanceInfo.v2 = v2;
if (instance != null) instance.v2 = v2;
updateInstanceEmojis(instance, domain);
}
@Override
public void onError(ErrorResponse errorResponse) {}
}).execNoAuth(domain);
public void onError(ErrorResponse errorResponse) {
updateInstanceEmojis(instance, domain);
}
}).execNoAuth(instance.uri);
}
private void updateInstanceEmojis(Instance instance, String domain){

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusCreatedEvent {
public final ScheduledStatus scheduledStatus;
public final String accountID;
public ScheduledStatusCreatedEvent(ScheduledStatus scheduledStatus, String accountID){
this.scheduledStatus = scheduledStatus;
this.accountID=accountID;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusDeletedEvent{
public final String id;
public final String accountID;
public ScheduledStatusDeletedEvent(String id, String accountID){
this.id=id;
this.accountID=accountID;
}
}

View File

@@ -0,0 +1,107 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.PaginatedList;
import me.grishka.appkit.api.SimpleCallback;
public class AnnouncementsFragment extends BaseStatusListFragment<Announcement> {
private Instance instance;
private AccountSession session;
private List<String> unreadIDs = null;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.sk_announcements);
session = AccountSessionManager.getInstance().getAccount(accountID);
instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
loadData();
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Announcement a) {
if(TextUtils.isEmpty(a.content)) return List.of();
Account instanceUser = new Account();
instanceUser.id = instanceUser.acct = instanceUser.username = session.domain;
instanceUser.displayName = instance.title;
instanceUser.url = "https://"+session.domain+"/about";
instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail;
instanceUser.emojis = List.of();
Status fakeStatus = a.toStatus();
return List.of(
HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead),
new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus)
);
}
public void onMarkAsRead(String id) {
if (unreadIDs == null) return;
unreadIDs.remove(id);
if (unreadIDs.size() == 0) setResult(true, null);
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
protected void addAccountToKnown(Announcement s) {}
@Override
public void onItemClick(String id) {
}
@Override
protected void onDataLoaded(List<Announcement> d, boolean more) {
unreadIDs = d.stream().filter(a -> !a.read).map(a -> a.id).collect(Collectors.toList());
super.onDataLoaded(d, more);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAnnouncements(true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Announcement> result){
onDataLoaded(result, false);
}
})
.exec(accountID);
}
}

View File

@@ -787,7 +787,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content);
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
titlePaint.setColor(getResources().getColor(R.color.gray_50));
titlePaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray50));
titlePaint.setTextSize(V.dp(22));
titlePaint.setTypeface(mediumTypeface);
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
@@ -798,7 +798,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(getResources().getColor(R.color.gray_200));
textPaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray200));
textPaint.setTextSize(V.dp(16));
String text=getString(R.string.sensitive_content_explain);
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)

View File

@@ -1,12 +1,16 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
@@ -29,11 +33,12 @@ import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -55,6 +60,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -67,13 +73,15 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.ProgressListener;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.EditStatus;
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
@@ -85,6 +93,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
@@ -100,6 +109,7 @@ import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ComposeEditText;
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import org.joinmastodon.android.utils.MastodonLanguage;
@@ -109,6 +119,12 @@ import org.parceler.Parcels;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
@@ -131,6 +147,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
private static final int MAX_ATTACHMENTS=4;
private static final String TAG="ComposeFragment";
@@ -154,9 +171,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private String accountID;
private int charCount, charLimit, trimmedCharCount;
private Button publishButton, languageButton;
private PopupMenu languagePopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
private ImageView sensitiveIcon;
private ComposeMediaLayout attachmentsView;
private TextView replyText;
@@ -165,8 +182,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private View addPollOptionBtn;
private View sensitiveItem;
private View pollAllowMultipleItem;
private View scheduleDraftView;
private ScrollView scrollView;
private boolean initiallyScrolled = false;
private TextView scheduleDraftText;
private CheckBox pollAllowMultipleCheckbox;
private TextView pollDurationView;
private MenuItem draftMenuItem, undraftMenuItem, scheduleMenuItem, unscheduleMenuItem;
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
@@ -182,6 +204,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private EditText spoilerEdit;
private boolean hasSpoiler;
private boolean sensitive;
private Instant scheduledAt = null;
private ProgressBar sendProgress;
private ImageView sendError;
private View sendingOverlay;
@@ -194,6 +217,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private boolean attachmentsErrorShowing;
private Status editingStatus;
private ScheduledStatus scheduledStatus;
private boolean redraftStatus;
private boolean pollChanged;
private boolean creatingView;
@@ -215,9 +239,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
languageResolver=new MastodonLanguage.LanguageResolver(instance);
redraftStatus=getArguments().getBoolean("redraftStatus", false);
if(getArguments().containsKey("editStatus")){
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
redraftStatus=getArguments().getBoolean("redraftStatus");
}
if(instance==null){
Nav.finish(this);
@@ -227,6 +251,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
if (bundle.containsKey("scheduledAt")) scheduledAt=(Instant) bundle.getSerializable("scheduledAt");
if(instance.maxTootChars>0)
charLimit=instance.maxTootChars;
else if(instance.configuration!=null && instance.configuration.statuses!=null && instance.configuration.statuses.maxCharacters>0)
@@ -255,6 +283,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
wm=activity.getSystemService(WindowManager.class);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
creatingView=true;
@@ -266,10 +295,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mainEditTextWrap=view.findViewById(R.id.toot_text_wrap);
charCounter=view.findViewById(R.id.char_counter);
charCounter.setText(String.valueOf(charLimit));
scrollView=view.findViewById(R.id.scroll_view);
selfName=view.findViewById(R.id.name);
selfUsername=view.findViewById(R.id.username);
selfAvatar=view.findViewById(R.id.avatar);
selfName=view.findViewById(R.id.self_name);
selfUsername=view.findViewById(R.id.self_username);
selfAvatar=view.findViewById(R.id.self_avatar);
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
selfUsername.setText('@'+self.username+'@'+instanceDomain);
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
@@ -287,6 +317,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn);
sensitiveIcon=view.findViewById(R.id.sensitive_icon);
sensitiveItem=view.findViewById(R.id.sensitive_item);
replyText=view.findViewById(R.id.reply_text);
@@ -295,7 +329,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
visibilityBtn.setOnClickListener(this::onVisibilityClick);
buildVisibilityPopup(visibilityBtn);
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
sensitiveItem.setOnClickListener(v->toggleSensitive());
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
@@ -345,8 +385,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftPollOption opt=createDraftPollOption();
opt.edit.setText(eopt.title);
}
pollDuration=(int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond();
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), editingStatus.poll.expiresAt);
pollDuration=scheduledStatus == null
? (int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond()
: Integer.parseInt(scheduledStatus.params.poll.expiresIn);
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), scheduledStatus == null
? editingStatus.poll.expiresAt
: Instant.now().plus(pollDuration, ChronoUnit.SECONDS));
updatePollOptionHints();
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
}else{
@@ -385,10 +429,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
} else {
loadDefaultStatusVisibility(savedInstanceState);
}
loadDefaultStatusVisibility(savedInstanceState);
updateVisibilityIcon();
visibilityPopup.getMenu().findItem(switch(statusVisibility){
case PUBLIC -> R.id.vis_public;
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private;
}).setChecked(true);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
@@ -425,6 +476,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putParcelableArrayList("attachments", serializedAttachments);
}
outState.putSerializable("visibility", statusVisibility);
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
}
@Override
@@ -507,6 +560,78 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
});
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
if(replyTo!=null){
View replyWrap = view.findViewById(R.id.reply_wrap);
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
int scrollHeight = scrollView.getHeight();
if (replyWrap.getMinimumHeight() != scrollHeight) {
replyWrap.setMinimumHeight(scrollHeight);
if (!initiallyScrolled) {
initiallyScrolled = true;
scrollView.post(() -> {
int bottom = scrollView.getChildAt(0).getBottom();
int delta = bottom - (scrollView.getScrollY() + scrollView.getHeight());
int space = GlobalUserPreferences.reduceMotion ? 0 : Math.min(V.dp(70), delta);
scrollView.scrollBy(0, delta - space);
if (!GlobalUserPreferences.reduceMotion) {
scrollView.postDelayed(() -> scrollView.smoothScrollBy(0, space), 130);
}
});
}
}
});
View originalPost = view.findViewById(R.id.original_post);
originalPost.setVisibility(View.VISIBLE);
originalPost.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(replyTo));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ThreadFragment.class, args);
});
ImageView avatar = view.findViewById(R.id.avatar);
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(replyTo.account.avatar));
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
}
};
avatar.setOutlineProvider(roundCornersOutline);
avatar.setClipToOutline(true);
avatar.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(replyTo.account));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ProfileFragment.class, args);
});
((TextView) view.findViewById(R.id.name)).setText(replyTo.account.displayName);
((TextView) view.findViewById(R.id.username)).setText(replyTo.account.getDisplayUsername());
view.findViewById(R.id.visibility).setVisibility(View.GONE);
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
});
ImageView moreBtn = view.findViewById(R.id.more);
moreBtn.setImageDrawable(visibilityIcon);
moreBtn.setBackground(null);
TextView timestamp = view.findViewById(R.id.timestamp);
if (replyTo.editedAt==null) timestamp.setText(UiUtils.formatRelativeTimestamp(getContext(), replyTo.createdAt));
else timestamp.setText(getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), replyTo.editedAt)));
if (replyTo.spoilerText != null && !replyTo.spoilerText.isBlank()) {
view.findViewById(R.id.spoiler_header).setVisibility(View.VISIBLE);
((TextView) view.findViewById(R.id.spoiler_title_inline)).setText(replyTo.spoilerText);
}
SpannableStringBuilder content = HtmlParser.parse(replyTo.content, replyTo.emojis, replyTo.mentions, replyTo.tags, accountID);
LinkedTextView text = view.findViewById(R.id.text);
if (content.length() > 0) text.setText(content);
else view.findViewById(R.id.display_item_text).setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
int visibilityNameRes = switch (replyTo.visibility) {
case PUBLIC -> R.string.visibility_public;
@@ -515,24 +640,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case DIRECT -> R.string.visibility_private;
};
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_at_symbol;
});
visibilityIcon.setBounds(0, 0, V.dp(20), V.dp(20));
Drawable replyArrow = getActivity().getDrawable(R.drawable.ic_fluent_arrow_reply_20_filled);
replyArrow.setBounds(0, 0, V.dp(20), V.dp(20));
replyText.setCompoundDrawables(replyArrow, null, visibilityIcon, null);
replyText.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(replyTo));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ThreadFragment.class, args);
scrollView.smoothScrollTo(0, 0);
});
ArrayList<String> mentions=new ArrayList<>();
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!replyTo.account.id.equals(ownID))
@@ -592,6 +703,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
ignoreSelectionChanges=false;
initialText=prefilledText;
}
if (getArguments().containsKey("selectionStart") || getArguments().containsKey("selectionEnd")) {
int selectionStart=getArguments().getInt("selectionStart", 0);
int selectionEnd=getArguments().getInt("selectionEnd", selectionStart);
mainEditText.setSelection(selectionStart, selectionEnd);
}
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
if(mediaUris!=null && !mediaUris.isEmpty()){
for(Uri uri:mediaUris){
@@ -611,42 +727,59 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
publishButton=new Button(getActivity());
publishButton.setText(editingStatus==null || redraftStatus ? R.string.publish : R.string.save);
publishButton.setOnClickListener(this::onPublishClick);
LinearLayout wrap=new LinearLayout(getActivity());
wrap.setOrientation(LinearLayout.HORIZONTAL);
sendProgress=new ProgressBar(getActivity());
LinearLayout.LayoutParams progressLP=new LinearLayout.LayoutParams(V.dp(24), V.dp(24));
progressLP.setMarginEnd(V.dp(16));
progressLP.gravity=Gravity.CENTER_VERTICAL;
wrap.addView(sendProgress, progressLP);
sendError=new ImageView(getActivity());
sendError.setImageResource(R.drawable.ic_fluent_error_circle_24_regular);
sendError.setImageTintList(getResources().getColorStateList(R.color.error_600));
sendError.setScaleType(ImageView.ScaleType.CENTER);
wrap.addView(sendError, progressLP);
sendError.setVisibility(View.GONE);
sendProgress.setVisibility(View.GONE);
LinearLayout.LayoutParams langParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
langParams.setMarginEnd(V.dp(8));
wrap.addView(buildLanguageSelector(), langParams);
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
LinearLayout wrap=new LinearLayout(getActivity());
getActivity().getLayoutInflater().inflate(R.layout.compose_action, wrap);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
updatePublishButtonState();
draftsBtn = wrap.findViewById(R.id.drafts_btn);
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
draftOptionsPopup.inflate(R.menu.compose_more);
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
draftOptionsPopup.setOnMenuItemClickListener(i->{
int id = i.getItemId();
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
else if (id == R.id.schedule) pickScheduledDateTime();
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
else navigateToUnsentPosts();
return true;
});
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
publishButton = wrap.findViewById(R.id.publish_btn);
languageButton = wrap.findViewById(R.id.language_btn);
sendProgress = wrap.findViewById(R.id.send_progress);
sendError = wrap.findViewById(R.id.send_error);
publishButton.setOnClickListener(this::onPublishClick);
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("hide_fab", true);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(draftsBtn.getWindowToken(), 0);
if (hasDraft()) {
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
} else {
// result for the previous ScheduledStatusList
setResult(true, null);
// finishing fragment in "onFragmentResult"
Nav.goForResult(getActivity(), ScheduledStatusListFragment.class, args, SCHEDULED_STATUS_OPENED_RESULT, this);
}
}
private void updateLanguage(String lang) {
updateLanguage(languageResolver.from(lang));
updateLanguage(lang == null ? languageResolver.getDefault() : languageResolver.from(lang));
}
private void updateLanguage(MastodonLanguage loc) {
@@ -656,21 +789,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
@SuppressLint("ClickableViewAccessibility")
private Button buildLanguageSelector() {
TypedValue typedValue = new TypedValue();
getActivity().getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true);
languageButton=new Button(getActivity());
languageButton.setTextColor(typedValue.data);
languageButton.setBackground(getActivity().getDrawable(R.drawable.bg_text_button));
languageButton.setPadding(V.dp(8), 0, V.dp(8), 0);
languageButton.setCompoundDrawablesRelativeWithIntrinsicBounds(getActivity().getDrawable(R.drawable.ic_fluent_local_language_16_regular), null, null, null);
languageButton.setCompoundDrawableTintList(languageButton.getTextColors());
languageButton.setCompoundDrawablePadding(V.dp(6));
private void buildLanguageSelector(Button btn) {
languagePopup=new PopupMenu(getActivity(), languageButton);
languageButton.setOnTouchListener(languagePopup.getDragToOpenListener());
languageButton.setOnClickListener(v->languagePopup.show());
btn.setOnTouchListener(languagePopup.getDragToOpenListener());
btn.setOnClickListener(v->languagePopup.show());
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
@@ -694,8 +816,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateLanguage(allLanguages.get(i.getItemId()));
return true;
});
return languageButton;
}
@Override
@@ -732,6 +852,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updatePublishButtonState();
}
private void resetPublishButtonText() {
int publishText = editingStatus==null || redraftStatus ? R.string.publish : R.string.save;
if (publishText == R.string.publish && !GlobalUserPreferences.publishButtonText.isEmpty()) {
publishButton.setText(GlobalUserPreferences.publishButtonText);
} else {
publishButton.setText(publishText);
}
}
private void updatePublishButtonState(){
uuid=null;
int nonEmptyPollOptionsCount=0;
@@ -747,6 +876,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
nonDoneAttachmentCount++;
}
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
sendError.setVisibility(View.GONE);
}
private void onCustomEmojiClick(Emoji emoji){
@@ -765,6 +895,43 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
publish();
}
private void publishErrorCallback(ErrorResponse error) {
wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true);
if (error != null) error.showToast(getActivity());
}
private void createScheduledStatusFinish(ScheduledStatus result) {
wm.removeView(sendingOverlay);
sendingOverlay=null;
Toast.makeText(getContext(), scheduledAt.isAfter(DRAFTS_AFTER_INSTANT) ?
R.string.sk_draft_saved : R.string.sk_post_scheduled, Toast.LENGTH_SHORT).show();
Nav.finish(ComposeFragment.this);
E.post(new ScheduledStatusCreatedEvent(result, accountID));
}
private void maybeDeleteScheduledPost(Runnable callback) {
if (scheduledStatus != null) {
new DeleteStatus.Scheduled(scheduledStatus.id).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
E.post(new ScheduledStatusDeletedEvent(scheduledStatus.id, accountID));
callback.run();
}
@Override
public void onError(ErrorResponse error) {
publishErrorCallback(error);
}
}).exec(accountID);
} else {
callback.run();
}
}
private void publish(){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
@@ -772,6 +939,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
req.visibility=statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
}
@@ -808,35 +976,32 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Callback<Status> resCallback=new Callback<>(){
@Override
public void onSuccess(Status result){
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
maybeDeleteScheduledPost(() -> {
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}else{
E.post(new StatusUpdatedEvent(result));
}
}else{
E.post(new StatusUpdatedEvent(result));
}
Nav.finish(ComposeFragment.this);
if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
Nav.go(getActivity(), ThreadFragment.class, args);
}
Nav.finish(ComposeFragment.this);
if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
Nav.go(getActivity(), ThreadFragment.class, args);
}
});
}
@Override
public void onError(ErrorResponse error){
wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true);
error.showToast(getActivity());
publishErrorCallback(error);
}
};
@@ -844,10 +1009,37 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
new EditStatus(req, editingStatus.id)
.setCallback(resCallback)
.exec(accountID);
}else{
}else if(req.scheduledAt == null){
new CreateStatus(req, uuid)
.setCallback(resCallback)
.exec(accountID);
}else if(req.scheduledAt.isAfter(Instant.now().plus(10, ChronoUnit.MINUTES))){
// checking for 10 instead of 5 minutes (as per mastodon) because i really don't want
// bugs to occur because the client's clock is wrong by a minute or two - the api
// returns a status instead of a scheduled status if scheduled time is less than 5
// minutes into the future and this is 1. unexpected for the user and 2. hard to handle
new CreateStatus.Scheduled(req, uuid)
.setCallback(new Callback<>() {
@Override
public void onSuccess(ScheduledStatus result) {
maybeDeleteScheduledPost(() -> {
createScheduledStatusFinish(result);
});
}
@Override
public void onError(ErrorResponse error) {
publishErrorCallback(error);
}
}).exec(accountID);
}else{
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_scheduled_too_soon_title)
.setMessage(R.string.sk_scheduled_too_soon)
.setPositiveButton(R.string.ok, (a, b)->{})
.show();
publishErrorCallback(null);
publishButton.setEnabled(false);
}
if (replyTo == null) {
@@ -867,6 +1059,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
List<String> existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList());
if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList())))
return true;
if(!statusVisibility.equals(editingStatus.visibility)) return true;
if(scheduledStatus != null && !scheduledStatus.scheduledAt.equals(scheduledAt)) return true;
return pollChanged;
}
boolean pollFieldsHaveContent=false;
@@ -911,14 +1105,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
break;
}
}
} else if (reqCode == SCHEDULED_STATUS_OPENED_RESULT && success && getActivity() != null) {
Nav.finish(this);
}
}
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
.setNegativeButton(R.string.cancel, null)
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> {
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
publish();
})
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
.show();
}
@@ -1097,7 +1296,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onProgress(long transferred, long total){
if(updateUploadEtaRunnable==null){
UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100);
// getting a NoSuchMethodError: No static method -$$Nest$mupdateUploadETAs(ComposeFragment;)V in class ComposeFragment
// when using method reference out of nowhere after changing code elsewhere. no idea. programming is awful, actually
// noinspection Convert2MethodRef
UiUtils.runOnUiThread(updateUploadEtaRunnable=()->ComposeFragment.this.updateUploadETAs(), 50);
}
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
if(Build.VERSION.SDK_INT>=24)
@@ -1260,7 +1462,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time));
}
}
UiUtils.runOnUiThread(updateUploadEtaRunnable, 100);
UiUtils.runOnUiThread(updateUploadEtaRunnable, 50);
}
private void onEditMediaDescriptionClick(View v){
@@ -1392,23 +1594,69 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if (attachments.isEmpty()) sensitive = false;
}
private void pickScheduledDateTime() {
LocalDateTime soon = LocalDateTime.now()
.plus(15, ChronoUnit.MINUTES) // so 14:59 doesn't get rounded up to…
.plus(1, ChronoUnit.HOURS) // …15:00, but rather 16:00
.withMinute(0);
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
.toInstant(OffsetDateTime.now().getOffset()));
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
}
private void updateScheduledAt(Instant scheduledAt) {
this.scheduledAt = scheduledAt;
updatePublishButtonState();
scheduleDraftView.setVisibility(scheduledAt == null ? View.GONE : View.VISIBLE);
draftMenuItem.setVisible(true);
scheduleMenuItem.setVisible(true);
undraftMenuItem.setVisible(false);
unscheduleMenuItem.setVisible(false);
if (scheduledAt != null) {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
if (scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
draftMenuItem.setVisible(false);
undraftMenuItem.setVisible(true);
scheduleTimeBtn.setVisibility(View.GONE);
scheduleDraftText.setText(R.string.sk_compose_draft);
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_regular, 0, 0, 0);
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_draft));
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_filled, 0, 0, 0);
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)
? R.string.save : R.string.sk_draft);
} else {
scheduleMenuItem.setVisible(false);
unscheduleMenuItem.setVisible(true);
String at = scheduledAt.atZone(ZoneId.systemDefault()).format(formatter);
scheduleTimeBtn.setVisibility(View.VISIBLE);
scheduleTimeBtn.setText(at);
scheduleDraftText.setText(R.string.sk_compose_scheduled);
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_schedule));
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_clock_20_filled, 0, 0, 0);
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.equals(scheduledAt)
? R.string.save : R.string.sk_schedule);
}
} else {
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_clock_20_regular, 0, 0, 0);
resetPublishButtonText();
}
}
private int getMediaAttachmentsCount(){
return attachments.size();
}
private void onVisibilityClick(View v){
PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility);
Menu m=menu.getMenu();
UiUtils.enablePopupMenuIcons(getActivity(), menu);
private void buildVisibilityPopup(View v){
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
m.setGroupCheckable(0, true, true);
m.findItem(switch(statusVisibility){
case PUBLIC -> R.id.vis_public;
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private;
}).setChecked(true);
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
@Override
public boolean onMenuItemClick(MenuItem item){
int id=item.getItemId();
@@ -1426,7 +1674,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return true;
}
});
menu.show();
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
@@ -1467,7 +1714,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
case DIRECT -> R.drawable.ic_at_symbol;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
});
}

View File

@@ -0,0 +1,107 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop {
private String nextMaxID;
private String accountId;
public FollowedHashtagsFragment() {
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
setTitle(R.string.sk_hashtags_you_follow);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetFollowedHashtags(offset==0 ? null : nextMaxID, null, count, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountId);
}
@Override
protected RecyclerView.Adapter getAdapter() {
return new HashtagsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull
@Override
public HashtagViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new HashtagViewHolder();
}
@Override
public void onBindViewHolder(@NonNull HashtagViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class HashtagViewHolder extends BindableViewHolder<Hashtag> implements UsableRecyclerView.Clickable{
private final TextView title;
public HashtagViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
}
@Override
public void onBind(Hashtag item) {
title.setText(item.name);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_number_symbol_24_regular), null, null, null);
}
@Override
public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
}
}
}

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -117,6 +118,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '));
}
private void onFabClick(View v){

View File

@@ -19,18 +19,22 @@ import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -43,12 +47,9 @@ import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -57,11 +58,14 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class HomeTimelineFragment extends StatusListFragment{
private static final int ANNOUNCEMENTS_RESULT = 654;
private ImageButton fab;
private ImageView toolbarLogo;
private Button toolbarShowNewPostsBtn;
private boolean newPostsBtnShown;
private AnimatorSet currentNewPostsAnim;
private MenuItem announcements;
private String maxID;
@@ -106,6 +110,8 @@ public class HomeTimelineFragment extends StatusListFragment{
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID));
updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
@@ -125,16 +131,40 @@ public class HomeTimelineFragment extends StatusListFragment{
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.home, menu);
announcements = menu.findItem(R.id.announcements);
new GetAnnouncements(false).setCallback(new Callback<>() {
@Override
public void onSuccess(List<Announcement> result) {
boolean hasUnread = result.stream().anyMatch(a -> !a.read);
announcements.setIcon(hasUnread ? R.drawable.ic_announcements_24_badged : R.drawable.ic_fluent_megaphone_24_regular);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
if (item.getItemId() == R.id.settings) Nav.go(getActivity(), SettingsFragment.class, args);
if (item.getItemId() == R.id.announcements) {
Nav.goForResult(getActivity(), AnnouncementsFragment.class, args, ANNOUNCEMENTS_RESULT, this);
}
return true;
}
@Override
public void onFragmentResult(int reqCode, boolean noMoreUnread, Bundle result){
if (reqCode == ANNOUNCEMENTS_RESULT && noMoreUnread) {
announcements.setIcon(R.drawable.ic_fluent_megaphone_24_regular);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
@@ -187,10 +217,8 @@ public class HomeTimelineFragment extends StatusListFragment{
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!filters.isEmpty()){
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
}
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
showNewPostsButton();
@@ -264,18 +292,14 @@ public class HomeTimelineFragment extends StatusListFragment{
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
outer:
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
for(Filter filter:filters){
if(filter.matches(s)){
continue outer;
}
if(filterPredicate.test(s)){
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.media.MediaRouter;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
@@ -12,6 +11,7 @@ import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -69,6 +69,7 @@ public class ListTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID));
}
private void onFabClick(View v){

View File

@@ -1,14 +1,9 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -23,17 +18,13 @@ import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
@@ -159,7 +150,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_list_timeline, list);
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@@ -167,8 +158,10 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_community_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {

View File

@@ -14,6 +14,7 @@ import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
@@ -73,15 +74,26 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() != R.id.follow_requests) return false;
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
return true;
if (item.getItemId() == R.id.follow_requests) {
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
return true;
} else if (item.getItemId() == R.id.clear_notifications) {
UiUtils.confirmDeleteNotification(getActivity(), accountID, null, ()->{
for (int i = 0; i < tabViews.length; i++) {
getFragmentForPage(i).reload();
}
});
return true;
}
return false;
}
@Override
@@ -109,6 +121,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override

View File

@@ -2,6 +2,8 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -10,7 +12,6 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
@@ -78,9 +79,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText, n, null) : null;
if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
if(titleItem!=null){
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem imgItem){
@@ -210,7 +211,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
private void removeNotification(Notification n){
public void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;

View File

@@ -10,6 +10,7 @@ import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
@@ -18,6 +19,8 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
@@ -161,6 +164,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private String getPrefilledText() {
return account == null || AccountSessionManager.getInstance().isSelf(accountID, account)
? null : '@'+account.acct+' ';
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
@@ -234,6 +242,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
pager.setOffscreenPageLimit(5);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -271,6 +280,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID, getPrefilledText()));
if(loaded){
bindHeaderView();
@@ -284,14 +294,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
UiUtils.copyText(username, '@'+usernameString);
return true;
});
@@ -548,17 +555,25 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
if(isOwnProfile)
return;
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
MenuItem mute = menu.findItem(R.id.mute);
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);
UiUtils.insetPopupMenuIcon(getContext(), mute);
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
if(relationship.following) {
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername()));
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.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);
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
manageUserLists.setVisible(true);
}else {
menu.findItem(R.id.hide_boosts).setVisible(false);
@@ -623,6 +638,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
}else if(id==R.id.followed_hashtags){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowedHashtagsFragment.class, args);
}else if(id==R.id.scheduled){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
}
return true;
}
@@ -934,9 +957,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
if(!AccountSessionManager.getInstance().isSelf(accountID, account)){
args.putString("prefilledText", '@'+account.acct+' ');
}
if(getPrefilledText() != null) args.putString("prefilledText", getPrefilledText());
Nav.go(getActivity(), ComposeFragment.class, args);
}

View File

@@ -0,0 +1,171 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
private String nextMaxID;
private ImageButton fab;
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
public ScheduledStatusListFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.sk_unsent_posts);
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
fab.setOnClickListener(v -> Nav.go(getActivity(), ComposeFragment.class, args));
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args));
if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null);
}
@Override
protected void addAccountToKnown(ScheduledStatus s) {}
@Override
public void onItemClick(String id) {
final Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = getStatusByID(id);
Status status = scheduledStatus.toStatus();
args.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
args.putParcelable("editStatus", Parcels.wrap(status));
args.putString("sourceText", status.text);
args.putString("sourceSpoiler", status.spoilerText);
args.putBoolean("redraftStatus", true);
setResult(true, null);
// closing this scheduled status list if another status list is opened from compose fragment
Nav.goForResult(getActivity(), ComposeFragment.class, args, SCHEDULED_STATUS_LIST_OPENED, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result) {
if (reqCode == SCHEDULED_STATUS_LIST_OPENED && success && getActivity() != null) {
Nav.finish(this);
}
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<ScheduledStatus> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountID);
}
// copied from StatusListFragment.java
@Subscribe
public void onScheduledStatusDeleted(ScheduledStatusDeletedEvent ev){
if(!ev.accountID.equals(accountID)) return;
ScheduledStatus status=getStatusByID(ev.id);
if(status==null) return;
removeStatus(status);
}
// copied from StatusListFragment.java
@Subscribe
public void onScheduledStatusCreated(ScheduledStatusCreatedEvent ev){
if(!ev.accountID.equals(accountID)) return;
prependItems(Collections.singletonList(ev.scheduledStatus), true);
scrollToTop();
}
// copied from StatusListFragment.java
protected void removeStatus(ScheduledStatus status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(status.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
// copied from StatusListFragment.java
protected ScheduledStatus getStatusByID(String id){
for(ScheduledStatus s:data){
if(s.id.equals(id)){
return s;
}
}
for(ScheduledStatus s:preloadedData){
if(s.id.equals(id)){
return s;
}
}
return null;
}
}

View File

@@ -9,6 +9,7 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -18,8 +19,11 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RadioButton;
@@ -42,15 +46,17 @@ import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.Consumer;
import androidx.annotation.DrawableRes;
@@ -58,6 +64,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageCache;
@@ -86,6 +94,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
setTitle(R.string.settings);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
@@ -98,13 +108,70 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new HeaderItem(R.string.settings_theme));
items.add(themeItem=new ThemeItem());
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
items.add(new ButtonItem(R.string.sk_settings_color_palette, R.drawable.ic_fluent_color_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.color_palettes);
popupMenu.getMenu().findItem(R.id.m3_color).setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick);
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
b.setText(switch(GlobalUserPreferences.color){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
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;
});
}));
items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b->{
updatePublishText(b);
b.setOnClickListener(l->{
FrameLayout inputWrap = new FrameLayout(getContext());
EditText input = new EditText(getContext());
input.setHint(R.string.publish);
input.setText(GlobalUserPreferences.publishButtonText.trim());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(V.dp(16), V.dp(4), V.dp(16), V.dp(16));
input.setLayoutParams(params);
inputWrap.addView(input);
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(inputWrap)
.setPositiveButton(R.string.save, (d, which) -> {
GlobalUserPreferences.publishButtonText = input.getText().toString().trim();
GlobalUserPreferences.save();
updatePublishText(b);
})
.setNeutralButton(R.string.clear, (d, which) -> {
GlobalUserPreferences.publishButtonText = "";
GlobalUserPreferences.save();
updatePublishText(b);
})
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
});
}));
items.add(new SwitchItem(R.string.sk_settings_uniform_icon_for_notifications, R.drawable.ic_ntf_logo, GlobalUserPreferences.uniformNotificationIcon, i->{
GlobalUserPreferences.uniformNotificationIcon=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
GlobalUserPreferences.disableMarquee=i.checked;
GlobalUserPreferences.save();
}));
items.add(new ColorPicker());
items.add(new SwitchItem(R.string.sk_settings_reduce_motion, R.drawable.ic_fluent_star_emphasis_24_regular, GlobalUserPreferences.reduceMotion, i->{
GlobalUserPreferences.reduceMotion=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
GlobalUserPreferences.showFederatedTimeline=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
GlobalUserPreferences.playGifs=i.checked;
GlobalUserPreferences.save();
@@ -121,6 +188,25 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
GlobalUserPreferences.disableSwipe=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{
GlobalUserPreferences.enableDeleteNotifications=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
items.add(new SmallTextItem(getString(translationAvailable ?
R.string.sk_settings_translation_availability_note_available :
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
items.add(new HeaderItem(R.string.home_timeline));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
@@ -135,11 +221,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.loadNewPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
GlobalUserPreferences.showFederatedTimeline=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
@@ -147,30 +228,46 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked)));
items.add(new HeaderItem(R.string.settings_boring));
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new HeaderItem(R.string.settings_account));
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_posting, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/preferences/other"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_filters, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/filters"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
items.add(new RedHeaderItem(R.string.settings_spicy));
items.add(new HeaderItem(instanceName));
items.add(new TextItem(R.string.sk_settings_rules, ()->{
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
items.add(new HeaderItem(R.string.sk_settings_about));
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
if (GithubSelfUpdater.needSelfUpdating()) {
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon")));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{
GlobalUserPreferences.recentLanguages.remove(accountID);
GlobalUserPreferences.save();
})));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
private void updatePublishText(Button btn) {
if (GlobalUserPreferences.publishButtonText.isBlank()) btn.setText(R.string.publish);
else btn.setText(GlobalUserPreferences.publishButtonText);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
@@ -433,6 +530,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.text=getString(text);
}
public HeaderItem(String text) {
this.text=text;
}
@Override
public int getViewType(){
return 0;
@@ -467,7 +568,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
public class ColorPicker extends Item{
public class ButtonItem extends Item{
private int text;
private int icon;
private Consumer<Button> buttonConsumer;
public ButtonItem(@StringRes int text, @DrawableRes int icon, Consumer<Button> buttonConsumer) {
this.text = text;
this.icon = icon;
this.buttonConsumer = buttonConsumer;
}
@Override
public int getViewType(){
return 8;
@@ -490,19 +601,42 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class SmallTextItem extends Item {
private String text;
public SmallTextItem(String text) {
this.text = text;
}
@Override
public int getViewType() {
return 9;
}
}
private class TextItem extends Item{
private String text;
private Runnable onClick;
private boolean loading;
private int icon;
public TextItem(@StringRes int text, Runnable onClick) {
this(text, onClick, false);
this(text, onClick, false, 0);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
public TextItem(@StringRes int text, Runnable onClick, boolean loading) {
this(text, onClick, loading, 0);
}
public TextItem(@StringRes int text, Runnable onClick, @DrawableRes int icon) {
this(text, onClick, false, icon);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading, @DrawableRes int icon){
this.text=getString(text);
this.onClick=onClick;
this.loading=loading;
this.icon=icon;
}
@Override
@@ -558,7 +692,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
case 5 -> new HeaderViewHolder(true);
case 6 -> new FooterViewHolder();
case 7 -> new UpdateViewHolder();
case 8 -> new ColorPickerViewHolder();
case 8 -> new ButtonViewHolder();
case 9 -> new SmallTextViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@@ -687,38 +822,24 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
}
private class ColorPickerViewHolder extends BindableViewHolder<ColorPicker>{
private class ButtonViewHolder extends BindableViewHolder<ButtonItem>{
private final Button button;
private final PopupMenu popupMenu;
private final ImageView icon;
private final TextView text;
@SuppressLint("ClickableViewAccessibility")
public ColorPickerViewHolder(){
super(getActivity(), R.layout.item_settings_color_picker, list);
public ButtonViewHolder(){
super(getActivity(), R.layout.item_settings_button, list);
text=findViewById(R.id.text);
icon=findViewById(R.id.icon);
button=findViewById(R.id.color_picker_button);
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.color_picker);
popupMenu.getMenu().findItem(R.id.m3_color).setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick);
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
button=findViewById(R.id.button);
}
@Override
public void onBind(ColorPicker item){
icon.setImageResource(R.drawable.ic_fluent_color_24_regular);
button.setText(switch(GlobalUserPreferences.color){
case MATERIAL3 -> R.string.sk_color_theme_material3;
case PINK -> R.string.sk_color_theme_pink;
case PURPLE -> R.string.sk_color_theme_purple;
case GREEN -> R.string.sk_color_theme_green;
case BLUE -> R.string.sk_color_theme_blue;
case BROWN -> R.string.sk_color_theme_brown;
case RED -> R.string.sk_color_theme_red;
case YELLOW -> R.string.sk_color_theme_yellow;
default -> throw new IllegalStateException("Unexpected value: "+GlobalUserPreferences.color);
});
public void onBind(ButtonItem item){
text.setText(item.text);
icon.setImageResource(item.icon);
item.buttonConsumer.accept(button);
}
}
@@ -768,17 +889,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
private final ProgressBar progress;
private final ImageView icon;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
progress = itemView.findViewById(R.id.progress);
icon = itemView.findViewById(R.id.icon);
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
progress.animate().alpha(item.loading ? 1 : 0);
if (item.icon != 0) icon.setImageDrawable(getActivity().getTheme().getDrawable(item.icon));
}
@Override
@@ -787,6 +911,24 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class SmallTextViewHolder extends BindableViewHolder<SmallTextItem> {
private final TextView text;
;
public SmallTextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
}
@Override
public void onBind(SmallTextItem item){
text.setText(item.text);
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
}
}
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
private final TextView text;
public FooterViewHolder(){

View File

@@ -54,7 +54,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -139,7 +139,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
action=getString(R.string.edit_multiple_changed);
}
}
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null));
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null, null));
}
return items;
}

View File

@@ -29,7 +29,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true);
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
}
@Override
@@ -189,8 +189,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
}
}
for(Status s:preloadedData){
if(s.id.equals(ev.id)){
s.update(ev);
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
}
}
}

View File

@@ -5,7 +5,6 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
@@ -17,6 +16,7 @@ import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.Collections;
@@ -92,16 +92,10 @@ public class ThreadFragment extends StatusListFragment{
}
private List<Status> filterStatuses(List<Status> statuses){
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
if(filters.isEmpty())
return statuses;
return statuses.stream().filter(status->{
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
}).collect(Collectors.toList());
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
return statuses.stream()
.filter(statusFilterPredicate)
.collect(Collectors.toList());
}
@Override

View File

@@ -104,6 +104,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override

View File

@@ -75,7 +75,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null);
};
}

View File

@@ -139,6 +139,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
headerView.findViewById(R.id.visibility).setVisibility(View.GONE);
headerView.findViewById(R.id.separator).setVisibility(View.GONE);
headerView.findViewById(R.id.timestamp).setVisibility(View.GONE);
headerView.findViewById(R.id.unread_indicator).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.username)).setText(R.string.sk_app_username);
((TextView) headerView.findViewById(R.id.name)).setText(R.string.sk_app_name);
((ImageView) headerView.findViewById(R.id.avatar)).setImageDrawable(getActivity().getDrawable(R.mipmap.ic_launcher));
@@ -205,11 +206,6 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_custom, list);
// itemView.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
// TypedValue value = new TypedValue();
// getActivity().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, value, true);
// itemView.setBackground(getActivity().getTheme().getDrawable(R.drawable.bg_search_field));
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);

View File

@@ -65,7 +65,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
@@ -77,8 +77,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override

View File

@@ -155,7 +155,7 @@ public class SignupFragment extends ToolbarFragment{
}
private void onButtonClick(){
if(!password.getText().equals(passwordConfirm.getText())){
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
passwordConfirmWrap.setErrorState();
return;

View File

@@ -237,7 +237,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem isdi){
isdi.horizontalInset=V.dp(40+32);

View File

@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Switch;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
@@ -28,15 +29,17 @@ import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class ReportCommentFragment extends MastodonToolbarFragment{
private String accountID;
private Account reportAccount;
private Button btn;
private View buttonBar;
private View buttonBar, forwardReportItem;
private TextView forwardReportText;
private Switch forwardReportSwitch;
private EditText commentEdit;
private boolean forwardReport;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -77,7 +80,17 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
commentEdit=view.findViewById(R.id.text);
forwardReportSwitch = view.findViewById(R.id.forward_report_switch);
forwardReportItem = view.findViewById(R.id.forward_report);
forwardReportText = view.findViewById(R.id.forward_report_text);
String domain = reportAccount.getDomain();
if (domain == null) {
forwardReportItem.setVisibility(View.GONE);
} else {
forwardReportItem.setOnClickListener(this::onForwardReportClick);
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
forwardReportSwitch.setChecked(forwardReport = true);
}
return view;
}
@@ -102,7 +115,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), true)
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardReport)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
@@ -123,6 +136,11 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
.exec(accountID);
}
private void onForwardReportClick(View v) {
forwardReport = !forwardReport;
forwardReportSwitch.setChecked(forwardReport);
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))

View File

@@ -164,6 +164,10 @@ public class Account extends BaseModel{
return '@'+acct;
}
public String getShortUsername() {
return '@'+acct.split("@")[0];
}
@Override
public String toString(){
return "Account{"+

View File

@@ -0,0 +1,63 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
@Parcel
public class Announcement extends BaseModel implements DisplayItemsParent {
@RequiredField
public String id;
@RequiredField
public String content;
public Instant startsAt;
public Instant endsAt;
public boolean published;
public boolean allDay;
public Instant publishedAt;
public Instant updatedAt;
public boolean read;
public List<Emoji> emojis;
public List<Mention> mentions;
public List<Hashtag> tags;
@Override
public String toString() {
return "Announcement{" +
"id='" + id + '\'' +
", content='" + content + '\'' +
", startsAt=" + startsAt +
", endsAt=" + endsAt +
", published=" + published +
", allDay=" + allDay +
", publishedAt=" + publishedAt +
", updatedAt=" + updatedAt +
", read=" + read +
", emojis=" + emojis +
", mentions=" + mentions +
", tags=" + tags +
'}';
}
public Status toStatus() {
Status s = new Status();
s.id = id;
s.mediaAttachments = List.of();
s.createdAt = startsAt != null ? startsAt : publishedAt;
if (updatedAt != null) s.editedAt = updatedAt;
s.content = s.text = content;
s.spoilerText = "";
s.visibility = StatusPrivacy.PUBLIC;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
return s;
}
@Override
public String getID() {
return id;
}
}

View File

@@ -6,12 +6,14 @@ import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
@Parcel
public class Filter extends BaseModel{
@RequiredField
public String id;
@@ -21,6 +23,7 @@ public class Filter extends BaseModel{
public Instant expiresAt;
public boolean irreversible;
public boolean wholeWord;
public FilterAction filterAction;
@SerializedName("context")
private List<FilterContext> _context;
@@ -76,4 +79,11 @@ public class Filter extends BaseModel{
@SerializedName("thread")
THREAD
}
public enum FilterAction{
@SerializedName("hide")
HIDE,
@SerializedName("warn")
WARN
}
}

View File

@@ -0,0 +1,8 @@
package org.joinmastodon.android.model;
import org.parceler.Parcel;
@Parcel
public class FilterResult extends BaseModel {
public Filter filter;
}

View File

@@ -57,6 +57,11 @@ public class Poll extends BaseModel{
public String title;
public Integer votesCount;
public Option() {}
public Option(String title) {
this.title = title;
}
@Override
public String toString(){
return "Option{"+

View File

@@ -0,0 +1,79 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
@Parcel
public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
@RequiredField
public String id;
@RequiredField
public Instant scheduledAt;
@RequiredField
public Params params;
@RequiredField
public List<Attachment> mediaAttachments;
@Override
public String getID() {
return id;
}
@Parcel
public static class Params {
@RequiredField
public String text;
public String spoilerText;
@RequiredField
public StatusPrivacy visibility;
public long inReplyToId;
public ScheduledPoll poll;
public boolean sensitive;
public boolean withRateLimit;
public String language;
public String idempotency;
public String applicationId;
public List<String> mediaIds;
}
@Parcel
public static class ScheduledPoll {
@RequiredField
public String expiresIn;
@RequiredField
public List<String> options;
public boolean multiple;
public boolean hideTotals;
public Poll toPoll() {
Poll p = new Poll();
p.voted = true;
p.emojis = List.of();
p.ownVotes = List.of();
p.multiple = multiple;
p.options = options.stream().map(Option::new).collect(Collectors.toList());
return p;
}
}
public Status toStatus() {
Status s = new Status();
s.id = id;
s.mediaAttachments = mediaAttachments;
s.createdAt = scheduledAt;
s.content = s.text = params.text;
s.spoilerText = params.spoilerText;
s.visibility = params.visibility;
s.language = params.language;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
if (params.poll != null) s.poll = params.poll.toPoll();
return s;
}
}

View File

@@ -40,6 +40,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public long favouritesCount;
public long repliesCount;
public Instant editedAt;
public List<FilterResult> filtered;
public String url;
public String inReplyToId;

View File

@@ -4,6 +4,7 @@ import android.app.AlertDialog;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import me.grishka.appkit.utils.V;
@@ -31,8 +32,16 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
if(titleID!=0){
View title=alert.findViewById(titleID);
if(title!=null){
int iconID=getContext().getResources().getIdentifier("icon", "id", "android");
int alertTitleID=getContext().getResources().getIdentifier("alertTitle", "id", "android");
if (alertTitleID != 0 && iconID != 0) {
ImageView icon = title.findViewById(iconID);
if (icon.getDrawable() != null) {
title.findViewById(alertTitleID).setPadding(V.dp(8), 0, 0, 0);
}
}
int pad=V.dp(24);
title.setPadding(pad, pad, pad, pad);
title.setPadding(pad, pad, pad, V.dp(12));
}
}
int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android");

View File

@@ -98,7 +98,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_at_symbol;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
});
}

View File

@@ -1,31 +1,39 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.function.Consumer;
import me.grishka.appkit.Nav;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
@@ -51,6 +59,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private final ImageView share;
private static final Animation opacityOut, opacityIn;
private View touchingView = null;
private boolean longClickPerformed = false;
private final Runnable longClickRunnable = () -> {
longClickPerformed = touchingView != null && touchingView.performLongClick();
if (longClickPerformed && touchingView != null) {
touchingView.startAnimation(opacityIn);
touchingView.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
}
};
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info){
@@ -61,12 +79,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
};
static {
opacityOut = new AlphaAnimation(1, 0.5f);
opacityOut.setDuration(200);
opacityOut = new AlphaAnimation(1, 0.55f);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(0.5f, 1);
opacityIn.setDuration(150);
opacityIn = new AlphaAnimation(0.55f, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
}
@@ -90,18 +108,23 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
View bookmark=findViewById(R.id.bookmark_btn);
reply.setOnTouchListener(this::onButtonTouch);
reply.setOnClickListener(this::onReplyClick);
reply.setOnLongClickListener(this::onReplyLongClick);
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
boost.setOnTouchListener(this::onButtonTouch);
boost.setOnClickListener(this::onBoostClick);
boost.setOnLongClickListener(this::onBoostLongClick);
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
favorite.setOnTouchListener(this::onButtonTouch);
favorite.setOnClickListener(this::onFavoriteClick);
favorite.setOnLongClickListener(this::onFavoriteLongClick);
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
bookmark.setOnTouchListener(this::onButtonTouch);
bookmark.setOnClickListener(this::onBookmarkClick);
bookmark.setOnLongClickListener(this::onBookmarkLongClick);
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
share.setOnTouchListener(this::onButtonTouch);
share.setOnClickListener(this::onShareClick);
share.setOnLongClickListener(this::onShareLongClick);
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
}
@@ -110,6 +133,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount);
reply.setSelected(item.status.repliesCount > 0);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
@@ -127,37 +151,149 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
}
private boolean onButtonTouch(View v, MotionEvent event){
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
touchingView = null;
v.removeCallbacks(longClickRunnable);
if (!longClickPerformed) v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
if (disabled) return true;
if (action == MotionEvent.ACTION_UP && !longClickPerformed) v.performClick();
else if (!longClickPerformed) v.startAnimation(opacityIn);
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
v.setPivotX(V.dp(20));
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
v.startAnimation(opacityOut);
}
return true;
}
private void onReplyClick(View v){
v.startAnimation(opacityIn);
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("replyTo", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}
private boolean onButtonTouch(View v, MotionEvent event){
int action = event.getAction();
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
v.setPivotX(V.dp(20));
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(100).start();
if (action == MotionEvent.ACTION_UP) v.performClick();
} else if (action == MotionEvent.ACTION_DOWN) {
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
}
private boolean onReplyLongClick(View v) {
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 -> {
Bundle args=new Bundle();
String accountID = session.getID();
args.putString("account", accountID);
UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> {
args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
}, null);
return true;
}
private void onBoostClick(View v){
v.startAnimation(opacityOut);
boost.setSelected(!item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, r->{
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
}
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
}
private boolean onBoostLongClick(View v){
Context ctx = itemView.getContext();
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
AccountSession session = AccountSessionManager.getInstance().getAccount(item.accountID);
Consumer<StatusPrivacy> doReblog = (visibility) -> {
v.startAnimation(opacityOut);
session.getStatusInteractionController()
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
dialog.dismiss();
};
View separator = menu.findViewById(R.id.separator);
TextView reblogHeader = menu.findViewById(R.id.reblog_header);
TextView undoReblog = menu.findViewById(R.id.delete_reblog);
TextView reblogAs = menu.findViewById(R.id.reblog_as);
TextView itemPublic = menu.findViewById(R.id.vis_public);
TextView itemUnlisted = menu.findViewById(R.id.vis_unlisted);
TextView itemFollowers = menu.findViewById(R.id.vis_followers);
undoReblog.setVisibility(item.status.reblogged ? View.VISIBLE : View.GONE);
separator.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE);
itemPublic.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PUBLIC) ? View.GONE : View.VISIBLE);
itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE);
itemFollowers.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PRIVATE) ? View.GONE : View.VISIBLE);
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_community_24_regular);
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_checkmark_24_regular);
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
// e.g. post visibility is unlisted, but default is public
// in this case, we want to display the check mark on the most visible visibility
if (defaultVisibility != null && item.status.visibility.isLessVisibleThan(defaultVisibility)) {
for (StatusPrivacy vis : StatusPrivacy.values()) {
if (vis.equals(item.status.visibility)) {
defaultVisibility = vis;
break;
}
}
}
itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null);
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);
undoReblog.setOnClickListener(c->doReblog.accept(null));
itemPublic.setOnClickListener(c->doReblog.accept(StatusPrivacy.PUBLIC));
itemUnlisted.setOnClickListener(c->doReblog.accept(StatusPrivacy.UNLISTED));
itemFollowers.setOnClickListener(c->doReblog.accept(StatusPrivacy.PRIVATE));
reblogAs.setOnClickListener(c->{
dialog.dismiss();
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.reblogged,
(ic, status, consumer) -> ic.setReblogged(status, true, null, consumer),
R.string.sk_reblog_as,
R.string.sk_reblogged_as,
R.string.sk_already_reblogged,
// TODO: replace once available: https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/android/library/src/main/res/drawable/ic_fluent_arrow_repeat_all_28_regular.xml
R.drawable.ic_fluent_arrow_repeat_all_24_regular
);
});
menu.findViewById(R.id.quote).setOnClickListener(c->{
dialog.dismiss();
v.startAnimation(opacityIn);
Bundle args=new Bundle();
args.putString("account", item.accountID);
StringBuilder prefilledText = new StringBuilder().append("\n\n");
String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id;
if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' ');
prefilledText.append(item.status.url);
args.putString("prefilledText", prefilledText.toString());
args.putInt("selectionStart", 0);
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
dialog.show();
return true;
}
private void onFavoriteClick(View v){
v.startAnimation(opacityOut);
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn);
@@ -165,21 +301,54 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
});
}
private boolean onFavoriteLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.favourited,
(ic, status, consumer) -> ic.setFavorited(status, true, consumer),
R.string.sk_favorite_as,
R.string.sk_favorited_as,
R.string.sk_already_favorited,
R.drawable.ic_fluent_star_28_regular
);
return true;
}
private void onBookmarkClick(View v){
v.startAnimation(opacityOut);
bookmark.setSelected(item.status.bookmarked);
bookmark.setSelected(!item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
v.startAnimation(opacityIn);
});
}
private boolean onBookmarkLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.bookmarked,
(ic, status, consumer) -> ic.setBookmarked(status, true, consumer),
R.string.sk_bookmark_as,
R.string.sk_bookmarked_as,
R.string.sk_already_bookmarked,
R.drawable.ic_fluent_bookmark_28_regular
);
return true;
}
private void onShareClick(View v){
v.startAnimation(opacityIn);
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, item.status.url);
v.getContext().startActivity(Intent.createChooser(intent, v.getContext().getString(R.string.share_toot_title)));
}
private boolean onShareLongClick(View v){
UiUtils.copyText(v, item.status.url);
return true;
}
private int descriptionForId(int id){
if(id==R.id.reply_btn)
return R.string.button_reply;

View File

@@ -11,6 +11,7 @@ import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
@@ -22,16 +23,23 @@ import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.announcements.DismissAnnouncement;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
@@ -39,8 +47,13 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
@@ -62,15 +75,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private boolean hasVisibilityToggle;
boolean needBottomPadding;
private String extraText;
private Notification notification;
private ScheduledStatus scheduledStatus;
private Announcement announcement;
private Consumer<String> consumeReadAnnouncement;
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText, Notification notification, ScheduledStatus scheduledStatus){
super(parentID, parentFragment);
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
this.user=user;
this.createdAt=createdAt;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName);
this.status=status;
this.notification=notification;
this.scheduledStatus=scheduledStatus;
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
emojiHelper.setText(parsedName);
if(status!=null){
@@ -87,6 +107,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
this.extraText=extraText;
}
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt, parentFragment, accountID, fakeStatus, null, null, null);
item.announcement = a;
item.consumeReadAnnouncement = consumeReadID;
return item;
}
@Override
public Type getType(){
return Type.HEADER;
@@ -106,8 +133,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView name, username, timestamp, extraText;
private final ImageView avatar, more, visibility;
private final TextView name, username, timestamp, extraText, separator;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator;
private final PopupMenu optionsMenu;
private Relationship relationship;
private APIRequest<?> currentRelationshipRequest;
@@ -123,22 +150,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
super(activity, R.layout.display_item_header, parent);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
separator=findViewById(R.id.separator);
timestamp=findViewById(R.id.timestamp);
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
visibility=findViewById(R.id.visibility);
deleteNotification=findViewById(R.id.delete_notification);
unreadIndicator=findViewById(R.id.unread_indicator);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(roundCornersOutline);
avatar.setClipToOutline(true);
more.setOnClickListener(this::onMoreClick);
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
if (item.parentFragment instanceof NotificationsListFragment fragment) {
fragment.removeNotification(item.notification);
}
}));
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
int id=menuItem.getItemId();
if(id==R.id.edit || id==R.id.delete_and_redraft) {
final Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
@@ -153,6 +189,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text);
args.putString("sourceSpoiler", item.status.spoilerText);
args.putBoolean("redraftStatus", true);
args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else{
new GetStatusSourceText(item.status.id)
.setCallback(new Callback<>(){
@@ -178,8 +220,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
.exec(item.parentFragment.getAccountID());
}
}else if(id==R.id.delete){
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.pin || id==R.id.unpin){
if (item.scheduledStatus != null) {
UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{});
} else {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}
}else if(id==R.id.pin || id==R.id.unpin) {
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
@@ -191,8 +237,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putParcelable("status", Parcels.wrap(item.status));
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
}else if(id==R.id.open_in_browser) {
UiUtils.launchWebBrowser(activity, item.status.url);
}else if(id==R.id.copy_link){
UiUtils.copyText(parent, item.status.url);
}else if(id==R.id.follow){
if(relationship==null)
return true;
@@ -206,7 +254,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
progress.dismiss();
}, rel->{
relationship=rel;
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getShortUsername()), Toast.LENGTH_SHORT).show();
});
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
@@ -215,17 +263,43 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
return true;
});
UiUtils.enablePopupMenuIcons(activity, optionsMenu);
}
private void populateAccountsMenu(Menu menu) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
sessions.stream().filter(s -> !s.getID().equals(item.accountID)).forEach(s -> {
String username = "@"+s.self.username+"@"+s.domain;
menu.add(username).setOnMenuItemClickListener(c->{
UiUtils.openURL(item.parentFragment.getActivity(), s.getID(), item.status.url, false);
return true;
});
});
}
@Override
public void onBind(HeaderStatusDisplayItem item){
name.setText(item.parsedName);
username.setText('@'+item.user.acct);
if(item.status==null || item.status.editedAt==null)
separator.setVisibility(View.VISIBLE);
if (item.scheduledStatus!=null)
if (item.scheduledStatus.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT)) {
timestamp.setText(R.string.sk_draft);
} else {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
}
else if ((item.status==null || item.status.editedAt==null) && item.createdAt != null)
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
else
else if (item.status != null && item.status.editedAt != null)
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
else {
separator.setVisibility(View.GONE);
timestamp.setText("");
}
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
if(item.hasVisibilityToggle){
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
@@ -247,6 +321,42 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
currentRelationshipRequest.cancel();
}
relationship=null;
String desc;
if (item.announcement != null) {
if (unreadIndicator.getVisibility() == View.GONE) {
more.setAlpha(0f);
unreadIndicator.setAlpha(0f);
unreadIndicator.setVisibility(View.VISIBLE);
}
float alpha = item.announcement.read ? 0 : 1;
more.setImageResource(R.drawable.ic_fluent_checkmark_20_filled);
desc = item.parentFragment.getString(R.string.sk_mark_as_read);
more.animate().alpha(alpha);
unreadIndicator.animate().alpha(alpha);
more.setOnClickListener(v -> {
new DismissAnnouncement(item.announcement.id).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
item.consumeReadAnnouncement.accept(item.announcement.id);
item.announcement.read = true;
rebind();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(item.parentFragment.getActivity());
}
}).exec(item.accountID);
});
} else {
more.setImageResource(R.drawable.ic_fluent_more_vertical_20_filled);
desc = item.parentFragment.getString(R.string.more_options);
more.setOnClickListener(this::onMoreClick);
}
more.setContentDescription(desc);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc);
}
@Override
@@ -267,6 +377,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void onAvaClick(View v){
if (item.announcement != null) {
UiUtils.openURL(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.user.url);
return;
}
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.user));
@@ -298,15 +412,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void updateOptionsMenu(){
Account account=item.user;
if (item.announcement != null) return;
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
Menu menu=optionsMenu.getMenu();
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
SubMenu accountsMenu = openWithAccounts != null ? openWithAccounts.getSubMenu() : null;
if (hasMultipleAccounts && accountsMenu != null) {
openWithAccounts.setVisible(true);
accountsMenu.clear();
populateAccountsMenu(accountsMenu);
} else if (openWithAccounts != null) {
openWithAccounts.setVisible(false);
}
Account account=item.user;
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
boolean isPostScheduled=item.scheduledStatus!=null;
menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts);
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
menu.findItem(R.id.delete_and_redraft).setVisible(!isPostScheduled && item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(!isPostScheduled && item.status!=null);
menu.findItem(R.id.copy_link).setVisible(!isPostScheduled && item.status!=null);
MenuItem blockDomain=menu.findItem(R.id.block_domain);
MenuItem mute=menu.findItem(R.id.mute);
MenuItem block=menu.findItem(R.id.block);
@@ -322,7 +452,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
bookmark.setVisible(false);
}
*/
if(isOwnPost){
if(isPostScheduled || isOwnPost){
mute.setVisible(false);
block.setVisible(false);
report.setVisible(false);
@@ -333,16 +463,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
block.setVisible(true);
report.setVisible(true);
follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting));
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getDisplayUsername()));
if(!account.isLocal()){
blockDomain.setVisible(true);
blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
}else{
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getShortUsername()));
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
// if(!account.isLocal()){
// blockDomain.setVisible(true);
// blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
// }else{
blockDomain.setVisible(false);
}
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.getDisplayUsername()));
// }
boolean following = relationship!=null && relationship.following;
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, account.getShortUsername()));
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow);
}
}
}

View File

@@ -95,7 +95,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
}
private void onClick(View v){
UiUtils.launchWebBrowser(itemView.getContext(), item.status.card.url);
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url);
}
}
}

View File

@@ -94,6 +94,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
}else{
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
icon.setSelected(itemView.isSelected());
icon.setVisibility(View.VISIBLE);
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.MastodonApp.context;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.SpannableStringBuilder;
@@ -14,6 +15,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -30,10 +32,13 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
@DrawableRes
private int icon;
private StatusPrivacy visibility;
@DrawableRes
private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private View.OnClickListener handleClick;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, @Nullable View.OnClickListener handleClick){
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick){
super(parentID, parentFragment);
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
HtmlParser.parseCustomEmoji(ssb, emojis);
@@ -43,6 +48,17 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
this.handleClick=handleClick;
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
updateVisibility(visibility);
}
public void updateVisibility(StatusPrivacy visibility) {
this.visibility = visibility;
this.iconEnd = visibility != null ? switch (visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
default -> 0;
} : 0;
}
@Override
@@ -70,10 +86,18 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
text.setEnabled(!item.inset);
text.setClickable(!item.inset);
Context ctx = itemView.getContext();
int visibilityText = item.visibility != null ? switch (item.visibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
default -> 0;
} : 0;
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")");
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
}

View File

@@ -8,13 +8,16 @@ import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -73,26 +76,29 @@ public abstract class StatusDisplayItem{
};
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
if(status.reblog!=null){
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, i->{
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, i->{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, null, i->{
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}
HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus));
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
else

View File

@@ -12,6 +12,7 @@ import android.widget.LinearLayout;
import android.widget.Button;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSession;
@@ -24,6 +25,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.TranslatedStatus;
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.views.LinkedTextView;
import me.grishka.appkit.api.Callback;
@@ -31,6 +33,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.MovieDrawable;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class TextStatusDisplayItem extends StatusDisplayItem{
@@ -41,10 +44,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public boolean translated = false;
public TranslatedStatus translation = null;
private AccountSession session;
private Instance instanceInfo;
private boolean translateEnabled;
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){
super(parentID, parentFragment);
@@ -57,8 +57,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
translateEnabled = instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
}
@Override
@@ -85,7 +83,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
private final LinearLayout spoilerHeader;
private final TextView spoilerTitle, spoilerTitleInline, translateInfo;
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress;
private final Drawable backgroundColor, borderColor;
private final int backgroundColor, borderColor;
private final Button translateButton;
public Holder(Activity activity, ViewGroup parent){
@@ -103,14 +101,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
translateInfo=findViewById(R.id.translate_info);
translateProgress=findViewById(R.id.translate_progress);
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
TypedValue outValue=new TypedValue();
activity.getTheme().resolveAttribute(R.attr.colorBackgroundLight, outValue, true);
backgroundColor=activity.getDrawable(outValue.resourceId);
// activity.getTheme().resolveAttribute(R.attr.colorBackgroundLightest, outValue, true);
// backgroundColorInset=activity.getDrawable(outValue.resourceId);
activity.getTheme().resolveAttribute(R.attr.colorPollVoted, outValue, true);
borderColor=activity.getDrawable(outValue.resourceId);
backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight);
borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted);
}
@Override
@@ -121,10 +113,10 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setTextIsSelectable(item.textSelectable);
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
spoilerTitleInline.setBackground(item.inset ? null : backgroundColor);
spoilerTitleInline.setBackgroundColor(item.inset ? 0 : backgroundColor);
spoilerTitleInline.setPadding(spoilerTitleInline.getPaddingLeft(), item.inset ? 0 : V.dp(14), spoilerTitleInline.getPaddingRight(), item.inset ? 0 : V.dp(14));
borderTop.setBackground(item.inset ? null : borderColor);
borderBottom.setBackground(item.inset ? null : borderColor);
borderTop.setBackgroundColor(item.inset ? 0 : borderColor);
borderBottom.setBackgroundColor(item.inset ? 0 : borderColor);
if(!TextUtils.isEmpty(item.status.spoilerText)){
spoilerTitle.setText(item.parsedSpoilerText);
spoilerTitleInline.setText(item.parsedSpoilerText);
@@ -146,7 +138,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
itemView.setClickable(false);
}
translateWrap.setVisibility(item.textSelectable && item.translateEnabled &&
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
boolean translateEnabled = instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
translateWrap.setVisibility(
(!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) &&
translateEnabled &&
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
item.status.language != null &&
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))
@@ -157,6 +154,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
if (item.translation == null) {
translateProgress.setVisibility(View.VISIBLE);
translateButton.setClickable(false);
translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
new TranslateStatus(item.status.id).setCallback(new Callback<>() {
@Override
public void onSuccess(TranslatedStatus translatedStatus) {
@@ -164,6 +162,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
item.translated = true;
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
rebind();
}
@@ -171,6 +170,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void onError(ErrorResponse error) {
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
error.showToast(itemView.getContext());
}
}).exec(item.parentFragment.getAccountID());

View File

@@ -10,6 +10,8 @@ import android.text.Layout;
import android.text.Spanned;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import me.grishka.appkit.utils.V;
@@ -20,7 +22,11 @@ public class ClickableLinksDelegate {
private Path hlPath;
private LinkSpan selectedSpan;
private TextView view;
private final Runnable longClickRunnable = () -> {
if (selectedSpan != null) selectedSpan.onLongClick(view);
};
public ClickableLinksDelegate(TextView view) {
this.view=view;
hlPaint=new Paint();
@@ -30,6 +36,7 @@ public class ClickableLinksDelegate {
}
public boolean onTouch(MotionEvent event) {
long eventDuration = event.getEventTime() - event.getDownTime();
if(event.getAction()==MotionEvent.ACTION_DOWN){
int line=-1;
Rect rect=new Rect();
@@ -63,6 +70,7 @@ public class ClickableLinksDelegate {
}
hlPath=new Path();
selectedSpan=span;
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
@@ -90,8 +98,11 @@ public class ClickableLinksDelegate {
}
}
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
if (eventDuration <= ViewConfiguration.getLongPressTimeout()) {
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
}
view.removeCallbacks(longClickRunnable);
hlPath=null;
selectedSpan=null;
view.invalidate();
@@ -100,6 +111,7 @@ public class ClickableLinksDelegate {
if(event.getAction()==MotionEvent.ACTION_CANCEL){
hlPath=null;
selectedSpan=null;
view.removeCallbacks(longClickRunnable);
view.invalidate();
return false;
}

View File

@@ -111,14 +111,14 @@ public class HtmlParser{
@Override
public void head(@NonNull Node node, int depth){
if(node instanceof TextNode textNode){
ssb.append(textNode.text());
ssb.append(textNode.getWholeText());
}else if(node instanceof Element el){
switch(el.nodeName()){
case "a" -> {
String href=el.attr("href");
LinkSpan.Type linkType;
String text=el.text();
if(el.hasClass("hashtag")){
String text=el.text();
if(text.startsWith("#")){
linkType=LinkSpan.Type.HASHTAG;
href=text.substring(1);
@@ -136,7 +136,7 @@ public class HtmlParser{
}else{
linkType=LinkSpan.Type.URL;
}
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, text), ssb.length(), el));
}
case "br" -> ssb.append('\n');
case "span" -> {
@@ -182,7 +182,7 @@ public class HtmlParser{
ssb.append("", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}else if(blockElements.contains(el.nodeName()) && node.nextSibling()!=null){
ssb.append("\n"); // line end
ssb.append("\n", new RelativeSizeSpan(0.75f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
ssb.append("\n", new RelativeSizeSpan(0.65f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
}
}
}
@@ -260,7 +260,7 @@ public class HtmlParser{
String url=matcher.group(3);
if(TextUtils.isEmpty(matcher.group(4)))
url="http://"+url;
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, url), matcher.start(3), matcher.end(3), 0);
}while(matcher.find()); // Find more URLs
return ssb;
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.view.View;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -13,12 +14,14 @@ public class LinkSpan extends CharacterStyle {
private String link;
private Type type;
private String accountID;
private String text;
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){
this.listener=listener;
this.link=link;
this.type=type;
this.accountID=accountID;
this.text=text;
}
public int getColor(){
@@ -38,6 +41,10 @@ public class LinkSpan extends CharacterStyle {
}
}
public void onLongClick(View view) {
UiUtils.copyText(view, getType() == Type.URL ? link : text);
}
public String getLink(){
return link;
}

View File

@@ -0,0 +1,68 @@
package org.joinmastodon.android.ui.utils;
import static org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.StyleRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import java.util.Map;
public class ColorPalette {
public static final Map<GlobalUserPreferences.ColorPreference, ColorPalette> palettes = Map.of(
ColorPreference.MATERIAL3, new ColorPalette(R.style.ColorPalette_Material3)
.dark(R.style.ColorPalette_Material3_Dark, R.style.ColorPalette_Material3_AutoLightDark),
ColorPreference.PINK, new ColorPalette(R.style.ColorPalette_Pink),
ColorPreference.PURPLE, new ColorPalette(R.style.ColorPalette_Purple),
ColorPreference.GREEN, new ColorPalette(R.style.ColorPalette_Green),
ColorPreference.BLUE, new ColorPalette(R.style.ColorPalette_Blue),
ColorPreference.BROWN, new ColorPalette(R.style.ColorPalette_Brown),
ColorPreference.RED, new ColorPalette(R.style.ColorPalette_Red),
ColorPreference.YELLOW, new ColorPalette(R.style.ColorPalette_Yellow)
);
private @StyleRes int base;
private @StyleRes int autoDark;
private @StyleRes int light;
private @StyleRes int dark;
private @StyleRes int black;
private @StyleRes int autoBlack;
public ColorPalette(@StyleRes int baseRes) { base = baseRes; }
public ColorPalette(@StyleRes int lightRes, @StyleRes int darkRes, @StyleRes int autoDarkRes, @StyleRes int blackRes, @StyleRes int autoBlackRes) {
light = lightRes;
dark = darkRes;
autoDark = autoDarkRes;
black = blackRes;
autoBlack = autoBlackRes;
}
public ColorPalette light(@StyleRes int res) { light = res; return this; }
public ColorPalette dark(@StyleRes int res, @StyleRes int auto) { dark = res; autoDark = auto; return this; }
public ColorPalette black(@StyleRes int res, @StyleRes int auto) { dark = res; autoBlack = auto; return this; }
public void apply(Context context) {
if (!((dark != 0 && autoDark != 0) || (black != 0 && autoBlack != 0) || light != 0 || base != 0)) {
throw new IllegalStateException("Invalid color scheme definition");
}
Resources.Theme t = context.getTheme();
if (base != 0) t.applyStyle(base, true);
if (light != 0 && theme.equals(ThemePreference.LIGHT)) t.applyStyle(light, true);
else if (theme.equals(ThemePreference.DARK)) {
if (dark != 0 && !trueBlackTheme) t.applyStyle(dark, true);
else if (black != 0 && trueBlackTheme) t.applyStyle(black, true);
} else if (theme.equals(ThemePreference.AUTO)) {
if (autoDark != 0 && !trueBlackTheme) t.applyStyle(autoDark, true);
else if (autoBlack != 0 && trueBlackTheme) t.applyStyle(autoBlack, true);
}
}
}

View File

@@ -1,9 +1,17 @@
package org.joinmastodon.android.ui.utils;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -20,9 +28,10 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -36,23 +45,28 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
@@ -60,16 +74,21 @@ import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.parceler.Parcels;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -80,9 +99,12 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent;
@@ -326,22 +348,29 @@ public class UiUtils{
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
}
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed){
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
}
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed){
new M3AlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(confirmButton, (dlg, i)->onConfirmed.run())
.setNegativeButton(R.string.cancel, null)
.setIcon(icon)
.show();
}
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),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
currentlyBlocked ? R.drawable.ic_fluent_person_28_regular : R.drawable.ic_fluent_person_prohibited_28_regular,
()->{
new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
@@ -365,7 +394,9 @@ public class UiUtils{
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
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_shield_28_regular,
()->{
new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
@@ -386,7 +417,9 @@ public class UiUtils{
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), ()->{
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute),
currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
()->{
new SetAccountMuted(account.id, !currentlyMuted)
.setCallback(new Callback<>(){
@Override
@@ -411,24 +444,53 @@ public class UiUtils{
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
showConfirmationAlert(activity, 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_delete_and_redraft : R.string.delete, ()->{
new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
showConfirmationAlert(activity,
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_delete_and_redraft : R.string.delete,
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID);
});
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID)
);
}
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback){
boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT);
showConfirmationAlert(activity,
isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title,
isDraft ? R.string.sk_confirm_delete_draft : R.string.sk_confirm_delete_scheduled_post,
R.string.delete,
R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus.Scheduled(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object nothing){
resultCallback.run();
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID)
);
}
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
@@ -436,6 +498,7 @@ public class UiUtils{
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
pinned ? R.drawable.ic_fluent_pin_28_regular : R.drawable.ic_fluent_pin_off_28_regular,
()->{
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@@ -458,6 +521,26 @@ public class UiUtils{
);
}
public static void confirmDeleteNotification(Activity activity, String accountID, Notification notification, Runnable callback) {
showConfirmationAlert(activity,
notification == null ? R.string.sk_clear_all_notifications : R.string.sk_delete_notification,
notification == null ? R.string.sk_clear_all_notifications_confirm : R.string.sk_delete_notification_confirm,
notification == null ? R.string.sk_clear_all_notifications_confirm_action : R.string.sk_delete_notification_confirm_action,
notification == null ? R.drawable.ic_fluent_mail_inbox_dismiss_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DismissNotification(notification != null ? notification.id : null).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
callback.run();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){
setRelationshipToActionButton(relationship, button, false);
}
@@ -625,6 +708,42 @@ public class UiUtils{
return bitmap;
}
public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
insetPopupMenuIcon(item, iconTint);
}
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint);
icon=new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb);
}
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
try {
Method m = menu.getClass().getDeclaredMethod(
"setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu, true);
enableMenuIcons(context, menu, asAction);
}
catch(Exception ignored){}
}
}
public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
insetPopupMenuIcon(item, iconTint);
}
}
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
Menu m=menu.getMenu();
if(Build.VERSION.SDK_INT>=29){
@@ -636,123 +755,158 @@ public class UiUtils{
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
}
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26){
item.setIconTintList(iconTint);
}else{
icon.setTintList(iconTint);
}
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
item.setTitle(ssb);
}
enableMenuIcons(context, m);
}
public static void setUserPreferredTheme(Context context){
// boolean isDarkTheme = isDarkTheme();
switch(GlobalUserPreferences.color){
case PINK:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
case LIGHT ->
R.style.Theme_Mastodon_Light;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
});
break;
case PURPLE:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Original : R.style.Theme_Mastodon_AutoLightDark_Original;
case LIGHT ->
R.style.Theme_Mastodon_Light_Original;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Original : R.style.Theme_Mastodon_Dark_Original;
});
break;
case GREEN:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Green : R.style.Theme_Mastodon_AutoLightDark_Green;
case LIGHT ->
R.style.Theme_Mastodon_Light_Green;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Green : R.style.Theme_Mastodon_Dark_Green;
});
break;
case BLUE:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Blue : R.style.Theme_Mastodon_AutoLightDark_Blue;
case LIGHT ->
R.style.Theme_Mastodon_Light_Blue;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Blue : R.style.Theme_Mastodon_Dark_Blue;
});
break;
case BROWN:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Brown : R.style.Theme_Mastodon_AutoLightDark_Brown;
case LIGHT ->
R.style.Theme_Mastodon_Light_Brown;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Brown : R.style.Theme_Mastodon_Dark_Brown;
});
break;
case RED:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Red : R.style.Theme_Mastodon_AutoLightDark_Red;
case LIGHT ->
R.style.Theme_Mastodon_Light_Red;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Red : R.style.Theme_Mastodon_Dark_Red;
});
break;
case YELLOW:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Yellow : R.style.Theme_Mastodon_AutoLightDark_Yellow;
case LIGHT ->
R.style.Theme_Mastodon_Light_Yellow;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Yellow : R.style.Theme_Mastodon_Dark_Yellow;
});
break;
case MATERIAL3:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Material3 : R.style.Theme_Mastodon_AutoLightDark_Material3;
case LIGHT ->
R.style.Theme_Mastodon_Light_Material3;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Material3 : R.style.Theme_Mastodon_Dark_Material3;
});
break;
}
context.setTheme(switch (theme) {
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
default -> trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
});
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context);
}
public static boolean isDarkTheme(){
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
if(theme==GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
return GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK;
return theme==GlobalUserPreferences.ThemePreference.DARK;
}
public static void openURL(Context context, @Nullable String accountID, String url){
// https://mastodon.foo.bar/@User
// https://mastodon.foo.bar/@User/43456787654678
// https://pleroma.foo.bar/users/User
// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://friendica.foo.bar/profile/user
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
// https://pixelfed.social/p/connyduck/391263492998670833
// https://pixelfed.social/connyduck
// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2
// https://gts.foo.bar/@goblin
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
//
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
public static boolean looksLikeMastodonUrl(String urlString) {
URI uri;
try {
uri = new URI(urlString);
} catch (URISyntaxException e) {
return false;
}
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false;
String it = uri.getPath();
return it.matches("^/@[^/]+$") ||
it.matches("^/@[^/]+/\\d+$") ||
it.matches("^/users/\\w+$") ||
it.matches("^/notice/[a-zA-Z0-9]+$") ||
it.matches("^/objects/[-a-f0-9]+$") ||
it.matches("^/notes/[a-z0-9]+$") ||
it.matches("^/display/[-a-f0-9]+$") ||
it.matches("^/profile/\\w+$") ||
it.matches("^/p/\\w+/\\d+$") ||
it.matches("^/\\w+$") ||
it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/users/[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/o/[a-f0-9]+$");
}
public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
}
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems(
sessions.stream().map(AccountSession::getFullUsername).toArray(String[]::new),
(dialog, which) -> sessionConsumer.accept(sessions.get(which))
)
.setTitle(titleRes == 0 ? R.string.choose_account : titleRes)
.setIcon(iconRes);
if (transformDialog != null) transformDialog.accept(builder);
builder.show();
}
@FunctionalInterface
public interface InteractionPerformer {
void interact(StatusInteractionController ic, Status status, Consumer<Status> resultConsumer);
}
public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate<Status> checkInteracted, InteractionPerformer interactionPerformer, @StringRes int interactAsRes, @StringRes int interactedAsAccountRes, @StringRes int alreadyInteractedRes, @DrawableRes int iconRes) {
pickAccount(context, accountID, interactAsRes, iconRes, session -> {
lookupStatus(context, sourceStatus, session.getID(), accountID, status -> {
if (checkInteracted.test(status)) {
Toast.makeText(context, alreadyInteractedRes, Toast.LENGTH_SHORT).show();
return;
}
StatusInteractionController ic = AccountSessionManager.getInstance().getAccount(session.getID()).getRemoteStatusInteractionController();
interactionPerformer.interact(ic, status, s -> {
if (checkInteracted.test(s)) {
Toast.makeText(context, context.getString(interactedAsAccountRes, session.getFullUsername()), Toast.LENGTH_SHORT).show();
}
});
});
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> statusConsumer) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
statusConsumer.accept(queryStatus);
return;
}
new GetSearchResults(queryStatus.url, GetSearchResults.Type.STATUSES, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0));
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
dialog.setTitle(R.string.sk_loading_fediverse_resource_title);
}
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel());
if (url != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.open_in_browser), (d, which) -> {
d.cancel();
launchWebBrowser(context, url);
});
}
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser){
Uri uri=Uri.parse(url);
if(accountID!=null && "https".equals(uri.getScheme()) && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
List<String> path=uri.getPathSegments();
// Match URLs like https://mastodon.social/@Gargron/108132679274083591
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$")){
List<String> path=uri.getPathSegments();
if(accountID!=null && "https".equals(uri.getScheme())){
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
new GetStatusByID(path.get(1))
.setCallback(new Callback<>(){
@Override
@@ -766,14 +920,88 @@ public class UiUtils{
@Override
public void onError(ErrorResponse error){
error.showToast(context);
launchWebBrowser(context, url);
if (launchBrowser) launchWebBrowser(context, url);
}
})
.wrapProgress((Activity)context, R.string.loading, true)
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} else if (looksLikeMastodonUrl(url)) {
new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Bundle args=new Bundle();
args.putString("account", accountID);
if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args);
} else if (!results.accounts.isEmpty()) {
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
if (launchBrowser) launchWebBrowser(context, url);
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
}
}
launchWebBrowser(context, url);
}
public static void copyText(View v, String text) {
Context context = v.getContext();
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
}
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
private static String getSystemProperty(String key){
try{
Class<?> props=Class.forName("android.os.SystemProperties");
Method get=props.getMethod("get", String.class);
return (String)get.invoke(null, key);
}catch(Exception ignore){}
return null;
}
public static boolean isMIUI(){
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
}
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText){
Bundle args = new Bundle();
if (prefilledText != null) args.putString("prefilledText", prefilledText);
return pickAccountForCompose(activity, accountID, args);
}
public static boolean pickAccountForCompose(Activity activity, String accountID){
return pickAccountForCompose(activity, accountID, (String) null);
}
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
args.putString("account", session.getID());
Nav.go(activity, ComposeFragment.class, args);
}, null);
return true;
} else {
return false;
}
}
}

View File

@@ -4,6 +4,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import java.time.Instant;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -21,6 +22,16 @@ public class StatusFilterPredicate implements Predicate<Status>{
@Override
public boolean test(Status status){
if(status.filtered!=null){
if (status.filtered.isEmpty()){
return true;
}
boolean matches=status.filtered.stream()
.map(filterResult->filterResult.filter)
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
.anyMatch(filter->filter.filterAction==Filter.FilterAction.HIDE);
return !matches;
}
for(Filter filter:filters){
if(filter.matches(status))
return false;

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/gray_50"/>
<item android:color="?colorGray50"/>
</selector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_800"/>
<solid android:color="?colorGray800"/>
<corners android:radius="10dp"/>
<padding android:top="16dp" android:left="16dp" android:right="16dp" android:bottom="16dp"/>
</shape>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray_25"/>
<solid android:color="?colorGray25"/>
<corners android:radius="10dp"/>
<padding android:top="16dp" android:left="16dp" android:right="16dp" android:bottom="16dp"/>
</shape>

View File

@@ -3,7 +3,7 @@
android:color="?android:colorControlHighlight">
<item>
<shape>
<solid android:color="?colorSecondary"/>
<solid android:color="?attr/colorComposeButtonBackground"/>
<corners android:radius="16dp"/>
</shape>
</item>

View File

@@ -2,7 +2,7 @@
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/highlight_over_dark">
<item>
<shape android:shape="oval">
<solid android:color="?colorSearchHint"/>
<solid android:color="?colorGray600"/>
</shape>
</item>
</ripple>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:tint="@color/gray_800">
<shape android:tint="?colorGray800">
<solid android:color="#CC000000"/>
</shape>
</item>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_megaphone_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
<shape android:shape="oval">
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
<solid android:color="?android:colorAccent"/>
</shape>
</item>
</layer-list>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14.0653,14.9329C14.0639,14.9132 14.0475,14.8978 14.0278,14.8978C14.0125,14.8978 13.9987,14.9072 13.993,14.9214C13.6711,15.7244 13.2265,16.3336 12.6594,16.7489C12.0948,17.1603 11.4132,17.366 10.6146,17.366C9.5096,17.366 8.6183,16.9587 7.9407,16.144C7.2632,15.3293 6.9244,14.2202 6.9244,12.8167C6.9244,10.9857 7.3922,9.4773 8.3279,8.2916C9.2716,7.1059 10.4574,6.513 11.8851,6.513C12.4658,6.513 12.9659,6.6421 13.3854,6.9002C13.8048,7.1583 14.0992,7.489 14.2686,7.8923H14.317L14.3417,7.6527C14.3964,7.122 14.8435,6.7187 15.3769,6.7187C15.9898,6.7187 16.4699,7.246 16.4136,7.8562C16.2791,9.3148 16.1731,10.4964 16.0956,11.4011C16.0069,12.5223 15.9625,13.216 15.9625,13.4822C15.9544,14.1839 16.0512,14.7082 16.2529,15.0551C16.4545,15.4019 16.761,15.5753 17.1724,15.5753C17.9226,15.5753 18.5477,15.1276 19.0478,14.2323C19.5479,13.337 19.7979,12.1754 19.7979,10.7477C19.7979,8.7151 19.1446,7.0494 17.8379,5.7508C16.5312,4.4521 14.7284,3.8028 12.4295,3.8028C10.0177,3.8028 8.0415,4.6094 6.5009,6.2226C4.9683,7.8358 4.2021,9.8484 4.2021,12.2601C4.2021,14.6316 4.9159,16.5191 6.3436,17.9226C7.7713,19.318 9.7193,20.0157 12.1875,20.0157C13.1393,20.0157 14.0307,19.923 14.8615,19.7374C15.3602,19.6309 15.8386,19.4953 16.2966,19.3307C16.7909,19.153 17.3563,19.3639 17.5632,19.8467C17.7578,20.3007 17.5634,20.8314 17.1019,21.0073C16.5257,21.2269 15.9039,21.4046 15.2365,21.5402C14.2525,21.75 13.1837,21.8548 12.0302,21.8548C9.0135,21.8548 6.5856,20.9837 4.7465,19.2414C2.9155,17.4991 2,15.1922 2,12.3206C2,9.3765 2.9679,6.9204 4.9038,4.9522C6.8397,2.9841 9.3442,2 12.4174,2C15.2002,2 17.491,2.7985 19.2898,4.3956C21.0966,5.9847 22,8.0738 22,10.663C22,12.6473 21.4878,14.2605 20.4634,15.5027C19.4471,16.7449 18.217,17.366 16.7731,17.366C15.9504,17.366 15.3051,17.1482 14.8373,16.7126C14.3765,16.2837 14.1192,15.6904 14.0653,14.9329ZM12.1512,8.2674C11.2398,8.2674 10.5098,8.707 9.9613,9.5862C9.4128,10.4573 9.1385,11.5342 9.1385,12.8167C9.1385,13.6798 9.32,14.3573 9.683,14.8494C10.054,15.3333 10.542,15.5753 11.147,15.5753C12.0423,15.5753 12.7481,15.1236 13.2644,14.2202C13.7806,13.3087 14.0387,12.0988 14.0387,10.5904C14.0387,9.8403 13.8734,9.2676 13.5426,8.8723C13.2119,8.4691 12.7481,8.2674 12.1512,8.2674Z"
android:fillColor="#667085"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector android:height="34dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="34dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 4.5c-4.142 0-7.5 3.358-7.5 7.5 0 4.142 3.358 7.5 7.5 7.5 4.142 0 7.5-3.358 7.5-7.5 0-0.376-0.028-0.746-0.081-1.108C19.352 10.438 19.684 10 20.143 10c0.37 0 0.696 0.256 0.752 0.623C20.965 11.072 21 11.532 21 12c0 4.97-4.03 9-9 9s-9-4.03-9-9 4.03-9 9-9c2.305 0 4.408 0.867 6 2.292V4.25c0-0.414 0.336-0.75 0.75-0.75s0.75 0.336 0.75 0.75v3C19.5 7.664 19.164 8 18.75 8h-3C15.336 8 15 7.664 15 7.25s0.336-0.75 0.75-0.75h1.35c-1.34-1.241-3.13-2-5.1-2z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="28dp" android:height="28dp" android:viewportWidth="28" android:viewportHeight="28">
<path android:pathData="M19.832 6.5c-1.61-1.254-3.634-2-5.832-2-5.247 0-9.5 4.253-9.5 9.5s4.253 9.5 9.5 9.5 9.5-4.253 9.5-9.5c0-0.452-0.03-0.659-0.1-1.139l-0.001-0.002c-0.06-0.41 0.223-0.791 0.633-0.851 0.41-0.06 0.791 0.223 0.851 0.633l0.002 0.012C24.957 13.143 25 13.438 25 14c0 6.075-4.925 11-11 11S3 20.075 3 14 7.925 3 14 3c2.66 0 5.099 0.944 7 2.514V3.75C21 3.336 21.336 3 21.75 3s0.75 0.336 0.75 0.75v3.5C22.5 7.664 22.164 8 21.75 8h-3.5c-0.414 0-0.75-0.336-0.75-0.75s0.336-0.75 0.75-0.75h1.582z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M14.723 16.221c-0.293 0.293-0.293 0.768 0 1.06 0.293 0.294 0.768 0.293 1.061 0l4.997-5.003c0.292-0.293 0.292-0.768 0-1.06L15.783 6.22c-0.293-0.293-0.768-0.293-1.06 0-0.294 0.293-0.294 0.767-0.001 1.06l3.72 3.72H10.6c-1.595 0-2.81 0.242-3.889 0.764L6.466 11.89c-1.109 0.593-1.983 1.467-2.576 2.576C3.28 15.606 3 16.884 3 18.6c0 0.414 0.336 0.75 0.75 0.75S4.5 19.014 4.5 18.6c0-1.484 0.228-2.52 0.713-3.428 0.453-0.847 1.113-1.507 1.96-1.96 0.838-0.448 1.786-0.676 3.094-0.709L10.6 12.5h7.837l-3.715 3.721z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="28dp" android:height="28dp" android:viewportWidth="28" android:viewportHeight="28">
<path android:pathData="M10.03 5.47c0.293 0.293 0.293 0.767 0 1.06L5.56 11h8.69C20.187 11 25 15.813 25 21.75c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75c0-5.109-4.141-9.25-9.25-9.25H5.56l4.47 4.47c0.293 0.293 0.293 0.767 0 1.06-0.293 0.293-0.767 0.293-1.06 0l-5.75-5.75c-0.293-0.293-0.293-0.767 0-1.06l5.75-5.75c0.293-0.293 0.767-0.293 1.06 0z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="28dp" android:height="28dp" android:viewportWidth="28" android:viewportHeight="28">
<path android:pathData="M6 6.75C6 4.955 7.455 3.5 9.25 3.5h9.5C20.545 3.5 22 4.955 22 6.75v18c0 0.285-0.161 0.545-0.416 0.672-0.256 0.126-0.56 0.098-0.787-0.075L14 20.192l-6.797 5.157c-0.227 0.172-0.532 0.2-0.787 0.074C6.161 25.295 6 25.035 6 24.75v-18zM9.25 5C8.284 5 7.5 5.784 7.5 6.75v16.49l6.047-4.587c0.268-0.204 0.638-0.204 0.906 0L20.5 23.24V6.75C20.5 5.784 19.716 5 18.75 5h-9.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.562,3C5.419,3 2.062,6.358 2.062,10.5C2.062,11.633 2.313,12.709 2.764,13.673C2.51,14.671 2.226,15.784 2.04,16.515C1.807,17.428 2.629,18.259 3.544,18.039C4.294,17.859 5.447,17.582 6.474,17.337C7.417,17.763 8.462,18 9.562,18C13.704,18 17.062,14.642 17.062,10.5C17.062,6.358 13.704,3 9.562,3ZM3.562,10.5C3.562,7.186 6.248,4.5 9.562,4.5C12.875,4.5 15.562,7.186 15.562,10.5C15.562,13.814 12.875,16.5 9.562,16.5C8.601,16.5 7.695,16.275 6.892,15.875L6.648,15.754L6.384,15.817C5.461,16.036 4.395,16.292 3.596,16.484C3.795,15.705 4.058,14.672 4.286,13.776L4.356,13.5L4.226,13.247C3.801,12.425 3.562,11.491 3.562,10.5ZM14.562,21C12.592,21 10.8,20.241 9.462,19C9.495,19 9.528,19 9.562,19C10.28,19 10.977,18.911 11.643,18.743C12.507,19.225 13.502,19.5 14.562,19.5C15.522,19.5 16.428,19.275 17.232,18.875L17.475,18.754L17.74,18.817C18.661,19.036 19.705,19.263 20.479,19.426C20.304,18.676 20.065,17.671 19.837,16.776L19.767,16.5L19.897,16.247C20.322,15.425 20.562,14.491 20.562,13.5C20.562,11.385 19.468,9.526 17.815,8.458C17.636,7.734 17.365,7.048 17.015,6.411C19.952,7.427 22.062,10.217 22.062,13.5C22.062,14.633 21.81,15.709 21.359,16.674C21.612,17.682 21.868,18.774 22.03,19.477C22.235,20.362 21.455,21.163 20.563,20.977C19.836,20.825 18.693,20.581 17.649,20.337C16.707,20.763 15.661,21 14.562,21Z"
android:fillColor="@color/fluent_default_icon_tint" />
<path
android:pathData="M7.297,8.312L11.85,8.312A0.75,0.75 0,0 1,12.6 9.062L12.6,9.062A0.75,0.75 0,0 1,11.85 9.812L7.297,9.812A0.75,0.75 0,0 1,6.547 9.062L6.547,9.062A0.75,0.75 0,0 1,7.297 8.312z"
android:fillColor="@color/fluent_default_icon_tint" />
<path
android:pathData="M7.297,11.312L10.323,11.312A0.75,0.75 0,0 1,11.073 12.062L11.073,12.062A0.75,0.75 0,0 1,10.323 12.812L7.297,12.812A0.75,0.75 0,0 1,6.547 12.062L6.547,12.062A0.75,0.75 0,0 1,7.297 11.312z"
android:fillColor="@color/fluent_default_icon_tint" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_chat_multiple_24_regular_text" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_chat_multiple_24_regular"/>
</selector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M7.032 13.907l-3.471-3.905C3.285 9.692 2.81 9.664 2.5 9.939 2.193 10.215 2.165 10.69 2.44 11l4 4.5c0.287 0.322 0.786 0.336 1.091 0.031l10.5-10.5c0.293-0.293 0.293-0.767 0-1.06-0.293-0.293-0.767-0.293-1.06 0l-9.938 9.937z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M10 2c4.418 0 8 3.582 8 8s-3.582 8-8 8-8-3.582-8-8 3.582-8 8-8zm0 1c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7-3.134-7-7-7zm3.358 4.646c0.174 0.174 0.193 0.443 0.058 0.638l-0.058 0.07-4.005 4.004c-0.173 0.174-0.442 0.193-0.637 0.058l-0.07-0.058-2-2c-0.195-0.195-0.195-0.511 0-0.707C6.82 9.478 7.09 9.46 7.284 9.594l0.07 0.057L9 11.298l3.651-3.652c0.196-0.195 0.512-0.195 0.707 0z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M13 10c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M10 2c4.418 0 8 3.582 8 8s-3.582 8-8 8-8-3.582-8-8 3.582-8 8-8zM9.5 5C9.224 5 9 5.224 9 5.5v5l0.008 0.09C9.05 10.823 9.255 11 9.5 11h3l0.09-0.008C12.823 10.95 13 10.745 13 10.5c0-0.276-0.224-0.5-0.5-0.5H10V5.5L9.992 5.41C9.95 5.177 9.745 5 9.5 5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M10 2c4.418 0 8 3.582 8 8s-3.582 8-8 8-8-3.582-8-8 3.582-8 8-8zm0 1c-3.866 0-7 3.134-7 7s3.134 7 7 7 7-3.134 7-7-3.134-7-7-7zM9.5 5c0.245 0 0.45 0.177 0.492 0.41L10 5.5V10h2.5c0.276 0 0.5 0.224 0.5 0.5 0 0.245-0.177 0.45-0.41 0.492L12.5 11h-3c-0.245 0-0.45-0.177-0.492-0.41L9 10.5v-5C9 5.224 9.224 5 9.5 5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M15.25 13.5h-4c-0.414 0-0.75-0.336-0.75-0.75v-6C10.5 6.336 10.836 6 11.25 6S12 6.336 12 6.75V12h3.25c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75zM12 2C6.478 2 2 6.478 2 12s4.478 10 10 10 10-4.478 10-10S17.522 2 12 2z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333zM11.25 6c0.38 0 0.694 0.282 0.743 0.648L12 6.75V12h3.25c0.414 0 0.75 0.336 0.75 0.75 0 0.38-0.282 0.694-0.648 0.743L15.25 13.5h-4c-0.38 0-0.694-0.282-0.743-0.648L10.5 12.75v-6C10.5 6.336 10.836 6 11.25 6z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M21.78 3.28c0.293-0.293 0.293-0.767 0-1.06-0.293-0.293-0.768-0.293-1.06 0l-10 10-0.47 1.53 1.53-0.47 10-10zM6.25 3C4.455 3 3 4.455 3 6.25v11.5C3 19.545 4.455 21 6.25 21h11.5c1.795 0 3.25-1.455 3.25-3.25v-8C21 9.336 20.664 9 20.25 9S19.5 9.336 19.5 9.75v8c0 0.966-0.784 1.75-1.75 1.75H6.25c-0.966 0-1.75-0.784-1.75-1.75V6.25c0-0.966 0.784-1.75 1.75-1.75h8C14.664 4.5 15 4.164 15 3.75S14.664 3 14.25 3h-8z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M8.5 4h3c0-0.828-0.672-1.5-1.5-1.5S8.5 3.172 8.5 4zm-1 0c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5h5C17.776 4 18 4.224 18 4.5S17.776 5 17.5 5h-1.054l-1.194 10.344C15.077 16.858 13.796 18 12.272 18H7.728c-1.524 0-2.805-1.142-2.98-2.656L3.554 5H2.5C2.224 5 2 4.776 2 4.5S2.224 4 2.5 4h5zM9 8c0-0.276-0.224-0.5-0.5-0.5S8 7.724 8 8v6c0 0.276 0.224 0.5 0.5 0.5S9 14.276 9 14V8zm2.5-0.5C11.224 7.5 11 7.724 11 8v6c0 0.276 0.224 0.5 0.5 0.5S12 14.276 12 14V8c0-0.276-0.224-0.5-0.5-0.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M3.897 4.054L3.97 3.97c0.266-0.267 0.683-0.29 0.976-0.073L5.03 3.97 10 8.939l4.97-4.97c0.266-0.266 0.683-0.29 0.976-0.072L16.03 3.97c0.267 0.266 0.29 0.683 0.073 0.976L16.03 5.03 11.061 10l4.97 4.97c0.266 0.266 0.29 0.683 0.072 0.976L16.03 16.03c-0.266 0.267-0.683 0.29-0.976 0.073L14.97 16.03 10 11.061l-4.97 4.97c-0.266 0.266-0.683 0.29-0.976 0.072L3.97 16.03c-0.267-0.266-0.29-0.683-0.073-0.976L3.97 14.97 8.939 10l-4.97-4.97C3.704 4.764 3.68 4.347 3.898 4.054L3.97 3.97 3.897 4.054z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M15.613 8.32l-3.936-3.936-8.038 8.039-0.117 0.128c-0.185 0.22-0.322 0.48-0.398 0.76l-1.106 4.055-0.015 0.08c-0.038 0.34 0.282 0.628 0.63 0.534l4.054-1.106 0.165-0.053c0.271-0.1 0.518-0.257 0.723-0.462l8.038-8.039zm1.568-5.503c-1.087-1.087-2.849-1.087-3.936 0l-0.861 0.86 3.936 3.936 0.86-0.86 0.131-0.14c0.955-1.093 0.911-2.754-0.13-3.796zM11.648 3H2.5C2.224 3 2 3.224 2 3.5S2.224 4 2.5 4h8.148l1-1zm-3 3H2.5C2.224 6 2 6.223 2 6.5 2 6.776 2.224 7 2.5 7h5.148l1-1zm-3 3l-1 1H2.5C2.224 10 2 9.776 2 9.5 2 9.223 2.224 9 2.5 9h3.148z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M13.245 2.817L3.64 12.423 3.522 12.55c-0.185 0.22-0.322 0.48-0.398 0.76l-1.106 4.055-0.015 0.08c-0.038 0.34 0.282 0.628 0.63 0.534l4.054-1.106 0.165-0.053c0.271-0.1 0.518-0.257 0.723-0.462l9.606-9.606 0.13-0.14c0.955-1.093 0.911-2.754-0.13-3.796-1.087-1.087-2.849-1.087-3.936 0zM4.346 13.13l8.039-8.038 2.521 2.52-8.038 8.04-0.098 0.085-0.107 0.072c-0.075 0.044-0.155 0.077-0.239 0.1l-3.212 0.876 0.877-3.211 0.042-0.123c0.05-0.12 0.123-0.229 0.215-0.321zm12.128-9.606l0.11 0.12c0.584 0.7 0.547 1.744-0.11 2.402l-0.861 0.86-2.521-2.521 0.86-0.86 0.12-0.11c0.7-0.585 1.744-0.548 2.402 0.11zM11.648 3H2.5C2.224 3 2 3.224 2 3.5S2.224 4 2.5 4h8.148l1-1zm-3 3H2.5C2.224 6 2 6.223 2 6.5 2 6.776 2.224 7 2.5 7h5.148l1-1zm-4 4l1-1H2.5C2.224 9 2 9.223 2 9.5 2 9.776 2.224 10 2.5 10h2.148z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M13.94 5l5.061 5.06L9.063 20c-0.277 0.277-0.621 0.477-1 0.58l-5.115 1.395c-0.56 0.153-1.073-0.361-0.92-0.921l1.394-5.116c0.103-0.377 0.303-0.722 0.58-0.999L13.94 5zm-7.414 6l-1.5 1.5H2.75C2.337 12.5 2 12.165 2 11.75 2 11.336 2.337 11 2.75 11h3.775zm14.352-8.174l0.153 0.144 0.145 0.153c1.25 1.405 1.203 3.56-0.145 4.908L20.061 9 15 3.94l0.97-0.97c1.348-1.348 3.503-1.396 4.908-0.144zM10.526 7l-1.5 1.5H2.75C2.337 8.5 2 8.165 2 7.75 2 7.336 2.337 7 2.75 7h7.775zm4-4l-1.5 1.5H2.75C2.337 4.5 2 4.165 2 3.75 2 3.336 2.337 3 2.75 3h11.775z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M20.878 2.826l0.153 0.144 0.145 0.153c1.25 1.405 1.203 3.56-0.145 4.908L9.063 19.999c-0.277 0.277-0.621 0.477-1 0.58l-5.115 1.395c-0.56 0.153-1.073-0.361-0.92-0.921l1.394-5.116c0.103-0.377 0.303-0.722 0.58-0.999L15.97 2.97c1.348-1.348 3.503-1.396 4.908-0.144zM15.001 6.06l-9.938 9.938c-0.092 0.092-0.16 0.207-0.193 0.333l-1.05 3.85 3.85-1.05c0.125-0.035 0.24-0.101 0.332-0.194L17.94 9l-2.939-2.94zM6.526 11l-1.5 1.5H2.75C2.337 12.5 2 12.165 2 11.75 2 11.336 2.337 11 2.75 11h3.775zm4-4l-1.5 1.5H2.75C2.337 8.5 2 8.165 2 7.75 2 7.336 2.337 7 2.75 7h7.775zm6.505-2.97L16.061 5 19 7.94l0.97-0.97c0.812-0.812 0.812-2.128 0-2.94-0.811-0.811-2.127-0.811-2.939 0zM14.526 3l-1.5 1.5H2.75C2.337 4.5 2 4.165 2 3.75 2 3.336 2.337 3 2.75 3h11.775z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

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