Compare commits

..

1401 Commits

Author SHA1 Message Date
LucasGGamerM
e15dd6024f Bumped version number 2022-12-07 13:28:07 -03:00
LucasGGamerM
e52dffeece Fix notification logo and lets start splash screen button color 2022-12-07 12:46:26 -03:00
LucasGGamerM
5b85bb427d bumping version code 2022-12-07 09:50:03 -03:00
LucasGGamerM
4d62388617 Fixing the notification icon once again... 2022-12-07 09:36:50 -03:00
LucasGGamerM
04b8055474 Fixing the notification icon once more 2022-12-07 09:22:18 -03:00
LucasGGamerM
3c34b6a7d2 Upping the version code once more, and fixing the self updater 2022-12-06 22:06:49 -03:00
LucasGGamerM
de4964c2cd Upping the version code and changing notification icon. This should be the first release 2022-12-06 21:58:35 -03:00
LucasGGamerM
fbcaa05c03 Changing the name of the archivesBaseName 2022-12-06 21:49:46 -03:00
LucasGGamerM
883f28696e Editing a while lot of files. New icon, new notification icon, a bunch of new stuff! 2022-12-06 21:21:43 -03:00
LucasGGamerM
df52230837 Editing the readme just in case 2022-12-06 19:23:28 -03:00
LucasGGamerM
a90f26a37a Setting update client 2022-12-06 19:20:47 -03:00
LucasGGamerM
8c1f76d7fa Initial Moshidon "release" 2022-12-06 19:17:07 -03:00
LucasGGamerM
f384d44f8f Changing my app id 2022-12-06 13:03:26 -03:00
LucasGGamerM
4ab6ed55f5 Changing my version number and string 2022-12-06 12:41:03 -03:00
LucasGGamerM
cf99bf5152 Merge branch 'proper_implementation_of_the_color_picker'
Just fixing stuff here and there
2022-12-05 17:47:03 -03:00
LucasGGamerM
10779717cf Make follow requests icon badge follow the color scheme and also make it that the profile top bar menu also follows the theme. This should be it 2022-12-05 17:20:40 -03:00
LucasGGamerM
db4c1bfe47 Merge branch 'proper_implementation_of_the_color_picker'
Just making it tidy and better :D
2022-12-05 14:11:20 -03:00
LucasGGamerM
27afba1cf2 Making it so that the boost icon is also following the theme when clicked 2022-12-05 14:10:34 -03:00
LucasGGamerM
4895425b40 Adding a proper logo to the top of the home timeline 2022-12-05 13:27:09 -03:00
LucasGGamerM
004c414fba Editing some drawable files to make them also follow the theme 2022-12-05 09:41:56 -03:00
LucasGGamerM
c8e38b134c Fixing weird bug with the buttons for the second time 2022-12-05 09:03:48 -03:00
LucasGGamerM
de5a911286 Fixing weird bug with the buttons 2022-12-04 22:52:27 -03:00
LucasGGamerM
606cd7442e Make it so that the publish button also follows the theme 2022-12-04 14:45:42 -03:00
LucasGGamerM
3ebc972268 Fixing the TrueBlack themes for everything 2022-12-04 14:00:10 -03:00
LucasGGamerM
4e39bb381c Making it so that the fab follows the theme 2022-12-04 13:14:38 -03:00
LucasGGamerM
b6178681b0 Adding yellow theme 2022-12-04 11:42:41 -03:00
LucasGGamerM
29abf70cec Adding orange theme, tweaking the blue and green theme 2022-12-04 11:16:58 -03:00
LucasGGamerM
8d63be513d Fix readability issue on the light blue theme 2022-12-04 10:24:51 -03:00
LucasGGamerM
e63b9d0dd6 Adding an icon to the color picker setting 2022-12-03 22:29:41 -03:00
LucasGGamerM
b1fda17ac7 Make badged settings icon follow accent colors 2022-12-03 16:48:12 -03:00
LucasGGamerM
bad44b145c Adding blue theme and refactoring styles.xml 2022-12-03 16:25:28 -03:00
LucasGGamerM
77669cedf6 More polishes over the green theme 2022-12-03 13:44:40 -03:00
LucasGGamerM
19238c389f Making the green theme more readable 2022-12-03 12:29:51 -03:00
LucasGGamerM
1747ff98b5 Adding a green theme 2022-12-02 14:00:58 -03:00
LucasGGamerM
8fa5824e3e Disabling the icons for the color picker menu 2022-12-02 11:58:40 -03:00
LucasGGamerM
6a674d7a7e Polishes 2022-12-01 19:55:53 -03:00
LucasGGamerM
dad3b8cd6b Proper implementation on the color picker. 2022-12-01 19:42:21 -03:00
LucasGGamerM
9179d2198d Fully fixed now, it should be ready to release. 2022-11-29 13:09:32 -03:00
LucasGGamerM
d096bef234 Fixing the fix of the bug I found. 2022-11-28 21:30:32 -03:00
LucasGGamerM
f2c47a1b84 Fixing another bug I found. 2022-11-28 21:24:00 -03:00
LucasGGamerM
bc2ac4e915 Fixed a few bugs from the earlier commit. 2022-11-28 16:47:04 -03:00
LucasGGamerM
ff215412c8 Adding mastodon original colors toggle. Partially fixing #90 2022-11-28 15:40:29 -03:00
sk
e6bb319d8b add changelog and update readme 2022-11-28 18:43:57 +01:00
sk
3101f1ad17 bump version 2022-11-28 18:30:49 +01:00
sk
4416dfcae3 Merge branch 'feature/clickable-reply-line-compose' into fork 2022-11-28 18:28:31 +01:00
sk
924b974b4b make reply line open post 2022-11-28 18:26:21 +01:00
sk
5d1dc97ac3 change contribute link 2022-11-28 17:54:02 +01:00
sk
5cce8ca72c add release without federated timeline 2022-11-28 17:50:26 +01:00
sk
f2a0680af0 Merge branch 'fork' of github.com:sk22/megalodon into fork 2022-11-28 12:07:38 +01:00
sk
6203ded864 Merge branch 'main' into fork 2022-11-28 12:07:35 +01:00
sk22
17f1eb88e4 Merge pull request #110 from Surendrajat/rip-appcenter
Get rid of Microsoft appcenter related stuff
2022-11-28 10:11:18 +01:00
sk
9a52cc033a set custom redirect uri
closes #89
2022-11-28 10:00:57 +01:00
Grishka
4b16262a1a Sync last seen notification ID with server 2022-11-27 13:39:50 +03:00
Grishka
10e7cbf022 Merge branch 'add/verified-profile-fields' 2022-11-27 13:15:13 +03:00
Grishka
531b8ead04 Make verified fields more like on iOS 2022-11-27 13:14:31 +03:00
Grishka
4b2c94ab52 Implement bookmarks and add favorites list
Closes #22, at last
2022-11-27 12:43:07 +03:00
Grishka
a98becf2f4 Update splash logo to purple 2022-11-27 11:40:55 +03:00
Grishka
54f9eace67 Fix #422 2022-11-27 10:02:47 +03:00
Grishka
e4c9eb089a Hide posts when muting, blocking or unfollowing an account 2022-11-26 23:09:46 +03:00
Grishka
0e635aec23 Allow copying the username in profile 2022-11-26 20:33:02 +03:00
Grishka
dc90c09cea Shorten interaction counters 2022-11-26 20:24:27 +03:00
Grishka
06cb335a0a Add tooltips to some icon buttons
closes #423
2022-11-26 20:21:48 +03:00
Grishka
5a681d3557 Fix #403 2022-11-26 20:16:43 +03:00
Grishka
4200486aeb fix 2022-11-26 20:14:30 +03:00
Grishka
62411a563f Fix poll expiration
fixes #238, fixes #417
2022-11-26 20:13:46 +03:00
Grishka
2cabe94ba0 Fix #398 2022-11-26 20:02:30 +03:00
Grishka
4a6baae97a Make URLs clickable in instance rules
closes #389
2022-11-26 19:29:05 +03:00
Grishka
bb12a66781 Fix #313 2022-11-26 18:40:17 +03:00
surendrajat
efa9f524f9 Get rid of Microsoft appcenter related stuff 2022-11-26 13:37:03 +05:30
sk
be0c7777b7 fix typo 2022-11-26 01:01:22 +01:00
sk
92652f6fbd update fastlane descriptions 2022-11-26 01:00:22 +01:00
sk
4ee781dfd5 update fastlane descriptions 2022-11-26 00:04:05 +01:00
sk
bf8ac4bc69 set archive name 2022-11-25 23:37:12 +01:00
sk
a8e7840c04 remove unused script 2022-11-25 23:30:36 +01:00
sk
c91ebda1ff set gradle project name 2022-11-25 23:23:52 +01:00
sk
e7dc5030d5 update fastlane config 2022-11-25 22:09:53 +01:00
sk
4cf55e23ba update readme 2022-11-25 22:08:19 +01:00
sk
8159af0b58 update fastlane config 2022-11-25 22:06:18 +01:00
sk
7489c6ffbe bump version 2022-11-25 21:02:44 +01:00
sk
5eea0632b1 Merge branch 'compact-extended-footer' into fork 2022-11-25 21:00:27 +01:00
sk
e2a3812f14 fix wrong unlisted icon
closes #86
2022-11-25 21:00:20 +01:00
sk
bbf64414b3 add monochrome icons to adaptive icons 2022-11-25 20:50:43 +01:00
sk
b264dbf744 add custom fastlane config 2022-11-25 20:35:08 +01:00
sk
e2572f9c10 Merge remote-tracking branch 'origin/fork' into fork 2022-11-25 18:31:33 +01:00
sk
4e14af595c fix integrated updater 2022-11-25 18:31:18 +01:00
sk22
6693ac516a Update README.md 2022-11-25 18:29:52 +01:00
sk22
10702dba17 Update README.md 2022-11-25 01:41:42 +01:00
sk22
0a2c5b582d Update README.md 2022-11-25 01:40:59 +01:00
sk22
f5c7d4f07c Update README.md 2022-11-24 21:32:42 +01:00
sk
5415ab934f bump version 2022-11-24 20:45:18 +01:00
sk
e0ced24c34 Merge remote-tracking branch 'upstream/l10n_master' into fork 2022-11-24 20:43:58 +01:00
sk
569441c0f7 update readme 2022-11-24 20:39:54 +01:00
sk
bb08f3841d re-add logo to readme 2022-11-24 20:38:51 +01:00
sk
6e52ebc299 rebranding 2022-11-24 20:37:04 +01:00
sk
b0d8084ac2 update gradle 2022-11-24 20:36:47 +01:00
sk
fa754bc03a add app id suffix for debug builds 2022-11-24 20:36:26 +01:00
Eugen Rochko
3e903f4a1d New translations short_description.txt (Icelandic) 2022-11-24 20:21:45 +01:00
Eugen Rochko
353b1873cd New translations full_description.txt (Icelandic) 2022-11-24 20:21:44 +01:00
Eugen Rochko
f4de7d18f3 New translations strings.xml (Icelandic) 2022-11-24 20:21:43 +01:00
Eugen Rochko
5dbac5fc6b New translations strings.xml (Icelandic) 2022-11-24 17:26:26 +01:00
Eugen Rochko
172d44997f New translations strings.xml (Portuguese, Brazilian) 2022-11-24 17:26:25 +01:00
Eugen Rochko
57b0b04c00 New translations strings.xml (Catalan) 2022-11-24 15:40:25 +01:00
Eugen Rochko
ca9ce43b07 New translations strings.xml (Indonesian) 2022-11-24 12:25:39 +01:00
sk
781332a7a9 Merge remote-tracking branch 'upstream/master' into fork 2022-11-23 22:01:37 +01:00
Eugen Rochko
ef41122aca New translations full_description.txt (German) 2022-11-23 14:46:25 +01:00
Eugen Rochko
9e7676b62a New translations strings.xml (German) 2022-11-23 14:46:24 +01:00
Eugen Rochko
56492c07f5 New translations strings.xml (German) 2022-11-22 11:49:19 +01:00
Eugen Rochko
ce9fabd406 New translations title.txt (Icelandic) 2022-11-22 10:52:17 +01:00
Eugen Rochko
0dd6c43117 New translations short_description.txt (Icelandic) 2022-11-22 10:52:16 +01:00
Eugen Rochko
5159aab19c New translations full_description.txt (Icelandic) 2022-11-22 10:52:15 +01:00
Eugen Rochko
4e2cf247e9 New translations strings.xml (Icelandic) 2022-11-22 10:52:14 +01:00
Eugen Rochko
9df02d9857 New translations title.txt (Belarusian) 2022-11-22 10:52:13 +01:00
Eugen Rochko
bb4a5202d7 New translations short_description.txt (Belarusian) 2022-11-22 10:52:12 +01:00
Eugen Rochko
2320014eb3 New translations full_description.txt (Belarusian) 2022-11-22 10:52:10 +01:00
Eugen Rochko
3692c2b205 New translations strings.xml (Belarusian) 2022-11-22 10:52:09 +01:00
sk22
9f1c0e534c Update README.md 2022-11-22 10:16:08 +01:00
sk22
78557751b6 Update README.md 2022-11-22 10:13:41 +01:00
sk22
c0af10df5b Update README.md 2022-11-22 10:12:23 +01:00
sk22
7deb9089e3 Update README.md 2022-11-22 10:00:34 +01:00
sk22
8f1f845595 Update README.md 2022-11-22 10:00:01 +01:00
sk22
1afd971bf4 Update README.md 2022-11-22 09:23:17 +01:00
sk22
7c3b753439 Update README.md 2022-11-22 09:23:04 +01:00
Grishka
9facdb938d Add a section about translations to readme 2022-11-22 11:48:02 +04:00
Eugen Rochko
7856858aea New translations strings.xml (Sinhala) 2022-11-22 08:37:11 +01:00
Eugen Rochko
d0328957f5 New translations strings.xml (Sinhala) 2022-11-22 07:24:00 +01:00
Gregory K
4d868cc5aa Merge pull request #400 from mastodon/fix-readme
Add note on contributions and trademarks to README
2022-11-22 06:59:44 +03:00
Eugen Rochko
ca9e515bd5 Merge pull request #232 from Poussinou/patch-1
Create FUNDING.yml
2022-11-22 04:34:03 +01:00
Eugen Rochko
524c0d607b Merge pull request #372 from sveinki/patch-1
Typo in full_description.txt
2022-11-22 04:33:07 +01:00
Eugen Rochko
df1d451e82 Add note on contributions and trademarks to README 2022-11-22 04:28:10 +01:00
sk22
01c75a9fa4 Update README.md 2022-11-22 00:37:20 +01:00
sk
8e840c2584 only download artifact "mastodos.apk" 2022-11-22 00:22:36 +01:00
sk
adf628de44 bump version 2022-11-21 23:14:58 +01:00
sk
7e4169644d update website 2022-11-21 23:11:33 +01:00
sk
e42988b51e Merge remote-tracking branch 'upstream/master' into fork 2022-11-21 21:58:55 +01:00
sk
7c2fd2b734 add script to get latest upstream commit 2022-11-21 21:58:35 +01:00
sk
569e4e13ea update readme 2022-11-21 21:57:52 +01:00
sk
b7b5785a97 Merge branch 'ui/profile-header-tweaks' into fork 2022-11-21 21:18:44 +01:00
sk
95d6e51ae0 Merge branch 'feature/copy-username' into fork 2022-11-21 20:58:25 +01:00
sk
fc43d919e5 tweak profile layout 2022-11-21 20:57:30 +01:00
sk
a8d19529cd Merge branch 'fix/lock-shifts-layout' into ui/profile-header-tweaks 2022-11-21 20:36:35 +01:00
sk
c2c44ab25e Merge branch 'fix/lock-shifts-layout' into feature/copy-username 2022-11-21 20:28:22 +01:00
sk
96a56873c3 add long-click to copy username 2022-11-21 20:14:45 +01:00
sk
0100967597 Merge branch 'fix/lock-shifts-layout' into fork 2022-11-21 20:12:17 +01:00
sk
5769b378e2 set 16dp lock on baseline 2022-11-21 20:12:04 +01:00
Grishka
2c61551e5c Add a tool to generate locales_config.xml 2022-11-21 22:04:30 +04:00
sk
c9b11a23a1 Merge branch 'ui/profile-header-tweaks' into fork 2022-11-21 18:26:56 +01:00
sk
456d05a489 close #66 2022-11-21 18:26:45 +01:00
sk
8ca7d9d7b4 Merge branch 'main' into ui/profile-header-tweaks 2022-11-21 18:10:56 +01:00
sk
2321549dca Merge branch 'main' into fork 2022-11-21 18:10:41 +01:00
sk
5ae96328d5 Merge branch 'feature/post-notifications-toggle' into fork 2022-11-21 18:08:27 +01:00
sk
dbf3da3629 hopefully fix #76 2022-11-21 18:08:16 +01:00
sk22
9a351de9b4 Update README.md 2022-11-21 17:22:12 +01:00
sk
f871fa6743 Merge branch 'fork' of github.com:sk22/mastodos into fork 2022-11-21 17:21:38 +01:00
sk
85f6441a56 make store icon pink 2022-11-21 17:21:27 +01:00
sk
dd8354f91b Merge branch 'clickable-boost-reply-line' into fork 2022-11-21 17:18:46 +01:00
sk
21d78de3cc make reply line match parent width 2022-11-21 17:18:38 +01:00
Grishka
158af27309 Fix #363 2022-11-21 18:36:33 +04:00
sk22
b0446af54a Merge pull request #75 from AiOO/translate-korean
Apply Korean translation for new strings
2022-11-21 15:33:15 +01:00
Ahn Kiwook
064f0155f6 Apply Korean translation for new strings 2022-11-21 23:03:50 +09:00
Eugen Rochko
6e8542e33b New translations strings.xml (Korean) 2022-11-21 14:56:20 +01:00
sk
fbf4522260 remove "debuggable" from release config
fixes #72
2022-11-21 14:11:46 +01:00
sk
8918bd5ce2 Merge branch 'main' into fork 2022-11-21 14:10:49 +01:00
Grishka
187693883c Fix #94
TODO support 4.0 filteing
2022-11-21 14:10:30 +04:00
Eugen Rochko
9017d00541 New translations strings.xml (Chinese Traditional) 2022-11-21 03:43:50 +01:00
Eugen Rochko
9182bd1a15 New translations strings.xml (Basque) 2022-11-21 02:38:55 +01:00
Eugen Rochko
5cdd726d21 New translations full_description.txt (German) 2022-11-20 23:59:20 +01:00
Eugen Rochko
d00fbe074b New translations strings.xml (German) 2022-11-20 23:59:19 +01:00
sk
40868f90f9 bump version 2022-11-20 18:07:21 +01:00
sk
1f89acec34 Merge branch 'clickable-boost-reply-line' into fork 2022-11-20 17:46:28 +01:00
sk
d75ce99a68 make reblog/reply line clickable
fixes #62
see mastodon#287
2022-11-20 17:46:17 +01:00
Eugen Rochko
365fac5efe New translations strings.xml (Basque) 2022-11-20 17:31:49 +01:00
sk
0a8846fa2a Merge branch 'main' into fork 2022-11-20 17:21:28 +01:00
sk
5253e2e24a Merge branch 'use-bold-boost-icon' into fork 2022-11-20 17:11:42 +01:00
sk
3565223611 use bold boost icon
fixes #363
2022-11-20 17:09:23 +01:00
sk
2343d7a046 disable updater for debug build 2022-11-20 16:10:01 +01:00
sk
8256fbadb6 make release debuggable 2022-11-20 16:09:42 +01:00
Eugen Rochko
1d60031f4c New translations strings.xml (Vietnamese) 2022-11-20 15:03:02 +01:00
Eugen Rochko
2c7ed4be3e New translations strings.xml (Italian) 2022-11-20 13:57:40 +01:00
Eugen Rochko
3b9d4d3f9d New translations strings.xml (French) 2022-11-20 13:57:39 +01:00
Eugen Rochko
78824fa425 New translations strings.xml (Catalan) 2022-11-20 12:38:33 +01:00
Eugen Rochko
e271a4a330 New translations strings.xml (Kabyle) 2022-11-20 11:20:59 +01:00
Grishka
b898dc010e Show an error if a server has signups closed
closes #377
2022-11-20 13:36:23 +04:00
Grishka
de369633ec Fix #386 2022-11-20 12:54:56 +04:00
Gregory K
3f075eab13 Merge pull request #387 from sk22/fix-screenreader-middle-dot
Omit “middle dot” for screen reader
2022-11-20 07:54:06 +03:00
Eugen Rochko
3fb063bee4 New translations strings.xml (Catalan) 2022-11-20 02:30:29 +01:00
sk
96e5f854a5 Merge branch 'ui/profile-header-tweaks' into fork 2022-11-20 02:22:21 +01:00
sk
0f5211f718 change header height 2022-11-20 02:20:13 +01:00
sk
480915f377 change headerless header color 2022-11-20 02:20:01 +01:00
sk
15d559ad6a Revert "Updated Catalan strings"
This reverts commit 2d710cb558.
2022-11-20 02:02:29 +01:00
sk
3b7a6e9385 bump version 2022-11-20 01:37:02 +01:00
sk
314a15973c Merge branch 'fix-screenreader-middle-dot' into fork 2022-11-20 01:35:40 +01:00
sk
e99917945a omit middle dot for screen reader 2022-11-20 01:34:02 +01:00
Eugen Rochko
c43dd5aa49 New translations strings.xml (Catalan) 2022-11-20 01:31:55 +01:00
sk
d3ba8a4d0f Merge branch 'fork' of github.com:sk22/mastodos into fork 2022-11-20 01:22:03 +01:00
sk22
9162b31ac1 Merge pull request #58 from rbnval/patch-1
Updated Catalan strings
2022-11-20 01:21:49 +01:00
sk22
51a80f3e03 remove whitespaces 2022-11-20 01:21:32 +01:00
sk
a8c49b59f6 Merge branch 'ui/larger-post-buttons' into fork 2022-11-20 01:16:39 +01:00
sk
c79942c13f make three-dot/visibility buttons larger
see mastodon#337
2022-11-20 01:15:53 +01:00
rbnval
da121495c0 Update strings.xml 2022-11-20 01:14:46 +01:00
rbnval
2d710cb558 Updated Catalan strings 2022-11-20 01:03:50 +01:00
sk
f0a51a15a9 Merge branch 'feature/follow_hashtags' into fork 2022-11-20 00:18:00 +01:00
sk
ae68b1e646 add error check 2022-11-20 00:17:16 +01:00
sk
5da58d7834 Merge branch 'main' into feature/follow_hashtags 2022-11-20 00:04:35 +01:00
sk
24023e9843 Merge branch 'true-black-improvements' into fork 2022-11-20 00:02:21 +01:00
sk
29d9871e77 improve true black styles 2022-11-20 00:01:53 +01:00
sk
32d182f03a Merge branch 'feature/display-reply-visibility' into fork 2022-11-19 23:44:07 +01:00
sk
da4f54751e don't display when visibility is unknown 2022-11-19 23:43:52 +01:00
sk
393c538464 bump version 2022-11-19 22:54:21 +01:00
sk
ddcf61dc95 Merge branch 'true-black-improvements' into fork 2022-11-19 22:41:50 +01:00
sk
5b70c035d2 fix #54, #55 2022-11-19 22:40:13 +01:00
sk
289564381d bump version 2022-11-19 20:41:27 +01:00
sk
3fbcce8570 Merge branch 'true-black-improvements' into fork 2022-11-19 20:38:16 +01:00
sk
505b755df6 use @color/black instead of #000
to avoid crash when getting null resource ID
2022-11-19 20:38:01 +01:00
sk
4b9618cad5 bump version 2022-11-19 20:18:50 +01:00
sk
e7b0e022d6 Merge branch 'main' into fork 2022-11-19 20:18:20 +01:00
sk
7ffc58d52c update readme 2022-11-19 20:17:51 +01:00
Grishka
30151cafc2 Yes I can flip that to true 2022-11-19 23:13:50 +04:00
sk
d0ce157069 Merge branch 'feature/posts-notifications-tab' into fork 2022-11-19 20:13:16 +01:00
sk
be7d65989d add posts notifications tab
closes #38
2022-11-19 20:12:52 +01:00
sk
08656a2678 Merge branch 'feature/display-reply-visibility' into fork 2022-11-19 19:39:26 +01:00
sk
b0f76739ba implement showing original visibility on reply
closes #41
2022-11-19 19:39:09 +01:00
sk
a09efd084e Merge branch 'true-black-improvements' into fork 2022-11-19 16:36:34 +01:00
sk
0bb85d71e4 improve real black mode
closes #48, improves upon mastodon#84
2022-11-19 16:36:12 +01:00
thomas
7248ab9801 true dark improvement 2022-11-19 16:31:26 +01:00
sk
cd4d83e139 Merge branch 'settings/disable-marquee' into fork 2022-11-19 16:11:41 +01:00
sk
f1f7c1341c add option to disable scrolling title bars
closes #50
as mentioned in mastodon#305
2022-11-19 16:09:45 +01:00
Eugen Rochko
0285158edc New translations strings.xml (Polish) 2022-11-19 15:59:12 +01:00
sk
a7ab6945f9 Merge branch 'feature/lists' into fork 2022-11-19 15:48:59 +01:00
sk
896583aeec fix compose button prepending list id
closes #51
2022-11-19 15:48:47 +01:00
Eugen Rochko
9a6a3422fb New translations strings.xml (Japanese) 2022-11-18 17:40:42 +01:00
Eugen Rochko
523eb70ca6 New translations strings.xml (German) 2022-11-18 02:14:50 +01:00
Eugen Rochko
b57972ae0f New translations full_description.txt (German) 2022-11-17 23:29:50 +01:00
Eugen Rochko
37598df24e New translations strings.xml (German) 2022-11-17 23:29:49 +01:00
sk
eccda2ed61 bump version 2022-11-17 21:25:35 +01:00
sk
b819c757b7 Merge branch 'feature/cw-above-text' into fork 2022-11-17 21:24:59 +01:00
sk
b1f7acb76b fix content warnings overflowing notifications 2022-11-17 21:24:43 +01:00
sk
c23df0c065 tweak content warning styles 2022-11-17 20:46:39 +01:00
sk
e514e24036 Merge branch 'feature/follow-requests' into fork 2022-11-17 20:13:26 +01:00
sk
fdf9417c5b hide follow requests icon when none waiting 2022-11-17 20:13:14 +01:00
sk
859468a03f update readme 2022-11-17 19:57:31 +01:00
sk
4623d403f7 Merge branch 'feature/follow-requests' into fork 2022-11-17 19:49:25 +01:00
sk
038c3c0fb9 hide follow requests button unless profile locked 2022-11-17 19:48:59 +01:00
sk
7f0e2bf03d Merge branch 'feature/follow-requests' into fork 2022-11-17 18:53:42 +01:00
sk
521ba8766c implement follow requests list
closes #39
2022-11-17 18:52:22 +01:00
Eugen Rochko
f04df2d2c4 New translations strings.xml (Korean) 2022-11-17 13:16:17 +01:00
Sveinn í Felli
881762852e Typo in full_description.txt
Removing an extra "and" on line 3.
2022-11-17 09:49:59 +00:00
sk
0c5cd2dc9e bump version 2022-11-17 01:05:27 +01:00
sk
01655a3465 Merge branch 'feature/cw-above-text' into fork 2022-11-17 01:05:02 +01:00
sk
95b7dbfba3 remove unused variable 2022-11-17 01:04:51 +01:00
Jeff Bowen
5d056d5bea AboutViewHolder: Set the background of the field item to green when verified 2022-11-16 19:01:32 -05:00
Jeff Bowen
f500cc7ebf IsoInstantTypeAdapter: Enable parsing of 'offset' date times in profile response 2022-11-16 19:00:11 -05:00
sk
5fc44e906f update readme 2022-11-17 00:58:55 +01:00
sk
7bcfd8939e Merge branch 'feature/cw-above-text' into fork 2022-11-17 00:56:08 +01:00
sk
753624a185 add option to always reveal content warnings 2022-11-17 00:53:41 +01:00
sk
5c9e41f4ff add content warning above text 2022-11-17 00:43:37 +01:00
sk
0a711ca4ba Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-16 23:04:58 +01:00
sk
0c854a893b Merge branch 'main' into feature/mark-media-as-sensitive 2022-11-16 22:38:20 +01:00
Eugen Rochko
8d6eb0f810 New translations strings.xml (Scottish Gaelic) 2022-11-16 22:13:57 +01:00
Eugen Rochko
94de724d4e New translations strings.xml (Chinese Traditional) 2022-11-16 22:13:56 +01:00
sk
aab4284624 Merge branch 'main' into fork 2022-11-16 21:58:16 +01:00
Eugen Rochko
555e8838d4 New translations short_description.txt (Scottish Gaelic) 2022-11-16 19:41:58 +01:00
Eugen Rochko
58beb73595 New translations full_description.txt (Scottish Gaelic) 2022-11-16 19:41:57 +01:00
Eugen Rochko
865f66aa30 New translations strings.xml (Scottish Gaelic) 2022-11-16 19:41:56 +01:00
Eugen Rochko
82e95010a2 New translations strings.xml (Arabic) 2022-11-16 18:34:36 +01:00
Eugen Rochko
68a053cb76 New translations full_description.txt (Indonesian) 2022-11-16 10:21:47 +01:00
Eugen Rochko
2467017382 New translations strings.xml (Indonesian) 2022-11-16 10:21:46 +01:00
Eugen Rochko
d3a7faba51 New translations strings.xml (Indonesian) 2022-11-16 08:43:53 +01:00
Eugen Rochko
eb7574d282 New translations strings.xml (Indonesian) 2022-11-16 07:46:00 +01:00
Eugen Rochko
689328931a New translations short_description.txt (Ukrainian) 2022-11-15 17:19:19 +01:00
Eugen Rochko
d3a2ce0a57 New translations full_description.txt (Ukrainian) 2022-11-15 17:19:18 +01:00
Eugen Rochko
50a092a2c4 New translations strings.xml (Ukrainian) 2022-11-15 17:19:17 +01:00
Eugen Rochko
a852b66d94 New translations strings.xml (Ukrainian) 2022-11-15 15:28:02 +01:00
Eugen Rochko
109d967f2d New translations strings.xml (Ukrainian) 2022-11-15 14:12:09 +01:00
Grishka
a336f6be89 Support domain redirects via /.well-known/host-meta
Closes #312
2022-11-15 15:16:58 +04:00
Grishka
79d99a9484 Probably fix #297 2022-11-15 14:11:56 +04:00
Gregory K
e4e1dc102b Merge pull request #321 from julroy67/fix-onboarding-category-cjk-cutoff
Fix CJK text cut off for category names on Onboarding screen
2022-11-15 10:13:03 +03:00
Grishka
539ac26e0d Fix #300 2022-11-14 22:59:34 +04:00
sk
4f597cfbbb Merge branch 'feature/lists' into fork 2022-11-14 19:01:44 +01:00
sk
8d500f153e fix #43 2022-11-14 19:01:32 +01:00
sk
ef53c859c3 Merge branch 'main' into fork 2022-11-14 18:49:32 +01:00
Eugen Rochko
752435857d New translations strings.xml (Slovenian) 2022-11-14 16:16:57 +01:00
Eugen Rochko
03e68ba56c New translations strings.xml (Vietnamese) 2022-11-14 16:16:56 +01:00
Eugen Rochko
ef37d8afc4 New translations strings.xml (Czech) 2022-11-14 13:17:06 +01:00
Eugen Rochko
1ac390f6ee New translations full_description.txt (Dutch) 2022-11-14 12:02:24 +01:00
Eugen Rochko
586d04f311 New translations strings.xml (Dutch) 2022-11-14 12:02:23 +01:00
Eugen Rochko
6e81469b45 New translations strings.xml (German) 2022-11-14 08:59:56 +01:00
Eugen Rochko
a8431a498d New translations strings.xml (French) 2022-11-14 07:47:57 +01:00
Eugen Rochko
a2dca57eb5 New translations strings.xml (Thai) 2022-11-13 20:35:08 +01:00
Eugen Rochko
fdfb0a377d New translations strings.xml (Turkish) 2022-11-13 19:37:52 +01:00
Eugen Rochko
f5a9a11032 New translations strings.xml (Italian) 2022-11-13 19:37:51 +01:00
Samuel Kaiser
0cf2cc4361 Update README.md 2022-11-13 18:16:33 +01:00
Eugen Rochko
32c6fc9a59 New translations strings.xml (Slovenian) 2022-11-13 17:32:45 +01:00
Eugen Rochko
b87b086dfa New translations strings.xml (Japanese) 2022-11-13 17:32:42 +01:00
Eugen Rochko
caa77a9c54 New translations strings.xml (Italian) 2022-11-13 17:32:41 +01:00
Eugen Rochko
ecf0c2b173 New translations strings.xml (French) 2022-11-13 17:32:35 +01:00
Eugen Rochko
8f7ef0d564 New translations strings.xml (German) 2022-11-13 17:32:33 +01:00
Eugen Rochko
d8e9bbd6b1 New translations strings.xml (Chinese Traditional) 2022-11-13 17:32:32 +01:00
Eugen Rochko
174029376c New translations strings.xml (Thai) 2022-11-13 17:32:20 +01:00
Eugen Rochko
8e06f72064 New translations strings.xml (Chinese Simplified) 2022-11-13 17:32:18 +01:00
Grishka
2c11b879ae Fix #283 2022-11-13 19:44:40 +04:00
Grishka
58735094f1 Fix #319 2022-11-13 19:42:25 +04:00
Grishka
3671803f49 Fix #336 2022-11-13 19:18:42 +04:00
Gregory K
56b42071ac Merge pull request #354 from cypressious/fix-activation-button-bar
Fix button bar layout in onboarding activation for smaller widths
2022-11-13 17:48:54 +03:00
Eugen Rochko
f75a7e793d New translations full_description.txt (Romanian) 2022-11-13 15:26:59 +01:00
Eugen Rochko
a395b82c85 New translations short_description.txt (Romanian) 2022-11-13 14:10:11 +01:00
Eugen Rochko
699f36bf1a New translations full_description.txt (Romanian) 2022-11-13 14:10:10 +01:00
Eugen Rochko
ef57b7425d New translations strings.xml (Romanian) 2022-11-13 14:10:09 +01:00
Eugen Rochko
c21b2b6a43 New translations strings.xml (Romanian) 2022-11-13 13:05:06 +01:00
Kirill Rakhman
ba930f72e8 fix button bar layout in onboarding activation for smaller widths 2022-11-13 12:58:33 +01:00
Eugen Rochko
c5d4318d57 New translations short_description.txt (Slovenian) 2022-11-13 09:24:42 +01:00
Eugen Rochko
7a4621ef13 New translations full_description.txt (Slovenian) 2022-11-13 09:24:41 +01:00
Eugen Rochko
fa53a5ed4f New translations strings.xml (Slovenian) 2022-11-13 09:24:40 +01:00
Eugen Rochko
f2ec2c5333 New translations title.txt (Slovenian) 2022-11-13 08:02:32 +01:00
Eugen Rochko
49e005eb84 New translations strings.xml (French) 2022-11-13 08:02:31 +01:00
Eugen Rochko
ad9b19646d New translations strings.xml (Italian) 2022-11-13 08:02:30 +01:00
Eugen Rochko
80336c7fae New translations strings.xml (Japanese) 2022-11-13 08:02:29 +01:00
Eugen Rochko
996c91420c New translations strings.xml (Chinese Simplified) 2022-11-13 08:02:28 +01:00
Eugen Rochko
48396344e3 New translations strings.xml (Thai) 2022-11-13 08:02:27 +01:00
Eugen Rochko
0a8c61226e New translations strings.xml (Bengali) 2022-11-13 08:02:25 +01:00
Eugen Rochko
e64d8ccf09 New translations strings.xml (German) 2022-11-13 08:02:23 +01:00
Eugen Rochko
9791121392 New translations full_description.txt (Romanian) 2022-11-13 08:02:21 +01:00
Eugen Rochko
06c5002aa5 New translations short_description.txt (Romanian) 2022-11-13 08:02:20 +01:00
Eugen Rochko
fcdbc2bc8d New translations title.txt (Romanian) 2022-11-13 08:02:20 +01:00
Eugen Rochko
749511630a New translations strings.xml (Irish) 2022-11-13 08:02:19 +01:00
Eugen Rochko
4befc0c045 New translations full_description.txt (Irish) 2022-11-13 08:02:18 +01:00
Eugen Rochko
8b2287fa84 New translations short_description.txt (Irish) 2022-11-13 08:02:17 +01:00
Eugen Rochko
3272b7553b New translations title.txt (Irish) 2022-11-13 08:02:16 +01:00
Eugen Rochko
00f7cff402 New translations strings.xml (Slovenian) 2022-11-13 08:02:15 +01:00
Eugen Rochko
87f45a74f0 New translations full_description.txt (Slovenian) 2022-11-13 08:02:14 +01:00
Eugen Rochko
0cd0fe1952 New translations short_description.txt (Slovenian) 2022-11-13 08:02:13 +01:00
Eugen Rochko
458effc27c New translations strings.xml (Romanian) 2022-11-13 08:02:13 +01:00
Eugen Rochko
4422b774b7 New translations strings.xml (Chinese Traditional) 2022-11-13 08:02:12 +01:00
Grishka
d9d8717eeb And more languages 2022-11-13 10:31:03 +04:00
Grishka
0056324100 Add new languages to locale config 2022-11-13 10:23:09 +04:00
Grishka
e49b4daafe Merge branch 'l10n_master' 2022-11-13 10:16:25 +04:00
Eugen Rochko
ad86f1b55e New translations title.txt (Scottish Gaelic) 2022-11-13 07:07:02 +01:00
Eugen Rochko
a8bc0f037f New translations title.txt (Hindi) 2022-11-13 07:07:01 +01:00
Eugen Rochko
9ae0c3f414 New translations title.txt (Bengali) 2022-11-13 07:07:00 +01:00
Eugen Rochko
44ef4d08e7 New translations short_description.txt (Bengali) 2022-11-13 07:06:53 +01:00
Eugen Rochko
bba5c51649 New translations full_description.txt (Bengali) 2022-11-13 07:06:52 +01:00
Eugen Rochko
b7129340f3 New translations strings.xml (Bengali) 2022-11-13 07:06:52 +01:00
Eugen Rochko
d695e1b21b New translations short_description.txt (Hebrew) 2022-11-13 07:06:51 +01:00
Eugen Rochko
148bd1cfb7 New translations full_description.txt (Hebrew) 2022-11-13 07:06:50 +01:00
Eugen Rochko
e619d0f1ac New translations full_description.txt (Swedish) 2022-11-13 07:06:49 +01:00
Eugen Rochko
475f486626 New translations short_description.txt (Hindi) 2022-11-13 07:06:48 +01:00
Eugen Rochko
6108043fae New translations full_description.txt (Hindi) 2022-11-13 07:06:47 +01:00
Eugen Rochko
76491090b0 New translations strings.xml (Hindi) 2022-11-13 07:06:46 +01:00
Eugen Rochko
5770a9c491 New translations short_description.txt (Scottish Gaelic) 2022-11-13 07:06:45 +01:00
Eugen Rochko
b8325d7387 New translations full_description.txt (Scottish Gaelic) 2022-11-13 07:06:43 +01:00
Eugen Rochko
f3a65dc169 New translations full_description.txt (Thai) 2022-11-13 07:06:32 +01:00
Eugen Rochko
d21495d5be New translations full_description.txt (Chinese Simplified) 2022-11-13 07:06:30 +01:00
Eugen Rochko
d95ff25573 New translations strings.xml (Scottish Gaelic) 2022-11-13 07:06:27 +01:00
Eugen Rochko
5dc170673f New translations title.txt (Sinhala) 2022-11-13 07:06:26 +01:00
Eugen Rochko
c148f7f23b New translations title.txt (Indonesian) 2022-11-13 07:06:24 +01:00
Eugen Rochko
1612e377d2 New translations title.txt (Dutch) 2022-11-13 07:06:23 +01:00
Eugen Rochko
f03074e91a New translations strings.xml (Turkish) 2022-11-13 07:06:22 +01:00
Eugen Rochko
345847652f New translations strings.xml (Swedish) 2022-11-13 07:06:21 +01:00
Eugen Rochko
c71bc55e95 New translations strings.xml (Korean) 2022-11-13 07:06:18 +01:00
Eugen Rochko
8521fe852f New translations strings.xml (Japanese) 2022-11-13 07:06:17 +01:00
Eugen Rochko
714444562f New translations strings.xml (Italian) 2022-11-13 07:06:16 +01:00
Eugen Rochko
212c4a43c5 New translations strings.xml (Hebrew) 2022-11-13 07:06:12 +01:00
Eugen Rochko
103dc55e1b New translations strings.xml (Finnish) 2022-11-13 07:06:11 +01:00
Eugen Rochko
b50161dc8f New translations strings.xml (Basque) 2022-11-13 07:06:10 +01:00
Eugen Rochko
9d390a06bf New translations strings.xml (Czech) 2022-11-13 07:06:08 +01:00
Eugen Rochko
635ca15722 New translations strings.xml (Catalan) 2022-11-13 07:06:07 +01:00
Eugen Rochko
e7c1b50cc1 New translations strings.xml (Spanish) 2022-11-13 07:06:06 +01:00
Eugen Rochko
21410566ef New translations strings.xml (French) 2022-11-13 07:06:05 +01:00
Eugen Rochko
d6a4fddbb9 New translations full_description.txt (Chinese Traditional) 2022-11-13 07:06:04 +01:00
Eugen Rochko
a911b1bd9a New translations strings.xml (Arabic) 2022-11-13 07:06:03 +01:00
Eugen Rochko
be4ee9436d New translations strings.xml (German) 2022-11-13 07:06:02 +01:00
Eugen Rochko
6447d470f9 New translations strings.xml (Chinese Traditional) 2022-11-13 07:06:01 +01:00
Eugen Rochko
82aba8d276 New translations strings.xml (Ukrainian) 2022-11-13 07:06:00 +01:00
Eugen Rochko
eeec1f8bed New translations strings.xml (Chinese Simplified) 2022-11-13 07:05:59 +01:00
Eugen Rochko
2043c74937 New translations strings.xml (Polish) 2022-11-13 07:05:57 +01:00
Eugen Rochko
844322a860 New translations short_description.txt (Sinhala) 2022-11-13 07:05:56 +01:00
Eugen Rochko
d458b9d825 New translations full_description.txt (Sinhala) 2022-11-13 07:05:55 +01:00
Eugen Rochko
cbd835b136 New translations strings.xml (Sinhala) 2022-11-13 07:05:54 +01:00
Eugen Rochko
8260d38d0f New translations short_description.txt (Indonesian) 2022-11-13 07:05:53 +01:00
Eugen Rochko
4fd8a8877e New translations full_description.txt (Indonesian) 2022-11-13 07:05:52 +01:00
Eugen Rochko
b3d7ffa799 New translations strings.xml (Indonesian) 2022-11-13 07:05:51 +01:00
Eugen Rochko
d6f20edbe5 New translations short_description.txt (Dutch) 2022-11-13 07:05:51 +01:00
Eugen Rochko
7eda308388 New translations full_description.txt (Dutch) 2022-11-13 07:05:50 +01:00
Eugen Rochko
1c256c2bba New translations strings.xml (Dutch) 2022-11-13 07:05:49 +01:00
Eugen Rochko
32f6a5664f New translations strings.xml (Vietnamese) 2022-11-13 07:05:48 +01:00
Eugen Rochko
13321ce5e2 New translations short_description.txt (Portuguese, Brazilian) 2022-11-13 07:05:47 +01:00
Eugen Rochko
d81e698d3b New translations full_description.txt (Polish) 2022-11-13 07:05:45 +01:00
Eugen Rochko
ee3123f45a New translations full_description.txt (Vietnamese) 2022-11-13 07:05:44 +01:00
Eugen Rochko
4bf1a5215c New translations strings.xml (Kabyle) 2022-11-13 07:05:43 +01:00
Eugen Rochko
d1b7c84473 New translations strings.xml (Thai) 2022-11-13 07:05:40 +01:00
Eugen Rochko
75c02f8d3c New translations full_description.txt (Portuguese, Brazilian) 2022-11-13 07:05:39 +01:00
Eugen Rochko
86f583ada4 New translations strings.xml (Portuguese, Brazilian) 2022-11-13 07:05:38 +01:00
Gregory K
8d60264ee3 Merge pull request #344 from sk22/fix-edit-polls
Apply poll duration when editing status
2022-11-13 08:55:31 +03:00
sk
94d2406fa4 bump version 2022-11-12 20:32:30 +01:00
sk
8e8d24c828 Merge branch 'feature/delete-redraft' into fork 2022-11-12 20:31:57 +01:00
sk
91bd600f93 don't lock visibility for redrafted posts
fixes #40
2022-11-12 20:31:30 +01:00
sk
11067b9818 update readme 2022-11-12 20:24:00 +01:00
sk
7bfd841769 Merge branch 'feature/lists' into fork 2022-11-12 20:23:16 +01:00
sk
5c593a1025 improve adding/removing users from lists 2022-11-12 20:20:45 +01:00
sk
625134605b implement adding/removing users from lists 2022-11-12 16:14:45 +01:00
sk
1e0ae6e570 Merge branch 'main' into list-timeline-views 2022-11-12 12:35:05 +01:00
sk
f3efd1e3bc Merge branch 'feature/follow-requests' into fork 2022-11-12 03:22:27 +01:00
sk
dba8bd1862 clean up, revert unnecessary changes 2022-11-12 03:21:40 +01:00
sk
d78e3a738d bump version 2022-11-12 03:06:40 +01:00
sk
3898ad0c1c update readme 2022-11-12 03:06:11 +01:00
sk
5632016220 Merge branch 'feature/follow-requests' into fork 2022-11-12 03:05:02 +01:00
sk
12fdd70ad0 implement accepting/rejecting follow requests
closes #13
2022-11-12 03:03:52 +01:00
sk
7fd6a6f83e Merge branch 'feature/follow-requests' into fork 2022-11-12 03:03:14 +01:00
sk
515592e8ea implement accepting/rejecting follow requests
closes #13
2022-11-12 03:01:59 +01:00
sk
5fa81e6c8a add accept/decline buttons 2022-11-12 01:18:07 +01:00
sk
c22a139a5d bump version 2022-11-11 21:09:31 +01:00
sk
bed95e54d2 update readme 2022-11-11 21:07:36 +01:00
sk
f568415d8c Merge branch 'feature/favs-list' into fork 2022-11-11 21:06:53 +01:00
sk
c1137cf7b7 implement favorited posts list
closes #4
2022-11-11 21:05:06 +01:00
sk
7f678945be add title for github pages, update readme 2022-11-11 20:44:14 +01:00
sk
bb72d43270 update readme layout 2022-11-11 20:29:01 +01:00
sk
67524f6e53 update readme 2022-11-11 20:23:11 +01:00
sk
b2a7b62902 Rename “Community“ to “Local” 2022-11-11 19:57:19 +01:00
sk
a356bdeee3 change github and website url 2022-11-11 19:33:26 +01:00
sk
1438e65c10 change gh pages jekyll theme 2022-11-11 19:31:51 +01:00
sk
76043e87ad Merge branch 'fix-edit-polls' into fork 2022-11-11 19:20:16 +01:00
sk
3b28bd6ce9 get poll duration from edited status
fixes mastodon#343
2022-11-11 19:10:45 +01:00
sk
1236b16a3a Merge branch 'feature/delete-redraft' into fork 2022-11-11 18:00:09 +01:00
sk
5ec4e5339b fix null pointer exception 2022-11-11 17:59:53 +01:00
sk
0643f72a0b Merge branch 'feature/delete-redraft' into fork 2022-11-11 17:57:27 +01:00
sk
443b985b06 navigate to re-drafted status when opened 2022-11-11 17:53:04 +01:00
sk
aecaba2a92 show reply icon as workaround for mastodon#341 2022-11-11 17:18:21 +01:00
sk
977f3d0635 fix re-drafting replies 2022-11-11 17:17:52 +01:00
sk
5742493185 re-implement redraft from upstream edit function 2022-11-11 16:12:26 +01:00
sk
8b2d06c548 Merge branch 'main' into feature/delete-redraft 2022-11-11 15:36:33 +01:00
sk
56b76080ec Revert "implement deleting and re-drafting"
This reverts commit 3a4d13b1c6.
2022-11-11 15:35:30 +01:00
sk
1a12659a23 Revert "preserve visibility when re-drafting"
This reverts commit e8b43c7179.
2022-11-11 15:35:26 +01:00
sk
d711ed986c set hiding interactions counts as default 2022-11-11 14:58:39 +01:00
sk
53cb809996 bump version, update readme 2022-11-11 13:26:48 +01:00
sk
ff0a77d6b5 Merge branch 'settings/hide-interaction-numbers' into fork 2022-11-11 13:25:35 +01:00
sk
596ec3230f add option to hide interaction counts 2022-11-11 13:23:53 +01:00
sk
fa75c8eb6f Merge branch 'compact-extended-footer' into fork 2022-11-11 13:03:51 +01:00
sk
6c7a17fb81 change edit history icon 2022-11-11 13:03:39 +01:00
sk
0ad8f926cc set reblogs and favs always in one line 2022-11-11 12:54:17 +01:00
sk
764ab60fea bump version 2022-11-11 03:15:52 +01:00
sk
9b13bdb06f update readme 2022-11-11 03:15:32 +01:00
sk
9283ca8878 Merge branch 'list-timeline-views' into fork 2022-11-11 03:06:55 +01:00
sk
679ede1124 update strings 2022-11-11 03:02:17 +01:00
sk
f4b1bde8c5 improve list item styling 2022-11-11 02:42:58 +01:00
sk
eff6cc3e17 Merge branch 'compact-extended-footer' into fork 2022-11-11 02:11:52 +01:00
sk
03044b86b1 add clickable app name button 2022-11-11 02:11:37 +01:00
sk
90ff88f02b Merge branch 'compact-extended-footer' into fork 2022-11-11 01:35:13 +01:00
sk
e1f378977a set fixed visibility icon size 2022-11-11 01:35:01 +01:00
sk
a0f3d2862c Merge branch 'compact-extended-footer' into fork 2022-11-11 01:27:46 +01:00
sk
0c64145368 implement new old extended footer design
fixes #34
2022-11-11 01:27:17 +01:00
sk
a1474fb461 bump version 2022-11-09 23:29:30 +01:00
sk
965ebc8669 Merge branch 'feature/post-notifications-toggle' into fork 2022-11-09 23:28:59 +01:00
sk
3514152439 fix issues with post notification button 2022-11-09 23:28:48 +01:00
sk
1b1dc7085e update readme 2022-11-09 18:42:49 +01:00
sk
94d004724f Merge branch 'feature/post-notifications-toggle' into fork 2022-11-09 18:41:14 +01:00
sk
e9f5d235cb Implement post notifications button
Closes #10, mastodon#81
2022-11-09 18:39:28 +01:00
sk
9692afb323 bump version 2022-11-09 18:38:58 +01:00
sk
3ae37a700b Merge branch 'feature/post-notifications-toggle' into fork 2022-11-09 18:38:20 +01:00
sk
b166ca705e Implement post notifications button 2022-11-09 18:38:10 +01:00
sk
e6a67543f4 Bump version, update README 2022-11-09 15:54:33 +01:00
sk
7d38f031f1 Merge branch 'feature/follow_hashtags' into fork
Fixes #31, mastodon#233
2022-11-09 15:49:29 +01:00
sk
cf864e6f49 Implement following hashtags 2022-11-09 15:48:01 +01:00
sk
f4b1629a1d Merge branch 'feature/filter-home-timeline' into fork 2022-11-09 10:34:47 +01:00
sk
1fbb97021e align code 2022-11-09 10:34:36 +01:00
sk
229c815a59 move option to home timeline section 2022-11-09 10:32:12 +01:00
sk
ee8d78637d Merge branch 'settings/load-new-posts' into fork 2022-11-09 10:31:30 +01:00
sk
4ede842171 add option to disable automatically loading new posts 2022-11-09 10:30:25 +01:00
sk
8306e78ce3 Merge branch 'feature/filter-home-timeline' into fork 2022-11-09 10:12:16 +01:00
sk
65d093ee9d fix new posts not filtering 2022-11-09 10:11:55 +01:00
sk
aa48233a75 Merge branch 'main' into fork 2022-11-09 09:48:03 +01:00
Julien Humbert
20d838f576 Fix CJK text cut off for category names on Onboarding screen 2022-11-08 23:48:04 +09:00
Grishka
ae50e618c0 Fix #316 2022-11-08 09:41:07 +03:00
Grishka
defa8b014e Fix #315 2022-11-08 09:38:26 +03:00
Grishka
159d91f390 Crash fixes 2022-11-08 09:36:56 +03:00
sk
430d4ec93b Merge branch 'spoiler-height-independent' into fork 2022-11-08 00:55:10 +01:00
sk
ae64784daf revert increased vertical padding
i changed my mind
2022-11-08 00:54:48 +01:00
sk
18df7c32a4 Merge branch 'feature/filter-home-timeline' into fork 2022-11-08 00:27:31 +01:00
sk
4615612a65 use home_timeline string 2022-11-08 00:27:19 +01:00
sk
6b226cdcad bump version 2022-11-08 00:21:33 +01:00
sk
1776709b38 update readme 2022-11-08 00:21:17 +01:00
sk
75a35cd680 Merge branch 'feature/filter-home-timeline' into fork 2022-11-08 00:18:19 +01:00
sk
18d4f2fa36 add timeline filters for replies/boosts
fixes #32, mastodon#210
2022-11-08 00:13:06 +01:00
sk
bca936f6a2 move contribute to the spicy zone
i've been planning to do this for a while :D
2022-11-07 23:37:10 +01:00
sk
0b59379c4e update readme 2022-11-07 23:35:31 +01:00
sk
58abdad62a bump version 2022-11-07 21:18:41 +01:00
sk
c693414cc3 update readme 2022-11-07 21:18:37 +01:00
sk
ee158e1aba Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-07 21:04:44 +01:00
sk
40d478aaec tweak styles 2022-11-07 21:04:28 +01:00
sk
e7fb96b3ff Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-07 20:47:15 +01:00
sk
ef75427b45 fix option not being there when editing 2022-11-07 20:46:49 +01:00
sk
2ff4f00774 Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-07 20:17:39 +01:00
sk
534fd8c119 implement "mark media as sensitive" button
fixes #21
2022-11-07 20:15:16 +01:00
sk
2d2cd89454 Merge branch 'spoiler-height-independent' into fork 2022-11-07 17:32:55 +01:00
sk
9f3f5ca7c1 make spoiler height independent of content
fixes #12, fixes mastodon/mastodon-android#166
2022-11-07 17:32:37 +01:00
Samuel Kaiser
c0e6f17c83 Merge pull request #28 from br4yd/patch-1
Add German translation for the federated timeline
2022-11-07 16:57:21 +01:00
sk
677f1cb42d Merge remote-tracking branch 'origin/upstream' into fork 2022-11-07 16:56:27 +01:00
Gregory K
1ac6a04a46 Merge pull request #309 from julroy67/perapp-language
Add Android 13 per-app language preferences
2022-11-07 06:49:58 +03:00
Julien Humbert
21927d2e25 Add missing line in Android Manifest 2022-11-07 12:32:20 +09:00
Julien Humbert
c35441f5f7 Add per-app language preferences 2022-11-07 12:27:26 +09:00
br4yd
3718fe6601 Add German translation for the federated timeline
I added the translation strings for the federation timeline tab to the German translation file and also translated the values.
2022-11-06 22:21:53 +01:00
sk
095f234bd5 bump version 2022-11-06 11:16:31 +01:00
sk
f10c0e06db Merge branch 'fork' of github.com:sk22/mastodon-android-fork into fork 2022-11-06 11:13:42 +01:00
sk
ebbd5d1fa3 Merge branch 'upstream' into fork 2022-11-06 11:13:26 +01:00
Grishka
c8abf26040 Fix #301 2022-11-06 05:43:17 +03:00
Grishka
bc733af147 Actually no, this makes more sense 2022-11-06 05:39:22 +03:00
Grishka
77a2fd2a60 Fix #298 2022-11-06 05:36:43 +03:00
Samuel Kaiser
5cfc5eb08a Update README.md 2022-11-05 11:27:02 +01:00
Samuel Kaiser
b832d2df26 Update README.md 2022-11-04 18:02:33 +01:00
Samuel Kaiser
db34dc40ba Merge branch 'mastodon:master' into fork 2022-11-04 14:32:13 +01:00
Gregory K
bfa48c2d3e Merge pull request #289 from sk22/fix-editing-cw
Fix spoiler not being published when editing
2022-11-04 07:02:40 +03:00
Gregory K
b5e229a84d Merge pull request #288 from sk22/fix-editing-visibility
Fix wrong visibility displayed when editing
2022-11-04 07:02:19 +03:00
sk
ef207f885b increase update check interval 2022-11-04 03:37:32 +01:00
sk
c4eee28335 Merge branch 'feature/check-for-update-button' into fork 2022-11-04 03:34:06 +01:00
sk
6fe466779e add toash message 2022-11-04 03:33:41 +01:00
sk
e71db1b883 custom app name in strings 2022-11-04 03:11:10 +01:00
sk
b6efafe99d Merge branch 'feature/check-for-update-button' into fork 2022-11-04 03:09:39 +01:00
sk
688d466f8e implement manual update check settings item 2022-11-04 03:09:19 +01:00
Samuel Kaiser
3c5797932e add download badge 2022-11-03 17:42:31 +01:00
sk
48a5e262ce bump version 2022-11-03 17:18:57 +01:00
sk
63b0365208 remove reverted default visibility 2022-11-03 17:18:42 +01:00
sk
972c05d60b Revert "set unlisted as default visibility"
This reverts commit d34653750e.
2022-11-03 17:13:10 +01:00
sk
acb5778e0b bump version, again 2022-11-03 16:36:31 +01:00
sk
7694c50358 Merge branch 'fix-editing-cw' into fork 2022-11-03 16:35:31 +01:00
sk
7ffa368d10 fix spoiler not being published when editing 2022-11-03 16:33:12 +01:00
Y32Gcnte8z
53f8f41d88 remain visibility when editing 2022-11-03 16:15:17 +01:00
sk
93d57d847e fix newly published posts appearing twice
see https://github.com/mastodon/mastodon-android/issues/283
2022-11-03 16:14:12 +01:00
sk
83a4f5eec2 re-add missing onStatusUpdate event listener 2022-11-03 16:11:44 +01:00
obstsalatschuessel
21a526dda9 Add list timelines view
* get accounts list timelines from API
* display available lists in discover view tab
* display list timeline
2022-11-03 09:03:34 +01:00
Samuel Kaiser
836c2dba8d Merge branch 'mastodon:master' into fork 2022-11-02 22:32:57 +01:00
Samuel Kaiser
13ae8d2ebe Update README.md 2022-11-02 22:30:16 +01:00
Gregory K
98dafb4e49 Merge pull request #280 from sk22/fix-notifications-crash
Check whether title status item is null to fix null pointer exception
2022-11-03 00:27:07 +03:00
sk
85b8bae42e bump version 2022-11-02 22:15:26 +01:00
Samuel Kaiser
64416ef9ee Merge pull request #26 from Y32Gcnte8z/fork
Should we also change the client name in the footer of status?
2022-11-02 21:55:20 +01:00
Samuel Kaiser
6274ab3384 Merge pull request #27 from Y32Gcnte8z/visibility
Retain visibility of status when editing
2022-11-02 21:53:34 +01:00
sk
4ea5d94dc6 Merge branch 'fix-notifications-crash' into fork 2022-11-02 21:41:30 +01:00
sk
db4dd436b7 more concise null check 2022-11-02 21:36:14 +01:00
sk
6906ac6c8f remove unnecessary import 2022-11-02 21:34:05 +01:00
sk
bc0b56a90e avoid null in notification display items
fixes bug where type = status, but status = null,
causing buildDisplayItems to return a null title status item and an account card
2022-11-02 21:31:44 +01:00
sk
349d95b8ee fix syntax error 2022-11-02 20:25:42 +01:00
sk
8c8eb395de Merge remote-tracking branch 'origin/upstream' into fork 2022-11-02 20:22:33 +01:00
Grishka
50c844aa25 It was the wrong icon this whole time 2022-11-02 11:06:00 +03:00
Grishka
85b232330b Merge branch 'l10n_master' 2022-11-02 11:02:18 +03:00
Grishka
b4c305d094 Add privacy policy step to the signup flow 2022-11-02 11:01:52 +03:00
Grishka
be2ee3a029 Fix #277 2022-11-02 09:07:48 +03:00
sk
596799bf2f enable github update check 2022-11-01 23:28:55 +01:00
sk
10a405ef13 change self updater api url 2022-11-01 21:38:30 +01:00
Eugen Rochko
118c5d4b44 New translations strings.xml (French) 2022-11-01 21:31:10 +01:00
sk
a4cb05080a Merge branch 'upstream' into fork 2022-11-01 21:26:44 +01:00
Eugen Rochko
699ececc42 New translations strings.xml (French) 2022-11-01 20:17:30 +01:00
Gregory K
4f8f698911 Merge pull request #275 from davidmhewitt/fix-duplicate-emoji-crash
Fix crash on duplicate custom emojis
2022-11-01 21:51:24 +03:00
Eugen Rochko
30083837a5 New translations full_description.txt (Vietnamese) 2022-11-01 18:21:16 +01:00
Eugen Rochko
ea7aa6c52f New translations strings.xml (Vietnamese) 2022-11-01 18:21:15 +01:00
David Hewitt
244f2ed911 Fix crash on duplicate custom emojis
Fixes #274

Add a merge function to `Collectors.toMap` to discard any duplicate custom emojis that may be returned if a user uses the same custom emoji in both their name and profile.
2022-11-01 14:47:25 +00:00
Eugen Rochko
99a71c67e4 New translations full_description.txt (German) 2022-11-01 15:23:49 +01:00
Eugen Rochko
b3c0550a86 New translations strings.xml (German) 2022-11-01 15:23:47 +01:00
Eugen Rochko
39a0c6d08a New translations strings.xml (Vietnamese) 2022-11-01 11:03:52 +01:00
Gregory K
0ee494bcfc Merge pull request #273 from davidmhewitt/load-post-visibility-preference
Load post privacy preference
2022-11-01 00:30:01 +03:00
Eugen Rochko
4233c743e2 New translations strings.xml (Italian) 2022-10-31 22:06:56 +01:00
David Hewitt
eea00b0d53 Load post privacy preference
This queries the user's post visibility preference when opening the composer, and sets it on the composer.

In the case of composing a reply, the user's preference is only respected if it is "more private" than the privacy of the post being replied to, as this appears to be the behaviour in the web interface (and is what I'd expect)
2022-10-31 20:51:28 +00:00
Eugen Rochko
7cef4d282c New translations strings.xml (German) 2022-10-31 19:11:31 +01:00
Eugen Rochko
df6e085c97 New translations strings.xml (German) 2022-10-31 18:14:37 +01:00
Eugen Rochko
70cf48355e New translations strings.xml (German) 2022-10-31 10:54:54 +01:00
Eugen Rochko
da06c798d7 New translations strings.xml (German) 2022-10-31 09:23:34 +01:00
Eugen Rochko
9055c094e8 New translations strings.xml (Kabyle) 2022-10-31 07:46:18 +01:00
Eugen Rochko
5ab3ae3d59 New translations strings.xml (Chinese Traditional) 2022-10-31 07:46:17 +01:00
Eugen Rochko
92ebb2f339 New translations strings.xml (German) 2022-10-31 07:46:16 +01:00
Eugen Rochko
d8c034dfa3 New translations strings.xml (Arabic) 2022-10-31 07:46:15 +01:00
Eugen Rochko
247516849e New translations strings.xml (French) 2022-10-31 07:46:14 +01:00
Eugen Rochko
aad9996b6f New translations strings.xml (Spanish) 2022-10-31 07:46:13 +01:00
Eugen Rochko
76c1ad07a3 New translations strings.xml (Catalan) 2022-10-31 07:46:12 +01:00
Eugen Rochko
49599f48bb New translations strings.xml (Czech) 2022-10-31 07:46:11 +01:00
Eugen Rochko
158d128316 New translations strings.xml (Greek) 2022-10-31 07:46:09 +01:00
Eugen Rochko
00897e7388 New translations strings.xml (Basque) 2022-10-31 07:46:08 +01:00
Eugen Rochko
1867f294c8 New translations strings.xml (Finnish) 2022-10-31 07:46:07 +01:00
Eugen Rochko
eaa189d6b0 New translations strings.xml (Hebrew) 2022-10-31 07:46:06 +01:00
Eugen Rochko
87d11dcb0a New translations strings.xml (Armenian) 2022-10-31 07:46:05 +01:00
Eugen Rochko
ba99394890 New translations strings.xml (Italian) 2022-10-31 07:46:04 +01:00
Eugen Rochko
6b8236021e New translations strings.xml (Polish) 2022-10-31 07:46:03 +01:00
Eugen Rochko
68b12a579c New translations strings.xml (Japanese) 2022-10-31 07:46:01 +01:00
Eugen Rochko
3f0ae887b8 New translations strings.xml (Portuguese) 2022-10-31 07:46:00 +01:00
Eugen Rochko
187f9a2246 New translations strings.xml (Russian) 2022-10-31 07:45:59 +01:00
Eugen Rochko
e8566bc24b New translations strings.xml (Swedish) 2022-10-31 07:45:58 +01:00
Eugen Rochko
e375fc7d4d New translations strings.xml (Turkish) 2022-10-31 07:45:57 +01:00
Eugen Rochko
89157efce8 New translations strings.xml (Ukrainian) 2022-10-31 07:45:55 +01:00
Eugen Rochko
5e95291016 New translations strings.xml (Chinese Simplified) 2022-10-31 07:45:54 +01:00
Eugen Rochko
cbd6e668dc New translations strings.xml (Vietnamese) 2022-10-31 07:45:53 +01:00
Eugen Rochko
898e62490b New translations strings.xml (Galician) 2022-10-31 07:45:52 +01:00
Eugen Rochko
326ab4edf1 New translations strings.xml (Thai) 2022-10-31 07:45:51 +01:00
Eugen Rochko
f25a965478 New translations strings.xml (Croatian) 2022-10-31 07:45:50 +01:00
Eugen Rochko
f3f3bea7b3 New translations strings.xml (Bosnian) 2022-10-31 07:45:48 +01:00
Eugen Rochko
583b0788c6 New translations strings.xml (Occitan) 2022-10-31 07:45:47 +01:00
Eugen Rochko
ca29cee586 New translations strings.xml (Korean) 2022-10-31 07:45:46 +01:00
Eugen Rochko
ec905448b0 New translations strings.xml (Portuguese, Brazilian) 2022-10-31 07:45:45 +01:00
Gregory K
e8fa82d0de Merge pull request #269 from davidmhewitt/fix-image-keyboard
Fix receiving images from keyboards
2022-10-31 09:26:29 +03:00
Grishka
e381de812c Add self-updater for github builds 2022-10-31 09:26:17 +03:00
Eugen Rochko
1a9752d53b New translations strings.xml (German) 2022-10-30 23:05:59 +01:00
Eugen Rochko
09cc5c5fd2 New translations strings.xml (German) 2022-10-30 22:00:42 +01:00
Eugen Rochko
4e8d510d38 New translations strings.xml (German) 2022-10-30 20:57:57 +01:00
Eugen Rochko
7f7f6bae80 New translations strings.xml (German) 2022-10-30 17:30:49 +01:00
Eugen Rochko
358ff12fba New translations strings.xml (German) 2022-10-30 16:07:19 +01:00
David Hewitt
8ff3ecb4d4 Fix receiving images from keyboards
The call to `super.onCreateInputConnection` was overwriting the mimes in the `outAttrs`, so we can call that first and then modify the mimes.

This fixes the ability to insert GIFs with the default GBoard GIF menu for me.
2022-10-30 14:56:47 +00:00
Eugen Rochko
47df35f0fd New translations full_description.txt (Chinese Traditional) 2022-10-30 01:23:19 +02:00
Eugen Rochko
e226851e03 New translations strings.xml (Arabic) 2022-10-29 14:15:24 +02:00
Eugen Rochko
7228907682 New translations strings.xml (Arabic) 2022-10-29 13:13:13 +02:00
Eugen Rochko
1dbabed716 New translations strings.xml (German) 2022-10-28 16:21:33 +02:00
Eugen Rochko
4224cd037d New translations strings.xml (German) 2022-10-28 15:11:58 +02:00
Eugen Rochko
9aa6d3f531 New translations strings.xml (Chinese Traditional) 2022-10-28 05:30:56 +02:00
Eugen Rochko
c9823ae9d0 New translations strings.xml (Chinese Traditional) 2022-10-28 04:30:39 +02:00
Grishka
1fa8a9e858 Always show domain for own account 2022-10-26 18:28:57 +03:00
Grishka
212e8893b9 Fix editing 2022-10-26 03:01:39 +03:00
Grishka
367057421b Declare and request notifications permission (should fix #262) 2022-10-26 02:46:23 +03:00
Eugen Rochko
c821480842 New translations strings.xml (Polish) 2022-10-24 00:07:29 +02:00
Eugen Rochko
3ef7c11e3b New translations strings.xml (Portuguese, Brazilian) 2022-10-17 16:18:05 +02:00
Eugen Rochko
1112756bc2 New translations strings.xml (Galician) 2022-10-15 17:57:46 +02:00
Eugen Rochko
878ed43135 New translations strings.xml (Thai) 2022-10-15 16:57:55 +02:00
Eugen Rochko
01b746d30a New translations strings.xml (Thai) 2022-10-15 13:55:51 +02:00
Eugen Rochko
a900351729 New translations strings.xml (Arabic) 2022-10-08 17:35:18 +02:00
Eugen Rochko
4114a6e5d8 New translations strings.xml (Turkish) 2022-10-07 18:41:40 +02:00
Eugen Rochko
04dd232e38 New translations strings.xml (Italian) 2022-10-05 21:07:13 +02:00
Eugen Rochko
bc26dfe856 New translations strings.xml (Chinese Simplified) 2022-10-05 07:12:25 +02:00
Eugen Rochko
0322f845af New translations strings.xml (Chinese Simplified) 2022-10-05 06:12:27 +02:00
Eugen Rochko
7daa2d63e6 New translations strings.xml (Vietnamese) 2022-10-04 18:08:26 +02:00
Eugen Rochko
3a8a41f568 New translations strings.xml (Kabyle) 2022-10-04 06:41:10 +02:00
Eugen Rochko
65cd7d076b New translations strings.xml (French) 2022-10-04 06:41:09 +02:00
Eugen Rochko
a1f71091fe New translations strings.xml (Spanish) 2022-10-04 06:41:08 +02:00
Eugen Rochko
131834af86 New translations strings.xml (Arabic) 2022-10-04 06:41:07 +02:00
Eugen Rochko
c0789cbdb9 New translations strings.xml (Catalan) 2022-10-04 06:41:06 +02:00
Eugen Rochko
7d12d866ab New translations strings.xml (Czech) 2022-10-04 06:41:05 +02:00
Eugen Rochko
7c2589c35b New translations strings.xml (German) 2022-10-04 06:41:04 +02:00
Eugen Rochko
fd2031ccf5 New translations strings.xml (Basque) 2022-10-04 06:41:02 +02:00
Eugen Rochko
6a165ec9f4 New translations strings.xml (Italian) 2022-10-04 06:40:59 +02:00
Eugen Rochko
fd8dad487a New translations strings.xml (Japanese) 2022-10-04 06:40:58 +02:00
Eugen Rochko
e9443b841a New translations strings.xml (Thai) 2022-10-04 06:40:57 +02:00
Eugen Rochko
89ec6acac9 New translations strings.xml (Korean) 2022-10-04 06:40:56 +02:00
Eugen Rochko
9b389b346f New translations strings.xml (Portuguese) 2022-10-04 06:40:55 +02:00
Eugen Rochko
4a522bffc0 New translations strings.xml (Russian) 2022-10-04 06:40:54 +02:00
Eugen Rochko
9b62f8b7a2 New translations strings.xml (Turkish) 2022-10-04 06:40:52 +02:00
Eugen Rochko
96342d67e3 New translations strings.xml (Chinese Traditional) 2022-10-04 06:40:50 +02:00
Eugen Rochko
95fc2bef9e New translations strings.xml (Vietnamese) 2022-10-04 06:40:49 +02:00
Eugen Rochko
4f36fbe3b9 New translations strings.xml (Galician) 2022-10-04 06:40:48 +02:00
Eugen Rochko
a501d8b995 New translations strings.xml (Portuguese, Brazilian) 2022-10-04 06:40:47 +02:00
Eugen Rochko
74691650b3 New translations strings.xml (Croatian) 2022-10-04 06:40:46 +02:00
Eugen Rochko
9325781cbc New translations strings.xml (Bosnian) 2022-10-04 06:40:45 +02:00
Eugen Rochko
b383dd6419 New translations strings.xml (Polish) 2022-10-04 06:40:43 +02:00
Eugen Rochko
fe98ecf0cc New translations strings.xml (Chinese Simplified) 2022-10-04 06:40:42 +02:00
Grishka
01970ab69b Compose media attachment redesign 2022-10-04 07:35:31 +03:00
Grishka
3aa252f681 Fix editing 2022-10-01 01:12:31 +03:00
Grishka
18633291e6 Add monochrome icon 2022-09-29 01:23:00 +03:00
Eugen Rochko
b79619b6e4 New translations strings.xml (Chinese Simplified) 2022-09-24 19:11:57 +02:00
Eugen Rochko
dbb03ee688 New translations strings.xml (Thai) 2022-09-23 21:06:49 +02:00
Eugen Rochko
00894b41d2 New translations strings.xml (Thai) 2022-09-23 20:09:36 +02:00
Eugen Rochko
8868ace90d New translations full_description.txt (German) 2022-09-19 19:52:12 +02:00
Eugen Rochko
a7586eeba8 New translations strings.xml (German) 2022-09-19 19:52:10 +02:00
Eugen Rochko
19b89c606a New translations strings.xml (German) 2022-09-19 18:43:32 +02:00
Eugen Rochko
b6c1e7d11e New translations strings.xml (Italian) 2022-09-18 02:43:45 +02:00
Eugen Rochko
ca90c89b2a New translations strings.xml (Italian) 2022-09-18 01:24:36 +02:00
Eugen Rochko
170a758f5f New translations strings.xml (Polish) 2022-09-16 12:51:10 +02:00
Eugen Rochko
43dcd6e7f4 New translations strings.xml (Polish) 2022-09-16 11:20:42 +02:00
Eugen Rochko
063542b2f5 New translations strings.xml (Spanish) 2022-09-16 09:46:29 +02:00
Eugen Rochko
70e4ae4fb4 New translations short_description.txt (Czech) 2022-09-15 21:09:48 +02:00
Eugen Rochko
8f1a4c60df New translations full_description.txt (Czech) 2022-09-15 21:09:47 +02:00
Eugen Rochko
07212dba96 New translations strings.xml (Czech) 2022-09-15 21:09:46 +02:00
Eugen Rochko
fb95caadbe New translations full_description.txt (Czech) 2022-09-15 20:01:47 +02:00
Eugen Rochko
51e86b686d New translations strings.xml (Czech) 2022-09-15 20:01:46 +02:00
Eugen Rochko
6ec335087d New translations strings.xml (Chinese Simplified) 2022-09-15 17:03:34 +02:00
Y32Gcnte8z
96040e15fd remain visibility when editing 2022-09-15 22:38:44 +08:00
Y32Gcnte8z
12a5670441 change client name in footer of status 2022-09-15 21:21:50 +08:00
Eugen Rochko
b756fe2cdb New translations strings.xml (Italian) 2022-09-12 22:55:33 +02:00
Eugen Rochko
3732a4c844 New translations full_description.txt (German) 2022-09-10 20:18:22 +02:00
Eugen Rochko
5bd59bd999 New translations strings.xml (German) 2022-09-10 20:18:21 +02:00
Eugen Rochko
90ba5551d2 New translations strings.xml (German) 2022-09-10 18:59:59 +02:00
Eugen Rochko
484b6891fd New translations strings.xml (Chinese Simplified) 2022-09-10 16:02:25 +02:00
Eugen Rochko
0435d5f0c7 New translations strings.xml (Spanish) 2022-09-10 12:46:24 +02:00
Eugen Rochko
864c8de8de New translations strings.xml (Spanish) 2022-09-10 11:45:40 +02:00
Eugen Rochko
8bcea5bfb8 New translations strings.xml (Spanish) 2022-09-10 10:49:12 +02:00
Eugen Rochko
beded04579 New translations strings.xml (Vietnamese) 2022-09-09 19:55:30 +02:00
Eugen Rochko
5dcd41170c New translations strings.xml (Vietnamese) 2022-09-09 16:55:37 +02:00
sk
8aeda56fc8 merge upstream changes 2022-09-08 14:56:43 +02:00
Eugen Rochko
3bb921a859 New translations strings.xml (Chinese Traditional) 2022-09-03 08:09:40 +02:00
Eugen Rochko
38edbde645 New translations strings.xml (Chinese Traditional) 2022-09-03 07:11:51 +02:00
Grishka
f531a90b41 Better editing 2022-09-02 11:21:28 +03:00
Grishka
ff52c37868 Editing 2022-09-02 05:47:20 +03:00
Grishka
8fb2b454dd Post edit history + extended footer redesign 2022-09-02 02:00:25 +03:00
Grishka
265b2ad32c Fix #218 2022-08-29 21:21:08 +03:00
Grishka
ba3219d9fc Fix #209, fix #198 2022-08-29 00:57:09 +03:00
Eugen Rochko
bcac7401ee New translations strings.xml (Chinese Simplified) 2022-08-28 23:21:49 +02:00
Grishka
b44e3424e3 Fix #249 2022-08-29 00:12:15 +03:00
Eugen Rochko
c15f6519c0 New translations strings.xml (Vietnamese) 2022-08-01 08:15:42 +02:00
Eugen Rochko
2809b27be0 New translations strings.xml (Turkish) 2022-07-31 20:49:45 +02:00
Poussinou
68a9eba868 Create FUNDING.yml 2022-07-30 18:22:09 +02:00
Eugen Rochko
1b6e096bf9 New translations strings.xml (Kabyle) 2022-07-30 08:41:42 +02:00
Eugen Rochko
85db37f6a7 New translations full_description.txt (Czech) 2022-07-24 19:10:28 +02:00
Eugen Rochko
0cee490466 New translations strings.xml (Czech) 2022-07-24 19:10:27 +02:00
Eugen Rochko
a148c92da2 New translations full_description.txt (Czech) 2022-07-24 17:58:57 +02:00
sk
f073eba538 Merge branch 'feature/pin-posts' into fork 2022-07-22 12:18:42 +02:00
sk
7f78431eff minor code style fix 2022-07-22 12:18:16 +02:00
sk
24c1ac042c Merge branch 'master' into feature/pin-posts 2022-07-22 11:57:02 +02:00
sk
105fe68438 update app name, close #15 2022-07-22 11:51:10 +02:00
sk
46057af093 update readme 2022-07-22 11:50:04 +02:00
sk
750fa4c112 Merge branch 'master' into fork 2022-07-22 11:30:17 +02:00
Eugen Rochko
6272797834 New translations strings.xml (Czech) 2022-07-20 17:22:53 +02:00
Eugen Rochko
c048134ef2 New translations strings.xml (Czech) 2022-07-20 15:30:06 +02:00
Eugen Rochko
6f57cd9ffe New translations strings.xml (Czech) 2022-07-19 12:39:11 +02:00
Grishka
8b40643e63 Update more colors 2022-07-15 00:45:17 +03:00
Eugen Rochko
76815f8194 New translations strings.xml (Japanese) 2022-07-13 15:46:09 +02:00
Grishka
5968dcd05b Merge branch 'l10n_master' 2022-07-13 01:28:36 +03:00
Eugen Rochko
0c382bdbf6 New translations full_description.txt (Thai) 2022-07-08 15:39:13 +02:00
Grishka
74f03026cf Everything is purple now 2022-07-02 02:03:07 +03:00
Eugen Rochko
8ad6bd52ef New translations strings.xml (Russian) 2022-07-01 14:00:14 +02:00
Eugen Rochko
04da21edf3 New translations strings.xml (Catalan) 2022-06-15 20:48:39 +02:00
Eugen Rochko
ee709b6db7 New translations strings.xml (Spanish) 2022-06-15 20:48:38 +02:00
Eugen Rochko
b7432fe422 New translations full_description.txt (Spanish) 2022-06-15 19:26:46 +02:00
Eugen Rochko
8a0991d533 New translations strings.xml (Spanish) 2022-06-15 19:26:45 +02:00
Eugen Rochko
6fffe778d3 New translations strings.xml (Portuguese, Brazilian) 2022-06-13 06:36:58 +02:00
Eugen Rochko
b67b61dfe4 New translations strings.xml (Portuguese, Brazilian) 2022-06-13 05:36:15 +02:00
Eugen Rochko
65d86d9238 New translations strings.xml (Galician) 2022-06-10 07:15:17 +02:00
sk
c1e67c4f73 bump version 2022-06-08 21:44:39 +02:00
sk
e0e48f87eb Merge branch 'master' into fork 2022-06-08 21:42:42 +02:00
Grishka
b2db64022f Add pre-upload avatar and header resizing 2022-06-06 16:45:56 +03:00
Eugen Rochko
8336bfdf5c New translations strings.xml (Galician) 2022-06-06 11:07:30 +02:00
Samuel Kaiser
0ec14fe8fa Merge pull request #20 from Y32Gcnte8z/fork
fix simplified Chinese strings
2022-06-05 12:03:08 +02:00
Y32Gcnte8z
01a2f1d95c fix simplified Chinese strings 2022-06-04 13:20:56 +08:00
Eugen Rochko
b38bf5e431 New translations strings.xml (Chinese Simplified) 2022-06-03 15:20:59 +02:00
Samuel Kaiser
67b3e85837 Merge pull request #19 from Y32Gcnte8z/fork
New translations strings.xml (Chinese Simplified)
2022-06-03 11:31:19 +02:00
Eugen Rochko
310fb7db42 New translations strings.xml (Chinese Simplified) 2022-06-03 08:32:51 +02:00
Y32Gcnte8z
9f4d330ab1 New translations strings.xml (Chinese Simplified) 2022-06-03 13:55:49 +08:00
sk
25092fbcfb add icon to readme 2022-05-31 17:04:58 +02:00
sk
705e98729d initial pink branding 2022-05-31 16:51:05 +02:00
Eugen Rochko
2f24977996 New translations strings.xml (Chinese Simplified) 2022-05-29 10:45:49 +02:00
Eugen Rochko
6c336ba89e New translations strings.xml (Chinese Simplified) 2022-05-29 09:32:03 +02:00
sk
108d16a157 bump version 2022-05-26 22:44:31 +02:00
sk
e55ca6cc05 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:42:02 +02:00
sk
b8be1f184d hide redraft button when not applicable 2022-05-26 22:38:56 +02:00
sk
aa96ec54a3 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:09:39 +02:00
sk
e8b43c7179 preserve visibility when re-drafting 2022-05-26 22:09:02 +02:00
sk
b51b4a10ee Merge branch 'feature/delete-redraft' into fork 2022-05-26 21:44:07 +02:00
sk
f2b9ede27c Add proguard rules for parceler
according to http://parceler.org/
fixes issue where parceler can't find parcelable class
2022-05-26 21:43:39 +02:00
sk
a8c7d891f1 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:45:52 +02:00
sk
195c4d7b6d remove unused imports 2022-05-26 19:45:10 +02:00
sk
d280dc31e8 bump version 2022-05-26 19:35:36 +02:00
sk
eb0925c524 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:30:52 +02:00
sk
968de3664d fix german strings 2022-05-26 19:30:23 +02:00
sk
12f7336392 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:28:09 +02:00
sk
3a4d13b1c6 implement deleting and re-drafting 2022-05-26 19:19:42 +02:00
sk
273c841d9a Merge branch 'master' into feature/delete-redraft 2022-05-26 15:35:12 +02:00
Eugen Rochko
d1b1cb2082 New translations strings.xml (Korean) 2022-05-25 08:21:34 +02:00
Eugen Rochko
5bbe99be51 New translations full_description.txt (Portuguese) 2022-05-24 00:19:57 +02:00
Eugen Rochko
556d1e7433 New translations full_description.txt (Portuguese) 2022-05-23 23:03:11 +02:00
Eugen Rochko
293d7032ce New translations strings.xml (Russian) 2022-05-22 12:23:45 +02:00
sk
0186b7f8da Merge remote-tracking branch 'origin/fork' into fork 2022-05-22 02:14:06 +02:00
sk
d33654c793 bump version 2022-05-22 02:08:01 +02:00
Samuel Kaiser
86d2312615 Update README.md 2022-05-22 02:07:07 +02:00
sk
d1083c331b Merge branch 'feature/bookmarks' into fork 2022-05-22 02:04:08 +02:00
sk
ed7242217a add missing icon 2022-05-22 02:03:54 +02:00
sk
8fddaa8c82 implement bookmarks list!! 2022-05-22 02:03:29 +02:00
sk
00affe6e3e update readme 2022-05-21 23:38:56 +02:00
sk
f21b647ee0 bump version 2022-05-21 23:35:10 +02:00
sk
2a628a3791 Merge branch 'feature/back-returns-home' into fork 2022-05-21 23:34:18 +02:00
sk
ecd568503d make back button return home before exiting 2022-05-21 23:33:53 +02:00
sk
f9d0632a85 add missing files 2022-05-21 19:42:29 +02:00
sk
11905513b7 add missing icons 2022-05-21 19:40:49 +02:00
sk
9c89abf1c4 implement bookmark button 2022-05-21 19:27:44 +02:00
sk
4d950e43ac update readme 2022-05-21 18:59:13 +02:00
sk
99405f307d bump version 2022-05-21 18:49:59 +02:00
sk
f1bfe05263 Merge branch 'feature/always-preserve-cw' into fork 2022-05-21 18:49:07 +02:00
sk
0f223159c0 always preserve cw when replying 2022-05-21 18:48:48 +02:00
sk
ad9518e87c re-add deleted strings 2022-05-21 18:08:14 +02:00
sk
1c16cfb09e bump version 2022-05-21 18:05:01 +02:00
sk
d4a4b10017 Merge branch 'feature/compose-image-description-full-image' into fork 2022-05-21 18:03:29 +02:00
sk
74ae5bd04e change app name 2022-05-21 18:03:15 +02:00
sk
9638cf079f set image view height to wrap_content 2022-05-21 17:48:53 +02:00
sk
a6d161c1b4 minor code style change
for grishka's code style must prevail
2022-05-21 17:42:40 +02:00
sk
1136e40eb4 obey image max width 2022-05-21 17:39:28 +02:00
sk
98de3a2984 don't crop image when composing alt text 2022-05-21 17:27:31 +02:00
Eugen Rochko
48e7071450 New translations strings.xml (Arabic) 2022-05-19 22:57:49 +02:00
Eugen Rochko
bcc8d55c7b New translations strings.xml (Arabic) 2022-05-19 21:55:19 +02:00
Eugen Rochko
8d477efc28 New translations strings.xml (Czech) 2022-05-19 15:32:16 +02:00
Eugen Rochko
4ae21862a5 New translations strings.xml (Czech) 2022-05-19 14:30:33 +02:00
Eugen Rochko
6e6aebef35 New translations strings.xml (Czech) 2022-05-19 13:34:29 +02:00
Eugen Rochko
cd138032da New translations strings.xml (Czech) 2022-05-18 18:19:40 +02:00
Eugen Rochko
864e6fb9d0 New translations strings.xml (Czech) 2022-05-18 16:41:35 +02:00
Eugen Rochko
9d356b0635 New translations strings.xml (Czech) 2022-05-18 15:25:33 +02:00
Eugen Rochko
6c5d720a40 New translations strings.xml (Czech) 2022-05-18 14:28:15 +02:00
Eugen Rochko
d733d76ccf New translations strings.xml (Arabic) 2022-05-18 13:29:20 +02:00
Eugen Rochko
3483d8c3c0 New translations strings.xml (Portuguese, Brazilian) 2022-05-18 09:01:46 +02:00
Eugen Rochko
0f326c1362 New translations strings.xml (Kabyle) 2022-05-17 23:04:46 +02:00
Eugen Rochko
c6eda38400 New translations strings.xml (Kabyle) 2022-05-17 22:08:14 +02:00
Eugen Rochko
3c59c8cc0f New translations strings.xml (Arabic) 2022-05-17 22:08:13 +02:00
Eugen Rochko
8f1b9ec092 New translations strings.xml (French) 2022-05-17 22:08:12 +02:00
Eugen Rochko
5a42136395 New translations strings.xml (Czech) 2022-05-17 21:03:40 +02:00
Eugen Rochko
e9b347d130 New translations strings.xml (Thai) 2022-05-17 21:03:39 +02:00
Eugen Rochko
d86e203127 New translations strings.xml (Italian) 2022-05-17 18:03:57 +02:00
Eugen Rochko
c80417e671 New translations strings.xml (Ukrainian) 2022-05-17 18:03:56 +02:00
Eugen Rochko
55a55fbb1c New translations strings.xml (Croatian) 2022-05-17 18:03:55 +02:00
Eugen Rochko
b42236999b New translations strings.xml (Occitan) 2022-05-17 18:03:53 +02:00
Eugen Rochko
8ca8bd765b New translations strings.xml (Bosnian) 2022-05-17 18:03:51 +02:00
Eugen Rochko
7f4cf77283 New translations strings.xml (Portuguese, Brazilian) 2022-05-17 18:03:50 +02:00
Eugen Rochko
950d413bd1 New translations strings.xml (Chinese Traditional) 2022-05-17 18:03:49 +02:00
Eugen Rochko
475827b1c1 New translations strings.xml (Chinese Simplified) 2022-05-17 18:03:48 +02:00
Eugen Rochko
1c9164e559 New translations strings.xml (Galician) 2022-05-17 18:03:47 +02:00
Eugen Rochko
7311a394d8 New translations strings.xml (Turkish) 2022-05-17 18:03:46 +02:00
Eugen Rochko
aa09bc7ab2 New translations strings.xml (Russian) 2022-05-17 18:03:45 +02:00
Eugen Rochko
267a6a75ef New translations strings.xml (Portuguese) 2022-05-17 18:03:43 +02:00
Eugen Rochko
37660b4c73 New translations strings.xml (Polish) 2022-05-17 18:03:42 +02:00
Eugen Rochko
15b4d46ea1 New translations strings.xml (Korean) 2022-05-17 18:03:41 +02:00
Eugen Rochko
0b99e76b25 New translations strings.xml (Japanese) 2022-05-17 18:03:40 +02:00
Eugen Rochko
c851f666b3 New translations strings.xml (Basque) 2022-05-17 18:03:38 +02:00
Eugen Rochko
7662c81754 New translations strings.xml (German) 2022-05-17 18:03:36 +02:00
Eugen Rochko
5c1b583448 New translations strings.xml (Catalan) 2022-05-17 18:03:35 +02:00
Eugen Rochko
4071c1552d New translations strings.xml (Swedish) 2022-05-17 18:03:34 +02:00
Eugen Rochko
89856a81a3 New translations strings.xml (Spanish) 2022-05-17 18:03:33 +02:00
Eugen Rochko
005ddfb651 New translations strings.xml (Vietnamese) 2022-05-17 18:03:32 +02:00
Eugen Rochko
92d10d59c6 New translations strings.xml (Armenian) 2022-05-17 18:03:31 +02:00
Eugen Rochko
150f70edd8 New translations strings.xml (Arabic) 2022-05-17 18:03:22 +02:00
Eugen Rochko
73019eaade New translations strings.xml (Thai) 2022-05-17 18:03:17 +02:00
Eugen Rochko
46bac59ff5 New translations strings.xml (Finnish) 2022-05-17 18:03:15 +02:00
Eugen Rochko
9b162cb63b New translations strings.xml (Kabyle) 2022-05-17 18:03:13 +02:00
Eugen Rochko
7d216314c9 New translations strings.xml (French) 2022-05-17 18:03:11 +02:00
Grishka
080a320e12 Make the app name non-translatable 2022-05-17 18:47:11 +03:00
Eugen Rochko
28787b4068 New translations strings.xml (Vietnamese) 2022-05-17 15:22:20 +02:00
Eugen Rochko
a73ea62a9c New translations strings.xml (Galician) 2022-05-17 13:38:15 +02:00
Eugen Rochko
69b399e397 New translations strings.xml (Japanese) 2022-05-17 06:27:43 +02:00
Eugen Rochko
fc2c033e93 New translations strings.xml (Chinese Simplified) 2022-05-17 05:29:56 +02:00
Eugen Rochko
1d81abca5b New translations title.txt (Czech) 2022-05-17 04:16:52 +02:00
Eugen Rochko
0f3cd5d8d0 New translations short_description.txt (Czech) 2022-05-17 04:16:51 +02:00
Eugen Rochko
f0476f3187 New translations full_description.txt (Czech) 2022-05-17 04:16:50 +02:00
Eugen Rochko
b4677d14e5 New translations strings.xml (Czech) 2022-05-17 04:16:50 +02:00
Eugen Rochko
a8837bd4f8 New translations strings.xml (Basque) 2022-05-16 23:18:13 +02:00
Eugen Rochko
c6991a7067 New translations strings.xml (Chinese Simplified) 2022-05-16 19:37:18 +02:00
Eugen Rochko
0723e942f0 New translations strings.xml (Chinese Simplified) 2022-05-16 18:11:12 +02:00
sk
b08415ca8f bump version 2022-05-15 20:35:10 +02:00
sk
3639c69d36 Merge branch 'master' into fork 2022-05-15 20:34:14 +02:00
Eugen Rochko
52fd300d1e New translations strings.xml (German) 2022-05-15 20:26:29 +02:00
Grishka
37cefcaf6d Fix #164 2022-05-15 21:13:36 +03:00
Eugen Rochko
68c9f7a861 New translations full_description.txt (German) 2022-05-15 19:31:06 +02:00
Eugen Rochko
8eb0b12a09 New translations strings.xml (German) 2022-05-15 19:31:05 +02:00
Grishka
558adc6936 Add compose shortcut
closes #131
2022-05-15 19:14:24 +03:00
Eugen Rochko
68ecd7bc28 New translations strings.xml (German) 2022-05-14 18:55:50 +02:00
Eugen Rochko
5c7d4e389f New translations strings.xml (Polish) 2022-05-14 16:56:43 +02:00
sk
31e3a8592f bump version 2022-05-14 13:49:49 +02:00
sk
39655d5278 Merge branch 'master' into fork 2022-05-14 13:48:03 +02:00
Eugen Rochko
55fd74c227 New translations strings.xml (Italian) 2022-05-14 11:30:44 +02:00
Eugen Rochko
b65b7c53bc New translations strings.xml (Chinese Traditional) 2022-05-14 05:38:51 +02:00
Grishka
68d0862008 Close #122 2022-05-13 20:54:22 +03:00
Grishka
c9e13eefa5 Close #146 2022-05-13 20:49:35 +03:00
Grishka
349fbce5af Fix #128 2022-05-13 20:42:54 +03:00
Eugen Rochko
7a23c9b348 New translations strings.xml (Italian) 2022-05-13 19:23:19 +02:00
Grishka
95c66654aa Fix #149 2022-05-13 19:20:40 +03:00
Grishka
a8407571a4 Fix #151 2022-05-13 19:18:29 +03:00
Grishka
75538deb9b Fix #156 2022-05-13 19:10:27 +03:00
Grishka
601eec4607 Fix #157 2022-05-13 19:01:29 +03:00
Grishka
9b87d0bece Fix #153 2022-05-13 18:14:52 +03:00
Grishka
cb25632691 Delete statuses from cache and fix auto-refresh when posting 2022-05-13 17:57:41 +03:00
Grishka
63957250c5 Fix #141 + crash fixes 2022-05-13 17:51:28 +03:00
sk
d844a77e65 add ui items for redraft 2022-05-11 17:25:00 +02:00
Eugen Rochko
32cc760272 New translations full_description.txt (Polish) 2022-05-11 00:53:22 +02:00
Eugen Rochko
e105764aa8 New translations strings.xml (Polish) 2022-05-11 00:53:21 +02:00
Eugen Rochko
5a9a352e56 New translations full_description.txt (Polish) 2022-05-10 23:44:44 +02:00
Eugen Rochko
7deb2d452e New translations short_description.txt (Swedish) 2022-05-10 18:01:59 +02:00
Eugen Rochko
c3d2df88e8 New translations full_description.txt (Swedish) 2022-05-10 18:01:58 +02:00
Eugen Rochko
9943d19c31 New translations short_description.txt (Finnish) 2022-05-10 18:01:56 +02:00
Eugen Rochko
51c1e115c5 New translations full_description.txt (Finnish) 2022-05-10 18:01:55 +02:00
Eugen Rochko
f83ff93c68 New translations strings.xml (Finnish) 2022-05-10 18:01:54 +02:00
Eugen Rochko
7deb5d44c2 New translations strings.xml (Swedish) 2022-05-10 18:01:53 +02:00
Eugen Rochko
3140ae8046 New translations strings.xml (Finnish) 2022-05-10 17:00:35 +02:00
Eugen Rochko
772f79219b New translations strings.xml (Arabic) 2022-05-09 01:41:42 +02:00
Eugen Rochko
8830d67af0 New translations full_description.txt (Polish) 2022-05-09 00:34:14 +02:00
Eugen Rochko
89c02be41c New translations strings.xml (Polish) 2022-05-09 00:34:13 +02:00
Eugen Rochko
1d092c660b New translations strings.xml (Polish) 2022-05-08 23:32:42 +02:00
Eugen Rochko
a34084da5a New translations strings.xml (Vietnamese) 2022-05-07 17:18:03 +02:00
Eugen Rochko
212f5a9beb New translations full_description.txt (Korean) 2022-05-07 17:18:02 +02:00
Eugen Rochko
f6333de4e6 New translations strings.xml (Korean) 2022-05-07 17:17:59 +02:00
Eugen Rochko
5af22f1bab New translations strings.xml (Korean) 2022-05-07 16:19:40 +02:00
Eugen Rochko
02d866d7d6 New translations strings.xml (Turkish) 2022-05-06 23:36:32 +02:00
Eugen Rochko
fa7aa6240b New translations strings.xml (Turkish) 2022-05-06 22:37:34 +02:00
sk
bde2e398a8 bump version and change app name 2022-05-06 22:10:45 +02:00
sk
8d443b2051 Merge branch 'feature/pin-posts' into fork 2022-05-06 21:50:08 +02:00
sk
33d4b678ed update posts' pinned states 2022-05-06 21:49:33 +02:00
sk
3becad1468 fix created posts being added to pinned 2022-05-06 21:18:48 +02:00
sk
fad3ba3eae bump version 2022-05-06 19:49:36 +02:00
sk
cb16f95878 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:45:33 +02:00
sk
4e833490ff fix about section not being displayed 2022-05-06 19:45:09 +02:00
Samuel Kaiser
04a973f7b0 Update README.md 2022-05-06 19:28:43 +02:00
sk
0318169b74 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:24:58 +02:00
sk
972fb1e241 translate "pinned" strings to german 2022-05-06 19:21:39 +02:00
sk
9beb04b01d implement pinning and unpinning posts 2022-05-06 19:07:51 +02:00
sk
a3bea6ad24 add profile tab for pinned toots 2022-05-06 18:09:00 +02:00
Eugen Rochko
cb38e0d367 New translations strings.xml (Thai) 2022-05-06 17:20:31 +02:00
Eugen Rochko
a133a1d01f New translations strings.xml (Thai) 2022-05-06 16:22:05 +02:00
sk
7996e4ee4a change client name, versioning 2022-05-06 15:39:57 +02:00
Eugen Rochko
79bfc43431 New translations strings.xml (Thai) 2022-05-06 15:22:42 +02:00
sk
69c4bf4213 Merge branch 'feature/display-alt-text' into fork 2022-05-06 01:00:34 +02:00
sk
7cd5ca77f5 Merge remote-tracking branch 'origin/feature/display-alt-text' into feature/display-alt-text 2022-05-06 01:00:01 +02:00
sk
7e736d3cd3 clean up code 2022-05-06 00:59:43 +02:00
sk
13c2adba56 update version and readme 2022-05-06 00:34:08 +02:00
sk
010095a50e Merge branch 'master' into fork 2022-05-06 00:30:23 +02:00
Samuel Kaiser
f0cef2103f Merge branch 'mastodon:master' into feature/display-alt-text 2022-05-06 00:29:05 +02:00
sk
8ed731a48b edit application id for fork 2022-05-06 00:27:59 +02:00
sk
8660d43cb1 bump version 2022-05-06 00:21:08 +02:00
sk
0f495f620a Merge branch 'feature/display-alt-text' into fork 2022-05-06 00:17:07 +02:00
sk
ac81f10ea8 make button disappear when no description 2022-05-06 00:16:25 +02:00
sk
9aa95413e6 make text selectable 2022-05-06 00:15:56 +02:00
Eugen Rochko
72f3a51af7 New translations strings.xml (Italian) 2022-05-06 00:01:49 +02:00
sk
a0a28a0cb7 implement scroll-to-close 2022-05-05 23:39:24 +02:00
sk
11d88aed27 implement alt text as bottom sheet 2022-05-05 23:26:48 +02:00
Eugen Rochko
ee73b487ae New translations strings.xml (Italian) 2022-05-05 23:05:54 +02:00
Eugen Rochko
e580d2e890 New translations strings.xml (Thai) 2022-05-05 22:07:42 +02:00
Grishka
88504531d4 Crash fixes 2022-05-05 22:05:18 +03:00
Eugen Rochko
4f6f53061f New translations strings.xml (Thai) 2022-05-05 19:32:57 +02:00
sk
899c9cdf21 implement alt text as toast messages 2022-05-05 19:28:27 +02:00
Eugen Rochko
be23ec4176 New translations strings.xml (Thai) 2022-05-05 18:32:43 +02:00
Eugen Rochko
186636c2ef New translations strings.xml (French) 2022-05-05 18:32:42 +02:00
Grishka
4ad9fa030b Fix #127 2022-05-05 18:30:40 +03:00
Eugen Rochko
f0396ff418 New translations strings.xml (Polish) 2022-05-05 16:53:36 +02:00
sk
919d5cffb5 Merge branch 'master' into fork 2022-05-05 16:06:28 +02:00
Eugen Rochko
67e793b56a New translations strings.xml (Japanese) 2022-05-05 15:31:36 +02:00
Eugen Rochko
d84f011d27 New translations strings.xml (French) 2022-05-05 15:31:35 +02:00
Eugen Rochko
f685d9ccdd New translations strings.xml (French) 2022-05-05 14:36:22 +02:00
Eugen Rochko
380c742f54 New translations full_description.txt (Korean) 2022-05-05 06:07:07 +02:00
Eugen Rochko
ed84ea6162 New translations strings.xml (Korean) 2022-05-05 06:07:06 +02:00
Grishka
b4219bcaa0 Crash fix 2022-05-05 06:05:50 +03:00
Eugen Rochko
e99ffc0d4c New translations strings.xml (Korean) 2022-05-05 05:05:13 +02:00
Eugen Rochko
ca015db188 New translations strings.xml (Arabic) 2022-05-05 00:05:19 +02:00
Eugen Rochko
af3a3761f2 New translations strings.xml (French) 2022-05-04 19:48:30 +02:00
Eugen Rochko
95085b6306 New translations strings.xml (Italian) 2022-05-04 18:51:31 +02:00
Grishka
1a0435d32c Fix locale 2022-05-04 18:24:51 +03:00
Grishka
bd2a33da6a Fix 2022-05-04 18:10:04 +03:00
Grishka
84e8b08bff Merge remote-tracking branch 'origin/l10n_master' 2022-05-04 18:09:40 +03:00
Eugen Rochko
afc40cbb67 New translations strings.xml (Vietnamese) 2022-05-04 14:33:24 +02:00
Eugen Rochko
6902379af6 New translations strings.xml (Arabic) 2022-05-04 07:48:18 +02:00
Eugen Rochko
450bfa1fe8 New translations title.txt (Korean) 2022-05-04 07:48:16 +02:00
Eugen Rochko
629b1b4a34 New translations short_description.txt (Korean) 2022-05-04 07:48:15 +02:00
Eugen Rochko
8f49207b25 New translations full_description.txt (Korean) 2022-05-04 07:48:14 +02:00
Eugen Rochko
347d90ad14 New translations strings.xml (Korean) 2022-05-04 07:48:13 +02:00
Eugen Rochko
b503475fcf New translations strings.xml (Galician) 2022-05-04 06:49:01 +02:00
Eugen Rochko
fe18a43ba8 New translations strings.xml (Japanese) 2022-05-04 05:45:29 +02:00
Eugen Rochko
66e23bf55e New translations strings.xml (Chinese Traditional) 2022-05-04 02:49:50 +02:00
Eugen Rochko
716b6b13b7 New translations strings.xml (Russian) 2022-05-04 01:03:46 +02:00
Grishka
23ec3e64cf Add favorites/reblogs lists and extended footer for ThreadFragment
closes #41, closes #64
2022-05-04 01:04:59 +03:00
Grishka
e512a7ef90 Fix #52 2022-05-03 22:26:36 +03:00
Grishka
9823537474 Allow opening avatars and cover images in photo viewer
closes #24
2022-05-03 22:14:56 +03:00
Eugen Rochko
f9ea2b0de3 New translations strings.xml (Japanese) 2022-05-03 19:21:05 +02:00
Eugen Rochko
90293f81d9 New translations strings.xml (German) 2022-05-03 19:21:04 +02:00
Eugen Rochko
30785457b7 New translations strings.xml (Arabic) 2022-05-03 17:57:03 +02:00
Eugen Rochko
df5cb3d977 New translations strings.xml (Arabic) 2022-05-03 16:51:17 +02:00
Grishka
bbedf46b21 Accept URLs in instance search 2022-05-03 15:35:48 +03:00
Eugen Rochko
0d50f8c45b New translations strings.xml (Galician) 2022-05-03 07:17:13 +02:00
Grishka
d4e4d9fcde Use random IDs to match FCM notifications to accounts 2022-05-03 03:01:18 +03:00
Eugen Rochko
50381f1256 New translations strings.xml (Arabic) 2022-05-03 01:00:47 +02:00
Eugen Rochko
55a6b7bdd3 New translations strings.xml (Russian) 2022-05-03 01:00:46 +02:00
sk
12599db0ff change dev versioning 2022-05-02 23:00:55 +02:00
sk
c751c85c1c add version number for upstream changes 2022-05-02 22:43:05 +02:00
Eugen Rochko
c9eac418d2 New translations strings.xml (Italian) 2022-05-02 22:01:23 +02:00
sk
f1331a0f6d start fork versioning 2022-05-02 21:35:51 +02:00
sk
c75c9b60f9 add fork readme 2022-05-02 21:35:42 +02:00
sk
eb3adf1dfd Merge branch 'feature/add-federated-timeline' into fork 2022-05-02 21:35:14 +02:00
sk
6533163fd0 Merge branch 'master' into fork 2022-05-02 21:17:04 +02:00
Gregory K
fa75570254 Merge pull request #112 from sk22/fix/space-in-empty-reply
Fix compose containing a wrong space as initial reply text
2022-05-02 22:06:12 +03:00
Eugen Rochko
a51bcba87b New translations strings.xml (Italian) 2022-05-02 21:01:43 +02:00
sk
1406ea376d fix space as reply initial text 2022-05-02 20:59:22 +02:00
Eugen Rochko
4f4212124c New translations strings.xml (Thai) 2022-05-02 19:47:57 +02:00
Eugen Rochko
da773dfac9 New translations strings.xml (German) 2022-05-02 19:47:56 +02:00
sk
1becad6016 Merge branch 'feature/enable-unlisted-as-default' into fork 2022-05-02 19:32:26 +02:00
sk
d34653750e set unlisted as default visibility 2022-05-02 19:31:43 +02:00
sk
705592aefd set unlisted as default 2022-05-02 19:17:25 +02:00
sk
583325d6e8 add unlisted visibility option 2022-05-02 19:16:53 +02:00
Eugen Rochko
35185143a2 New translations strings.xml (Thai) 2022-05-02 18:38:07 +02:00
Eugen Rochko
f12a33a749 New translations strings.xml (Japanese) 2022-05-02 18:38:06 +02:00
sk
318d271127 add federation tab and change tab order 2022-05-02 18:31:29 +02:00
Eugen Rochko
77a2a5a629 New translations strings.xml (Vietnamese) 2022-05-02 17:03:32 +02:00
Eugen Rochko
d09302492e New translations strings.xml (Thai) 2022-05-02 12:02:52 +02:00
Eugen Rochko
ab5895b21c New translations strings.xml (Thai) 2022-05-02 05:00:55 +02:00
Eugen Rochko
26360613b1 New translations strings.xml (Catalan) 2022-05-02 05:00:54 +02:00
Eugen Rochko
bd020f077f New translations strings.xml (German) 2022-05-02 05:00:53 +02:00
Eugen Rochko
35622f3675 New translations strings.xml (Greek) 2022-05-02 05:00:52 +02:00
Eugen Rochko
7516bdf2e8 New translations strings.xml (Basque) 2022-05-02 05:00:51 +02:00
Eugen Rochko
d07e765873 New translations strings.xml (Hebrew) 2022-05-02 05:00:50 +02:00
Eugen Rochko
510c97a552 New translations strings.xml (Japanese) 2022-05-02 05:00:49 +02:00
Eugen Rochko
6d78a43bfe New translations strings.xml (Korean) 2022-05-02 05:00:48 +02:00
Eugen Rochko
6ac880828e New translations strings.xml (Polish) 2022-05-02 05:00:47 +02:00
Eugen Rochko
2eb01ed477 New translations strings.xml (Portuguese) 2022-05-02 05:00:46 +02:00
Eugen Rochko
c32ca51fa5 New translations strings.xml (Russian) 2022-05-02 05:00:45 +02:00
Eugen Rochko
998e560835 New translations strings.xml (Swedish) 2022-05-02 05:00:44 +02:00
Eugen Rochko
46325f46c1 New translations strings.xml (Turkish) 2022-05-02 05:00:43 +02:00
Eugen Rochko
9f1d82ed12 New translations strings.xml (Spanish) 2022-05-02 05:00:42 +02:00
Eugen Rochko
24c5a2bf6c New translations strings.xml (Chinese Simplified) 2022-05-02 05:00:41 +02:00
Eugen Rochko
050de32cae New translations strings.xml (Portuguese, Brazilian) 2022-05-02 05:00:41 +02:00
Eugen Rochko
1d295ca058 New translations strings.xml (Bosnian) 2022-05-02 05:00:40 +02:00
Eugen Rochko
1779c132cd New translations strings.xml (Occitan) 2022-05-02 05:00:39 +02:00
Eugen Rochko
f70fcb8ff8 New translations strings.xml (Croatian) 2022-05-02 05:00:38 +02:00
Eugen Rochko
a4878b427e New translations strings.xml (Ukrainian) 2022-05-02 05:00:37 +02:00
Eugen Rochko
fcc73b5877 New translations strings.xml (Italian) 2022-05-02 05:00:36 +02:00
Eugen Rochko
8e5bf91a01 New translations strings.xml (Galician) 2022-05-02 05:00:35 +02:00
Eugen Rochko
4faa8cf7a8 New translations strings.xml (Vietnamese) 2022-05-02 05:00:34 +02:00
Eugen Rochko
afe8fd89e4 New translations strings.xml (Arabic) 2022-05-02 05:00:33 +02:00
Eugen Rochko
4df4528e60 New translations strings.xml (Armenian) 2022-05-02 05:00:32 +02:00
Eugen Rochko
ff99430f4c New translations strings.xml (Kabyle) 2022-05-02 05:00:31 +02:00
Eugen Rochko
f4026f09a0 New translations strings.xml (Finnish) 2022-05-02 05:00:30 +02:00
Eugen Rochko
ea9a2047f6 New translations strings.xml (Chinese Traditional) 2022-05-02 05:00:29 +02:00
Eugen Rochko
c16d373de8 New translations strings.xml (French) 2022-05-02 05:00:28 +02:00
Grishka
5562bf936e toot -> post 2022-05-02 05:49:59 +03:00
Grishka
02a1f2ef8c Add following/followers lists
closes #25
2022-05-02 05:45:51 +03:00
Grishka
7b26649521 Probably fix #95 2022-05-02 01:20:07 +03:00
Eugen Rochko
e34542a420 New translations strings.xml (Armenian) 2022-05-01 22:17:54 +02:00
Eugen Rochko
a0007f2e41 New translations strings.xml (Japanese) 2022-05-01 17:28:38 +02:00
Eugen Rochko
e5067e8982 New translations strings.xml (Italian) 2022-05-01 15:18:59 +02:00
Eugen Rochko
779d93b689 New translations strings.xml (Italian) 2022-05-01 14:12:57 +02:00
Eugen Rochko
397f67af10 New translations strings.xml (Arabic) 2022-05-01 12:40:14 +02:00
Eugen Rochko
027c4e0e59 New translations strings.xml (Thai) 2022-05-01 11:42:07 +02:00
Eugen Rochko
c04278754e New translations strings.xml (Chinese Simplified) 2022-05-01 11:42:06 +02:00
Eugen Rochko
daba0836e0 New translations strings.xml (Turkish) 2022-05-01 11:42:05 +02:00
Eugen Rochko
52307de614 New translations strings.xml (Thai) 2022-05-01 10:38:21 +02:00
Eugen Rochko
6f8ce04c48 New translations strings.xml (Arabic) 2022-05-01 00:43:40 +02:00
Eugen Rochko
144efdffee New translations strings.xml (Portuguese) 2022-04-30 23:48:17 +02:00
Grishka
8059120136 Better account switcher 2022-05-01 00:44:28 +03:00
Eugen Rochko
7819f10b8b New translations strings.xml (Portuguese) 2022-04-30 22:50:21 +02:00
Eugen Rochko
999c2e4714 New translations strings.xml (French) 2022-04-30 20:52:24 +02:00
Grishka
ec38210dde Fix #99 2022-04-30 20:30:29 +03:00
Eugen Rochko
1ebb5ad46d New translations full_description.txt (Russian) 2022-04-30 17:55:02 +02:00
Eugen Rochko
004d7a7652 New translations full_description.txt (Russian) 2022-04-30 16:57:21 +02:00
Eugen Rochko
6b68bd58f1 New translations strings.xml (Japanese) 2022-04-30 13:40:19 +02:00
Eugen Rochko
775ae68314 New translations strings.xml (Japanese) 2022-04-30 12:40:08 +02:00
Eugen Rochko
347a53f03f New translations strings.xml (Italian) 2022-04-30 08:29:46 +02:00
Eugen Rochko
690792ed0d New translations strings.xml (Arabic) 2022-04-30 07:26:39 +02:00
Eugen Rochko
6ee44edf84 New translations strings.xml (Galician) 2022-04-30 07:26:38 +02:00
Eugen Rochko
fc2ba241a0 New translations strings.xml (Arabic) 2022-04-30 06:28:06 +02:00
Eugen Rochko
a1efdd7e03 New translations strings.xml (Arabic) 2022-04-30 05:30:09 +02:00
Eugen Rochko
a79b0a4f15 New translations full_description.txt (Russian) 2022-04-30 00:25:43 +02:00
Eugen Rochko
69b4cf93a3 New translations strings.xml (Russian) 2022-04-30 00:25:41 +02:00
Eugen Rochko
e207b5929d New translations strings.xml (Thai) 2022-04-29 23:26:44 +02:00
Eugen Rochko
03e08dddf5 New translations strings.xml (Arabic) 2022-04-29 23:26:43 +02:00
Eugen Rochko
ad60646b8e New translations short_description.txt (Portuguese) 2022-04-29 23:26:41 +02:00
Eugen Rochko
51938f5522 New translations full_description.txt (Portuguese) 2022-04-29 23:26:40 +02:00
Grishka
38eadca4e2 Report fixes 2022-04-30 00:02:13 +03:00
Eugen Rochko
0fb1b3228f New translations strings.xml (Thai) 2022-04-29 22:10:13 +02:00
Eugen Rochko
5a8ebdb13b New translations strings.xml (Arabic) 2022-04-29 22:10:12 +02:00
Eugen Rochko
046a45a25e New translations strings.xml (Italian) 2022-04-29 22:10:10 +02:00
Eugen Rochko
751028326a New translations strings.xml (Thai) 2022-04-29 21:09:16 +02:00
Eugen Rochko
74e049884b New translations strings.xml (Italian) 2022-04-29 21:09:15 +02:00
Grishka
31cb17d549 Update readme 2022-04-29 20:39:19 +03:00
Eugen Rochko
3b63ca1b55 New translations strings.xml (Thai) 2022-04-29 18:05:59 +02:00
Eugen Rochko
3eed854909 New translations strings.xml (Thai) 2022-04-29 17:08:52 +02:00
Grishka
10a5bf0a82 Fix notification policy layout for long strings 2022-04-29 17:38:48 +03:00
Grishka
a58a279e8c Client-side filtering is annoying 2022-04-29 17:25:32 +03:00
Grishka
0fe58e49b6 Display more relationship states + locked accounts
closes #85, #73, #30
2022-04-29 17:11:23 +03:00
Eugen Rochko
e4b187acd6 New translations short_description.txt (Thai) 2022-04-29 15:49:50 +02:00
Eugen Rochko
9a95944adb New translations strings.xml (Thai) 2022-04-29 15:49:49 +02:00
Eugen Rochko
21e441d683 New translations strings.xml (Thai) 2022-04-29 14:48:02 +02:00
Grishka
089e297656 fix #93 2022-04-29 15:17:29 +03:00
Eugen Rochko
93906ecf08 New translations strings.xml (Armenian) 2022-04-29 13:50:46 +02:00
Eugen Rochko
cdb836742e New translations short_description.txt (Japanese) 2022-04-29 13:50:44 +02:00
Eugen Rochko
80cff031d7 New translations full_description.txt (Japanese) 2022-04-29 13:50:43 +02:00
Eugen Rochko
cea17b22cb New translations strings.xml (Japanese) 2022-04-29 13:50:42 +02:00
Eugen Rochko
97bf165e9e New translations strings.xml (Japanese) 2022-04-29 12:51:33 +02:00
Eugen Rochko
36345582c7 New translations strings.xml (Vietnamese) 2022-04-29 11:51:19 +02:00
Eugen Rochko
940a4a9ce7 New translations strings.xml (Turkish) 2022-04-29 11:51:18 +02:00
Eugen Rochko
8362bca6bf New translations short_description.txt (Chinese Traditional) 2022-04-29 09:03:12 +02:00
Eugen Rochko
09ef005d0e New translations strings.xml (Chinese Traditional) 2022-04-29 09:03:11 +02:00
Eugen Rochko
5ec1ec26b7 New translations strings.xml (Chinese Traditional) 2022-04-29 07:49:14 +02:00
Eugen Rochko
3ee159a4a5 New translations full_description.txt (Chinese Traditional) 2022-04-29 06:30:57 +02:00
Eugen Rochko
084b0d3a0c New translations strings.xml (Chinese Traditional) 2022-04-29 06:30:56 +02:00
Eugen Rochko
b5692c1ddc New translations short_description.txt (Chinese Traditional) 2022-04-29 05:14:54 +02:00
Eugen Rochko
e986a7f023 New translations full_description.txt (Chinese Traditional) 2022-04-29 05:14:53 +02:00
Eugen Rochko
367843d12b New translations strings.xml (Russian) 2022-04-29 03:34:59 +02:00
Eugen Rochko
40186b0025 New translations full_description.txt (Arabic) 2022-04-29 03:34:51 +02:00
Grishka
2a65bdb08f Add strings for follow states 2022-04-29 03:45:41 +03:00
Grishka
93fbc52f6a More crash fixes 2022-04-29 03:37:18 +03:00
Eugen Rochko
6df0333d97 New translations full_description.txt (Arabic) 2022-04-29 02:22:53 +02:00
Grishka
4e4b5fcfe4 Probably a crash fix 2022-04-29 03:06:06 +03:00
Eugen Rochko
11363d6dea New translations title.txt (Arabic) 2022-04-29 01:21:47 +02:00
Eugen Rochko
3e5d369004 New translations short_description.txt (Arabic) 2022-04-29 01:21:46 +02:00
Eugen Rochko
b3fd81ce26 New translations full_description.txt (Arabic) 2022-04-29 01:21:45 +02:00
Eugen Rochko
68d4eae53f New translations title.txt (Thai) 2022-04-28 23:27:23 +02:00
Eugen Rochko
01e8a9026b New translations short_description.txt (Thai) 2022-04-28 23:27:23 +02:00
Eugen Rochko
b0039926e5 New translations full_description.txt (Thai) 2022-04-28 23:27:22 +02:00
Eugen Rochko
86ec53c4dc New translations strings.xml (Thai) 2022-04-28 23:27:21 +02:00
Eugen Rochko
b5d57998ae New translations title.txt (Finnish) 2022-04-28 23:27:20 +02:00
Eugen Rochko
1c77c6308e New translations short_description.txt (Finnish) 2022-04-28 23:27:19 +02:00
Eugen Rochko
acee26a573 New translations full_description.txt (Finnish) 2022-04-28 23:27:18 +02:00
Eugen Rochko
4a5f20c073 New translations strings.xml (Finnish) 2022-04-28 23:27:17 +02:00
Eugen Rochko
cebef82c83 New translations strings.xml (Turkish) 2022-04-28 23:27:16 +02:00
Grishka
620bc2285c Fix notification settings getting reset on app update 2022-04-28 23:32:43 +03:00
Grishka
f73849dbb7 Home timeline auto-refresh
close #32
2022-04-28 23:22:55 +03:00
Eugen Rochko
2c12e8bc2f New translations full_description.txt (Vietnamese) 2022-04-28 15:26:58 +02:00
Eugen Rochko
4e0a0a5065 New translations strings.xml (Vietnamese) 2022-04-28 15:26:56 +02:00
Eugen Rochko
d80f6a1c2c New translations strings.xml (Vietnamese) 2022-04-28 14:16:15 +02:00
Eugen Rochko
8081d5fa1a New translations strings.xml (Italian) 2022-04-28 12:29:33 +02:00
Eugen Rochko
d7c56b52ac New translations strings.xml (Galician) 2022-04-28 07:27:41 +02:00
Grishka
e8eb12532a Fixes 2022-04-28 01:35:20 +03:00
Eugen Rochko
e95d0c9914 New translations strings.xml (Kabyle) 2022-04-28 00:18:10 +02:00
Eugen Rochko
f1fd12639e New translations strings.xml (Catalan) 2022-04-28 00:18:09 +02:00
Eugen Rochko
3009d7e6fa New translations strings.xml (German) 2022-04-28 00:18:08 +02:00
Eugen Rochko
2438dfde2a New translations strings.xml (Basque) 2022-04-28 00:18:06 +02:00
Eugen Rochko
28e8332b67 New translations strings.xml (Polish) 2022-04-28 00:18:03 +02:00
Eugen Rochko
29bd34ab2b New translations strings.xml (Russian) 2022-04-28 00:18:01 +02:00
Eugen Rochko
6f2e8237de New translations strings.xml (Spanish) 2022-04-28 00:17:59 +02:00
Eugen Rochko
e2308fcb5d New translations strings.xml (Turkish) 2022-04-28 00:17:58 +02:00
Eugen Rochko
8054084537 New translations strings.xml (Portuguese, Brazilian) 2022-04-28 00:17:57 +02:00
Eugen Rochko
ec8f2dbdf4 New translations strings.xml (Bosnian) 2022-04-28 00:17:56 +02:00
Eugen Rochko
80bc1d8339 New translations strings.xml (Croatian) 2022-04-28 00:17:54 +02:00
Eugen Rochko
c1b28bde6b New translations strings.xml (Italian) 2022-04-28 00:17:53 +02:00
Eugen Rochko
7c3b5c4a15 New translations strings.xml (Galician) 2022-04-28 00:17:51 +02:00
Eugen Rochko
eac0fdbcbf New translations strings.xml (Vietnamese) 2022-04-28 00:17:50 +02:00
Eugen Rochko
1a129ad684 New translations strings.xml (Arabic) 2022-04-28 00:17:50 +02:00
Eugen Rochko
583758b231 New translations strings.xml (Chinese Simplified) 2022-04-28 00:17:48 +02:00
Eugen Rochko
254bc8c0ab New translations strings.xml (French) 2022-04-28 00:17:47 +02:00
Grishka
9a0c383da8 Change community -> server 2022-04-28 00:54:35 +03:00
Eugen Rochko
97843d5ca1 New translations strings.xml (Arabic) 2022-04-27 22:41:22 +02:00
Eugen Rochko
f2eac28006 New translations title.txt (Arabic) 2022-04-27 21:44:24 +02:00
Eugen Rochko
d0eae2d17f New translations strings.xml (Arabic) 2022-04-27 21:44:23 +02:00
Eugen Rochko
6c4d9a1d0f New translations short_description.txt (Arabic) 2022-04-27 19:55:54 +02:00
Eugen Rochko
fc9e38ea24 New translations full_description.txt (Arabic) 2022-04-27 19:55:53 +02:00
Eugen Rochko
7c07d521f3 New translations strings.xml (Arabic) 2022-04-27 19:55:52 +02:00
Eugen Rochko
73d7c40cdd New translations strings.xml (Arabic) 2022-04-27 18:59:22 +02:00
Eugen Rochko
ba3871fc2d New translations strings.xml (Vietnamese) 2022-04-27 15:14:28 +02:00
Eugen Rochko
f1f14b765a New translations strings.xml (Arabic) 2022-04-27 13:34:07 +02:00
Eugen Rochko
034a4b501a New translations strings.xml (Arabic) 2022-04-27 00:29:03 +02:00
Eugen Rochko
2db10585d5 New translations strings.xml (Arabic) 2022-04-26 23:31:54 +02:00
Eugen Rochko
cb6bd4180b New translations strings.xml (Arabic) 2022-04-26 22:33:36 +02:00
Eugen Rochko
cd099fc17e New translations strings.xml (Arabic) 2022-04-26 21:30:07 +02:00
Eugen Rochko
8136a9af63 New translations strings.xml (Turkish) 2022-04-26 15:18:28 +02:00
Eugen Rochko
dd67d9d078 New translations strings.xml (German) 2022-04-26 13:41:03 +02:00
Eugen Rochko
3fcab4122c New translations strings.xml (Catalan) 2022-04-26 13:41:02 +02:00
Eugen Rochko
fac79bbeaa New translations strings.xml (Galician) 2022-04-26 12:39:28 +02:00
Eugen Rochko
c21061e0a7 New translations strings.xml (Galician) 2022-04-26 11:22:52 +02:00
Eugen Rochko
0f3421296d New translations strings.xml (Kabyle) 2022-04-26 08:31:40 +02:00
Eugen Rochko
c6a8bd96bc New translations strings.xml (Kabyle) 2022-04-26 06:44:29 +02:00
Eugen Rochko
9201760103 New translations strings.xml (Kabyle) 2022-04-26 05:34:01 +02:00
Eugen Rochko
f0c521ea95 New translations strings.xml (Kabyle) 2022-04-26 04:30:59 +02:00
Eugen Rochko
aceb89242e New translations strings.xml (Kabyle) 2022-04-26 03:17:29 +02:00
Eugen Rochko
4ff2f369f6 New translations short_description.txt (Kabyle) 2022-04-26 02:21:49 +02:00
Eugen Rochko
226ac8303c New translations full_description.txt (Kabyle) 2022-04-26 02:21:48 +02:00
Eugen Rochko
5601554051 New translations strings.xml (Kabyle) 2022-04-26 02:21:47 +02:00
Eugen Rochko
f06492de56 New translations title.txt (Kabyle) 2022-04-26 01:16:06 +02:00
Eugen Rochko
f70f2af973 New translations short_description.txt (Kabyle) 2022-04-26 01:16:05 +02:00
Eugen Rochko
321fc5aa25 New translations full_description.txt (Kabyle) 2022-04-26 01:16:04 +02:00
Eugen Rochko
178207026f New translations strings.xml (Kabyle) 2022-04-26 01:16:03 +02:00
Eugen Rochko
dcb96dafeb New translations title.txt (Armenian) 2022-04-26 01:16:02 +02:00
Eugen Rochko
6d807e967f New translations short_description.txt (Armenian) 2022-04-26 01:16:01 +02:00
Eugen Rochko
10df38d9b1 New translations full_description.txt (Armenian) 2022-04-26 01:16:00 +02:00
Eugen Rochko
69c0873c8f New translations strings.xml (Armenian) 2022-04-26 01:15:59 +02:00
Eugen Rochko
b04e328a53 New translations title.txt (Arabic) 2022-04-26 01:15:58 +02:00
Eugen Rochko
fb5afae720 New translations short_description.txt (Arabic) 2022-04-26 01:15:57 +02:00
Eugen Rochko
6875f40480 New translations full_description.txt (Arabic) 2022-04-26 01:15:57 +02:00
Eugen Rochko
1ed79d2355 New translations strings.xml (Arabic) 2022-04-26 01:15:56 +02:00
Eugen Rochko
287e5fc058 New translations strings.xml (Italian) 2022-04-26 01:15:55 +02:00
Grishka
ed79cebc57 Merge remote-tracking branch 'origin/l10n_master' 2022-04-26 00:58:50 +03:00
Grishka
8e65459cb5 bump version 2022-04-26 00:58:33 +03:00
Eugen Rochko
e32063fa09 New translations strings.xml (Vietnamese) 2022-04-25 22:26:35 +02:00
Eugen Rochko
d979389715 New translations strings.xml (Italian) 2022-04-25 22:26:19 +02:00
Eugen Rochko
d0f2af2913 New translations strings.xml (Galician) 2022-04-25 22:26:19 +02:00
Eugen Rochko
309ccc0a70 New translations strings.xml (Turkish) 2022-04-25 22:26:18 +02:00
Grishka
89c7a13c59 Add info banners in discover sub-tabs 2022-04-25 22:27:18 +03:00
Eugen Rochko
ea4d520e23 New translations strings.xml (Italian) 2022-04-25 21:24:49 +02:00
Eugen Rochko
d6a42d0d6b New translations strings.xml (Galician) 2022-04-25 18:35:42 +02:00
Eugen Rochko
025458ce8c New translations strings.xml (Chinese Simplified) 2022-04-25 06:31:05 +02:00
Eugen Rochko
650822f3b3 New translations short_description.txt (Chinese Simplified) 2022-04-25 05:20:18 +02:00
Eugen Rochko
0422a8c590 New translations full_description.txt (Chinese Simplified) 2022-04-25 05:20:17 +02:00
Eugen Rochko
b640f6c68d New translations strings.xml (Chinese Simplified) 2022-04-25 05:20:16 +02:00
Eugen Rochko
a4b89b8a52 New translations strings.xml (Turkish) 2022-04-24 19:39:46 +02:00
Grishka
3c9670bbaa Fix notification background 2022-04-24 19:33:57 +03:00
Eugen Rochko
22d27b13e7 New translations strings.xml (Vietnamese) 2022-04-24 15:37:22 +02:00
Eugen Rochko
233f87d90b New translations strings.xml (Portuguese, Brazilian) 2022-04-24 15:37:20 +02:00
Grishka
0dd5e5af8d Fix #71 2022-04-24 15:41:15 +03:00
Grishka
61d537779b Local timeline 2022-04-24 13:51:03 +03:00
Eugen Rochko
faa6ed336d New translations strings.xml (Italian) 2022-04-23 22:37:59 +02:00
Grishka
f499444a86 Fix pre-upload image resizing 2022-04-23 22:25:58 +03:00
Eugen Rochko
d6202d005f New translations strings.xml (Turkish) 2022-04-23 16:15:22 +02:00
Eugen Rochko
2df42396c3 New translations short_description.txt (Croatian) 2022-04-23 01:17:53 +02:00
Eugen Rochko
73002a8dbf New translations full_description.txt (Croatian) 2022-04-23 01:17:52 +02:00
Eugen Rochko
a50a14492f New translations strings.xml (Croatian) 2022-04-23 01:17:51 +02:00
Eugen Rochko
d896402d39 New translations full_description.txt (Croatian) 2022-04-23 00:13:04 +02:00
Eugen Rochko
571e593041 New translations full_description.txt (Croatian) 2022-04-22 22:45:29 +02:00
Eugen Rochko
b7a6d5313d New translations strings.xml (French) 2022-04-22 20:29:34 +02:00
Eugen Rochko
c6ea07e43e New translations full_description.txt (Croatian) 2022-04-22 19:27:27 +02:00
Eugen Rochko
1d33f476d6 New translations full_description.txt (Polish) 2022-04-22 19:27:26 +02:00
Eugen Rochko
9dca538f96 New translations strings.xml (Polish) 2022-04-22 19:27:25 +02:00
Eugen Rochko
823a6d7905 New translations full_description.txt (Croatian) 2022-04-22 18:31:47 +02:00
Eugen Rochko
4b12adc0f8 New translations strings.xml (Polish) 2022-04-22 18:31:46 +02:00
Eugen Rochko
1720665212 New translations strings.xml (Galician) 2022-04-22 16:28:34 +02:00
Eugen Rochko
83e6bb2ced New translations strings.xml (Turkish) 2022-04-22 15:31:26 +02:00
Eugen Rochko
78342fbe74 New translations strings.xml (German) 2022-04-22 13:24:00 +02:00
Eugen Rochko
0cb502b244 New translations full_description.txt (Croatian) 2022-04-22 13:23:48 +02:00
Grishka
559835d849 Crash fixes 2022-04-22 14:15:52 +03:00
Eugen Rochko
fd4854adae New translations strings.xml (Russian) 2022-04-22 11:59:26 +02:00
Eugen Rochko
2b7d9e5536 New translations strings.xml (Portuguese) 2022-04-22 11:59:25 +02:00
Eugen Rochko
15d73ab9a2 New translations strings.xml (Portuguese) 2022-04-22 11:03:22 +02:00
Eugen Rochko
05a7dd9636 New translations strings.xml (Vietnamese) 2022-04-22 05:27:52 +02:00
Eugen Rochko
899e016594 New translations strings.xml (Vietnamese) 2022-04-22 04:30:12 +02:00
Eugen Rochko
2b752be6d0 New translations strings.xml (Croatian) 2022-04-22 02:50:55 +02:00
Eugen Rochko
0dd42a07dc New translations full_description.txt (French) 2022-04-22 02:50:54 +02:00
Eugen Rochko
9e527d9ab1 New translations strings.xml (French) 2022-04-22 02:50:53 +02:00
Eugen Rochko
3ee0782a61 New translations strings.xml (Croatian) 2022-04-22 01:35:49 +02:00
Eugen Rochko
2721f43e23 New translations full_description.txt (French) 2022-04-22 01:35:48 +02:00
Eugen Rochko
622b76e27e New translations strings.xml (French) 2022-04-22 01:35:47 +02:00
Eugen Rochko
805b2120d6 New translations strings.xml (Croatian) 2022-04-22 00:39:41 +02:00
Eugen Rochko
161be10c4b New translations strings.xml (Croatian) 2022-04-21 23:39:09 +02:00
Eugen Rochko
95be6ece9d New translations strings.xml (Occitan) 2022-04-21 23:39:08 +02:00
Eugen Rochko
1e73ffdba8 New translations strings.xml (French) 2022-04-21 23:39:07 +02:00
Eugen Rochko
7b07fe0ead New translations short_description.txt (French) 2022-04-21 22:43:29 +02:00
Eugen Rochko
64c91c7df6 New translations full_description.txt (French) 2022-04-21 22:43:28 +02:00
Eugen Rochko
1a36440c9a New translations strings.xml (French) 2022-04-21 22:43:27 +02:00
Eugen Rochko
b3b9d848c9 New translations full_description.txt (Turkish) 2022-04-21 21:24:33 +02:00
Eugen Rochko
acb898ae1f New translations strings.xml (Turkish) 2022-04-21 21:24:32 +02:00
Eugen Rochko
2fd3240aca New translations full_description.txt (Galician) 2022-04-21 20:28:43 +02:00
Eugen Rochko
12945a9da2 New translations short_description.txt (Turkish) 2022-04-21 20:28:42 +02:00
Eugen Rochko
2e3fc22185 New translations full_description.txt (Turkish) 2022-04-21 20:28:41 +02:00
Eugen Rochko
9925c3ac75 New translations strings.xml (Turkish) 2022-04-21 20:28:40 +02:00
Eugen Rochko
30d767441f New translations full_description.txt (Galician) 2022-04-21 19:15:06 +02:00
Eugen Rochko
3df196181f New translations strings.xml (Basque) 2022-04-21 19:15:05 +02:00
Eugen Rochko
1ba447db33 New translations strings.xml (French) 2022-04-21 19:15:03 +02:00
Eugen Rochko
8b8fcfcd8c New translations strings.xml (French) 2022-04-21 18:18:39 +02:00
Eugen Rochko
6a1e7ef866 New translations strings.xml (Vietnamese) 2022-04-21 16:54:57 +02:00
Grishka
ed8128fb72 Bump version 2022-04-21 17:32:15 +03:00
Grishka
b71e57a07e Fix layout 2022-04-21 17:06:56 +03:00
Grishka
51405b2bae Merge remote-tracking branch 'origin/l10n_master'
# Conflicts:
#	mastodon/src/main/res/values-de-rDE/strings.xml
#	mastodon/src/main/res/values-tr-rTR/strings.xml
2022-04-21 17:03:15 +03:00
Grishka
3bb4f80dc6 Crash fix 2022-04-21 16:52:25 +03:00
Eugen Rochko
daac3c5b52 New translations short_description.txt (Vietnamese) 2022-04-21 15:25:12 +02:00
Eugen Rochko
47f449000a New translations full_description.txt (Vietnamese) 2022-04-21 15:25:11 +02:00
Eugen Rochko
65d1491df3 New translations strings.xml (Vietnamese) 2022-04-21 15:25:10 +02:00
Eugen Rochko
14b9d91645 New translations strings.xml (Vietnamese) 2022-04-21 14:11:39 +02:00
Eugen Rochko
9367eca556 New translations title.txt (Vietnamese) 2022-04-21 12:30:18 +02:00
Eugen Rochko
2dee95b688 New translations short_description.txt (Vietnamese) 2022-04-21 12:30:17 +02:00
Eugen Rochko
303c729a27 New translations full_description.txt (Vietnamese) 2022-04-21 12:30:16 +02:00
Eugen Rochko
657761f432 New translations strings.xml (Vietnamese) 2022-04-21 12:30:15 +02:00
Eugen Rochko
60ac581838 New translations strings.xml (Galician) 2022-04-21 12:30:14 +02:00
Eugen Rochko
55e704ca15 New translations strings.xml (Galician) 2022-04-21 11:24:25 +02:00
Eugen Rochko
57dcab65f1 New translations short_description.txt (Italian) 2022-04-21 10:22:19 +02:00
Eugen Rochko
753dec4445 New translations full_description.txt (Italian) 2022-04-21 10:22:18 +02:00
Eugen Rochko
d0e6646cca New translations strings.xml (Italian) 2022-04-21 10:22:16 +02:00
Eugen Rochko
59b5bbb60e New translations short_description.txt (Galician) 2022-04-21 09:00:43 +02:00
Eugen Rochko
3c19b5754b New translations full_description.txt (Galician) 2022-04-21 09:00:42 +02:00
Eugen Rochko
8cddeef1fe New translations strings.xml (Galician) 2022-04-21 09:00:41 +02:00
Eugen Rochko
53b3943f10 New translations strings.xml (Italian) 2022-04-21 09:00:39 +02:00
Eugen Rochko
f63c71dd8f New translations title.txt (Galician) 2022-04-21 07:51:59 +02:00
Eugen Rochko
2274ac642d New translations short_description.txt (Galician) 2022-04-21 07:51:58 +02:00
Eugen Rochko
e56cecd267 New translations full_description.txt (Galician) 2022-04-21 07:51:57 +02:00
Eugen Rochko
590f6ac797 New translations strings.xml (Galician) 2022-04-21 07:51:56 +02:00
Eugen Rochko
24f8a1014c New translations title.txt (Italian) 2022-04-21 07:51:55 +02:00
Eugen Rochko
a9f2f49876 New translations short_description.txt (Italian) 2022-04-21 07:51:54 +02:00
Eugen Rochko
4602b77d43 New translations full_description.txt (Italian) 2022-04-21 07:51:54 +02:00
Eugen Rochko
65b6022a1e New translations strings.xml (Italian) 2022-04-21 07:51:53 +02:00
Eugen Rochko
3fa77ad412 New translations strings.xml (German) 2022-04-21 07:51:50 +02:00
Eugen Rochko
6b0a17ef2c New translations strings.xml (Ukrainian) 2022-04-21 04:15:57 +02:00
Eugen Rochko
e5a72b9699 New translations title.txt (Ukrainian) 2022-04-21 03:15:13 +02:00
Eugen Rochko
50e32862be New translations short_description.txt (Ukrainian) 2022-04-21 03:15:12 +02:00
Eugen Rochko
d58d07fd01 New translations full_description.txt (Ukrainian) 2022-04-21 03:15:11 +02:00
Eugen Rochko
7fabb924b9 New translations strings.xml (Ukrainian) 2022-04-21 03:15:10 +02:00
Eugen Rochko
02d9f743c1 New translations title.txt (Croatian) 2022-04-21 01:06:06 +02:00
Eugen Rochko
8af8214d4c New translations short_description.txt (Croatian) 2022-04-21 01:06:05 +02:00
Eugen Rochko
70abeddcc6 New translations full_description.txt (Croatian) 2022-04-21 01:06:04 +02:00
Eugen Rochko
c023f46172 New translations strings.xml (Croatian) 2022-04-21 01:06:03 +02:00
Eugen Rochko
142c6be4eb New translations short_description.txt (Basque) 2022-04-21 01:06:02 +02:00
Eugen Rochko
da3cb055c6 New translations full_description.txt (Basque) 2022-04-21 01:06:01 +02:00
Eugen Rochko
1fa2fbd9ba New translations strings.xml (Basque) 2022-04-21 01:06:00 +02:00
Eugen Rochko
c79e85eda5 New translations strings.xml (French) 2022-04-21 01:05:59 +02:00
Eugen Rochko
ea2474e5f8 New translations short_description.txt (Polish) 2022-04-20 23:42:16 +02:00
Eugen Rochko
c0b2e3a9fc New translations full_description.txt (Polish) 2022-04-20 23:42:15 +02:00
Eugen Rochko
82c6d99bb5 New translations strings.xml (Bosnian) 2022-04-20 23:42:14 +02:00
Eugen Rochko
1412432d32 New translations strings.xml (Polish) 2022-04-20 23:42:13 +02:00
Eugen Rochko
8e314146f2 New translations short_description.txt (Bosnian) 2022-04-20 22:46:11 +02:00
Eugen Rochko
9695a5a4de New translations short_description.txt (German) 2022-04-20 22:46:10 +02:00
Eugen Rochko
31d4ab18e2 New translations full_description.txt (Bosnian) 2022-04-20 22:46:09 +02:00
Eugen Rochko
005c07875d New translations full_description.txt (German) 2022-04-20 22:46:08 +02:00
Eugen Rochko
eb167bd217 New translations strings.xml (Bosnian) 2022-04-20 22:46:07 +02:00
Eugen Rochko
fc86407eac New translations strings.xml (French) 2022-04-20 22:46:05 +02:00
Eugen Rochko
1ec97650b0 New translations full_description.txt (German) 2022-04-20 21:50:25 +02:00
Eugen Rochko
dffbdfccc2 New translations full_description.txt (Catalan) 2022-04-20 21:50:24 +02:00
Eugen Rochko
111b499b04 New translations full_description.txt (Spanish) 2022-04-20 21:50:23 +02:00
Eugen Rochko
b71bbddf35 New translations strings.xml (Bosnian) 2022-04-20 21:50:21 +02:00
Eugen Rochko
d4cb5f82e1 New translations strings.xml (Russian) 2022-04-20 21:50:19 +02:00
Eugen Rochko
ab66dcaafb New translations strings.xml (Catalan) 2022-04-20 21:50:17 +02:00
Eugen Rochko
5e7bf73851 New translations short_description.txt (Catalan) 2022-04-20 21:50:15 +02:00
Eugen Rochko
066b131d95 New translations short_description.txt (Spanish) 2022-04-20 21:50:09 +02:00
Eugen Rochko
1caa17305e New translations short_description.txt (Russian) 2022-04-20 21:50:08 +02:00
Eugen Rochko
04f5f89cca New translations strings.xml (French) 2022-04-20 21:50:03 +02:00
Grishka
1ee55a643e Crash fixes 2022-04-20 22:38:20 +03:00
Eugen Rochko
1fa726178f New translations title.txt (Occitan) 2022-04-20 20:39:47 +02:00
Eugen Rochko
32f240188c New translations short_description.txt (Chinese Simplified) 2022-04-20 20:39:46 +02:00
Eugen Rochko
a6af059671 New translations short_description.txt (Chinese Traditional) 2022-04-20 20:39:45 +02:00
Eugen Rochko
007881f358 New translations short_description.txt (Portuguese, Brazilian) 2022-04-20 20:39:44 +02:00
Eugen Rochko
485f2821f2 New translations short_description.txt (Bosnian) 2022-04-20 20:39:43 +02:00
Eugen Rochko
2dcc622def New translations short_description.txt (Occitan) 2022-04-20 20:39:41 +02:00
Eugen Rochko
1bbfe4507e New translations title.txt (French) 2022-04-20 20:39:41 +02:00
Eugen Rochko
6d43dfd255 New translations title.txt (Spanish) 2022-04-20 20:39:40 +02:00
Eugen Rochko
c4f3b9f54d New translations title.txt (Catalan) 2022-04-20 20:39:39 +02:00
Eugen Rochko
0064e4d6f4 New translations title.txt (German) 2022-04-20 20:39:38 +02:00
Eugen Rochko
fe1e6d0c46 New translations title.txt (Greek) 2022-04-20 20:39:37 +02:00
Eugen Rochko
40bb3777d4 New translations title.txt (Basque) 2022-04-20 20:39:36 +02:00
Eugen Rochko
162343bee8 New translations short_description.txt (Turkish) 2022-04-20 20:39:35 +02:00
Eugen Rochko
733c18f6e2 New translations title.txt (Hebrew) 2022-04-20 20:39:34 +02:00
Eugen Rochko
10663b0a6f New translations title.txt (Korean) 2022-04-20 20:39:33 +02:00
Eugen Rochko
e8dca89de6 New translations title.txt (Polish) 2022-04-20 20:39:32 +02:00
Eugen Rochko
2923cf452f New translations title.txt (Portuguese) 2022-04-20 20:39:31 +02:00
Eugen Rochko
56b388700a New translations title.txt (Russian) 2022-04-20 20:39:30 +02:00
Eugen Rochko
0ec9632606 New translations title.txt (Swedish) 2022-04-20 20:39:29 +02:00
Eugen Rochko
680e729541 New translations title.txt (Turkish) 2022-04-20 20:39:28 +02:00
Eugen Rochko
f8ba72c242 New translations title.txt (Chinese Simplified) 2022-04-20 20:39:27 +02:00
Eugen Rochko
fcfad690de New translations title.txt (Chinese Traditional) 2022-04-20 20:39:26 +02:00
Eugen Rochko
1734501144 New translations title.txt (Portuguese, Brazilian) 2022-04-20 20:39:25 +02:00
Eugen Rochko
0904b82f4d New translations title.txt (Bosnian) 2022-04-20 20:39:25 +02:00
Eugen Rochko
8058a1fa56 New translations title.txt (Japanese) 2022-04-20 20:39:23 +02:00
Eugen Rochko
1eafdc9ed1 New translations short_description.txt (Swedish) 2022-04-20 20:39:23 +02:00
Eugen Rochko
65077dc7cc New translations short_description.txt (Russian) 2022-04-20 20:39:22 +02:00
Eugen Rochko
2764de76a5 New translations full_description.txt (Greek) 2022-04-20 20:39:21 +02:00
Eugen Rochko
4545c7ae0b New translations full_description.txt (German) 2022-04-20 20:39:20 +02:00
Eugen Rochko
1d458168e9 New translations full_description.txt (Catalan) 2022-04-20 20:39:19 +02:00
Eugen Rochko
7c6fbe45fd New translations full_description.txt (Spanish) 2022-04-20 20:39:18 +02:00
Eugen Rochko
4b408612fc New translations full_description.txt (French) 2022-04-20 20:39:17 +02:00
Eugen Rochko
f017ab078f New translations strings.xml (Bosnian) 2022-04-20 20:39:16 +02:00
Eugen Rochko
6cb4292feb New translations full_description.txt (Basque) 2022-04-20 20:39:13 +02:00
Eugen Rochko
f509f1009f New translations strings.xml (Turkish) 2022-04-20 20:39:12 +02:00
Eugen Rochko
3d795a3474 New translations strings.xml (Russian) 2022-04-20 20:39:11 +02:00
Eugen Rochko
b4fce77971 New translations strings.xml (German) 2022-04-20 20:39:07 +02:00
Eugen Rochko
9f01164d18 New translations strings.xml (Catalan) 2022-04-20 20:39:06 +02:00
Eugen Rochko
85fd234f15 New translations strings.xml (Spanish) 2022-04-20 20:39:04 +02:00
Eugen Rochko
630e925c4c New translations full_description.txt (Hebrew) 2022-04-20 20:39:03 +02:00
Eugen Rochko
9063049d73 New translations full_description.txt (Korean) 2022-04-20 20:39:02 +02:00
Eugen Rochko
3d81084ef2 New translations short_description.txt (Portuguese) 2022-04-20 20:39:01 +02:00
Eugen Rochko
002efcf02f New translations short_description.txt (Polish) 2022-04-20 20:39:00 +02:00
Eugen Rochko
315955243a New translations short_description.txt (Korean) 2022-04-20 20:38:59 +02:00
Eugen Rochko
a191b38592 New translations short_description.txt (Japanese) 2022-04-20 20:38:58 +02:00
Eugen Rochko
1b1eeb76e1 New translations short_description.txt (Hebrew) 2022-04-20 20:38:57 +02:00
Eugen Rochko
c6740f8a7e New translations short_description.txt (Basque) 2022-04-20 20:38:56 +02:00
Eugen Rochko
4b475b3680 New translations short_description.txt (Greek) 2022-04-20 20:38:55 +02:00
Eugen Rochko
3027a18c69 New translations short_description.txt (German) 2022-04-20 20:38:54 +02:00
Eugen Rochko
862cf42827 New translations short_description.txt (Catalan) 2022-04-20 20:38:53 +02:00
Eugen Rochko
b795f2a3f5 New translations short_description.txt (Spanish) 2022-04-20 20:38:53 +02:00
Eugen Rochko
429c543fd3 New translations full_description.txt (Japanese) 2022-04-20 20:38:52 +02:00
Eugen Rochko
0eeeb7a4c8 New translations short_description.txt (French) 2022-04-20 20:38:51 +02:00
Eugen Rochko
09872ab1a9 New translations full_description.txt (Bosnian) 2022-04-20 20:38:50 +02:00
Eugen Rochko
9b02b8aa6a New translations full_description.txt (Portuguese, Brazilian) 2022-04-20 20:38:49 +02:00
Eugen Rochko
d1807b886b New translations full_description.txt (Chinese Traditional) 2022-04-20 20:38:48 +02:00
Eugen Rochko
b5b4c1a760 New translations full_description.txt (Chinese Simplified) 2022-04-20 20:38:47 +02:00
Eugen Rochko
e692caac0c New translations full_description.txt (Turkish) 2022-04-20 20:38:46 +02:00
Eugen Rochko
d93cdec7a5 New translations full_description.txt (Swedish) 2022-04-20 20:38:45 +02:00
Eugen Rochko
a487eeb235 New translations full_description.txt (Russian) 2022-04-20 20:38:44 +02:00
Eugen Rochko
4b0c13c64d New translations full_description.txt (Portuguese) 2022-04-20 20:38:43 +02:00
Eugen Rochko
1f65236c11 New translations full_description.txt (Polish) 2022-04-20 20:38:42 +02:00
Eugen Rochko
ca4a6f83fb New translations full_description.txt (Occitan) 2022-04-20 20:38:41 +02:00
Grishka
5eb5a42465 Update crowdin.yml (please work this time) 2022-04-20 21:26:11 +03:00
Eugen Rochko
17474d63f7 New translations strings.xml (Occitan) 2022-04-20 20:18:54 +02:00
Eugen Rochko
5c5e0feb75 New translations strings.xml (German) 2022-04-20 20:18:53 +02:00
Eugen Rochko
b76783dc75 New translations strings.xml (Japanese) 2022-04-20 20:18:52 +02:00
Eugen Rochko
2f4568f59e New translations strings.xml (Portuguese) 2022-04-20 20:18:51 +02:00
Eugen Rochko
4862ad8584 New translations strings.xml (Russian) 2022-04-20 20:18:50 +02:00
Eugen Rochko
5caeb951b7 New translations strings.xml (Swedish) 2022-04-20 20:18:49 +02:00
Eugen Rochko
6fae00811e New translations strings.xml (Turkish) 2022-04-20 20:18:48 +02:00
Eugen Rochko
6e132ab8fb New translations strings.xml (Portuguese, Brazilian) 2022-04-20 20:18:47 +02:00
Eugen Rochko
95b5020e06 New translations strings.xml (Spanish) 2022-04-20 20:18:46 +02:00
Eugen Rochko
a4cd77b1df New translations strings.xml (Catalan) 2022-04-20 20:18:45 +02:00
Eugen Rochko
345a051fdd New translations strings.xml (Basque) 2022-04-20 20:18:44 +02:00
Eugen Rochko
840eea1980 New translations strings.xml (Hebrew) 2022-04-20 20:18:43 +02:00
Eugen Rochko
abb6d4bcc4 New translations strings.xml (Korean) 2022-04-20 20:18:42 +02:00
Eugen Rochko
b4329442b9 New translations strings.xml (Polish) 2022-04-20 20:18:41 +02:00
Eugen Rochko
293f3766c6 New translations strings.xml (Chinese Simplified) 2022-04-20 20:18:39 +02:00
Eugen Rochko
f6bd184a90 New translations strings.xml (Chinese Traditional) 2022-04-20 20:18:38 +02:00
Eugen Rochko
d9e89e58a0 New translations strings.xml (Bosnian) 2022-04-20 20:18:37 +02:00
Eugen Rochko
38d9483f06 New translations strings.xml (Greek) 2022-04-20 20:18:36 +02:00
Eugen Rochko
525c8fa8d6 New translations strings.xml (French) 2022-04-20 20:18:35 +02:00
Grishka
c9f54fb6cf Fix string 2022-04-20 21:18:04 +03:00
Grishka
6d2b391ba6 Remove string 2022-04-20 19:40:17 +03:00
Gregory K
6785885ebb Merge pull request #61 from mastodon/chore-crowdin-fastlane
Add fastlane metadata to Crowdin configuration
2022-04-20 19:30:11 +03:00
Grishka
f399f33a41 Fix Turkish and German strings 2022-04-20 19:29:47 +03:00
Eugen Rochko
9017cb42da Add fastlane metadata to Crowdin configuration 2022-04-20 18:20:58 +02:00
Gregory K
c274864419 Merge pull request #17 from EmirLogas/master
Add Turkish language
2022-04-20 19:12:42 +03:00
Gregory K
5c06b75a4d Merge pull request #13 from koyuawsmbrtn/master
Add German translation
2022-04-20 19:12:30 +03:00
Eugen Rochko
37f10f4db9 New translations strings.xml (Portuguese, Brazilian) 2022-04-20 18:01:59 +02:00
Eugen Rochko
77c2e8d6ac New translations strings.xml (Turkish) 2022-04-20 18:01:57 +02:00
Eugen Rochko
54476fabdc New translations strings.xml (Swedish) 2022-04-20 18:01:57 +02:00
Eugen Rochko
43ae3aa090 New translations strings.xml (Russian) 2022-04-20 18:01:54 +02:00
Eugen Rochko
fb050c5c20 New translations strings.xml (Portuguese) 2022-04-20 18:01:53 +02:00
Eugen Rochko
90fd49dbf7 New translations strings.xml (Japanese) 2022-04-20 18:01:51 +02:00
Eugen Rochko
2331710263 New translations strings.xml (German) 2022-04-20 18:01:50 +02:00
Eugen Rochko
287ddc388d New translations strings.xml (Spanish) 2022-04-20 18:01:49 +02:00
Eugen Rochko
ac8cbcdf39 New translations strings.xml (French) 2022-04-20 18:01:48 +02:00
Grishka
a294ad43d3 Add crowdin.yml 2022-04-20 18:57:48 +03:00
Grishka
07fe859e35 Fix compose autocomplete for emojis 2022-04-20 18:37:04 +03:00
Grishka
ad825d6803 Fix #43 2022-04-20 15:43:25 +03:00
Grishka
94c864c8ac Photo viewer & video player UI 2022-04-20 15:23:52 +03:00
Grishka
2e1f08a096 Accessibility improvements, close #39 2022-04-19 17:03:32 +03:00
Grishka
9276214433 Improve instance sorting 2022-04-19 15:51:23 +03:00
Grishka
957f3a2afd I'm dumb again 2022-04-19 07:58:38 +03:00
Grishka
b57df8e175 Use pattern-matching instanceof 2022-04-19 07:32:33 +03:00
Grishka
94b6189f36 Fix #34 2022-04-19 07:07:03 +03:00
Grishka
f267f68245 Crash fix 2022-04-19 07:03:35 +03:00
Grishka
5fff86a0c2 Update readme 2022-04-18 12:54:05 +03:00
Grishka
c6ff84ce10 Update gradle plugin, remove build tools version, set source to 17 2022-04-18 12:14:55 +03:00
Grishka
34ebd35fc0 Make instance catalog work if joinmastodon.org is unavailable 2022-04-15 17:12:12 +03:00
Grishka
19d0af01fd Show invite-only instances for login 2022-04-15 15:23:52 +03:00
Grishka
3928eee1df Onboarding/signup improvements, also fixes #14 2022-04-15 08:19:59 +03:00
Grishka
3a9be88ce6 Fix #18 2022-04-15 06:20:39 +03:00
Grishka
ced762e420 Open local statuses from links
Closes #10
2022-04-15 04:04:53 +03:00
Grishka
25a3135de8 Fixes 2022-04-15 03:53:30 +03:00
Grishka
886ae789f4 Renew FCM token on app update (#12) 2022-04-15 03:50:40 +03:00
Grishka
4566edcc4e Keep screen on during video playback (#7) 2022-04-15 02:14:03 +03:00
Emir
782ccf2633 Add Turkish language
Add Turkish language
2022-04-15 02:03:18 +03:00
Grishka
91ed70f8ba Fix #16 2022-04-15 01:24:28 +03:00
koyu
3715a400c3 Add German translation 2022-04-14 17:48:51 +02:00
Gregory K
801dc9c842 Merge pull request #6 from kyori19/fix-notification
Fix notification crashes caused by unknown notification type
2022-04-14 03:47:44 +03:00
kyori19
7f62470b9e Fix notification crashes caused by unknown notification type 2022-04-14 09:39:32 +09:00
Grishka
beb11bfb70 Add fastlane metadata (closes #2) 2022-04-13 23:23:22 +03:00
392 changed files with 23005 additions and 1325 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # mastodon
open_collective: # Replace with a single Open Collective username e.g., user1
ko_fi: xsk22
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username e.g., user1
issuehunt: # Replace with a single IssueHunt username e.g., user1
otechie: # Replace with a single Otechie username e.g., user1
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

135
README.md
View File

@@ -1,14 +1,133 @@
# Mastodon for Android
![Pink logo with pink shark](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
# Moshidon
This is the repository for the official Android app for Mastodon.
> A fork of [megalodon](https://github.com/sk22/megalodon) which is a fork of [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, bookmarks and an image description viewer.
**Warning! [The last version's integrated updater was broken](https://github.com/sk22/megalodon/issues/106) I already published a fixed version! If you're not updating through Izzy's F-Droid repository (more sources to come, hopefully!), you'll have to download the current release manually. Sorry about that!**
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=d92aad&label=download%20apk&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fsk22%2Fmastodon-android-fork%2Freleases%2Flatest&style=for-the-badge)](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
---
## 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”).**
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.
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).
### **Federated timeline**
**This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.**
Despite being one of the main features of federated social media, the Federated timeline wasnt included in the official Mastodon app supposedly, because this conflicts with Googles safety requirements for apps on the Play Store.
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!
### **Image description viewer**
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
This is important to **ensure the content youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
### **Pinning posts**
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in peoples profiles shows all the posts they pinned.**
On the Fediverse, its quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
### **Bookmarks**
**They allow for quickly saving posts and viewing them through the Bookmarks button on the top right of your profile.**
To bookmark a post, press the button between the Favorite and Share buttons on the bottom of the post. Bookmarks are saved privately, so the post authors wont know you saved their post the list of bookmarked posts is only visible to you.
## Installation
**Press the download button above 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.**
To install this app on your Android device, download the [latest release from GitHub](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk) and open it. You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/megalodon/releases) page.
Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)s automatic update checker. Megalodon will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
### Other sources
* **[Izzy's F-Droid repository](https://apt.izzysoft.de/fdroid/repo)**: https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk
---
## Release variants
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page.
**`megalodon.apk`**
Variant with an integrated updater. If you download Megalodon from here (and not from an app store), just download the regular `megalodon.apk`.
**`upstream-1234abc.apk`**
This is an **unmodified version** of the official [Mastodon for Android](https://github.com/mastodon/mastodon-android) app the respective Megalodon release is based on. Should you find any bugs in Megalodon (which you will), try to see if it occurs with this variant, too. The last 7 digits of the file name are important to know which version of the official app you're using.
<!-- **`megalodon-fdroid.apk`**
Variant without the integrated updater. This is the variant to be published to F-Droid.org where an integrated updater is not necessary. -->
---
## Detailed changes
### Features
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
* [Add “Federation” tab and change Discover tab order](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/add-federated-timeline) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/8))
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Implement deleting and re-drafting](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
* [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))
* [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)
* [List favorited posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/favs-list)
* [Accept/reject follow requests](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/follow-requests)
* [Display content warning title above text](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [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)
### Behavior
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
* [Always preserve content warnings when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
* [Display full image when adding image description](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [Set spoiler height independently to content height](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
### Visual
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
## Building
As this app is using Java 15 features, you need JDK 15 or newer to build it. Other than that, everything is pretty standard. You can either import the project into Android Studio and build it from there, or run the following command in the project directory:
As this app is using Java 17 features, you need JDK 17 or newer to build it. Other than that, everything is pretty standard. You can either import the project into Android Studio and build it from there, or run the following command in the project directory:
```
./gradlew assembleRelease
@@ -16,4 +135,8 @@ As this app is using Java 15 features, you need JDK 15 or newer to build it. Oth
## License
This project is released under the [GPL-3 License](./LICENSE).
This project is released under the [GPL-3 License](./LICENSE).
## Links
<a rel="me" href="https://floss.social/@megalodon">@megalodon<wbr>@floss.social</a>

2
_config.yml Normal file
View File

@@ -0,0 +1,2 @@
title: Megalodon
layout: default

17
_layouts/default.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Megalodon</title>
<link rel="icon" href="mastodon/src/main/res/mipmap-mdpi/ic_launcher_round.png">
<link rel="me" href="https://floss.social/@mastodon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css">
</head>
<body class="markdown-body">
<div style="margin: 0 auto; max-width: 45rem; padding: 2rem 1rem">
{{ content }}
</div>
</body>
</html>

View File

@@ -5,8 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.4"
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
classpath 'com.android.tools.build:gradle:7.3.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

5
crowdin.yml Normal file
View File

@@ -0,0 +1,5 @@
files:
- source: /mastodon/src/main/res/values/strings.xml
translation: /mastodon/src/main/res/values-%android_code%/strings.xml
- source: /fastlane/metadata/android/en-US/*.txt
translation: /fastlane/metadata/android/%locale%/%original_file_name%

View File

@@ -1,6 +1,6 @@
#Thu Jan 13 11:33:43 MSK 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

BIN
img/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

View File

@@ -1,53 +1,59 @@
plugins {
id 'com.android.application'
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
android {
compileSdk 31
buildToolsVersion "33.0.0-rc1"
compileSdk 33
defaultConfig {
applicationId "org.joinmastodon.android"
archivesBaseName = "moshidon"
applicationId "org.joinmastodon.android.moshinda"
minSdk 23
targetSdk 31
versionCode 27
versionName "1.0"
targetSdk 33
versionCode 56
versionName "1.1.4+fork.56.moshinda"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
"ja-rJP", "kab", "ko-rKR", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ru-rRU",
"sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
// minifyEnabled true
// shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug{
debuggable true
versionNameSuffix '-debug'
applicationIdSuffix '.debug'
}
appcenterPrivateBeta{
githubRelease{
initWith release
minifyEnabled false
shrinkResources false
versionNameSuffix "-priv-beta"
}
appcenterPublicBeta{
noFederatedRelease{
initWith release
versionNameSuffix "-beta"
versionNameSuffix '-nofederated'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_15
targetCompatibility JavaVersion.VERSION_15
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
sourceSets{
appcenterPrivateBeta{
setRoot "src/appcenter"
githubRelease{
setRoot "src/github"
}
appcenterPublicBeta{
setRoot "src/appcenter"
debug {
setRoot "src/github"
}
}
lintOptions{
checkReleaseBuilds false
abortOnError false
}
}
dependencies {
@@ -59,7 +65,7 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2'
implementation 'me.grishka.appkit:appkit:1.2.7'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'
@@ -68,12 +74,6 @@ dependencies {
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
def appCenterSdkVersion = "4.4.2"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'

View File

@@ -40,10 +40,9 @@
@com.squareup.otto.Subscribe <methods>;
}
-keep class com.microsoft.appcenter.** {
*;
}
-keepattributes LineNumberTable
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable
# Parceler library
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }

View File

@@ -1,23 +0,0 @@
package org.joinmastodon.android;
import android.app.Application;
import android.util.Log;
import com.microsoft.appcenter.AppCenter;
import com.microsoft.appcenter.crashes.Crashes;
import com.microsoft.appcenter.distribute.Distribute;
import com.microsoft.appcenter.distribute.UpdateTrack;
public class AppCenterWrapper{
private static final String TAG="AppCenterWrapper";
public static void init(Application app){
if(AppCenter.isConfigured())
return;
Log.i(TAG, "initializing AppCenter SDK, build type is "+BuildConfig.BUILD_TYPE);
if(BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta"))
Distribute.setUpdateTrack(UpdateTrack.PRIVATE);
AppCenter.start(app, BuildConfig.appCenterKey, Distribute.class, Crashes.class);
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.joinmastodon.android">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application>
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>-->
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<provider
android:authorities="${applicationId}.self_update_provider"
android:name=".updater.SelfUpdateContentProvider"
android:grantUriPermissions="true"
android:exported="false"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,356 @@
package org.joinmastodon.android.updater;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInstaller;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import java.io.File;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.Keep;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@Keep
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
private static final long CHECK_PERIOD=6*3600*1000L;
private static final String TAG="GithubSelfUpdater";
private UpdateState state=UpdateState.NO_UPDATE;
private UpdateInfo info;
private long downloadID;
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent){
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
MastodonApp.context.unregisterReceiver(this);
setState(UpdateState.DOWNLOADED);
}
}
};
public GithubSelfUpdaterImpl(){
SharedPreferences prefs=getPrefs();
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
info=new UpdateInfo();
info.version=prefs.getString("version", null);
info.size=prefs.getLong("apkSize", 0);
downloadID=prefs.getLong("downloadID", 0);
if(downloadID==0 || !getUpdateApkFile().exists()){
state=UpdateState.UPDATE_AVAILABLE;
}else{
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
if(state==UpdateState.DOWNLOADING){
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
// We are in a new version, running for the first time after update. Gotta clean things up.
long id=getPrefs().getLong("downloadID", 0);
if(id!=0){
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
}
getUpdateApkFile().delete();
getPrefs().edit()
.remove("apkSize")
.remove("version")
.remove("apkURL")
.remove("checkedByBuild")
.remove("downloadID")
.apply();
}
}
private SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
}
@Override
public void maybeCheckForUpdates(){
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
return;
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
if(timeSinceLastCheck>=CHECK_PERIOD){
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
}
@Override
public void checkForUpdates() {
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
private void actuallyCheckForUpdates(){
Request req=new Request.Builder()
.url("https://api.github.com/repos/LucasGGamerM/moshidon/releases/latest")
.build();
Call call=MastodonAPIController.getHttpClient().newCall(req);
try(Response resp=call.execute()){
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
String tag=obj.get("tag_name").getAsString();
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
return;
}
int newMajor=Integer.parseInt(matcher.group(1)),
newMinor=Integer.parseInt(matcher.group(2)),
newRevision=Integer.parseInt(matcher.group(3)),
newForkNumber=Integer.parseInt(matcher.group(4));
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
return;
}
int curMajor=Integer.parseInt(matcher.group(1)),
curMinor=Integer.parseInt(matcher.group(2)),
curRevision=Integer.parseInt(matcher.group(3)),
curForkNumber=Integer.parseInt(matcher.group(4));
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
if(newVersion>curVersion || newForkNumber>curForkNumber){
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
if("moshidon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
long size=asset.get("size").getAsLong();
String url=asset.get("browser_download_url").getAsString();
UpdateInfo info=new UpdateInfo();
info.size=size;
info.version=version;
this.info=info;
getPrefs().edit()
.putLong("apkSize", size)
.putString("version", version)
.putString("apkURL", url)
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
.remove("downloadID")
.apply();
break;
}
}
}
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
}catch(Exception x){
Log.w(TAG, "actuallyCheckForUpdates", x);
}finally{
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
}
}
private void setState(UpdateState state){
this.state=state;
E.post(new SelfUpdateStateChangedEvent(state));
}
@Override
public UpdateState getState(){
return state;
}
@Override
public UpdateInfo getUpdateInfo(){
return info;
}
public File getUpdateApkFile(){
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
}
@Override
public void downloadUpdate(){
if(state==UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
downloadID=dm.enqueue(
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
);
getPrefs().edit().putLong("downloadID", downloadID).apply();
setState(UpdateState.DOWNLOADING);
}
@Override
public void installUpdate(Activity activity){
if(state!=UpdateState.DOWNLOADED)
throw new IllegalStateException();
Uri uri;
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}else{
uri=Uri.fromFile(getUpdateApkFile());
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
activity.startActivity(intent);
// TODO figure out how to restart the app when updating via this new API
/*
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
try{
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
@Override
public void onCreated(int i){
}
@Override
public void onBadgingChanged(int i){
}
@Override
public void onActiveChanged(int i, boolean b){
}
@Override
public void onProgressChanged(int id, float progress){
}
@Override
public void onFinished(int id, boolean success){
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
});
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
PackageInstaller.Session session=installer.openSession(sid);
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
byte[] buffer=new byte[16384];
int read;
while((read=in.read(buffer))>0){
out.write(buffer, 0, read);
}
}
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
session.commit(intent.getIntentSender());
}catch(IOException x){
Log.w(TAG, "installUpdate", x);
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
}
*/
}
@Override
public float getDownloadProgress(){
if(state!=UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
if(cursor.moveToFirst()){
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
return total>0 ? (float)loaded/total : 0f;
}
}
return 0;
}
@Override
public void cancelDownload(){
if(state!=UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
dm.remove(downloadID);
downloadID=0;
getPrefs().edit().remove("downloadID").apply();
setState(UpdateState.UPDATE_AVAILABLE);
}
@Override
public void handleIntentFromInstaller(Intent intent, Activity activity){
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
activity.startActivity(confirmIntent);
}else if(status!=PackageInstaller.STATUS_SUCCESS){
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
}
}
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}else if(status!=PackageInstaller.STATUS_SUCCESS){
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
}
}
}
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
Intent restartIntent=new Intent(context, MainActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setPackage(context.getPackageName());
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
context.startActivity(restartIntent);
}else{
// Bypass activity starting restrictions by starting it from a notification
NotificationManager nm=context.getSystemService(NotificationManager.class);
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
nm.createNotificationChannel(chan);
Notification n=new Notification.Builder(context, "selfUpdateRestart")
.setContentTitle(context.getString(R.string.update_installed))
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
.setSmallIcon(R.drawable.ic_ntf_logo)
.build();
nm.notify(1, n);
}
}
}
}*/
}

View File

@@ -0,0 +1,62 @@
package org.joinmastodon.android.updater;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.FileNotFoundException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SelfUpdateContentProvider extends ContentProvider{
@Override
public boolean onCreate(){
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri){
if(isCorrectUri(uri))
return "application/vnd.android.package-archive";
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
return 0;
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
if(isCorrectUri(uri)){
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException();
}
private boolean isCorrectUri(Uri uri){
return "/update.apk".equals(uri.getPath());
}
}

View File

@@ -4,8 +4,10 @@
<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="${applicationId}.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
@@ -14,9 +16,10 @@
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:theme="@style/Theme.Mastodon.AutoLightDark.Original"
android:largeHeap="true">
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
@@ -30,7 +33,7 @@
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="mastodon-android-auth" android:host="callback"/>
<data android:scheme="megalodon-android-auth" android:host="callback"/>
</intent-filter>
</activity>
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -7,7 +7,14 @@ public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
public static boolean trueBlackTheme;
public static boolean showReplies;
public static boolean showBoosts;
public static boolean loadNewPosts;
public static boolean showInteractionCounts;
public static boolean alwaysExpandContentWarnings;
public static boolean disableMarquee;
public static ThemePreference theme;
public static ColorPreference color;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
@@ -18,21 +25,45 @@ public class GlobalUserPreferences{
playGifs=prefs.getBoolean("playGifs", true);
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
color=ColorPreference.values()[prefs.getInt("color", 1)];
}
public static void save(){
getPrefs().edit()
.putBoolean("playGifs", playGifs)
.putBoolean("useCustomTabs", useCustomTabs)
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)
.putBoolean("loadNewPosts", loadNewPosts)
.putBoolean("trueBlackTheme", trueBlackTheme)
.putBoolean("showInteractionCounts", showInteractionCounts)
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
.putBoolean("disableMarquee", disableMarquee)
.putInt("theme", theme.ordinal())
.putInt("color", color.ordinal())
.apply();
}
public enum ColorPreference{
PINK,
PURPLE,
GREEN,
BLUE,
ORANGE,
YELLOW
}
public enum ThemePreference{
AUTO,
LIGHT,
DARK
}
}

View File

@@ -1,14 +1,17 @@
package org.joinmastodon.android;
import android.app.Application;
import android.Manifest;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.SplashFragment;
@@ -16,10 +19,9 @@ import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.lang.reflect.InvocationTargetException;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
@@ -56,15 +58,16 @@ public class MainActivity extends FragmentStackActivity{
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else{
maybeRequestNotificationsPermission();
}
}
}
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
// Call the appcenter SDK wrapper through reflection because it is only present in beta builds
try{
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
}
}
@@ -91,7 +94,11 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
}
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
}*/
}
private void showFragmentForNotification(Notification notification, String accountID){
@@ -115,4 +122,21 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragment(fragment);
}
private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
return;
ComposeFragment compose=new ComposeFragment();
Bundle composeArgs=new Bundle();
composeArgs.putString("account", session.getID());
compose.setArguments(composeArgs);
showFragment(compose);
}
private void maybeRequestNotificationsPermission(){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
}
}
}

View File

@@ -6,8 +6,6 @@ import android.content.Context;
import org.joinmastodon.android.api.PushSubscriptionManager;
import java.lang.reflect.InvocationTargetException;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.NetworkUtils;
import me.grishka.appkit.utils.V;

View File

@@ -18,6 +18,7 @@ import android.util.Log;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.PushNotification;
@@ -52,10 +53,23 @@ public class PushNotificationReceiver extends BroadcastReceiver{
String k=intent.getStringExtra("k");
String p=intent.getStringExtra("p");
String s=intent.getStringExtra("s");
String accountID=intent.getStringExtra("x");
if(!TextUtils.isEmpty(accountID) && !TextUtils.isEmpty(k) && !TextUtils.isEmpty(p) && !TextUtils.isEmpty(s)){
String pushAccountID=intent.getStringExtra("x");
if(!TextUtils.isEmpty(pushAccountID) && !TextUtils.isEmpty(k) && !TextUtils.isEmpty(p) && !TextUtils.isEmpty(s)){
MastodonAPIController.runInBackground(()->{
try{
List<AccountSession> accounts=AccountSessionManager.getInstance().getLoggedInAccounts();
AccountSession account=null;
for(AccountSession acc:accounts){
if(pushAccountID.equals(acc.pushAccountID)){
account=acc;
break;
}
}
if(account==null){
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
return;
}
String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
new GetNotificationByID(pn.notificationId+"")
.setCallback(new Callback<>(){
@@ -124,7 +138,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setContentText(pn.body)
.setStyle(new Notification.BigTextStyle().bigText(pn.body))
.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_CANCEL_CURRENT))
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
.setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL)

View File

@@ -0,0 +1,39 @@
package org.joinmastodon.android.api;
import android.graphics.Rect;
import android.net.Uri;
import java.io.IOException;
public class AvatarResizedImageRequestBody extends ResizedImageRequestBody{
public AvatarResizedImageRequestBody(Uri uri, ProgressListener progressListener) throws IOException{
super(uri, 0, progressListener);
}
@Override
protected int[] getTargetSize(int srcWidth, int srcHeight){
float factor=400f/Math.min(srcWidth, srcHeight);
return new int[]{Math.round(srcWidth*factor), Math.round(srcHeight*factor)};
}
@Override
protected boolean needResize(int srcWidth, int srcHeight){
return srcHeight>400 || srcWidth!=srcHeight;
}
@Override
protected boolean needCrop(int srcWidth, int srcHeight){
return srcWidth!=srcHeight;
}
@Override
protected Rect getCropBounds(int srcWidth, int srcHeight){
Rect rect=new Rect();
if(srcWidth>srcHeight){
rect.set(srcWidth/2-srcHeight/2, 0, srcWidth/2-srcHeight/2+srcHeight, srcHeight);
}else{
rect.set(0, srcHeight/2-srcWidth/2, srcWidth, srcHeight/2-srcWidth/2+srcWidth);
}
return rect;
}
}

View File

@@ -14,11 +14,13 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.io.IOException;
import java.util.ArrayList;
@@ -33,7 +35,7 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=2;
private static final int DB_VERSION=3;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
@@ -41,6 +43,8 @@ public class CacheController{
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
private static final int POST_FLAG_GAP_AFTER=1;
static{
databaseThread.start();
}
@@ -49,14 +53,14 @@ public class CacheController{
this.accountID=accountID;
}
public void getHomeTimeline(String maxID, int count, boolean forceReload, Callback<PaginatedResponse<List<Status>>> callback){
public void getHomeTimeline(String maxID, int count, boolean forceReload, Callback<CacheablePaginatedResponse<List<Status>>> callback){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
@@ -65,33 +69,28 @@ public class CacheController{
do{
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
status.postprocess();
int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id;
for(Filter filter:filters){
if(filter.matches(status.getContentStatus().content))
if(filter.matches(status))
continue outer;
}
result.add(status);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
return;
}
}catch(IOException x){
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
}
}
new GetHomeTimeline(maxID, null, count)
new GetHomeTimeline(maxID, null, count, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(post->{
for(Filter filter:filters){
if(filter.matches(post.getContentStatus().content)){
return false;
}
}
return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null);
}
@@ -103,34 +102,39 @@ public class CacheController{
.exec(accountID);
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
}finally{
closeDelayed();
}
}, 0);
}
private void putHomeTimeline(List<Status> posts, boolean clear){
public void putHomeTimeline(List<Status> posts, boolean clear){
runOnDbThread((db)->{
if(clear)
db.delete("home_timeline", null, null);
ContentValues values=new ContentValues(2);
ContentValues values=new ContentValues(3);
for(Status s:posts){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
int flags=0;
if(s.hasGapAfter)
flags|=POST_FLAG_GAP_AFTER;
values.put("flags", flags);
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
}
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
@@ -142,7 +146,7 @@ public class CacheController{
newMaxID=ntf.id;
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status.getContentStatus().content))
if(filter.matches(ntf.status))
continue outer;
}
}
@@ -156,21 +160,21 @@ public class CacheController{
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
}
}
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status.getContentStatus().content)){
if(filter.matches(ntf.status)){
return false;
}
}
}
return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
putNotifications(result, onlyMentions, maxID==null);
putNotifications(result, onlyMentions, onlyPosts, maxID==null);
}
@Override
@@ -181,20 +185,23 @@ public class CacheController{
.exec(accountID);
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
}finally{
closeDelayed();
}
}, 0);
}
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean clear){
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean onlyPosts, boolean clear){
runOnDbThread((db)->{
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
db.delete(table, null, null);
ContentValues values=new ContentValues(3);
for(Notification n:notifications){
if(n.type==null){
continue;
}
values.put("id", n.id);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
@@ -227,6 +234,12 @@ public class CacheController{
});
}
public void deleteStatus(String id){
runOnDbThread((db)->{
db.delete("home_timeline", "`id`=?", new String[]{id});
});
}
public void clearRecentSearches(){
runOnDbThread((db)->db.delete("recent_searches", null, null));
}
@@ -305,6 +318,7 @@ public class CacheController{
`type` INTEGER NOT NULL
)""");
createRecentSearchesTable(db);
createPostsNotificationsTable(db);
}
@Override
@@ -312,6 +326,9 @@ public class CacheController{
if(oldVersion==1){
createRecentSearchesTable(db);
}
if(oldVersion==2){
createPostsNotificationsTable(db);
}
}
private void createRecentSearchesTable(SQLiteDatabase db){
@@ -322,6 +339,16 @@ public class CacheController{
`time` INTEGER NOT NULL
)""");
}
private void createPostsNotificationsTable(SQLiteDatabase db){
db.execSQL("""
CREATE TABLE `notifications_posts` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
)""");
}
}
@FunctionalInterface

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.api;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import java.io.IOException;
@@ -26,7 +27,10 @@ public class JsonObjectRequestBody extends RequestBody{
public void writeTo(BufferedSink sink) throws IOException{
try{
OutputStreamWriter writer=new OutputStreamWriter(sink.outputStream(), StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(obj, writer);
if(obj instanceof JsonElement)
writer.write(obj.toString());
else
MastodonAPIController.gson.toJson(obj, writer);
writer.flush();
}catch(JsonIOException x){
throw new IOException(x);

View File

@@ -1,14 +1,10 @@
package org.joinmastodon.android.api;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
@@ -16,11 +12,9 @@ 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 org.joinmastodon.android.model.BaseModel;
import java.io.IOException;
import java.io.Reader;
@@ -102,11 +96,11 @@ public class MastodonAPIController{
if(call.isCanceled())
return;
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed: "+e);
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
synchronized(req){
req.okhttpCall=null;
}
req.onError(e.getLocalizedMessage(), 0);
req.onError(e.getLocalizedMessage(), 0, e);
}
@Override
@@ -139,16 +133,16 @@ public class MastodonAPIController{
}catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code());
req.onError(x.getLocalizedMessage(), response.code(), x);
return;
}
try{
req.validateAndPostprocessResponse(respObj);
req.validateAndPostprocessResponse(respObj, response);
}catch(IOException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
req.onError(x.getLocalizedMessage(), response.code());
req.onError(x.getLocalizedMessage(), response.code(), x);
return;
}
@@ -161,7 +155,7 @@ public class MastodonAPIController{
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
if(error.has("details")){
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code());
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
JsonObject errorDetails=error.getAsJsonObject("details");
for(String key:errorDetails.keySet()){
@@ -178,12 +172,12 @@ public class MastodonAPIController{
err.detailedErrors=details;
req.onError(err);
}else{
req.onError(error.get("error").getAsString(), response.code());
req.onError(error.get("error").getAsString(), response.code(), null);
}
}catch(JsonIOException|JsonSyntaxException x){
req.onError(response.code()+" "+response.message(), response.code());
req.onError(response.code()+" "+response.message(), response.code(), x);
}catch(Exception x){
req.onError("Error parsing an API error", response.code());
req.onError("Error parsing an API error", response.code(), x);
}
}
}catch(Exception x){
@@ -195,7 +189,7 @@ public class MastodonAPIController{
}catch(Exception x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
req.onError(x.getLocalizedMessage(), 0);
req.onError(x.getLocalizedMessage(), 0, x);
}
}, 0);
}
@@ -203,4 +197,8 @@ public class MastodonAPIController{
public static void runInBackground(Runnable action){
thread.postRunnable(action, 0);
}
public static OkHttpClient getHttpClient(){
return httpClient;
}
}

View File

@@ -28,6 +28,7 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import okhttp3.Call;
import okhttp3.RequestBody;
import okhttp3.Response;
public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
private static final String TAG="MastodonAPIRequest";
@@ -75,9 +76,14 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
public MastodonAPIRequest<T> exec(String accountID){
account=AccountSessionManager.getInstance().getAccount(accountID);
domain=account.domain;
account.getApiController().submitRequest(this);
try{
account=AccountSessionManager.getInstance().getAccount(accountID);
domain=account.domain;
account.getApiController().submitRequest(this);
}catch(Exception x){
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1, x));
}
return this;
}
@@ -153,7 +159,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
@CallSuper
public void validateAndPostprocessResponse(T respObj) throws IOException{
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){
((BaseModel) respObj).postprocess();
}else if(respObj instanceof List){
@@ -185,15 +191,18 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
void onError(ErrorResponse err){
invokeErrorCallback(err);
if(!canceled)
invokeErrorCallback(err);
}
void onError(String msg, int httpStatus){
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
void onError(String msg, int httpStatus, Throwable exception){
if(!canceled)
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
}
void onSuccess(T resp){
invokeSuccessCallback(resp);
if(!canceled)
invokeSuccessCallback(resp);
}
@Override

View File

@@ -7,8 +7,8 @@ import java.util.Map;
public class MastodonDetailedErrorResponse extends MastodonErrorResponse{
public Map<String, List<FieldError>> detailedErrors;
public MastodonDetailedErrorResponse(String error, int httpStatus){
super(error, httpStatus);
public MastodonDetailedErrorResponse(String error, int httpStatus, Throwable exception){
super(error, httpStatus, exception);
}
public static class FieldError{

View File

@@ -12,10 +12,12 @@ import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{
public final String error;
public final int httpStatus;
public final Throwable underlyingException;
public MastodonErrorResponse(String error, int httpStatus){
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
this.error=error;
this.httpStatus=httpStatus;
this.underlyingException=exception;
}
@Override
@@ -26,6 +28,8 @@ public class MastodonErrorResponse extends ErrorResponse{
@Override
public void showToast(Context context){
if(context==null)
return;
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -96,10 +96,12 @@ public class PushSubscriptionManager{
public static void tryRegisterFCM(){
deviceToken=getPrefs().getString("deviceToken", null);
if(!TextUtils.isEmpty(deviceToken)){
registerAllAccountsForPush();
int tokenVersion=getPrefs().getInt("version", 0);
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE){
registerAllAccountsForPush(false);
return;
}
Log.i(TAG, "tryRegisterFCM: no token found or app was updated. Trying to get push token...");
Intent intent = new Intent("com.google.iid.TOKEN_REQUEST");
intent.setPackage(GSF_PACKAGE);
intent.putExtra(EXTRA_APPLICATION_PENDING_INTENT,
@@ -119,16 +121,12 @@ public class PushSubscriptionManager{
return !TextUtils.isEmpty(deviceToken);
}
public void registerAccountForPush(){
registerAccountForPush(null);
}
public void registerAccountForPush(PushSubscription subscription){
if(TextUtils.isEmpty(deviceToken))
throw new IllegalStateException("No device push token available");
MastodonAPIController.runInBackground(()->{
Log.d(TAG, "registerAccountForPush: started for "+accountID);
String encodedPublicKey, encodedAuthKey;
String encodedPublicKey, encodedAuthKey, pushAccountID;
try{
KeyPairGenerator generator=KeyPairGenerator.getInstance("EC");
ECGenParameterSpec spec=new ECGenParameterSpec(EC_CURVE_NAME);
@@ -138,11 +136,17 @@ public class PushSubscriptionManager{
privateKey=keyPair.getPrivate();
encodedPublicKey=Base64.encodeToString(serializeRawPublicKey(publicKey), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
authKey=new byte[16];
new SecureRandom().nextBytes(authKey);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
SecureRandom secureRandom=new SecureRandom();
secureRandom.nextBytes(authKey);
byte[] randomAccountID=new byte[16];
secureRandom.nextBytes(randomAccountID);
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null)
return;
session.pushPrivateKey=Base64.encodeToString(privateKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
session.pushPublicKey=Base64.encodeToString(publicKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
session.pushAuthKey=encodedAuthKey=Base64.encodeToString(authKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
session.pushAccountID=pushAccountID=Base64.encodeToString(randomAccountID, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
AccountSessionManager.getInstance().writeAccountsFile();
}catch(NoSuchAlgorithmException|InvalidAlgorithmParameterException e){
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
@@ -153,14 +157,16 @@ public class PushSubscriptionManager{
encodedAuthKey,
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
subscription==null ? PushSubscription.Policy.ALL : subscription.policy,
accountID)
pushAccountID)
.setCallback(new Callback<>(){
@Override
public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null)
return;
session.pushSubscription=result;
AccountSessionManager.getInstance().writeAccountsFile();
Log.d(TAG, "Successfully registered "+accountID+" for push notifications");
@@ -181,7 +187,9 @@ public class PushSubscriptionManager{
.setCallback(new Callback<>(){
@Override
public void onSuccess(PushSubscription result){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null)
return;
if(result.policy!=subscription.policy)
result.policy=subscription.policy;
session.pushSubscription=result;
@@ -194,7 +202,9 @@ public class PushSubscriptionManager{
if(((MastodonErrorResponse)error).httpStatus==404){ // Not registered for push, register now
registerAccountForPush(subscription);
}else{
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null)
return;
session.needUpdatePushSettings=true;
session.pushSubscription=subscription;
AccountSessionManager.getInstance().writeAccountsFile();
@@ -354,10 +364,12 @@ public class PushSubscriptionManager{
return info.toByteArray();
}
private static void registerAllAccountsForPush(){
private static void registerAllAccountsForPush(boolean forceReRegister){
if(!arePushNotificationsAvailable())
return;
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
if(session.pushSubscription==null)
session.getPushSubscriptionManager().registerAccountForPush();
if(session.pushSubscription==null || forceReRegister)
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
else if(session.needUpdatePushSettings)
session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription);
}
@@ -371,9 +383,9 @@ public class PushSubscriptionManager{
deviceToken=intent.getStringExtra("registration_id");
if(deviceToken.startsWith(KID_VALUE))
deviceToken=deviceToken.substring(KID_VALUE.length()+1);
getPrefs().edit().putString("deviceToken", deviceToken).apply();
getPrefs().edit().putString("deviceToken", deviceToken).putInt("version", BuildConfig.VERSION_CODE).apply();
Log.i(TAG, "Successfully registered for FCM");
registerAllAccountsForPush();
registerAllAccountsForPush(true);
}else{
Log.e(TAG, "FCM registration intent did not contain registration_id: "+intent);
Bundle extras=intent.getExtras();

View File

@@ -12,8 +12,10 @@ import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileOutputStream;
@@ -30,62 +32,107 @@ public class ResizedImageRequestBody extends CountingRequestBody{
private File tempFile;
private Uri uri;
private String contentType;
private int maxSize;
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
super(progressListener);
this.uri=uri;
contentType=MastodonApp.context.getContentResolver().getType(uri);
this.maxSize=maxSize;
BitmapFactory.Options opts=new BitmapFactory.Options();
opts.inJustDecodeBounds=true;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
if("file".equals(uri.getScheme())){
BitmapFactory.decodeFile(uri.getPath(), opts);
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
}else{
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
}
contentType=MastodonApp.context.getContentResolver().getType(uri);
}
if(opts.outWidth*opts.outHeight>maxSize){
if(TextUtils.isEmpty(contentType))
contentType="image/jpeg";
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
Bitmap bitmap;
if(Build.VERSION.SDK_INT>=29){
bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight())));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth())));
if(Build.VERSION.SDK_INT>=28){
ImageDecoder.Source source;
if("file".equals(uri.getScheme())){
source=ImageDecoder.createSource(new File(uri.getPath()));
}else{
source=ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri);
}
bitmap=ImageDecoder.decodeBitmap(source, (decoder, info, _source)->{
int[] size=getTargetSize(info.getSize().getWidth(), info.getSize().getHeight());
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
decoder.setTargetSize(targetWidth, targetHeight);
decoder.setTargetSize(size[0], size[1]);
// Breaks images in mysterious ways
// if(needCrop(size[0], size[1]))
// decoder.setCrop(getCropBounds(size[0], size[1]));
});
if(needCrop(bitmap.getWidth(), bitmap.getHeight())){
Rect crop=getCropBounds(bitmap.getWidth(), bitmap.getHeight());
bitmap=Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height());
}
}else{
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight)));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth)));
int[] size=getTargetSize(opts.outWidth, opts.outHeight);
int targetWidth=size[0];
int targetHeight=size[1];
float factor=opts.outWidth/(float)targetWidth;
opts=new BitmapFactory.Options();
opts.inSampleSize=(int)factor;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
bitmap=BitmapFactory.decodeStream(in, null, opts);
if("file".equals(uri.getScheme())){
bitmap=BitmapFactory.decodeFile(uri.getPath(), opts);
}else{
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
bitmap=BitmapFactory.decodeStream(in, null, opts);
}
}
if(factor%1f!=0f){
Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), new Paint(Paint.FILTER_BITMAP_FLAG));
boolean needCrop=needCrop(targetWidth, targetHeight);
if(factor%1f!=0f || needCrop){
Rect srcBounds=null;
Rect dstBounds;
if(needCrop){
Rect crop=getCropBounds(targetWidth, targetHeight);
dstBounds=new Rect(0, 0, crop.width(), crop.height());
srcBounds=new Rect(
Math.round(crop.left/(float)targetWidth*bitmap.getWidth()),
Math.round(crop.top/(float)targetHeight*bitmap.getHeight()),
Math.round(crop.right/(float)targetWidth*bitmap.getWidth()),
Math.round(crop.bottom/(float)targetHeight*bitmap.getHeight())
);
}else{
dstBounds=new Rect(0, 0, targetWidth, targetHeight);
}
Bitmap scaled=Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888);
new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG));
bitmap=scaled;
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
int rotation;
int orientation=0;
if("file".equals(uri.getScheme())){
ExifInterface exif=new ExifInterface(uri.getPath());
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
ExifInterface exif=new ExifInterface(in);
int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
rotation=switch(orientation){
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
default -> 0;
};
}
if(rotation!=0){
Matrix matrix=new Matrix();
matrix.setRotate(rotation);
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
}
}
int rotation=switch(orientation){
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
default -> 0;
};
if(rotation!=0){
Matrix matrix=new Matrix();
matrix.setRotate(rotation);
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
}
}
tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image");
boolean isPNG="image/png".equals(contentType);
tempFile=File.createTempFile("mastodon_tmp_resized", null);
try(FileOutputStream out=new FileOutputStream(tempFile)){
if("image/png".equals(contentType)){
if(isPNG){
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
}else{
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
@@ -94,9 +141,13 @@ public class ResizedImageRequestBody extends CountingRequestBody{
}
length=tempFile.length();
}else{
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst();
length=cursor.getInt(0);
if("file".equals(uri.getScheme())){
length=new File(uri.getPath()).length();
}else{
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst();
length=cursor.getInt(0);
}
}
}
}
@@ -125,4 +176,22 @@ public class ResizedImageRequestBody extends CountingRequestBody{
}
}
}
protected int[] getTargetSize(int srcWidth, int srcHeight){
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)srcWidth/srcHeight)));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)srcHeight/srcWidth)));
return new int[]{targetWidth, targetHeight};
}
protected boolean needResize(int srcWidth, int srcHeight){
return srcWidth*srcHeight>maxSize;
}
protected boolean needCrop(int srcWidth, int srcHeight){
return false;
}
protected Rect getCropBounds(int srcWidth, int srcHeight){
return null;
}
}

View File

@@ -4,6 +4,7 @@ import android.os.Looper;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
@@ -18,6 +19,7 @@ public class StatusInteractionController{
private final String accountID;
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){
this.accountID=accountID;
@@ -58,6 +60,7 @@ public class StatusInteractionController{
status.favouritesCount++;
else
status.favouritesCount--;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged){
@@ -95,5 +98,36 @@ public class StatusInteractionController{
status.reblogsCount++;
else
status.reblogsCount--;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setBookmarked(Status status, boolean bookmarked){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
if(current!=null){
current.cancel();
}
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
runningBookmarkRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
}

View File

@@ -25,10 +25,21 @@ public class IsoInstantTypeAdapter extends TypeAdapter<Instant>{
in.nextNull();
return null;
}
try{
return DateTimeFormatter.ISO_INSTANT.parse(in.nextString(), Instant::from);
}catch(DateTimeParseException x){
String nextString;
try {
nextString = in.nextString();
}catch(Exception e){
return null;
}
try{
return DateTimeFormatter.ISO_INSTANT.parse(nextString, Instant::from);
}catch(DateTimeParseException x){}
try{
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(nextString, Instant::from);
}catch(DateTimeParseException x){}
return null;
}
}

View File

@@ -0,0 +1,42 @@
package org.joinmastodon.android.api.gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
public class JsonArrayBuilder{
private JsonArray arr=new JsonArray();
public JsonArrayBuilder add(JsonElement el){
arr.add(el);
return this;
}
public JsonArrayBuilder add(String el){
arr.add(el);
return this;
}
public JsonArrayBuilder add(Number el){
arr.add(el);
return this;
}
public JsonArrayBuilder add(boolean el){
arr.add(el);
return this;
}
public JsonArrayBuilder add(JsonObjectBuilder el){
arr.add(el.build());
return this;
}
public JsonArrayBuilder add(JsonArrayBuilder el){
arr.add(el.build());
return this;
}
public JsonArray build(){
return arr;
}
}

View File

@@ -0,0 +1,42 @@
package org.joinmastodon.android.api.gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
public class JsonObjectBuilder{
private JsonObject obj=new JsonObject();
public JsonObjectBuilder add(String key, JsonElement el){
obj.add(key, el);
return this;
}
public JsonObjectBuilder add(String key, String el){
obj.addProperty(key, el);
return this;
}
public JsonObjectBuilder add(String key, Number el){
obj.addProperty(key, el);
return this;
}
public JsonObjectBuilder add(String key, boolean el){
obj.addProperty(key, el);
return this;
}
public JsonObjectBuilder add(String key, JsonObjectBuilder el){
obj.add(key, el.build());
return this;
}
public JsonObjectBuilder add(String key, JsonArrayBuilder el){
obj.add(key, el.build());
return this;
}
public JsonObject build(){
return obj;
}
}

View File

@@ -0,0 +1,57 @@
package org.joinmastodon.android.api.requests;
import android.net.Uri;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.HeaderPaginationList;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.Response;
public abstract class HeaderPaginationRequest<I> extends MastodonAPIRequest<HeaderPaginationList<I>>{
private static final Pattern LINK_HEADER_PATTERN=Pattern.compile("(?:(?:,\\s*)?<([^>]+)>|;\\s*(\\w+)=['\"](\\w+)['\"])");
public HeaderPaginationRequest(HttpMethod method, String path, Class<HeaderPaginationList<I>> respClass){
super(method, path, respClass);
}
public HeaderPaginationRequest(HttpMethod method, String path, TypeToken<HeaderPaginationList<I>> respTypeToken){
super(method, path, respTypeToken);
}
@Override
public void validateAndPostprocessResponse(HeaderPaginationList<I> respObj, Response httpResponse) throws IOException{
super.validateAndPostprocessResponse(respObj, httpResponse);
String link=httpResponse.header("Link");
if(!TextUtils.isEmpty(link)){
Matcher matcher=LINK_HEADER_PATTERN.matcher(link);
String url=null;
while(matcher.find()){
if(url==null){
String _url=matcher.group(1);
if(_url==null)
continue;
url=_url;
}else{
String paramName=matcher.group(2);
String paramValue=matcher.group(3);
if(paramName==null || paramValue==null)
return;
if("rel".equals(paramName)){
switch(paramValue){
case "next" -> respObj.nextPageUri=Uri.parse(url);
case "prev" -> respObj.prevPageUri=Uri.parse(url);
}
url=null;
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class AuthorizeFollowRequest extends MastodonAPIRequest<Relationship>{
public AuthorizeFollowRequest(String id){
super(HttpMethod.POST, "/follow_requests/"+id+"/authorize", Relationship.class);
setRequestBody(new Object());
}
}

View File

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

View File

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

View File

@@ -21,18 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true");
addQueryParameter("exclude_reblogs", "true");
}
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
}
}
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
PINNED,
MEDIA,
NO_REBLOGS
NO_REBLOGS,
OWN_POSTS_AND_REPLIES
}
}

View File

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

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Preferences;
public class GetPreferences extends MastodonAPIRequest<Preferences> {
public GetPreferences(){
super(HttpMethod.GET, "/preferences", Preferences.class);
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class RejectFollowRequest extends MastodonAPIRequest<Relationship>{
public RejectFollowRequest(String id){
super(HttpMethod.POST, "/follow_requests/"+id+"/reject", Relationship.class);
setRequestBody(new Object());
}
}

View File

@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
if(followed)
setRequestBody(new Request(showReblogs, null));
setRequestBody(new Request(showReblogs, notify));
else
setRequestBody(new Object());
}

View File

@@ -2,13 +2,16 @@ package org.joinmastodon.android.api.requests.accounts;
import android.net.Uri;
import org.joinmastodon.android.api.AvatarResizedImageRequestBody;
import org.joinmastodon.android.api.ContentUriRequestBody;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.ResizedImageRequestBody;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
import okhttp3.MultipartBody;
@@ -39,21 +42,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
}
@Override
public RequestBody getRequestBody(){
public RequestBody getRequestBody() throws IOException{
MultipartBody.Builder bldr=new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("display_name", displayName)
.addFormDataPart("note", bio);
if(avatar!=null){
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null));
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new AvatarResizedImageRequestBody(avatar, null));
}else if(avatarFile!=null){
bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile));
bldr.addFormDataPart("avatar", avatarFile.getName(), new AvatarResizedImageRequestBody(Uri.fromFile(avatarFile), null));
}
if(cover!=null){
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null));
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ResizedImageRequestBody(cover, 1500*500, null));
}else if(coverFile!=null){
bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile));
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
}
if(fields.isEmpty()){
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.util.List;
public class AddAccountsToList extends MastodonAPIRequest<Object> {
public AddAccountsToList(String listId, List<String> accountIds){
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
Request req = new Request();
req.accountIds = accountIds;
setRequestBody(req);
}
public static class Request{
public List<String> accountIds;
}
}

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
import java.util.List;
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
public GetLists() {
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
}
public GetLists(String accountID) {
super(HttpMethod.GET, "/accounts/"+accountID+"/lists", new TypeToken<>(){});
}
}

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.util.List;
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
public RemoveAccountsFromList(String listId, List<String> accountIds){
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
Request req = new Request();
req.accountIds = accountIds;
setRequestBody(req);
}
public static class Request{
public List<String> accountIds;
}
}

View File

@@ -0,0 +1,21 @@
package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
import org.joinmastodon.android.model.Marker;
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
super(HttpMethod.POST, "/markers", Response.class);
JsonObjectBuilder builder=new JsonObjectBuilder();
if(lastSeenHomePostID!=null)
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
if(lastSeenNotificationID!=null)
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
setRequestBody(builder.build());
}
public static class Response{
public Marker home, notifications;
}
}

View File

@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
}
private static class Request{
public String clientName="Mastodon for Android";
public String clientName="Megalodon";
public String redirectUris=AccountSessionManager.REDIRECT_URI;
public String scopes=AccountSessionManager.SCOPE;
public String website="https://app.joinmastodon.org/android";
public String website="https://sk22.github.io/megalodon";
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class EditStatus extends MastodonAPIRequest<Status>{
public EditStatus(CreateStatus.Request req, String id){
super(HttpMethod.PUT, "/statuses/"+id, Status.class);
setRequestBody(req);
}
}

View File

@@ -0,0 +1,21 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Attachment;
import java.io.IOException;
import okhttp3.Response;
public class GetAttachmentByID extends MastodonAPIRequest<Attachment>{
public GetAttachmentByID(String id){
super(HttpMethod.GET, "/media/"+id, Attachment.class);
}
@Override
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
if(httpResponse.code()==206)
respObj.url="";
super.validateAndPostprocessResponse(respObj, httpResponse);
}
}

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.Status;
public class GetBookmarkedStatuses extends HeaderPaginationRequest<Status>{
public GetBookmarkedStatuses(String maxID, int limit){
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

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.Status;
public class GetFavoritedStatuses extends HeaderPaginationRequest<Status>{
public GetFavoritedStatuses(String maxID, int limit){
super(HttpMethod.GET, "/favourites", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@@ -0,0 +1,33 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import okhttp3.Response;
public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
public GetStatusEditHistory(String id){
super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){});
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException{
int i=0;
for(Status s:respObj){
s.uri="";
s.id="fakeID"+i;
s.visibility=StatusPrivacy.PUBLIC;
s.mentions=Collections.emptyList();
s.tags=Collections.emptyList();
i++;
}
super.validateAndPostprocessResponse(respObj, httpResponse);
}
}

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.Account;
public class GetStatusFavorites extends HeaderPaginationRequest<Account>{
public GetStatusFavorites(String id, String maxID, int limit){
super(HttpMethod.GET, "/statuses/"+id+"/favourited_by", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

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.Account;
public class GetStatusReblogs extends HeaderPaginationRequest<Account>{
public GetStatusReblogs(String id, String maxID, int limit){
super(HttpMethod.GET, "/statuses/"+id+"/reblogged_by", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.BaseModel;
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
public GetStatusSourceText(String id){
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
}
@AllFieldsAreRequired
public static class Response extends BaseModel{
public String id;
public String text;
public String spoilerText;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusBookmarked extends MastodonAPIRequest<Status>{
public SetStatusBookmarked(String id, boolean bookmarked){
super(HttpMethod.POST, "/statuses/"+id+"/"+(bookmarked ? "bookmark" : "unbookmark"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusPinned extends MastodonAPIRequest<Status>{
public SetStatusPinned(String id, boolean pinned){
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -17,6 +17,7 @@ import java.io.IOException;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
private Uri uri;
@@ -40,6 +41,18 @@ public class UploadAttachment extends MastodonAPIRequest<Attachment>{
return this;
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
@Override
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
if(respObj.url==null)
respObj.url="";
super.validateAndPostprocessResponse(respObj, httpResponse);
}
@Override
public RequestBody getRequestBody() throws IOException{
MultipartBody.Builder builder=new MultipartBody.Builder()

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.tags;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
public class GetHashtag extends MastodonAPIRequest<Hashtag> {
public GetHashtag(String name){
super(HttpMethod.GET, "/tags/"+name, Hashtag.class);
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.tags;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
public class SetHashtagFollowed extends MastodonAPIRequest<Hashtag>{
public SetHashtagFollowed(String name, boolean followed){
super(HttpMethod.POST, "/tags/"+name+"/"+(followed ? "follow" : "unfollow"), Hashtag.class);
setRequestBody(new Object());
}
}

View File

@@ -8,12 +8,14 @@ import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
public GetHomeTimeline(String maxID, String minID, int limit){
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID){
super(HttpMethod.GET, "/timelines/home", 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

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

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.api.requests.timelines;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
if(local)
addQueryParameter("local", "true");
if(remote)
addQueryParameter("remote", "true");
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@@ -27,6 +27,7 @@ public class AccountSession{
public boolean needUpdatePushSettings;
public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>();
public String pushAccountID;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController;

View File

@@ -2,15 +2,22 @@ package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
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;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
@@ -54,7 +61,7 @@ import me.grishka.appkit.api.ErrorResponse;
public class AccountSessionManager{
private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI="mastodon-android-auth://callback";
public static final String REDIRECT_URI="megalodon-android-auth://callback";
private static final AccountSessionManager instance=new AccountSessionManager();
@@ -85,11 +92,12 @@ public class AccountSessionManager{
domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session);
}
}catch(IOException|JsonParseException x){
}catch(Exception x){
Log.e(TAG, "Error loading accounts", x);
}
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
@@ -100,8 +108,9 @@ public class AccountSessionManager{
writeAccountsFile();
updateInstanceEmojis(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush();
session.getPushSubscriptionManager().registerAccountForPush(null);
}
maybeUpdateShortcuts();
}
public synchronized void writeAccountsFile(){
@@ -133,10 +142,20 @@ public class AccountSessionManager{
return session;
}
@Nullable
public AccountSession tryGetAccount(String id){
return sessions.get(id);
}
@Nullable
public AccountSession getLastActiveAccount(){
if(sessions.isEmpty() || lastActiveAccountID==null)
return null;
if(!sessions.containsKey(lastActiveAccountID)){
// TODO figure out why this happens. It should not be possible.
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
writeAccountsFile();
}
return getAccount(lastActiveAccountID);
}
@@ -171,6 +190,7 @@ public class AccountSessionManager{
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
nm.deleteNotificationChannelGroup(id);
}
maybeUpdateShortcuts();
}
@NonNull
@@ -191,12 +211,13 @@ public class AccountSessionManager{
.path("/oauth/authorize")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", result.clientId)
.appendQueryParameter("redirect_uri", "mastodon-android-auth://callback")
.appendQueryParameter("redirect_uri", "megalodon-android-auth://callback")
.appendQueryParameter("scope", SCOPE)
.build();
new CustomTabsIntent.Builder()
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.setShowTitle(true)
.build()
.launchUrl(activity, uri);
}
@@ -347,7 +368,7 @@ public class AccountSessionManager{
customEmojis.put(domain, groupCustomEmojis(emojis));
instances.put(domain, emojis.instance);
instancesLastUpdated.put(domain, emojis.lastUpdated);
}catch(IOException|JsonParseException x){
}catch(Exception x){
Log.w(TAG, "Error reading instance info file for "+domain, x);
}
}
@@ -384,6 +405,29 @@ public class AccountSessionManager{
writeAccountsFile();
}
private void maybeUpdateShortcuts(){
if(Build.VERSION.SDK_INT<26)
return;
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
// There are no shortcuts, but there are accounts. Add a compose shortcut.
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
}else if(sessions.isEmpty()){
// There are shortcuts, but no accounts. Disable existing shortcuts.
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
}else{
sm.enableShortcuts(Collections.singletonList("compose"));
}
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
public class FollowRequestHandledEvent {
public String accountID;
public boolean accepted;
public Account account;
public Relationship relationship;
public FollowRequestHandledEvent(String accountID, boolean accepted, Account account, Relationship rel){
this.accountID=accountID;
this.accepted=accepted;
this.account=account;
this.relationship=rel;
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.events;
public class NotificationDeletedEvent{
public final String id;
public NotificationDeletedEvent(String id){
this.id=id;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
public class RemoveAccountPostsEvent{
public final String accountID;
public final String postsByAccountID;
public final boolean isUnfollow;
public RemoveAccountPostsEvent(String accountID, String postsByAccountID, boolean isUnfollow){
this.accountID=accountID;
this.postsByAccountID=postsByAccountID;
this.isUnfollow=isUnfollow;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.updater.GithubSelfUpdater;
public class SelfUpdateStateChangedEvent{
public final GithubSelfUpdater.UpdateState state;
public SelfUpdateStateChangedEvent(GithubSelfUpdater.UpdateState state){
this.state=state;
}
}

View File

@@ -4,8 +4,8 @@ import org.joinmastodon.android.model.Status;
public class StatusCountersUpdatedEvent{
public String id;
public int favorites, reblogs, replies;
public boolean favorited, reblogged;
public long favorites, reblogs, replies;
public boolean favorited, reblogged, bookmarked, pinned;
public StatusCountersUpdatedEvent(Status s){
id=s.id;
@@ -14,5 +14,7 @@ public class StatusCountersUpdatedEvent{
replies=s.repliesCount;
favorited=s.favourited;
reblogged=s.reblogged;
bookmarked=s.bookmarked;
pinned=s.pinned;
}
}

View File

@@ -3,9 +3,11 @@ package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Status;
public class StatusCreatedEvent{
public Status status;
public final Status status;
public final String accountID;
public StatusCreatedEvent(Status status){
public StatusCreatedEvent(Status status, String accountID){
this.status=status;
this.accountID=accountID;
}
}

View File

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

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Status;
public class StatusUpdatedEvent{
public Status status;
public StatusUpdatedEvent(Status status){
this.status=status;
}
}

View File

@@ -7,9 +7,12 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.parceler.Parcels;
import java.util.Collections;
@@ -47,10 +50,14 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
if(user==null) // TODO figure out why this happens
return;
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null)
return;
onDataLoaded(result, !result.isEmpty());
}
})
@@ -72,6 +79,7 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
return;
if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){
// Keep replies to self, discard all other replies
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
@@ -82,4 +90,29 @@ public class AccountTimelineFragment extends StatusListFragment{
}
prependItems(Collections.singletonList(ev.status), true);
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
return;
Status status=getStatusByID(ev.id);
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
}

View File

@@ -19,6 +19,7 @@ import android.view.WindowInsets;
import android.widget.Toolbar;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
@@ -31,6 +32,8 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.TileGridLayoutManager;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
@@ -79,6 +82,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(GlobalUserPreferences.disableMarquee){
setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false);
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
}
@@ -150,9 +157,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
UsableRecyclerView list=(UsableRecyclerView) this.list;
for(int i=0; i<list.getChildCount(); i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof ImageLoaderViewHolder){
for(int j=0; j<list.getImageCountForItem(holder.getAbsoluteAdapterPosition()); j++){
((ImageLoaderViewHolder) holder).clearImage(j);
if(holder instanceof ImageLoaderViewHolder ivh){
int pos=holder.getAbsoluteAdapterPosition();
if(pos<0)
continue;
for(int j=0;j<list.getImageCountForItem(pos);j++){
ivh.clearImage(j);
}
}
}
@@ -169,18 +179,18 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){
final Status status=_status.reblog!=null ? _status.reblog : _status;
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
private ImageStatusDisplayItem.Holder transitioningHolder;
private ImageStatusDisplayItem.Holder<?> transitioningHolder;
@Override
public void setPhotoViewVisibility(int index, boolean visible){
ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
if(holder!=null)
holder.photo.setAlpha(visible ? 1f : 0f);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
if(holder!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
@@ -223,7 +233,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
if(holder!=null)
return holder.photo.getDrawable();
return null;
@@ -234,14 +244,21 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer=null;
}
private ImageStatusDisplayItem.Holder findPhotoViewHolder(int index){
@Override
public void onRequestPermissions(String[] permissions){
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
}
private ImageStatusDisplayItem.Holder<?> findPhotoViewHolder(int index){
if(list==null)
return null;
int offset=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(parentID)){
if(item instanceof ImageStatusDisplayItem){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index);
if(holder instanceof ImageStatusDisplayItem.Holder){
return (ImageStatusDisplayItem.Holder) holder;
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
return imgHolder;
}
return null;
}
@@ -271,6 +288,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
outRect.setEmpty();
return;
}
String id=((StatusDisplayItem.Holder<?>) holder).getItemID();
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
@@ -303,9 +324,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
position-=getMainAdapterOffset();
if(position>=0 && position<displayItems.size()){
StatusDisplayItem item=displayItems.get(position);
if(item instanceof ImageStatusDisplayItem){
PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem) item).tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem) item).thisTile;
if(item instanceof ImageStatusDisplayItem imgItem){
PhotoLayoutHelper.TiledLayoutResult layout=imgItem.tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgItem.thisTile;
int spans=0;
for(int i=0;i<tile.colSpan;i++){
spans+=layout.columnSizes[tile.startCol+i];
@@ -323,7 +344,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
list.invalidateItemDecorations();
}
private void updateToolbar(){
@@ -331,6 +351,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(toolbar==null)
return;
toolbar.setOnClickListener(v->scrollToTop());
toolbar.setNavigationContentDescription(R.string.back);
}
protected int getMainAdapterOffset(){
@@ -348,6 +369,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public abstract void onItemClick(String id);
protected void updatePoll(String itemID, Status status, Poll poll){
status.poll=poll;
int firstOptionIndex=-1, footerIndex=-1;
int i=0;
for(StatusDisplayItem item:displayItems){
@@ -390,8 +412,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
if(vh instanceof PollFooterStatusDisplayItem.Holder){
PollFooterStatusDisplayItem.Holder footer=(PollFooterStatusDisplayItem.Holder) vh;
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
if(footer.getItemID().equals(holder.getItemID())){
footer.rebind();
break;
@@ -423,7 +444,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}
@@ -441,13 +462,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
status.spoilerRevealed=true;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null)
header.rebind();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(true);
}
updateImagesSpoilerState(status, itemID);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
@@ -456,15 +475,30 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
}
holder.rebind();
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){
updateImagesSpoilerState(status, holder.getItemID());
}
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(status.spoilerRevealed);
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public String getAccountID(){
return accountID;
}
@@ -513,7 +547,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof StatusDisplayItem.Holder && ((StatusDisplayItem.Holder<?>) holder).getItemID().equals(id) && type.isInstance(holder))
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
return type.cast(holder);
}
return null;
@@ -523,7 +557,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
ArrayList<H> holders=new ArrayList<>();
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof StatusDisplayItem.Holder && ((StatusDisplayItem.Holder<?>) holder).getItemID().equals(id) && type.isInstance(holder))
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
holders.add(type.cast(holder));
}
return holders;
@@ -550,6 +584,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return true;
}
public ArrayList<StatusDisplayItem> getDisplayItems(){
return displayItems;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
@@ -562,6 +600,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
super.onApplyWindowInsets(insets);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
if(requestCode==PhotoViewer.PERMISSION_REQUEST && currentPhotoViewer!=null){
currentPhotoViewer.onRequestPermissionsResult(permissions, grantResults);
}
}
@Override
public void onPause(){
super.onPause();
if(currentPhotoViewer!=null)
currentPhotoViewer.onPause();
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){
@@ -597,8 +649,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
if(position>=displayItems.size()) // TODO fix this in the image loader, these crashes are driving me crazy
return null;
return displayItems.get(position).getImageRequest(image);
}
@@ -632,8 +682,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
View bottomSibling=parent.getChildAt(i+1);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder && siblingHolder instanceof StatusDisplayItem.Holder
&& !((StatusDisplayItem.Holder<?>) holder).getItemID().equals(((StatusDisplayItem.Holder<?>) siblingHolder).getItemID())){
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}
@@ -644,8 +694,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof ImageStatusDisplayItem.Holder){
ImageStatusDisplayItem.Holder<?> imgHolder=(ImageStatusDisplayItem.Holder<?>) holder;
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
hiddenMediaPaint.setColor(0x80000000);
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
@@ -658,8 +707,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof ImageStatusDisplayItem.Holder){
ImageStatusDisplayItem.Holder<?> imgHolder=(ImageStatusDisplayItem.Holder<?>) holder;
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
if(tile.startCol==0 && tile.startRow==0 && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
@@ -731,6 +779,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
private void rebuildMediaHiddenLayouts(int width){
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));

View File

@@ -0,0 +1,37 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status;
import me.grishka.appkit.api.SimpleCallback;
public class BookmarkedStatusListFragment extends StatusListFragment{
private String nextMaxID;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.bookmarks);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetBookmarkedStatuses(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Status> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountID);
}
}

View File

@@ -30,7 +30,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeImageDescriptionFragment extends ToolbarFragment{
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
private String accountID, attachmentID;
private EditText edit;
private Button saveButton;

View File

@@ -0,0 +1,37 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status;
import me.grishka.appkit.api.SimpleCallback;
public class FavoritedStatusListFragment extends StatusListFragment{
private String nextMaxID;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.your_favorites);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetFavoritedStatuses(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Status> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountID);
}
}

View File

@@ -0,0 +1,349 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders;
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.ProgressBarButton;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
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;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
private String nextMaxID;
public FollowRequestsListFragment(){
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
loadData();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setTitle(R.string.follow_requests);
}
@Override
protected void doLoadData(int offset, int count){
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
currentRequest=new GetFollowRequests(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Account> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(AccountWrapper::new).collect(Collectors.toList()), false);
loadRelationships();
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter getAdapter(){
return new AccountsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.bottom=outRect.left=outRect.right=V.dp(16);
if(parent.getChildAdapterPosition(view)==0)
outRect.top=V.dp(16);
}
});
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
}
private void loadRelationships(){
relationships=Collections.emptyMap();
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
relationshipsRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof AccountViewHolder avh)
avh.rebind();
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequest=null;
}
}).exec(accountID);
}
@Override
public void onDestroyView(){
super.onDestroyView();
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
}
@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
}
@Override
public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getImageCountForItem(int position){
return 2+data.get(position).emojiHelper.getImageCount();
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountWrapper item=data.get(position);
if(image==0)
return item.avaRequest;
else if(image==1)
return item.coverRequest;
else
return item.emojiHelper.getImageRequest(image-2);
}
}
// literally the same as AccountCardStatusDisplayItem and DiscoverAccountsFragment. code should be generalized
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final ImageView cover, avatar;
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
private final ProgressBarButton actionButton, acceptButton, rejectButton;
private final ProgressBar actionProgress, acceptProgress, rejectProgress;
private final View actionWrap, acceptWrap, rejectWrap;
private Relationship relationship;
public AccountViewHolder(){
super(getActivity(), R.layout.item_discover_account, list);
cover=findViewById(R.id.cover);
avatar=findViewById(R.id.avatar);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
bio=findViewById(R.id.bio);
followersCount=findViewById(R.id.followers_count);
followersLabel=findViewById(R.id.followers_label);
followingCount=findViewById(R.id.following_count);
followingLabel=findViewById(R.id.following_label);
postsCount=findViewById(R.id.posts_count);
postsLabel=findViewById(R.id.posts_label);
actionButton=findViewById(R.id.action_btn);
actionProgress=findViewById(R.id.action_progress);
actionWrap=findViewById(R.id.action_btn_wrap);
acceptButton=findViewById(R.id.accept_btn);
acceptProgress=findViewById(R.id.accept_progress);
acceptWrap=findViewById(R.id.accept_btn_wrap);
rejectButton=findViewById(R.id.reject_btn);
rejectProgress=findViewById(R.id.reject_progress);
rejectWrap=findViewById(R.id.reject_btn_wrap);
itemView.setOutlineProvider(OutlineProviders.roundedRect(6));
itemView.setClipToOutline(true);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
cover.setClipToOutline(true);
actionButton.setOnClickListener(this::onActionButtonClick);
acceptButton.setOnClickListener(this::onFollowRequestButtonClick);
rejectButton.setOnClickListener(this::onFollowRequestButtonClick);
}
@Override
public void onBind(AccountWrapper item){
name.setText(item.parsedName);
username.setText('@'+item.account.acct);
bio.setText(item.parsedBio);
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
relationship=relationships.get(item.account.id);
if(relationship == null || !relationship.followedBy){
actionWrap.setVisibility(View.GONE);
acceptWrap.setVisibility(View.VISIBLE);
rejectWrap.setVisibility(View.VISIBLE);
// i hate that i wasn't able to do this in xml
acceptButton.setCompoundDrawableTintList(acceptButton.getTextColors());
acceptProgress.setIndeterminateTintList(acceptButton.getTextColors());
rejectButton.setCompoundDrawableTintList(rejectButton.getTextColors());
rejectProgress.setIndeterminateTintList(rejectButton.getTextColors());
}else if(relationship==null){
actionWrap.setVisibility(View.GONE);
acceptWrap.setVisibility(View.GONE);
rejectWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
acceptWrap.setVisibility(View.GONE);
rejectWrap.setVisibility(View.GONE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else if(index==1){
cover.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-2, image);
name.invalidate();
bio.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
if (!rel.requested && !rel.followedBy && adapter != null) {
data.remove(item);
adapter.notifyItemRemoved(getBindingAdapterPosition());
} else {
rebind();
}
});
}
private void onActionButtonClick(View v){
itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
rebind();
});
}
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
actionButton.setClickable(!visible);
}
}
protected class AccountWrapper{
public Account account;
public ImageLoaderRequest avaRequest, coverRequest;
public CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
public CharSequence parsedName, parsedBio;
public AccountWrapper(Account account){
this.account=account;
if(!TextUtils.isEmpty(account.avatar))
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
if(!TextUtils.isEmpty(account.header))
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(account.emojis.isEmpty()){
parsedName=account.displayName;
}else{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
}
}
}
}

View File

@@ -2,23 +2,34 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
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.ImageButton;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetHashtag;
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 java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class HashtagTimelineFragment extends StatusListFragment{
private String hashtag;
private boolean following;
private ImageButton fab;
private MenuItem followButton;
public HashtagTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -27,10 +38,61 @@ public class HashtagTimelineFragment extends StatusListFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
hashtag=getArguments().getString("hashtag");
updateTitle(getArguments().getString("hashtag"));
following=getArguments().getBoolean("following", false);
setHasOptionsMenu(true);
}
private void updateTitle(String hashtagName) {
hashtag = hashtagName;
setTitle('#'+hashtag);
}
private void updateFollowingState(boolean newFollowing) {
this.following = newFollowing;
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.hashtag_timeline, menu);
followButton = menu.findItem(R.id.follow_hashtag);
updateFollowingState(following);
followButton.setOnMenuItemClickListener(i -> {
updateFollowingState(!following);
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag i) {
if (i.following == following) Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
updateFollowingState(i.following);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
updateFollowingState(!following);
}
}).exec(accountID);
return true;
});
new GetHashtag(hashtag).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag hashtag) {
updateTitle(hashtag.name);
updateFollowingState(hashtag.following);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)

View File

@@ -2,12 +2,9 @@ package org.joinmastodon.android.fragments;
import android.app.Fragment;
import android.app.NotificationManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -18,17 +15,13 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.PushNotificationReceiver;
import org.joinmastodon.android.R;
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.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.discover.SearchFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.parceler.Parcels;
@@ -38,8 +31,6 @@ import java.util.ArrayList;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
@@ -137,7 +128,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
});
}
}else{
}
return content;
@@ -212,8 +202,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){
if(newFragment instanceof ScrollableToTop)
((ScrollableToTop) newFragment).scrollToTop();
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
@@ -223,8 +213,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
private void maybeTriggerLoading(Fragment newFragment){
if(newFragment instanceof LoaderFragment){
LoaderFragment lf=(LoaderFragment) newFragment;
if(newFragment instanceof LoaderFragment lf){
if(!lf.loaded && !lf.dataLoading)
lf.loadData();
}else if(newFragment instanceof DiscoverFragment){
@@ -239,21 +228,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private boolean onTabLongClick(@IdRes int tab){
if(tab==R.id.tab_profile){
ArrayList<String> options=new ArrayList<>();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new M3AlertDialogBuilder(getActivity())
.setItems(options.toArray(new String[0]), (dialog, which)->{
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
})
.setNegativeButton(R.string.add_account, (dialog, which)->{
Nav.go(getActivity(), SplashFragment.class, null);
})
.show();
ArrayList<String> options=new ArrayList<>();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new AccountSwitcherSheet(getActivity()).show();
return true;
}
return false;
@@ -262,9 +241,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public boolean onBackPressed(){
if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed();
if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed();
if (searchFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home);
return true;
}
return false;
}

View File

@@ -1,35 +1,67 @@
package org.joinmastodon.android.fragments;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
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.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toolbar;
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.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.PaginatedResponse;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
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;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class HomeTimelineFragment extends StatusListFragment{
private ImageButton fab;
private ImageView toolbarLogo;
private Button toolbarShowNewPostsBtn;
private boolean newPostsBtnShown;
private AnimatorSet currentNewPostsAnim;
private String maxID;
@@ -44,17 +76,27 @@ public class HomeTimelineFragment extends StatusListFragment{
loadData();
}
private List<Status> filterPosts(List<Status> items) {
return items.stream().filter(i ->
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || i.reblog == null)
).collect(Collectors.toList());
}
@Override
protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
@Override
public void onSuccess(PaginatedResponse<List<Status>> result){
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
if(getActivity()==null)
return;
onDataLoaded(result.items, !result.items.isEmpty());
List<Status> filteredItems = filterPosts(result.items);
onDataLoaded(filteredItems, !result.items.isEmpty());
maxID=result.maxID;
if(result.isFromCache())
loadNewPosts();
}
});
}
@@ -65,6 +107,19 @@ public class HomeTimelineFragment extends StatusListFragment{
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
hideNewPostsButton();
}
}
});
if(GithubSelfUpdater.needSelfUpdating()){
E.register(this);
updateUpdateState(GithubSelfUpdater.getInstance().getState());
}
}
@Override
@@ -89,11 +144,15 @@ public class HomeTimelineFragment extends StatusListFragment{
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
if(!getArguments().getBoolean("noAutoLoad")){
if(!loaded && !dataLoading){
loadData();
}else if(!dataLoading){
loadNewPosts();
}
}
}
@Subscribe
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
}
@@ -104,12 +163,284 @@ public class HomeTimelineFragment extends StatusListFragment{
Nav.go(getActivity(), ComposeFragment.class, args);
}
private void loadNewPosts(){
if (!GlobalUserPreferences.loadNewPosts) return;
dataLoading=true;
// The idea here is that we request the timeline such that if there are fewer than `limit` posts,
// we'll get the currently topmost post as last in the response. This way we know there's no gap
// between the existing and newly loaded parts of the timeline.
String sinceID=data.size()>1 ? data.get(1).id : "1";
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
currentRequest=null;
dataLoading=false;
result = filterPosts(result);
if(result.isEmpty() || getActivity()==null)
return;
Status last=result.get(result.size()-1);
List<Status> toAdd;
if(!data.isEmpty() && last.id.equals(data.get(0).id)){ // This part intersects with the existing one
toAdd=result.subList(0, result.size()-1); // Remove the already known last post
}else{
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());
}
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
showNewPostsButton();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
}
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
dataLoading=false;
}
})
.exec(accountID);
}
@Override
public void onGapClick(GapStatusDisplayItem.Holder item){
if(dataLoading)
return;
item.getItem().loading=true;
V.setVisibilityAnimated(item.progress, View.VISIBLE);
V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem();
dataLoading=true;
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
currentRequest=null;
dataLoading=false;
if(getActivity()==null)
return;
int gapPos=displayItems.indexOf(gap);
if(gapPos==-1)
return;
if(result.isEmpty()){
displayItems.remove(gapPos);
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
Status gapStatus=getStatusByID(gap.parentID);
if(gapStatus!=null){
gapStatus.hasGapAfter=false;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
}
}else{
Set<String> idsBelowGap=new HashSet<>();
boolean belowGap=false;
int gapPostIndex=0;
for(Status s:data){
if(belowGap){
idsBelowGap.add(s.id);
}else if(s.id.equals(gap.parentID)){
belowGap=true;
s.hasGapAfter=false;
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
}else{
gapPostIndex++;
}
}
int endIndex=0;
for(Status s:result){
endIndex++;
if(idsBelowGap.contains(s.id))
break;
}
if(endIndex==result.size()){
result.get(result.size()-1).hasGapAfter=true;
}else{
result=result.subList(0, endIndex);
}
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:
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
for(Filter filter:filters){
if(filter.matches(s)){
continue outer;
}
}
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.
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
}else{
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
}
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
}
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
dataLoading=false;
gap.loading=false;
Activity a=getActivity();
if(a!=null){
error.showToast(a);
int gapPos=displayItems.indexOf(gap);
if(gapPos>=0)
adapter.notifyItemChanged(gapPos);
}
}
})
.exec(accountID);
}
@Override
public void onRefresh(){
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
dataLoading=false;
}
super.onRefresh();
}
private void updateToolbarLogo(){
ImageView logo=new ImageView(getActivity());
logo.setScaleType(ImageView.ScaleType.CENTER);
logo.setImageResource(R.drawable.logo);
logo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
toolbarLogo=new ImageView(getActivity());
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
toolbarLogo.setImageResource(R.drawable.logo);
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
// toolbarLogo =new TextView(getActivity());
// toolbarLogo.setText(getString(R.string.app_name).toLowerCase(Locale.getDefault()));
// toolbarLogo.setTextAppearance(R.style.app_title);
toolbarShowNewPostsBtn=new Button(getActivity());
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
toolbarShowNewPostsBtn.setTextColor(0xffffffff);
toolbarShowNewPostsBtn.setStateListAnimator(null);
toolbarShowNewPostsBtn.setBackgroundResource(R.drawable.bg_button_new_posts);
toolbarShowNewPostsBtn.setText(R.string.see_new_posts);
toolbarShowNewPostsBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_fluent_arrow_up_16_filled, 0, 0, 0);
toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors());
toolbarShowNewPostsBtn.setCompoundDrawablePadding(V.dp(8));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(toolbarShowNewPostsBtn);
toolbarShowNewPostsBtn.setOnClickListener(this::onNewPostsBtnClick);
if(newPostsBtnShown){
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
toolbarLogo.setVisibility(View.INVISIBLE);
toolbarLogo.setAlpha(0f);
}else{
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
toolbarShowNewPostsBtn.setAlpha(0f);
toolbarShowNewPostsBtn.setScaleX(.8f);
toolbarShowNewPostsBtn.setScaleY(.8f);
toolbarLogo.setVisibility(View.VISIBLE);
}
FrameLayout logoWrap=new FrameLayout(getActivity());
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
Toolbar toolbar=getToolbar();
toolbar.addView(logo, new Toolbar.LayoutParams(Gravity.CENTER));
toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
}
private void showNewPostsButton(){
if(newPostsBtnShown)
return;
newPostsBtnShown=true;
if(currentNewPostsAnim!=null){
currentNewPostsAnim.cancel();
}
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
);
set.setDuration(300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
toolbarLogo.setVisibility(View.INVISIBLE);
currentNewPostsAnim=null;
}
});
currentNewPostsAnim=set;
set.start();
}
private void hideNewPostsButton(){
if(!newPostsBtnShown)
return;
newPostsBtnShown=false;
if(currentNewPostsAnim!=null){
currentNewPostsAnim.cancel();
}
toolbarLogo.setVisibility(View.VISIBLE);
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
);
set.setDuration(300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
currentNewPostsAnim=null;
}
});
currentNewPostsAnim=set;
set.start();
}
private void onNewPostsBtnClick(View v){
if(newPostsBtnShown){
hideNewPostsButton();
scrollToTop();
}
}
@Override
public void onDestroyView(){
super.onDestroyView();
if(GithubSelfUpdater.needSelfUpdating()){
E.unregister(this);
}
}
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateState(ev.state);
}
@Override
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true;
}
}

View File

@@ -0,0 +1,84 @@
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;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ListTimelineFragment extends StatusListFragment {
private String listID;
private String listTitle;
private ImageButton fab;
public ListTimelineFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
listID=getArguments().getString("listID");
listTitle=getArguments().getString("listTitle");
setTitle(listTitle);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
// TODO: implement edit, delete
// inflater.inflate(R.menu.list, menu);
}
@Override
protected void doLoadData(int offset, int count) {
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<Status> result) {
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
protected void onShown() {
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
}
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
}
@Override
protected void onSetFabBottomInset(int inset) {
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
}
}

View File

@@ -0,0 +1,188 @@
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;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
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 {
private String accountId;
private String profileAccountId;
private String profileDisplayUsername;
private HashMap<String, Boolean> userInListBefore = new HashMap<>();
private HashMap<String, Boolean> userInList = new HashMap<>();
private int inProgress = 0;
public ListTimelinesFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
profileDisplayUsername=args.getString("profileDisplayUsername");
setTitle(getString(R.string.lists_with_user, profileDisplayUsername));
// setHasOptionsMenu(true);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
// @Override
// public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// Button saveButton=new Button(getActivity());
// saveButton.setText(R.string.save);
// saveButton.setOnClickListener(this::onSaveClick);
// LinearLayout wrap=new LinearLayout(getActivity());
// wrap.setOrientation(LinearLayout.HORIZONTAL);
// wrap.addView(saveButton, 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(R.string.save);
// item.setActionView(wrap);
// item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// }
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(Object o) {}
}).exec(accountId);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
@Override
protected RecyclerView.Adapter getAdapter() {
return new ListsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_list_timeline, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
UiUtils.openListTimeline(getActivity(), accountId, item);
}
}
}

View File

@@ -0,0 +1,33 @@
package org.joinmastodon.android.fragments;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.View;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import androidx.annotation.CallSuper;
import me.grishka.appkit.fragments.ToolbarFragment;
public abstract class MastodonToolbarFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
updateToolbar();
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
}
@CallSuper
protected void updateToolbar(){
Toolbar toolbar=getToolbar();
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
toolbar.setNavigationContentDescription(R.string.back);
}
}
}

View File

@@ -2,16 +2,23 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.Fragment;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
@@ -20,18 +27,23 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class NotificationsFragment extends ToolbarFragment implements ScrollableToTop{
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{
private TabLayout tabLayout;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
private String accountID;
@@ -42,14 +54,36 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
setRetainInstance(true);
accountID=getArguments().getString("account");
E.register(this);
}
@Override
public void onDestroy() {
super.onDestroy();
E.unregister(this);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setHasOptionsMenu(true);
setTitle(R.string.notifications);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
}
@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;
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false);
@@ -57,12 +91,13 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[2];
tabViews=new FrameLayout[3];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.notifications_all;
case 1 -> R.id.notifications_mentions;
case 2 -> R.id.notifications_posts;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -81,8 +116,7 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
if(position==0)
return;
Fragment _page=getFragmentForPage(position);
if(_page instanceof BaseRecyclerFragment){
BaseRecyclerFragment page=(BaseRecyclerFragment) _page;
if(_page instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
@@ -102,9 +136,15 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
mentionsFragment=new NotificationsListFragment();
mentionsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyPosts", true);
postsFragment=new NotificationsListFragment();
postsFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.notifications_all, allNotificationsFragment)
.add(R.id.notifications_mentions, mentionsFragment)
.add(R.id.notifications_posts, postsFragment)
.commit();
}
@@ -114,6 +154,7 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
tab.setText(switch(position){
case 0 -> R.string.all_notifications;
case 1 -> R.string.mentions;
case 2 -> R.string.posts;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
@@ -124,16 +165,21 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
updateToolbar();
public void refreshFollowRequestsBadge() {
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Account> accounts) {
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
}
@Override
public void onError(ErrorResponse errorResponse) {}
}).exec(accountID);
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
@Subscribe
public void onFollowRequestHandled(FollowRequestHandledEvent ev) {
refreshFollowRequestsBadge();
}
@Override
@@ -142,11 +188,14 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
}
public void loadData(){
refreshFollowRequestsBadge();
if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading)
allNotificationsFragment.loadData();
}
private void updateToolbar(){
@Override
protected void updateToolbar(){
super.updateToolbar();
getToolbar().setOutlineProvider(null);
}
@@ -154,6 +203,7 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
return switch(page){
case 0 -> allNotificationsFragment;
case 1 -> mentionsFragment;
case 2 -> postsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
};
}
@@ -174,7 +224,7 @@ public class NotificationsFragment extends ToolbarFragment implements Scrollable
@Override
public int getItemCount(){
return 2;
return 3;
}
@Override

View File

@@ -1,10 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.View;
@@ -12,28 +8,29 @@ import com.squareup.otto.Subscribe;
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;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
@@ -41,6 +38,7 @@ import me.grishka.appkit.utils.V;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private boolean onlyMentions;
private boolean onlyPosts;
private String maxID;
@Override
@@ -59,6 +57,15 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onAttach(Activity activity){
super.onAttach(activity);
onlyMentions=getArguments().getBoolean("onlyMentions", false);
onlyPosts=getArguments().getBoolean("onlyPosts", false);
}
@Override
public void onRefresh() {
super.onRefresh();
if (getParentFragment() instanceof NotificationsFragment notificationsFragment) {
notificationsFragment.refreshFollowRequestsBadge();
}
}
@Override
@@ -76,17 +83,19 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
if(titleItem!=null){
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem){
((ImageStatusDisplayItem) item).horizontalInset=V.dp(32);
if(item instanceof ImageStatusDisplayItem imgItem){
imgItem.horizontalInset=V.dp(32);
}
}
}
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else{
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
}else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account, n);
return Arrays.asList(titleItem, card);
}else{
return Collections.emptyList();
}
}
@@ -102,7 +111,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
if(getActivity()==null)
@@ -116,6 +125,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
.collect(Collectors.toSet());
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset==0 && !result.items.isEmpty()){
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
}
}
});
}
@@ -126,8 +139,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof AccountCardStatusDisplayItem.Holder)
((AccountCardStatusDisplayItem.Holder) holder).rebind();
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
accountHolder.rebind();
}
}
@@ -160,90 +173,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private int bgColor=UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground);
private int borderColor=UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted);
private RectF rect=new RectF();
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
int pos=0;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
pos=holder.getAbsoluteAdapterPosition();
boolean inset=(holder instanceof StatusDisplayItem.Holder) && ((StatusDisplayItem.Holder<?>) holder).getItem().inset;
if(inset){
if(rect.isEmpty()){
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
}else{
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
rect.right=Math.max(rect.right, child.getX()+child.getHeight());
}
}else if(!rect.isEmpty()){
drawInsetBackground(c);
rect.setEmpty();
}
}
if(!rect.isEmpty()){
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
rect.bottom=parent.getHeight()+V.dp(10);
}
drawInsetBackground(c);
rect.setEmpty();
}
}
private void drawInsetBackground(Canvas c){
paint.setStyle(Paint.Style.FILL);
paint.setColor(bgColor);
rect.inset(V.dp(4), V.dp(4));
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
paint.setColor(borderColor);
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
boolean inset=((StatusDisplayItem.Holder<?>) holder).getItem().inset;
int pos=holder.getAbsoluteAdapterPosition();
if(inset){
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
int pad;
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
pad=V.dp(16);
else
pad=V.dp(12);
boolean insetLeft=true, insetRight=true;
if(holder instanceof ImageStatusDisplayItem.Holder){
PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem.Holder<?>) holder).getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem.Holder<?>) holder).getItem().thisTile;
// only inset those items that are on the edges of the layout
insetLeft=tile.startCol==0;
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
// inset all items in the bottom row
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
bottomSiblingInset=false;
}
if(insetLeft)
outRect.left=pad;
if(insetRight)
outRect.right=pad;
if(!topSiblingInset)
outRect.top=pad;
if(!bottomSiblingInset)
outRect.bottom=pad;
}
}
}
});
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
private Notification getNotificationByID(String id){
@@ -267,4 +197,37 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
return;
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
.collect(Collectors.toList());
for(Notification n:toRemove){
removeNotification(n);
}
}
private void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(n.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(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
}

View File

@@ -1,9 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Fragment;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
@@ -13,8 +11,6 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.TextView;
@@ -22,7 +18,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -84,7 +79,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
list.setClipToPadding(false);
dividerPaint.setStyle(Paint.Style.STROKE);
dividerPaint.setStrokeWidth(V.dp(1));
dividerPaint.setColor(getResources().getColor(R.color.gray_200)); // TODO themes
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
@@ -188,7 +183,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
private ShapeDrawable background=new ShapeDrawable();
protected ShapeDrawable background=new ShapeDrawable();
public BaseViewHolder(int layout){
super(getActivity(), layout, list);
@@ -225,6 +220,20 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
super.onBind(item);
title.setText(item.parsedName);
value.setText(item.parsedValue);
if(item.verifiedAt!=null){
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
check.setTint(textColor);
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
}else{
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
value.setCompoundDrawables(null, null, null, null);
}
}
@Override

View File

@@ -1,19 +1,26 @@
package org.joinmastodon.android.fragments;
import static android.content.Context.CLIPBOARD_SERVICE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -28,9 +35,11 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -42,12 +51,17 @@ import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
@@ -90,10 +104,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
private ProgressBarButton actionButton;
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@@ -101,9 +115,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private float titleTransY;
private View postsBtn, followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress;
private ProgressBar actionProgress, notifyProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
private Account account;
private String accountID;
@@ -118,6 +133,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private boolean refreshing;
private View fab;
private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar);
@@ -170,6 +187,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsLabel=content.findViewById(R.id.posts_label);
postsBtn=content.findViewById(R.id.posts_btn);
actionButton=content.findViewById(R.id.profile_action_btn);
notifyButton=content.findViewById(R.id.notify_btn);
pager=content.findViewById(R.id.pager);
scrollView=content.findViewById(R.id.scroller);
tabbar=content.findViewById(R.id.tabbar);
@@ -177,7 +195,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit=content.findViewById(R.id.name_edit);
bioEdit=content.findViewById(R.id.bio_edit);
actionProgress=content.findViewById(R.id.action_progress);
notifyProgress=content.findViewById(R.id.notify_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
avatar.setOutlineProvider(new ViewOutlineProvider(){
@Override
@@ -197,14 +217,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
};
tabViews=new FrameLayout[4];
tabViews=new FrameLayout[5];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_posts;
case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media;
case 3 -> R.id.profile_about;
case 2 -> R.id.profile_pinned_posts;
case 3 -> R.id.profile_media;
case 4 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -212,7 +233,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabViews[i]=tabView;
}
pager.setOffscreenPageLimit(4);
pager.setOffscreenPageLimit(5);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -228,8 +249,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media;
case 3 -> R.string.profile_about;
case 2 -> R.string.pinned_posts;
case 3 -> R.string.media;
case 4 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
@@ -244,6 +266,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
});
actionButton.setOnClickListener(this::onActionButtonClick);
notifyButton.setOnClickListener(this::onNotifyButtonClick);
avatar.setOnClickListener(this::onAvatarClick);
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
@@ -257,6 +280,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
fab.setVisibility(View.GONE);
}
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
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();
}
return true;
});
return sizeWrapper;
}
@@ -283,6 +321,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
@@ -307,6 +347,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
@@ -330,8 +371,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(position==0)
return;
Fragment _page=getFragmentForPage(position);
if(_page instanceof BaseRecyclerFragment){
BaseRecyclerFragment page=(BaseRecyclerFragment) _page;
if(_page instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
@@ -388,31 +428,58 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
pinnedPostsFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
}
}
private void bindHeaderView(){
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
username.setText('@'+account.acct);
bio.setText(HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID));
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
lock.setTint(username.getCurrentTextColor());
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BASELINE), 0);
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
}else{
bio.setVisibility(View.VISIBLE);
bio.setText(parsedBio);
}
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, account.statusesCount)));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
UiUtils.loadCustomEmojiInTextView(name);
UiUtils.loadCustomEmojiInTextView(bio);
notifyButton.setVisibility(View.GONE);
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
actionButton.setText(R.string.edit_profile);
}else{
@@ -455,6 +522,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
toolbarSubtitleView.setTranslationY(titleTransY);
}
getToolbar().setOnClickListener(v->scrollToTop());
getToolbar().setNavigationContentDescription(R.string.back);
}
@Override
@@ -477,17 +545,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return;
}
if(relationship==null)
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(R.menu.profile, menu);
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
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()));
if(relationship.following)
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()));
else
}else {
menu.findItem(R.id.hide_boosts).setVisible(false);
menu.findItem(R.id.manage_user_lists).setVisible(false);
}
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
else
@@ -497,8 +570,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
if(id==R.id.share) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
@@ -519,7 +592,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
@@ -533,6 +606,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.bookmarks){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
}else if(id==R.id.favorites){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
}else if(id==R.id.manage_user_lists){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
}
return true;
}
@@ -547,8 +634,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationship=result.get(0);
updateRelationship();
if(!result.isEmpty()){
relationship=result.get(0);
updateRelationship();
}
}
@Override
@@ -562,17 +651,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void updateRelationship(){
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
UiUtils.setRelationshipToActionButton(relationship, notifyButton, true);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying);
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username));
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatar.getTop()-topBarsH){
float avaAlpha=Math.max(1f-((scrollY-(avatar.getTop()-topBarsH))/(float)V.dp(38)), 0f);
avatar.setAlpha(avaAlpha);
if(scrollY>avatarBorder.getTop()-topBarsH){
float avaAlpha=Math.max(1f-((scrollY-(avatarBorder.getTop()-topBarsH))/(float)V.dp(38)), 0f);
avatarBorder.setAlpha(avaAlpha);
}else{
avatar.setAlpha(1f);
avatarBorder.setAlpha(1f);
}
if(scrollY>cover.getHeight()-topBarsH){
@@ -594,14 +688,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
}
}
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
case 3 -> aboutFragment;
case 2 -> pinnedPostsFragment;
case 3 -> mediaFragment;
case 4 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@@ -627,18 +725,33 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
actionButton.setClickable(!visible);
}
private void setNotifyProgressVisible(boolean visible){
notifyButton.setTextVisible(!visible);
notifyProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
notifyButton.setClickable(!visible);
}
private void loadAccountInfoAndEnterEditMode(){
if(editModeLoading)
return;
editModeLoading=true;
setActionProgressVisible(true);
new GetOwnAccount()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
editModeLoading=false;
if(getActivity()==null)
return;
enterEditMode(result);
setActionProgressVisible(false);
}
@Override
public void onError(ErrorResponse error){
editModeLoading=false;
if(getActivity()==null)
return;
error.showToast(getActivity());
setActionProgressVisible(false);
}
@@ -653,9 +766,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.done);
pager.setCurrentItem(3);
pager.setCurrentItem(4);
ArrayList<Animator> animators=new ArrayList<>();
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false);
}
@@ -696,7 +809,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
}
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
@@ -714,7 +827,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
@@ -776,15 +889,42 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return false;
}
private List<Attachment> createFakeAttachments(String url, Drawable drawable){
Attachment att=new Attachment();
att.type=Attachment.Type.IMAGE;
att.url=url;
att.meta=new Attachment.Metadata();
att.meta.width=drawable.getIntrinsicWidth();
att.meta.height=drawable.getIntrinsicHeight();
return Collections.singletonList(att);
}
private void onNotifyButtonClick(View v) {
UiUtils.performToggleAccountNotifications(getActivity(), account, accountID, relationship, actionButton, this::setNotifyProgressVisible, this::updateRelationship);
}
private void onAvatarClick(View v){
if(isInEditMode){
startImagePicker(AVATAR_RESULT);
}else{
Drawable ava=avatar.getDrawable();
if(ava==null)
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
private void onCoverClick(View v){
if(isInEditMode){
startImagePicker(COVER_RESULT);
}else{
Drawable drawable=cover.getDrawable();
if(drawable==null || drawable instanceof ColorDrawable)
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}
@@ -823,6 +963,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
scrollView.smoothScrollTo(0, 0);
}
private void onFollowersOrFollowingClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Class<? extends Fragment> cls;
if(v.getId()==R.id.followers_btn)
cls=FollowerListFragment.class;
else if(v.getId()==R.id.following_btn)
cls=FollowingListFragment.class;
else
return;
Nav.go(getActivity(), cls, args);
}
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
@@ -854,7 +1008,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public int getItemCount(){
return loaded ? 4 : 0;
return loaded ? tabViews.length : 0;
}
@Override

View File

@@ -13,6 +13,8 @@ public interface ScrollableToTop{
* @param list
*/
default void smoothScrollRecyclerViewToTop(RecyclerView list){
if(list==null) // TODO find out why this happens because it should not be possible
return;
if(list.getChildCount()>0 && list.getChildAdapterPosition(list.getChildAt(0))>10){
list.scrollToPosition(0);
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){

View File

@@ -1,42 +1,53 @@
package org.joinmastodon.android.fragments;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
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.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 java.util.ArrayList;
import java.util.function.Consumer;
@@ -48,13 +59,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SettingsFragment extends ToolbarFragment{
public class SettingsFragment extends MastodonToolbarFragment{
private UsableRecyclerView list;
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
@@ -64,6 +74,7 @@ public class SettingsFragment extends ToolbarFragment{
private PushSubscription pushSubscription;
private ImageView themeTransitionWindowView;
private TextItem checkForUpdateItem;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -74,9 +85,22 @@ public class SettingsFragment extends ToolbarFragment{
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
GithubSelfUpdater.UpdateState state=updater.getState();
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
items.add(new UpdateItem());
}
}
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 SwitchItem(R.string.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 HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
@@ -87,6 +111,28 @@ public class SettingsFragment extends ToolbarFragment{
GlobalUserPreferences.useCustomTabs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
GlobalUserPreferences.showInteractionCounts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.home_timeline));
items.add(new SwitchItem(R.string.settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
GlobalUserPreferences.showBoosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
GlobalUserPreferences.loadNewPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
@@ -98,11 +144,15 @@ public class SettingsFragment extends ToolbarFragment{
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_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
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 RedHeaderItem(R.string.settings_spicy));
if (GithubSelfUpdater.needSelfUpdating()) {
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
items.add(new TextItem(R.string.settings_contribute_fork, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/LucasGGamerM/moshidon")));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
@@ -132,7 +182,7 @@ public class SettingsFragment extends ToolbarFragment{
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
// Add 32dp gaps between sections
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
outRect.top=V.dp(32);
}
});
@@ -151,17 +201,37 @@ public class SettingsFragment extends ToolbarFragment{
@Override
public void onDestroy(){
super.onDestroy();
if(needUpdateNotificationSettings){
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if(GithubSelfUpdater.needSelfUpdating())
E.register(this);
}
@Override
public void onDestroyView(){
super.onDestroyView();
if(GithubSelfUpdater.needSelfUpdating())
E.unregister(this);
}
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
GlobalUserPreferences.theme=theme;
GlobalUserPreferences.save();
restartActivityToApplyNewTheme();
}
private void onColorPreferenceClick(GlobalUserPreferences.ColorPreference color){
GlobalUserPreferences.color=color;
GlobalUserPreferences.save();
restartActivityToApplyNewTheme();
}
private void onTrueBlackThemeChanged(SwitchItem item){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
@@ -240,8 +310,7 @@ public class SettingsFragment extends ToolbarFragment{
}
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
index++;
while(items.get(index) instanceof SwitchItem){
SwitchItem si=(SwitchItem) items.get(index);
while(items.get(index) instanceof SwitchItem si){
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
if(holder!=null)
@@ -283,9 +352,9 @@ public class SettingsFragment extends ToolbarFragment{
private void onLoggedOut(){
AccountSessionManager.getInstance().removeAccount(accountID);
getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
getActivity().finish();
}
private void clearImageCache(){
@@ -296,6 +365,30 @@ public class SettingsFragment extends ToolbarFragment{
});
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING;
if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind();
UpdateItem updateItem = null;
if(items.get(0) instanceof UpdateItem item0) {
updateItem = item0;
} else if (ev.state != GithubSelfUpdater.UpdateState.CHECKING
&& ev.state != GithubSelfUpdater.UpdateState.NO_UPDATE) {
updateItem = new UpdateItem();
items.add(0, updateItem);
list.setAdapter(new SettingsAdapter());
}
if(updateItem != null && list.findViewHolderForAdapterPosition(0) instanceof UpdateViewHolder uvh){
uvh.bind(updateItem);
}
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
}
}
private static abstract class Item{
public abstract int getViewType();
}
@@ -341,6 +434,13 @@ public class SettingsFragment extends ToolbarFragment{
}
}
public class ColorPicker extends Item{
@Override
public int getViewType(){
return 8;
}
}
private static class ThemeItem extends Item{
@Override
@@ -360,10 +460,16 @@ public class SettingsFragment extends ToolbarFragment{
private class TextItem extends Item{
private String text;
private Runnable onClick;
private boolean loading;
public TextItem(@StringRes int text, Runnable onClick){
public TextItem(@StringRes int text, Runnable onClick) {
this(text, onClick, false);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
this.text=getString(text);
this.onClick=onClick;
this.loading=loading;
}
@Override
@@ -397,6 +503,14 @@ public class SettingsFragment extends ToolbarFragment{
}
}
private class UpdateItem extends Item{
@Override
public int getViewType(){
return 7;
}
}
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
@NonNull
@Override
@@ -410,6 +524,8 @@ public class SettingsFragment extends ToolbarFragment{
case 4 -> new TextViewHolder();
case 5 -> new HeaderViewHolder(true);
case 6 -> new FooterViewHolder();
case 7 -> new UpdateViewHolder();
case 8 -> new ColorPickerViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@@ -436,7 +552,7 @@ public class SettingsFragment extends ToolbarFragment{
super(getActivity(), R.layout.item_settings_header, list);
text=(TextView) itemView;
if(red)
text.setTextColor(getResources().getColor(R.color.error_700));
text.setTextColor(getResources().getColor(UiUtils.isDarkTheme() ? R.color.error_400 : R.color.error_700));
}
@Override
@@ -514,7 +630,8 @@ public class SettingsFragment extends ToolbarFragment{
private class SubitemHolder{
public TextView text;
public ImageView icon, checkbox;
public ImageView icon;
public RadioButton checkbox;
public SubitemHolder(View view){
text=view.findViewById(R.id.text);
@@ -529,14 +646,76 @@ public class SettingsFragment extends ToolbarFragment{
public void bind(int text, int icon, boolean checked){
this.text.setText(text);
this.icon.setImageResource(icon);
checkbox.setSelected(checked);
checkbox.setChecked(checked);
}
public void setChecked(boolean checked){
checkbox.setSelected(checked);
checkbox.setChecked(checked);
}
}
}
private class ColorPickerViewHolder extends BindableViewHolder<ColorPicker>{
private final Button button;
private final PopupMenu popupMenu;
private final ImageView icon;
@SuppressLint("ClickableViewAccessibility")
public ColorPickerViewHolder(){
super(getActivity(), R.layout.item_settings_color_picker, list);
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.setOnMenuItemClickListener(item->{
GlobalUserPreferences.ColorPreference pref;
int id=item.getItemId();
if(id==R.id.pink_color) {
pref = GlobalUserPreferences.ColorPreference.PINK;
onColorPreferenceClick(pref);
}
else if(id==R.id.purple_color) {
pref = GlobalUserPreferences.ColorPreference.PURPLE;
onColorPreferenceClick(pref);
}
else if(id==R.id.green_color) {
pref = GlobalUserPreferences.ColorPreference.GREEN;
onColorPreferenceClick(pref);
}
else if(id==R.id.blue_color) {
pref = GlobalUserPreferences.ColorPreference.BLUE;
onColorPreferenceClick(pref);
}
else if(id==R.id.orange_color) {
pref = GlobalUserPreferences.ColorPreference.ORANGE;
onColorPreferenceClick(pref);
}
else if(id==R.id.yellow_color) {
pref = GlobalUserPreferences.ColorPreference.YELLOW;
onColorPreferenceClick(pref);
}
else
return false;
return true;
});
// UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
}
@Override
public void onBind(ColorPicker item){
icon.setImageResource(R.drawable.ic_color_theme_preference);
button.setText(switch(GlobalUserPreferences.color){
case PINK -> R.string.pink_color;
case PURPLE -> R.string.purple_color;
case GREEN -> R.string.green_color;
case BLUE -> R.string.blue_color;
case ORANGE -> R.string.orange_color;
case YELLOW -> R.string.yellow_color;
});
}
}
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
private final Button button;
@@ -582,14 +761,18 @@ public class SettingsFragment extends ToolbarFragment{
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
private final ProgressBar progress;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text=(TextView) itemView;
text = itemView.findViewById(R.id.text);
progress = itemView.findViewById(R.id.progress);
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
progress.animate().alpha(item.loading ? 1 : 0);
}
@Override
@@ -610,4 +793,75 @@ public class SettingsFragment extends ToolbarFragment{
text.setText(item.text);
}
}
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
private final TextView text;
private final Button button;
private final ImageButton cancelBtn;
private final ProgressBar progress;
private ObjectAnimator rotationAnimator;
private Runnable progressUpdater=this::updateProgress;
public UpdateViewHolder(){
super(getActivity(), R.layout.item_settings_update, list);
text=findViewById(R.id.text);
button=findViewById(R.id.button);
cancelBtn=findViewById(R.id.cancel_btn);
progress=findViewById(R.id.progress);
button.setOnClickListener(v->{
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
switch(updater.getState()){
case UPDATE_AVAILABLE -> updater.downloadUpdate();
case DOWNLOADED -> updater.installUpdate(getActivity());
}
});
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
rotationAnimator.setInterpolator(new LinearInterpolator());
rotationAnimator.setDuration(1500);
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
}
@Override
public void onBind(UpdateItem item){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
GithubSelfUpdater.UpdateState state=updater.getState();
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
text.setText(getString(R.string.update_available, info.version));
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
}else{
text.setText(getString(R.string.update_ready, info.version));
button.setText(R.string.install_update);
}
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
rotationAnimator.start();
button.setVisibility(View.INVISIBLE);
cancelBtn.setVisibility(View.VISIBLE);
progress.setVisibility(View.VISIBLE);
updateProgress();
}else{
rotationAnimator.cancel();
button.setVisibility(View.VISIBLE);
cancelBtn.setVisibility(View.GONE);
progress.setVisibility(View.GONE);
progress.removeCallbacks(progressUpdater);
}
}
private void updateProgress(){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
return;
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
if(Build.VERSION.SDK_INT>=24)
progress.setProgress(value, true);
else
progress.setProgress(value);
progress.postDelayed(progressUpdater, 1000);
}
}
}

View File

@@ -0,0 +1,157 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEditHistoryFragment extends StatusListFragment{
private String id;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id=getArguments().getString("id");
loadData();
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.edit_history);
}
@Override
protected void doLoadData(int offset, int count){
new GetStatusEditHistory(id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
onDataLoaded(result, false);
}
})
.exec(accountID);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
String action="";
if(idx==data.size()-1){
action=getString(R.string.edit_original_post);
}else{
enum StatusEditChangeType{
TEXT_CHANGED,
SPOILER_ADDED,
SPOILER_REMOVED,
SPOILER_CHANGED,
POLL_ADDED,
POLL_REMOVED,
POLL_CHANGED,
MEDIA_ADDED,
MEDIA_REMOVED,
MEDIA_REORDERED,
MARKED_SENSITIVE,
MARKED_NOT_SENSITIVE
}
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
Status prev=data.get(idx+1);
if(!Objects.equals(s.content, prev.content)){
changes.add(StatusEditChangeType.TEXT_CHANGED);
}
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
if(s.spoilerText==null){
changes.add(StatusEditChangeType.SPOILER_REMOVED);
}else if(prev.spoilerText==null){
changes.add(StatusEditChangeType.SPOILER_ADDED);
}else{
changes.add(StatusEditChangeType.SPOILER_CHANGED);
}
}
if(s.poll!=null || prev.poll!=null){
if(s.poll==null){
changes.add(StatusEditChangeType.POLL_REMOVED);
}else if(prev.poll==null){
changes.add(StatusEditChangeType.POLL_ADDED);
}else if(!s.poll.id.equals(prev.poll.id)){
changes.add(StatusEditChangeType.POLL_CHANGED);
}
}
List<String> newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
List<String> prevAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
boolean addedOrRemoved=false;
if(!newAttachmentIDs.containsAll(prevAttachmentIDs)){
changes.add(StatusEditChangeType.MEDIA_REMOVED);
addedOrRemoved=true;
}
if(!prevAttachmentIDs.containsAll(newAttachmentIDs)){
changes.add(StatusEditChangeType.MEDIA_ADDED);
addedOrRemoved=true;
}
if(!addedOrRemoved && !newAttachmentIDs.equals(prevAttachmentIDs)){
changes.add(StatusEditChangeType.MEDIA_REORDERED);
}
if(s.sensitive && !prev.sensitive){
changes.add(StatusEditChangeType.MARKED_SENSITIVE);
}else if(prev.sensitive && !s.sensitive){
changes.add(StatusEditChangeType.MARKED_NOT_SENSITIVE);
}
if(changes.size()==1){
action=getString(switch(changes.iterator().next()){
case TEXT_CHANGED -> R.string.edit_text_edited;
case SPOILER_ADDED -> R.string.edit_spoiler_added;
case SPOILER_REMOVED -> R.string.edit_spoiler_removed;
case SPOILER_CHANGED -> R.string.edit_spoiler_edited;
case POLL_ADDED -> R.string.edit_poll_added;
case POLL_REMOVED -> R.string.edit_poll_removed;
case POLL_CHANGED -> R.string.edit_poll_edited;
case MEDIA_ADDED -> R.string.edit_media_added;
case MEDIA_REMOVED -> R.string.edit_media_removed;
case MEDIA_REORDERED -> R.string.edit_media_reordered;
case MARKED_SENSITIVE -> R.string.edit_marked_sensitive;
case MARKED_NOT_SENSITIVE -> R.string.edit_marked_not_sensitive;
});
}else{
action=getString(R.string.edit_multiple_changed);
}
}
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null));
}
return items;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
@Override
public boolean isItemEnabled(String id){
return false;
}
}

View File

@@ -6,17 +6,21 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
@@ -61,6 +65,59 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusUpdated(StatusUpdatedEvent ev){
ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
for(int i=0;i<data.size();i++){
Status s=data.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
s.reblog=ev.status;
statusesForDisplayItems.add(s);
}else if(s.id.equals(ev.status.id)){
data.set(i, ev.status);
statusesForDisplayItems.add(ev.status);
}
}
for(int i=0;i<preloadedData.size();i++){
Status s=preloadedData.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
s.reblog=ev.status;
}else if(s.id.equals(ev.status.id)){
preloadedData.set(i, ev.status);
}
}
if(statusesForDisplayItems.isEmpty())
return;
for(Status s:statusesForDisplayItems){
int i=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(s.id)){
int start=i;
for(;i<displayItems.size();i++){
if(!displayItems.get(i).parentID.equals(s.id))
break;
}
List<StatusDisplayItem> postItems=displayItems.subList(start, i);
postItems.clear();
postItems.addAll(buildDisplayItems(s));
int oldSize=i-start, newSize=postItems.size();
if(oldSize==newSize){
adapter.notifyItemRangeChanged(start, newSize);
}else if(oldSize<newSize){
adapter.notifyItemRangeChanged(start, oldSize);
adapter.notifyItemRangeInserted(start+oldSize, newSize-oldSize);
}else{
adapter.notifyItemRangeChanged(start, newSize);
adapter.notifyItemRangeRemoved(start+newSize, oldSize-newSize);
}
break;
}
i++;
}
}
}
protected Status getContentStatusByID(String id){
Status s=getStatusByID(id);
return s==null ? null : s.getContentStatus();
@@ -80,6 +137,40 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return null;
}
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return false;
}
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
List<Status> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(s->s.account.id.equals(ev.postsByAccountID) || (s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID)))
.collect(Collectors.toList());
for(Status s:toRemove){
removeStatus(s);
}
}
protected void removeStatus(Status 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);
}
public class EventListener{
@Subscribe
@@ -89,18 +180,17 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
s.update(ev);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder && ((FooterStatusDisplayItem.Holder) holder).getItem().status==s.getContentStatus()){
((FooterStatusDisplayItem.Holder) holder).rebind();
return;
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
footer.rebind();
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
footer.rebind();
}
}
return;
}
}
for(Status s:preloadedData){
if(s.id.equals(ev.id)){
s.update(ev);
return;
}
}
}
@@ -112,26 +202,21 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
Status status=getStatusByID(ev.id);
if(status==null)
return;
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
removeStatus(status);
}
@Subscribe
public void onStatusCreated(StatusCreatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
StatusListFragment.this.onStatusCreated(ev);
}
@Subscribe
public void onStatusUpdated(StatusUpdatedEvent ev){
StatusListFragment.this.onStatusUpdated(ev);
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
@@ -143,5 +228,14 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
}
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID))
return;
if(ev.isUnfollow && !shouldRemoveAccountPostsWhenUnfollowing())
return;
StatusListFragment.this.onRemoveAccountPostsEvent(ev);
}
}
}

View File

@@ -11,6 +11,8 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -41,11 +43,14 @@ public class ThreadFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
if(s==mainStatus){
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
if(item instanceof TextStatusDisplayItem)
((TextStatusDisplayItem) item).textSelectable=true;
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
}
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
}
return items;
}
@@ -56,6 +61,8 @@ public class ThreadFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if(getActivity()==null)
return;
if(refreshing){
data.clear();
displayItems.clear();
@@ -64,7 +71,8 @@ public class ThreadFragment extends StatusListFragment{
}
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
footerProgress.setVisibility(View.GONE);
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
int prevCount=displayItems.size();
onAppendItems(result.descendants);
@@ -89,7 +97,7 @@ public class ThreadFragment extends StatusListFragment{
return statuses;
return statuses.stream().filter(status->{
for(Filter filter:filters){
if(filter.matches(status.getContentStatus().content))
if(filter.matches(status))
return false;
}
return true;

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.model.Account;
import org.parceler.Parcels;
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
protected Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct);
}
}

View File

@@ -0,0 +1,395 @@
package org.joinmastodon.android.fragments.account_list;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseAccountListFragment.AccountItem>{
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
public BaseAccountListFragment(){
super(40);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
}
@Override
protected void onDataLoaded(List<AccountItem> d, boolean more){
if(refreshing){
relationships.clear();
}
loadRelationships(d);
super.onDataLoaded(d, more);
}
@Override
public void onRefresh(){
for(APIRequest<?> req:relationshipsRequests){
req.cancel();
}
relationshipsRequests.clear();
super.onRefresh();
}
protected void loadRelationships(List<AccountItem> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req);
req.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequests.remove(req);
for(Relationship rel:result){
relationships.put(rel.id, rel);
}
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof AccountViewHolder avh){
avh.bindRelationship();
}
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequests.remove(req);
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter getAdapter(){
return new AccountsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// list.setPadding(0, V.dp(16), 0, V.dp(16));
list.setClipToPadding(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
updateToolbar();
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
}
@CallSuper
protected void updateToolbar(){
Toolbar toolbar=getToolbar();
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
toolbar.setNavigationContentDescription(R.string.back);
if(hasSubtitle()){
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
toolbar.setTitleTextColor(color);
toolbar.setSubtitleTextColor(color);
}
}
}
protected boolean hasSubtitle(){
return true;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, V.dp(16), 0, V.dp(16));
}
super.onApplyWindowInsets(insets);
}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
}
@Override
public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getImageCountForItem(int position){
return data.get(position).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountItem item=data.get(position);
return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1);
}
}
protected class AccountViewHolder extends BindableViewHolder<AccountItem> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username;
private final ImageView avatar;
private final Button button;
private final PopupMenu contextMenu;
private final View menuAnchor;
public AccountViewHolder(){
super(getActivity(), R.layout.item_account_list, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
avatar=findViewById(R.id.avatar);
button=findViewById(R.id.button);
menuAnchor=findViewById(R.id.menu_anchor);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
button.setOnClickListener(this::onButtonClick);
contextMenu=new PopupMenu(getActivity(), menuAnchor);
contextMenu.inflate(R.menu.profile);
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
}
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
bindRelationship();
}
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(rel, button);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@Override
public boolean onLongClick(){
return false;
}
@Override
public boolean onLongClick(float x, float y){
Relationship relationship=relationships.get(item.account.id);
if(relationship==null)
return false;
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
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()));
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.lists_with_user, account.getDisplayUsername()));
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
if(relationship.following){
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
hideBoosts.setVisible(true);
}else{
hideBoosts.setVisible(false);
}
MenuItem blockDomain=menu.findItem(R.id.block_domain);
if(!account.isLocal()){
blockDomain.setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
blockDomain.setVisible(true);
}else{
blockDomain.setVisible(false);
}
menuAnchor.setTranslationX(x);
menuAnchor.setTranslationY(y);
contextMenu.show();
return true;
}
private void onButtonClick(View v){
ProgressDialog progress=new ProgressDialog(getActivity());
progress.setMessage(getString(R.string.loading));
progress.setCancelable(false);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
itemView.setHasTransientState(progressShown);
if(progressShown)
progress.show();
else
progress.dismiss();
}, result->{
relationships.put(item.account.id, result);
bindRelationship();
});
}
private boolean onContextMenuItemSelected(MenuItem item){
Relationship relationship=relationships.get(this.item.account.id);
if(relationship==null)
return false;
Account account=this.item.account;
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
bindRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(AccountViewHolder.this.item.account.id, result);
bindRelationship();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
private void updateRelationship(Relationship r){
relationships.put(item.account.id, r);
bindRelationship();
}
}
protected static class AccountItem{
public final Account account;
public final ImageLoaderRequest avaRequest;
public final CustomEmojiHelper emojiHelper;
public final CharSequence parsedName;
public AccountItem(Account account){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
}
}

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowers;
import org.joinmastodon.android.model.Account;
public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@Override
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
return new GetAccountFollowers(account.id, maxID, count);
}
}

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