Compare commits

...

806 Commits

Author SHA1 Message Date
Grishka
645af12c3f Merge branch 'l10n_master' 2023-09-12 02:32:38 +03:00
Grishka
fadc42d72b New version 2023-09-12 02:32:23 +03:00
Eugen Rochko
fc831e7d42 New translations strings.xml (Portuguese, Brazilian) 2023-09-11 22:36:20 +02:00
Eugen Rochko
2998ee9145 New translations strings.xml (Finnish) 2023-09-11 20:09:41 +02:00
Eugen Rochko
971c4e5879 New translations strings.xml (Finnish) 2023-09-11 19:00:28 +02:00
Eugen Rochko
b396ee7987 New translations strings.xml (Indonesian) 2023-09-10 15:07:27 +02:00
Eugen Rochko
0f803cd4fa New translations strings.xml (Finnish) 2023-09-09 20:30:25 +02:00
Eugen Rochko
167a14b8db New translations strings.xml (Finnish) 2023-09-09 19:29:29 +02:00
Eugen Rochko
81cbc2d10c New translations strings.xml (Ukrainian) 2023-09-09 15:40:28 +02:00
Eugen Rochko
9bd8aff99b New translations strings.xml (Finnish) 2023-09-09 12:08:15 +02:00
Eugen Rochko
a770828165 New translations strings.xml (Finnish) 2023-09-09 11:09:40 +02:00
Eugen Rochko
ab457035ff New translations strings.xml (Finnish) 2023-09-09 08:34:27 +02:00
Grishka
f886e4c1d2 Fix #658, fix #620 2023-09-09 03:39:27 +03:00
Eugen Rochko
be73c9e81c New translations strings.xml (Basque) 2023-09-07 02:28:42 +02:00
Eugen Rochko
1c2183bf1a New translations strings.xml (Slovenian) 2023-09-06 22:29:03 +02:00
Gregory K
1789d90dc3 Merge pull request #685 from LucasGGamerM/mastodon-android
fix(editing-alt-text): fix small oversight on editing existing attachments without alt text
2023-09-06 01:42:41 +03:00
LucasGGamerM
57306ff7fe fix(editing-alt-text): fix small oversight on editing existing attachments without alt text
This makes the implementation hopefully bug free
2023-09-05 19:37:12 -03:00
Eugen Rochko
f0eb6573f4 New translations strings.xml (Portuguese, Brazilian) 2023-09-04 08:10:24 +02:00
Eugen Rochko
e7f5dd3357 New translations strings.xml (Bengali) 2023-09-04 07:10:06 +02:00
Eugen Rochko
8101bb9ea1 New translations strings.xml (Portuguese, Brazilian) 2023-09-04 07:10:05 +02:00
Eugen Rochko
228fdc8ffe New translations strings.xml (Swedish) 2023-09-03 11:26:37 +02:00
Eugen Rochko
e9df125cde New translations strings.xml (Swedish) 2023-09-01 16:10:32 +02:00
Gregory K
16ef577a7a Merge pull request #678 from LucasGGamerM/mastodon-android
fix: fix alt texts not being able to be edited
2023-08-31 20:31:39 +03:00
LucasGGamerM
734b3bced6 fix: fix alt texts not being able to be edited
fixes #70 cc: @sk22
2023-08-31 14:18:32 -03:00
Gregory K
5f6f3c94c9 Merge pull request #677 from tinsukE/gap-local-filter
When loading gap posts, apply filters before building display items.
2023-08-31 15:06:07 +03:00
Angelo Suzuki
09ba42a974 When loading gap posts, apply filters before building display items.
This will make sure that items that are filtered out don't show up on the interface.
Fixes #675
2023-08-31 14:01:58 +02:00
Eugen Rochko
d76e823489 New translations strings.xml (Italian) 2023-08-29 11:52:38 +02:00
Eugen Rochko
900b204bb0 New translations strings.xml (Portuguese, Brazilian) 2023-08-27 16:51:00 +02:00
Eugen Rochko
17d679901a New translations strings.xml (Japanese) 2023-08-27 15:42:52 +02:00
Eugen Rochko
d8036779f8 New translations strings.xml (Japanese) 2023-08-27 14:46:15 +02:00
Eugen Rochko
3f6bda28b3 New translations strings.xml (Vietnamese) 2023-08-26 17:43:48 +02:00
Eugen Rochko
c0b4f4dd79 New translations strings.xml (Vietnamese) 2023-08-26 16:06:27 +02:00
Eugen Rochko
f36aee44c6 New translations strings.xml (Scottish Gaelic) 2023-08-25 09:54:23 +02:00
Eugen Rochko
cd24526a9d New translations strings.xml (Swedish) 2023-08-24 17:48:37 +02:00
Eugen Rochko
a345ac1390 New translations strings.xml (Icelandic) 2023-08-24 17:48:36 +02:00
Eugen Rochko
3d987b8e1d New translations strings.xml (Thai) 2023-08-23 21:39:07 +02:00
Eugen Rochko
57043912e0 New translations strings.xml (Chinese Traditional) 2023-08-23 19:16:40 +02:00
Eugen Rochko
00aef5ea6b New translations full_description.txt (Arabic) 2023-08-23 13:02:31 +02:00
Eugen Rochko
369b69668c New translations strings.xml (Arabic) 2023-08-23 13:02:29 +02:00
Eugen Rochko
65245f4560 New translations strings.xml (Arabic) 2023-08-23 09:39:50 +02:00
Eugen Rochko
4d4fdc97d4 New translations strings.xml (French) 2023-08-23 09:39:49 +02:00
Eugen Rochko
c96577891c New translations strings.xml (Scottish Gaelic) 2023-08-23 07:00:18 +02:00
Eugen Rochko
f48b2fc9cb New translations strings.xml (Thai) 2023-08-23 07:00:13 +02:00
Eugen Rochko
2fca2580ed New translations strings.xml (Bengali) 2023-08-23 07:00:12 +02:00
Eugen Rochko
7adc1da361 New translations strings.xml (Persian) 2023-08-23 07:00:11 +02:00
Eugen Rochko
6dc24dde43 New translations strings.xml (Indonesian) 2023-08-23 07:00:10 +02:00
Eugen Rochko
4929e0e6ec New translations strings.xml (Portuguese, Brazilian) 2023-08-23 07:00:09 +02:00
Eugen Rochko
16a8b8ed71 New translations strings.xml (Icelandic) 2023-08-23 07:00:08 +02:00
Eugen Rochko
ad1412817e New translations strings.xml (Galician) 2023-08-23 07:00:07 +02:00
Eugen Rochko
d9e6bb3bea New translations strings.xml (Vietnamese) 2023-08-23 07:00:06 +02:00
Eugen Rochko
8970404638 New translations strings.xml (Chinese Traditional) 2023-08-23 07:00:05 +02:00
Eugen Rochko
2a2241d7f9 New translations strings.xml (Chinese Simplified) 2023-08-23 07:00:04 +02:00
Eugen Rochko
db1a47e8eb New translations strings.xml (Ukrainian) 2023-08-23 07:00:03 +02:00
Eugen Rochko
3e57061cef New translations strings.xml (Turkish) 2023-08-23 07:00:02 +02:00
Eugen Rochko
cd200f8450 New translations strings.xml (Slovenian) 2023-08-23 07:00:00 +02:00
Eugen Rochko
782013079f New translations strings.xml (Russian) 2023-08-23 06:59:59 +02:00
Eugen Rochko
e5db8acd66 New translations strings.xml (Polish) 2023-08-23 06:59:57 +02:00
Eugen Rochko
1a6a8019c8 New translations strings.xml (Norwegian) 2023-08-23 06:59:57 +02:00
Eugen Rochko
e935eef29f New translations strings.xml (Dutch) 2023-08-23 06:59:56 +02:00
Eugen Rochko
381defda51 New translations strings.xml (Japanese) 2023-08-23 06:59:54 +02:00
Eugen Rochko
02ae80c204 New translations strings.xml (Italian) 2023-08-23 06:59:53 +02:00
Eugen Rochko
82214b30e8 New translations strings.xml (Armenian) 2023-08-23 06:59:52 +02:00
Eugen Rochko
33a1f48602 New translations strings.xml (Greek) 2023-08-23 06:59:48 +02:00
Eugen Rochko
aee845e5cc New translations strings.xml (German) 2023-08-23 06:59:47 +02:00
Eugen Rochko
cd780f6006 New translations strings.xml (Danish) 2023-08-23 06:59:46 +02:00
Eugen Rochko
d4741fefa0 New translations strings.xml (Czech) 2023-08-23 06:59:46 +02:00
Eugen Rochko
7e1e8a2616 New translations strings.xml (Belarusian) 2023-08-23 06:59:44 +02:00
Eugen Rochko
d73c05cdfc New translations strings.xml (Arabic) 2023-08-23 06:59:43 +02:00
Eugen Rochko
78323023cb New translations strings.xml (Spanish) 2023-08-23 06:59:42 +02:00
Eugen Rochko
2cf084c98f New translations strings.xml (French) 2023-08-23 06:59:41 +02:00
Grishka
e5bdeba1d7 Fix strings 2023-08-23 00:44:40 +03:00
Grishka
8d7db7774f Merge branch 'l10n_master' 2023-08-23 00:40:45 +03:00
Grishka
78d22c670c Fix reporting 2023-08-23 00:40:38 +03:00
Eugen Rochko
0a679109f5 New translations strings.xml (Arabic) 2023-08-22 17:14:41 +02:00
Eugen Rochko
e843142b7e New translations strings.xml (Arabic) 2023-08-22 13:15:25 +02:00
Eugen Rochko
72e728f655 New translations strings.xml (French) 2023-08-22 13:15:23 +02:00
Eugen Rochko
ef56792f56 New translations strings.xml (Arabic) 2023-08-22 11:39:27 +02:00
Eugen Rochko
504a6959e8 New translations strings.xml (French) 2023-08-22 11:39:25 +02:00
Eugen Rochko
b8e3060887 New translations strings.xml (Arabic) 2023-08-21 21:00:00 +02:00
Eugen Rochko
1aa1ede421 New translations strings.xml (Arabic) 2023-08-21 20:03:05 +02:00
Eugen Rochko
480dba7629 New translations strings.xml (Arabic) 2023-08-21 19:05:39 +02:00
Eugen Rochko
9b9c66a149 New translations strings.xml (Arabic) 2023-08-21 17:43:00 +02:00
Eugen Rochko
0f5eb923ee New translations strings.xml (Arabic) 2023-08-21 16:13:06 +02:00
Eugen Rochko
90ed28e7a0 New translations strings.xml (French) 2023-08-20 17:34:05 +01:00
Eugen Rochko
d2b45c1c84 New translations strings.xml (Swedish) 2023-08-20 12:43:05 +01:00
Eugen Rochko
a119ba5f80 New translations strings.xml (Danish) 2023-08-19 17:08:25 +01:00
Eugen Rochko
8c1191a08f New translations strings.xml (Danish) 2023-08-19 16:09:27 +01:00
Eugen Rochko
4275d596e6 New translations strings.xml (Thai) 2023-08-19 12:46:09 +01:00
Eugen Rochko
cc83f2baf3 New translations strings.xml (Danish) 2023-08-18 01:51:02 +01:00
Eugen Rochko
728496b831 New translations strings.xml (Danish) 2023-08-18 00:06:12 +01:00
Eugen Rochko
bbc99162c6 New translations strings.xml (Danish) 2023-08-17 16:47:55 +01:00
Eugen Rochko
eed3af9e3e New translations strings.xml (Arabic) 2023-08-16 22:54:50 +02:00
Eugen Rochko
50187ff376 New translations strings.xml (Arabic) 2023-08-16 21:48:52 +02:00
Eugen Rochko
5f30919fb4 New translations strings.xml (Arabic) 2023-08-16 18:38:58 +02:00
Eugen Rochko
14c3cfac85 New translations strings.xml (French) 2023-08-16 15:50:45 +02:00
Eugen Rochko
e978f02765 New translations strings.xml (French) 2023-08-16 14:09:02 +02:00
Eugen Rochko
8d877c480f New translations strings.xml (French) 2023-08-16 12:20:32 +02:00
Eugen Rochko
c53efee9a1 New translations strings.xml (Thai) 2023-08-15 22:47:32 +02:00
Eugen Rochko
148c461e86 New translations strings.xml (Danish) 2023-08-15 16:40:55 +02:00
Eugen Rochko
fcadb9883d New translations strings.xml (Danish) 2023-08-15 14:59:58 +02:00
Eugen Rochko
bb6491e10a New translations full_description.txt (Danish) 2023-08-15 14:59:57 +02:00
Eugen Rochko
6248ccf376 New translations full_description.txt (Danish) 2023-08-15 13:53:21 +02:00
Eugen Rochko
c9e08f36fa New translations full_description.txt (Danish) 2023-08-15 03:19:07 +02:00
Eugen Rochko
10b95d753b New translations full_description.txt (Danish) 2023-08-15 02:15:33 +02:00
Eugen Rochko
c3989083cf New translations strings.xml (Portuguese) 2023-08-14 15:33:01 +02:00
Eugen Rochko
01db585094 New translations strings.xml (Basque) 2023-08-14 01:42:56 +02:00
Eugen Rochko
cc67cb330c New translations full_description.txt (Basque) 2023-08-14 00:10:18 +02:00
Eugen Rochko
52ed3c5a04 New translations strings.xml (Basque) 2023-08-14 00:10:17 +02:00
Eugen Rochko
5976f6230a New translations strings.xml (Chinese Simplified) 2023-08-13 18:58:06 +02:00
Eugen Rochko
3553f03a95 New translations strings.xml (Slovenian) 2023-08-13 11:06:06 +02:00
Eugen Rochko
d6e2d889c3 New translations strings.xml (Slovenian) 2023-08-12 16:02:40 +02:00
Eugen Rochko
a777b3b450 New translations strings.xml (Slovenian) 2023-08-12 14:07:49 +02:00
Eugen Rochko
9957efbea0 New translations strings.xml (Norwegian) 2023-08-12 01:06:29 +02:00
Eugen Rochko
22e7b9730f New translations strings.xml (Vietnamese) 2023-08-11 17:50:37 +02:00
Eugen Rochko
91470b8509 New translations strings.xml (Vietnamese) 2023-08-11 16:21:15 +02:00
Eugen Rochko
c9d5327328 New translations strings.xml (Russian) 2023-08-11 12:41:37 +02:00
Eugen Rochko
1aa61b72e5 New translations strings.xml (Russian) 2023-08-11 10:45:11 +02:00
Eugen Rochko
3ca5edc3fc New translations strings.xml (Russian) 2023-08-11 09:19:15 +02:00
Eugen Rochko
a092ebaeb3 New translations strings.xml (Russian) 2023-08-11 08:03:02 +02:00
Eugen Rochko
5b9e84c255 New translations strings.xml (Russian) 2023-08-11 07:07:00 +02:00
Eugen Rochko
9c058b926f New translations strings.xml (Russian) 2023-08-10 12:30:16 +02:00
Eugen Rochko
4f2d2ae6e8 New translations strings.xml (Russian) 2023-08-10 11:10:22 +02:00
Eugen Rochko
75aa26a018 New translations strings.xml (Russian) 2023-08-09 14:50:26 +02:00
Eugen Rochko
0f795254e5 New translations strings.xml (Russian) 2023-08-09 13:28:03 +02:00
Eugen Rochko
33592f0a83 New translations strings.xml (Swedish) 2023-08-07 12:43:38 +02:00
Eugen Rochko
d6fd01eaca New translations strings.xml (Swedish) 2023-08-07 10:56:51 +02:00
Eugen Rochko
ee6e0ff26c New translations strings.xml (Russian) 2023-08-04 19:26:18 +02:00
Eugen Rochko
4d9574bf38 New translations strings.xml (Russian) 2023-08-04 17:51:30 +02:00
Eugen Rochko
813be9a2be New translations strings.xml (Russian) 2023-08-04 16:30:43 +02:00
Eugen Rochko
cc76ebfafb New translations strings.xml (Russian) 2023-08-04 14:58:02 +02:00
Eugen Rochko
7989ee0243 New translations strings.xml (Russian) 2023-08-04 13:58:08 +02:00
Eugen Rochko
3aa1997cfd New translations strings.xml (Russian) 2023-08-04 12:51:54 +02:00
Grishka
c3da15552e Merge branch 'l10n_master' 2023-08-03 17:22:04 +03:00
Grishka
a014fe9443 Fix media layout with unknown sizes 2023-08-03 17:21:40 +03:00
Eugen Rochko
92551d4ca3 New translations strings.xml (Portuguese, Brazilian) 2023-08-03 03:27:17 +02:00
Eugen Rochko
8010858e85 New translations strings.xml (Portuguese, Brazilian) 2023-08-03 02:26:22 +02:00
Eugen Rochko
4efb4875b0 New translations strings.xml (Swedish) 2023-08-02 09:16:24 +02:00
Eugen Rochko
c5d041e46d New translations strings.xml (Portuguese, Brazilian) 2023-08-01 23:11:09 +02:00
Eugen Rochko
53c2223aae New translations strings.xml (Portuguese, Brazilian) 2023-08-01 21:25:07 +02:00
Eugen Rochko
25034ac0ae New translations strings.xml (Portuguese, Brazilian) 2023-08-01 01:41:52 +02:00
Eugen Rochko
ac9de72b75 New translations strings.xml (Portuguese, Brazilian) 2023-08-01 00:42:41 +02:00
Eugen Rochko
1f48ad93f2 New translations strings.xml (Portuguese, Brazilian) 2023-07-31 23:46:36 +02:00
Eugen Rochko
38f7f7aa00 New translations strings.xml (Czech) 2023-07-31 14:32:20 +02:00
Eugen Rochko
fe8175c63a New translations strings.xml (French) 2023-07-30 19:39:54 +02:00
Eugen Rochko
2d9e01bbc1 New translations strings.xml (French) 2023-07-30 18:44:39 +02:00
Eugen Rochko
022a227b08 New translations strings.xml (Turkish) 2023-07-28 00:29:16 +02:00
Eugen Rochko
a2228259f1 New translations full_description.txt (Turkish) 2023-07-27 23:33:53 +02:00
Eugen Rochko
a61af7c56f New translations strings.xml (Turkish) 2023-07-27 23:33:52 +02:00
Eugen Rochko
5d6a646976 New translations strings.xml (French) 2023-07-27 23:33:51 +02:00
Eugen Rochko
628d0d7492 New translations strings.xml (French) 2023-07-27 22:05:11 +02:00
Eugen Rochko
b76c8745ec New translations strings.xml (Norwegian) 2023-07-25 22:57:26 +02:00
Eugen Rochko
51e67bc441 New translations strings.xml (Norwegian) 2023-07-25 21:52:29 +02:00
Gregory K
8887f75b70 Merge pull request #655 from nilathedragon/patch-1
Do not assume languages array will contain entries
2023-07-25 19:12:56 +03:00
Nila
9436a838c0 Do not assume languages array will contain entries 2023-07-25 17:28:41 +02:00
Eugen Rochko
ef120fa36f New translations strings.xml (Swedish) 2023-07-25 00:55:54 +02:00
Eugen Rochko
8c6385e2c5 New translations strings.xml (Swedish) 2023-07-24 23:57:42 +02:00
Eugen Rochko
0bd85d9905 New translations strings.xml (Persian) 2023-07-24 13:40:53 +02:00
Eugen Rochko
ce0dab7b28 New translations strings.xml (Czech) 2023-07-24 09:55:45 +02:00
Eugen Rochko
ddcc5670ce New translations strings.xml (Bengali) 2023-07-22 17:48:06 +02:00
Eugen Rochko
86afa184e2 New translations strings.xml (Bengali) 2023-07-22 16:26:35 +02:00
Eugen Rochko
77f341f139 New translations strings.xml (German) 2023-07-22 12:27:40 +02:00
Eugen Rochko
918b5d99c2 New translations strings.xml (Swedish) 2023-07-21 22:09:32 +02:00
Eugen Rochko
7a098d6eff New translations strings.xml (Swedish) 2023-07-21 21:06:04 +02:00
Eugen Rochko
71f81283f5 New translations strings.xml (Polish) 2023-07-20 13:51:45 +02:00
Eugen Rochko
058c7c3c33 New translations strings.xml (Danish) 2023-07-20 10:31:41 +02:00
Eugen Rochko
870e33879b New translations strings.xml (Indonesian) 2023-07-19 07:48:32 +02:00
Eugen Rochko
3ca82bdfc5 New translations strings.xml (Japanese) 2023-07-19 05:08:28 +02:00
Eugen Rochko
4721bad286 New translations short_description.txt (Armenian) 2023-07-18 16:30:06 +02:00
Eugen Rochko
f040cf2f07 New translations full_description.txt (Armenian) 2023-07-18 16:30:05 +02:00
Eugen Rochko
8d50717c90 New translations strings.xml (Armenian) 2023-07-18 16:30:04 +02:00
Eugen Rochko
2512ad3c95 New translations strings.xml (Armenian) 2023-07-18 14:49:00 +02:00
Eugen Rochko
bc7e007634 New translations strings.xml (Swedish) 2023-07-18 12:03:42 +02:00
Eugen Rochko
1f3c87e0c7 New translations strings.xml (Swedish) 2023-07-18 10:31:42 +02:00
Grishka
4bb255e0bb Fix #619 2023-07-17 20:25:08 +03:00
Eugen Rochko
73e08faee9 New translations strings.xml (Persian) 2023-07-13 20:45:58 +02:00
Eugen Rochko
02dc7711e4 New translations strings.xml (Persian) 2023-07-13 19:38:29 +02:00
Eugen Rochko
67b4d80e5b New translations strings.xml (Spanish) 2023-07-13 16:08:11 +02:00
Eugen Rochko
5168d2bb39 New translations strings.xml (Spanish) 2023-07-13 14:59:49 +02:00
Eugen Rochko
57190a75bf New translations strings.xml (Indonesian) 2023-07-13 11:54:32 +02:00
Eugen Rochko
f10e865895 New translations strings.xml (Indonesian) 2023-07-13 10:48:50 +02:00
Grishka
91b4dc412b Merge branch 'l10n_master' 2023-07-13 05:59:52 +03:00
Eugen Rochko
c896fd8df8 New translations strings.xml (Persian) 2023-07-10 20:31:28 +02:00
Eugen Rochko
72cd987284 New translations strings.xml (Persian) 2023-07-10 19:33:53 +02:00
Eugen Rochko
d868d05080 New translations strings.xml (Persian) 2023-07-10 18:35:13 +02:00
Eugen Rochko
9dfb039a69 New translations strings.xml (Persian) 2023-07-10 17:14:05 +02:00
Eugen Rochko
f2920e877b New translations strings.xml (Persian) 2023-07-10 14:23:55 +02:00
Eugen Rochko
ecb0b3f9d7 New translations strings.xml (Persian) 2023-07-10 13:27:29 +02:00
Eugen Rochko
67c128be69 New translations strings.xml (Persian) 2023-07-09 20:48:43 +02:00
Eugen Rochko
1ecbbc2d4b New translations strings.xml (Ukrainian) 2023-07-09 19:53:27 +02:00
Eugen Rochko
82c481b014 New translations strings.xml (Persian) 2023-07-09 19:53:26 +02:00
Eugen Rochko
e7da6d7897 New translations full_description.txt (Ukrainian) 2023-07-09 18:55:26 +02:00
Eugen Rochko
e9f1e3038b New translations strings.xml (Ukrainian) 2023-07-09 18:55:25 +02:00
Eugen Rochko
a591096819 New translations strings.xml (Ukrainian) 2023-07-09 17:59:09 +02:00
Eugen Rochko
f1ef60475f New translations strings.xml (Ukrainian) 2023-07-09 14:35:38 +02:00
Eugen Rochko
ce75bb3984 New translations strings.xml (Russian) 2023-07-08 15:46:20 +02:00
Eugen Rochko
d59235e04c New translations strings.xml (Ukrainian) 2023-07-08 13:47:53 +02:00
Eugen Rochko
42f8f7e58f New translations strings.xml (Ukrainian) 2023-07-08 12:50:49 +02:00
Eugen Rochko
f9cbc9ae27 New translations strings.xml (Ukrainian) 2023-07-08 11:54:51 +02:00
Eugen Rochko
a50d6599bf New translations strings.xml (Ukrainian) 2023-07-08 10:56:13 +02:00
Eugen Rochko
cc578b496e New translations strings.xml (Vietnamese) 2023-07-08 09:39:17 +02:00
Eugen Rochko
662944d246 New translations strings.xml (Ukrainian) 2023-07-08 09:39:16 +02:00
Eugen Rochko
42810df4a5 New translations full_description.txt (Ukrainian) 2023-07-08 08:40:55 +02:00
Eugen Rochko
c3a058d2e1 New translations strings.xml (Vietnamese) 2023-07-08 08:40:54 +02:00
Eugen Rochko
ce5d835ae5 New translations strings.xml (Ukrainian) 2023-07-08 08:40:53 +02:00
Eugen Rochko
60dd561729 New translations strings.xml (Vietnamese) 2023-07-08 07:25:45 +02:00
Eugen Rochko
08c9f9ad7d New translations strings.xml (Russian) 2023-07-08 03:34:48 +02:00
Eugen Rochko
d47907906d New translations strings.xml (Ukrainian) 2023-07-07 23:24:33 +02:00
Eugen Rochko
935f0f6e05 New translations strings.xml (Ukrainian) 2023-07-07 22:27:12 +02:00
Eugen Rochko
ac0b21d574 New translations strings.xml (Ukrainian) 2023-07-07 21:27:12 +02:00
Eugen Rochko
9b5d05369f New translations strings.xml (Ukrainian) 2023-07-07 20:30:29 +02:00
Eugen Rochko
489f9f5e59 New translations strings.xml (Ukrainian) 2023-07-07 19:30:49 +02:00
Eugen Rochko
e62f7c23c9 New translations full_description.txt (Ukrainian) 2023-07-07 18:29:20 +02:00
Eugen Rochko
91ed7d49b5 New translations strings.xml (Ukrainian) 2023-07-07 18:29:19 +02:00
Eugen Rochko
c9e9abd811 New translations strings.xml (Ukrainian) 2023-07-07 17:11:15 +02:00
Eugen Rochko
b1e5023f62 New translations strings.xml (Ukrainian) 2023-07-07 16:10:53 +02:00
Eugen Rochko
604690b3f5 New translations strings.xml (Japanese) 2023-07-07 11:50:09 +02:00
Eugen Rochko
13ada6ecc6 New translations strings.xml (French) 2023-07-07 10:47:28 +02:00
Eugen Rochko
b897eb913e New translations short_description.txt (Turkish) 2023-07-06 22:08:52 +02:00
Eugen Rochko
c25602a650 New translations full_description.txt (Turkish) 2023-07-06 22:08:51 +02:00
Eugen Rochko
2defc9af3f New translations strings.xml (Turkish) 2023-07-06 22:08:50 +02:00
Eugen Rochko
446525389b New translations strings.xml (Turkish) 2023-07-06 21:11:01 +02:00
Eugen Rochko
756b30d04f New translations strings.xml (Turkish) 2023-07-06 20:15:49 +02:00
Eugen Rochko
51ec842815 New translations strings.xml (German) 2023-07-06 20:15:48 +02:00
Eugen Rochko
c38822849e New translations strings.xml (German) 2023-07-06 19:01:48 +02:00
Eugen Rochko
3c69201f67 New translations strings.xml (Chinese Simplified) 2023-07-06 19:01:46 +02:00
Eugen Rochko
ed9d701406 New translations strings.xml (Turkish) 2023-07-06 17:29:14 +02:00
Eugen Rochko
e70c5aa2e9 New translations strings.xml (Chinese Simplified) 2023-07-06 17:29:13 +02:00
Eugen Rochko
0c4589b257 New translations strings.xml (Turkish) 2023-07-06 15:04:45 +02:00
Eugen Rochko
84d08392fb New translations strings.xml (Turkish) 2023-07-06 14:03:25 +02:00
Eugen Rochko
8ff117308d New translations strings.xml (Turkish) 2023-07-06 13:06:03 +02:00
Eugen Rochko
b6c703adbc New translations strings.xml (Bengali) 2023-07-05 18:26:29 +02:00
Eugen Rochko
22e6934de5 New translations strings.xml (Scottish Gaelic) 2023-07-05 17:21:45 +02:00
Eugen Rochko
1b8a1d69ac New translations strings.xml (Bengali) 2023-07-05 17:21:43 +02:00
Eugen Rochko
b6ae83937b New translations strings.xml (Turkish) 2023-07-05 16:09:20 +02:00
Eugen Rochko
7115556663 New translations strings.xml (Turkish) 2023-07-05 15:08:07 +02:00
Eugen Rochko
cb3296661e New translations strings.xml (French) 2023-07-05 09:09:16 +02:00
Eugen Rochko
6dd20a6df9 New translations strings.xml (French) 2023-07-05 07:58:16 +02:00
Eugen Rochko
71c1d0e59a New translations strings.xml (French) 2023-07-04 21:49:47 +02:00
Eugen Rochko
2b275e1ff7 New translations strings.xml (Japanese) 2023-07-04 15:23:25 +02:00
Eugen Rochko
a7fcae1033 New translations strings.xml (Greek) 2023-07-04 11:27:42 +02:00
Grishka
19bd189b33 🤔 2023-07-04 02:53:48 +03:00
Grishka
2d5089c047 Crash fix 2023-07-04 02:51:50 +03:00
Grishka
be7469bd54 Merge branch 'l10n_master' 2023-07-04 01:36:27 +03:00
Grishka
146d8daa6e Add editorconfig
So that PRs like #625 don't happen again
2023-07-04 01:05:55 +03:00
Eugen Rochko
f3928d9e09 New translations strings.xml (Scottish Gaelic) 2023-07-03 19:38:09 +02:00
Eugen Rochko
d4090d459d New translations strings.xml (Greek) 2023-07-03 17:49:55 +02:00
Eugen Rochko
7dd7554c08 New translations strings.xml (Icelandic) 2023-07-03 15:01:45 +02:00
Eugen Rochko
9de9a1d97d New translations strings.xml (Icelandic) 2023-07-03 13:53:20 +02:00
Eugen Rochko
04ee366fbe New translations strings.xml (Icelandic) 2023-07-03 12:41:02 +02:00
Eugen Rochko
c8784150fc New translations strings.xml (Icelandic) 2023-07-03 11:41:40 +02:00
Eugen Rochko
7b7bccb37a New translations full_description.txt (Japanese) 2023-07-03 10:26:59 +02:00
Eugen Rochko
84e2636bca New translations strings.xml (Japanese) 2023-07-03 10:26:58 +02:00
Eugen Rochko
dc73613b56 New translations strings.xml (Japanese) 2023-07-03 09:22:45 +02:00
Eugen Rochko
fd8868ef4d New translations strings.xml (Japanese) 2023-07-03 08:20:20 +02:00
Eugen Rochko
127df0b8e0 New translations strings.xml (Japanese) 2023-07-03 07:24:53 +02:00
Eugen Rochko
c2989df902 New translations strings.xml (Japanese) 2023-07-03 06:27:42 +02:00
Eugen Rochko
3f9ee99b69 New translations strings.xml (Japanese) 2023-07-03 05:11:26 +02:00
Eugen Rochko
7204c4e804 New translations strings.xml (Greek) 2023-07-02 23:45:59 +02:00
Eugen Rochko
f1131cf8e7 New translations strings.xml (Belarusian) 2023-07-02 23:45:58 +02:00
Eugen Rochko
5b9a8beb07 New translations strings.xml (Belarusian) 2023-07-02 22:43:38 +02:00
Eugen Rochko
9e18e35c66 New translations strings.xml (Turkish) 2023-07-02 21:45:11 +02:00
Eugen Rochko
9db3dfa955 New translations strings.xml (Polish) 2023-07-02 21:45:11 +02:00
Eugen Rochko
4a3e56d300 New translations strings.xml (Polish) 2023-07-02 20:27:01 +02:00
Eugen Rochko
3bb4125c50 New translations strings.xml (Japanese) 2023-07-02 19:31:37 +02:00
Eugen Rochko
ce58883618 New translations strings.xml (Japanese) 2023-07-02 18:03:47 +02:00
Grishka
e7d3c60bac Remove unused code that caused a crash for some users ¯\_(ツ)_/¯ 2023-07-02 18:52:46 +03:00
Eugen Rochko
08e90139ad New translations strings.xml (Japanese) 2023-07-02 16:35:17 +02:00
Eugen Rochko
0d4dc34453 New translations strings.xml (Chinese Simplified) 2023-07-02 16:35:16 +02:00
Grishka
fe142c4626 Pass the system timezone to server when signing up 2023-07-02 16:35:19 +03:00
Grishka
d8dfa6017d Make the default server configurable 2023-07-02 16:22:32 +03:00
Eugen Rochko
b7a5d4296b New translations strings.xml (Greek) 2023-07-02 11:19:25 +02:00
Grishka
85d4c1fc24 A bunch of crash fixes 2023-07-02 12:03:21 +03:00
Grishka
66489d79be Fix #611 2023-07-02 11:04:20 +03:00
Grishka
30a66a26c6 Minor fixes 2023-07-02 10:50:01 +03:00
Grishka
fbc3081e68 Fix #615 2023-07-02 09:54:53 +03:00
Eugen Rochko
7a66c94907 New translations full_description.txt (Chinese Simplified) 2023-07-02 06:12:35 +02:00
Eugen Rochko
4e38bc5769 New translations strings.xml (Chinese Simplified) 2023-07-02 06:12:34 +02:00
Eugen Rochko
0dc428dbd6 New translations full_description.txt (Chinese Simplified) 2023-07-02 04:57:11 +02:00
Eugen Rochko
6ac7fc94ea New translations full_description.txt (Indonesian) 2023-07-02 01:46:05 +02:00
Eugen Rochko
af28ed1783 New translations strings.xml (Thai) 2023-07-01 15:02:48 +02:00
Eugen Rochko
00daf084f2 New translations strings.xml (Italian) 2023-07-01 15:02:47 +02:00
Eugen Rochko
5518848e28 New translations strings.xml (Thai) 2023-07-01 13:40:15 +02:00
Eugen Rochko
6ded856b2f New translations strings.xml (Thai) 2023-07-01 12:30:46 +02:00
Eugen Rochko
d302f5132e New translations strings.xml (Italian) 2023-07-01 12:30:44 +02:00
Eugen Rochko
012e29ee3a New translations strings.xml (Greek) 2023-07-01 12:30:43 +02:00
Eugen Rochko
dbe9579d7f New translations strings.xml (Italian) 2023-07-01 11:28:20 +02:00
Eugen Rochko
c8d0221d9b New translations strings.xml (Greek) 2023-07-01 11:28:19 +02:00
Eugen Rochko
885c663d93 New translations strings.xml (Greek) 2023-07-01 10:06:44 +02:00
Grishka
3b399d5815 Merge branch 'l10n_master'
# Conflicts:
#	mastodon/src/main/res/values-ar-rSA/strings.xml
#	mastodon/src/main/res/values-be-rBY/strings.xml
#	mastodon/src/main/res/values-bn-rBD/strings.xml
#	mastodon/src/main/res/values-bs-rBA/strings.xml
#	mastodon/src/main/res/values-ca-rES/strings.xml
#	mastodon/src/main/res/values-cs-rCZ/strings.xml
#	mastodon/src/main/res/values-da-rDK/strings.xml
#	mastodon/src/main/res/values-de-rDE/strings.xml
#	mastodon/src/main/res/values-el-rGR/strings.xml
#	mastodon/src/main/res/values-es-rES/strings.xml
#	mastodon/src/main/res/values-eu-rES/strings.xml
#	mastodon/src/main/res/values-fa-rIR/strings.xml
#	mastodon/src/main/res/values-fil-rPH/strings.xml
#	mastodon/src/main/res/values-fr-rFR/strings.xml
#	mastodon/src/main/res/values-gd-rGB/strings.xml
#	mastodon/src/main/res/values-gl-rES/strings.xml
#	mastodon/src/main/res/values-hr-rHR/strings.xml
#	mastodon/src/main/res/values-hu-rHU/strings.xml
#	mastodon/src/main/res/values-hy-rAM/strings.xml
#	mastodon/src/main/res/values-in-rID/strings.xml
#	mastodon/src/main/res/values-is-rIS/strings.xml
#	mastodon/src/main/res/values-it-rIT/strings.xml
#	mastodon/src/main/res/values-iw-rIL/strings.xml
#	mastodon/src/main/res/values-ja-rJP/strings.xml
#	mastodon/src/main/res/values-kab/strings.xml
#	mastodon/src/main/res/values-ko-rKR/strings.xml
#	mastodon/src/main/res/values-my-rMM/strings.xml
#	mastodon/src/main/res/values-nl-rNL/strings.xml
#	mastodon/src/main/res/values-no-rNO/strings.xml
#	mastodon/src/main/res/values-oc-rFR/strings.xml
#	mastodon/src/main/res/values-pl-rPL/strings.xml
#	mastodon/src/main/res/values-pt-rBR/strings.xml
#	mastodon/src/main/res/values-pt-rPT/strings.xml
#	mastodon/src/main/res/values-ro-rRO/strings.xml
#	mastodon/src/main/res/values-ru-rRU/strings.xml
#	mastodon/src/main/res/values-si-rLK/strings.xml
#	mastodon/src/main/res/values-sl-rSI/strings.xml
#	mastodon/src/main/res/values-sv-rSE/strings.xml
#	mastodon/src/main/res/values-th-rTH/strings.xml
#	mastodon/src/main/res/values-tr-rTR/strings.xml
#	mastodon/src/main/res/values-uk-rUA/strings.xml
#	mastodon/src/main/res/values-vi-rVN/strings.xml
#	mastodon/src/main/res/values-zh-rCN/strings.xml
#	mastodon/src/main/res/values-zh-rTW/strings.xml
2023-06-30 20:37:37 +03:00
Grishka
9f18d1bc8b Bump version 2023-06-30 20:35:42 +03:00
Grishka
9a8cf61e38 More minor fixes
closes #612
2023-06-30 20:28:52 +03:00
Grishka
eb822282c0 Minor fixes 2023-06-30 09:18:48 +03:00
Grishka
273823a65f Fix #610 2023-06-30 08:25:05 +03:00
Grishka
f2aa1400c5 Fix crash 2023-06-29 22:31:15 +03:00
Grishka
cf8a9e1823 Fix #609 2023-06-29 22:29:51 +03:00
Grishka
b3320d534b Fix alert color 2023-06-29 22:24:35 +03:00
Grishka
f58b4c2989 Fix store screenshot generator 2023-06-29 22:17:59 +03:00
Eugen Rochko
47b3b1e307 New translations strings.xml (Hungarian) 2023-06-29 14:26:58 +02:00
Eugen Rochko
e5649c4a42 New translations strings.xml (Czech) 2023-06-29 14:26:57 +02:00
Eugen Rochko
cea77eca02 New translations strings.xml (Czech) 2023-06-29 13:11:45 +02:00
Eugen Rochko
4fbbcfba59 New translations strings.xml (Czech) 2023-06-29 12:06:31 +02:00
Eugen Rochko
6a24f70537 New translations strings.xml (Hungarian) 2023-06-29 11:09:23 +02:00
Eugen Rochko
4681160924 New translations strings.xml (Czech) 2023-06-29 11:09:22 +02:00
Eugen Rochko
6f8c2f4a44 New translations strings.xml (Hungarian) 2023-06-29 10:05:05 +02:00
Eugen Rochko
69de9dce38 New translations strings.xml (Italian) 2023-06-28 23:16:22 +02:00
Eugen Rochko
721ae9c68d New translations strings.xml (Thai) 2023-06-28 21:11:38 +02:00
Eugen Rochko
6a6ed89d29 New translations strings.xml (Thai) 2023-06-28 20:08:17 +02:00
Eugen Rochko
3e4377a366 New translations strings.xml (Italian) 2023-06-28 20:08:16 +02:00
Gregory K
287de66e0c Merge pull request #608 from sk22/fix/polls-update-content-status
Fix poll not updating content status when boosted
2023-06-28 18:29:12 +03:00
sk
df2ae9d964 fix poll not updating status when boosted
closes sk22#403
2023-06-28 17:24:46 +02:00
Eugen Rochko
ef55f1f49b New translations strings.xml (Thai) 2023-06-27 21:03:47 +02:00
Eugen Rochko
aebf7e9f1f New translations strings.xml (Thai) 2023-06-27 19:57:32 +02:00
Eugen Rochko
a014ce6eb5 New translations strings.xml (Thai) 2023-06-27 19:00:48 +02:00
Eugen Rochko
aed1efceb9 New translations strings.xml (Thai) 2023-06-27 17:58:06 +02:00
Eugen Rochko
124c375b14 New translations strings.xml (Thai) 2023-06-27 16:58:01 +02:00
Eugen Rochko
1d46e22a7f New translations strings.xml (Dutch) 2023-06-27 16:58:00 +02:00
Eugen Rochko
28c334429d New translations strings.xml (Czech) 2023-06-27 16:57:59 +02:00
Eugen Rochko
4726f98d4f New translations strings.xml (Thai) 2023-06-27 15:47:31 +02:00
Eugen Rochko
985f382436 New translations strings.xml (Thai) 2023-06-27 14:36:42 +02:00
Eugen Rochko
f822c788b0 New translations strings.xml (Armenian) 2023-06-27 13:38:29 +02:00
Eugen Rochko
46f8982aa6 New translations strings.xml (Vietnamese) 2023-06-27 05:57:29 +02:00
Eugen Rochko
7b3cec9289 New translations strings.xml (Vietnamese) 2023-06-27 04:48:02 +02:00
Eugen Rochko
a3f227cb8d New translations strings.xml (Thai) 2023-06-26 22:51:56 +02:00
Eugen Rochko
d71feb2cfc New translations strings.xml (Thai) 2023-06-26 21:47:41 +02:00
Grishka
b69565e9e6 Fix polls 2023-06-26 16:09:02 +03:00
Grishka
bf996feccf Add fading edge for alt text
closes #595
2023-06-26 16:04:24 +03:00
Eugen Rochko
b1143b0eec New translations strings.xml (Slovenian) 2023-06-26 08:30:13 +02:00
Eugen Rochko
ce977163c2 New translations strings.xml (Slovenian) 2023-06-26 07:28:02 +02:00
Eugen Rochko
ded74bda83 New translations strings.xml (Chinese Traditional) 2023-06-26 04:01:11 +02:00
Eugen Rochko
7180113397 New translations strings.xml (Chinese Traditional) 2023-06-26 02:37:02 +02:00
Grishka
81ac8a3bc9 More minor fixes 2023-06-25 14:24:13 +03:00
Grishka
4b74da5d38 Fix another color 2023-06-25 09:50:44 +03:00
Eugen Rochko
0375cfa260 New translations short_description.txt (Vietnamese) 2023-06-25 05:27:54 +02:00
Eugen Rochko
52108a675a New translations full_description.txt (Arabic) 2023-06-25 05:26:57 +02:00
Grishka
406e95d3f7 Fix #606 2023-06-25 06:19:36 +03:00
Grishka
a9f355dea9 Fix text colors 2023-06-25 03:12:17 +03:00
Eugen Rochko
1a63c12327 New translations strings.xml (Urdu (India)) 2023-06-25 02:07:40 +02:00
Eugen Rochko
7bc2f8b352 New translations strings.xml (Kabyle) 2023-06-25 02:07:39 +02:00
Eugen Rochko
d56ef227a7 New translations strings.xml (Igbo) 2023-06-25 02:07:38 +02:00
Eugen Rochko
fcc371ab5a New translations strings.xml (Occitan) 2023-06-25 02:07:37 +02:00
Eugen Rochko
9f43a99772 New translations strings.xml (Scottish Gaelic) 2023-06-25 02:07:36 +02:00
Eugen Rochko
9fc5a4e390 New translations strings.xml (Sinhala) 2023-06-25 02:07:35 +02:00
Eugen Rochko
a2b3f873f6 New translations strings.xml (Bosnian) 2023-06-25 02:07:34 +02:00
Eugen Rochko
f6c1509e48 New translations strings.xml (Filipino) 2023-06-25 02:07:33 +02:00
Eugen Rochko
d9bbb32a28 New translations strings.xml (Burmese) 2023-06-25 02:07:32 +02:00
Eugen Rochko
d2ef6e77af New translations strings.xml (Hindi) 2023-06-25 02:07:31 +02:00
Eugen Rochko
6133e9bac3 New translations strings.xml (Croatian) 2023-06-25 02:07:30 +02:00
Eugen Rochko
6ca48f35f1 New translations strings.xml (Thai) 2023-06-25 02:07:29 +02:00
Eugen Rochko
cb016b4383 New translations strings.xml (Bengali) 2023-06-25 02:07:28 +02:00
Eugen Rochko
d0b21df28b New translations strings.xml (Portuguese, Brazilian) 2023-06-25 02:07:28 +02:00
Eugen Rochko
ce48ee888a New translations strings.xml (Icelandic) 2023-06-25 02:07:27 +02:00
Eugen Rochko
5cd8bc5a46 New translations strings.xml (Galician) 2023-06-25 02:07:26 +02:00
Eugen Rochko
4109cd75d3 New translations strings.xml (Vietnamese) 2023-06-25 02:07:25 +02:00
Eugen Rochko
97a889e019 New translations strings.xml (Chinese Traditional) 2023-06-25 02:07:24 +02:00
Eugen Rochko
b9a1b3591d New translations strings.xml (Ukrainian) 2023-06-25 02:07:22 +02:00
Eugen Rochko
fde84e3cfb New translations strings.xml (Turkish) 2023-06-25 02:07:21 +02:00
Eugen Rochko
0985eb4fac New translations strings.xml (Swedish) 2023-06-25 02:07:21 +02:00
Eugen Rochko
5957c1a221 New translations strings.xml (Slovenian) 2023-06-25 02:07:20 +02:00
Eugen Rochko
e7e34aa2c8 New translations strings.xml (Russian) 2023-06-25 02:07:19 +02:00
Eugen Rochko
b8742591b8 New translations strings.xml (Portuguese) 2023-06-25 02:07:17 +02:00
Eugen Rochko
50e73ac12e New translations strings.xml (Polish) 2023-06-25 02:07:16 +02:00
Eugen Rochko
9ed277a9b2 New translations strings.xml (Norwegian) 2023-06-25 02:07:15 +02:00
Eugen Rochko
c9e2984d68 New translations strings.xml (Dutch) 2023-06-25 02:07:14 +02:00
Eugen Rochko
00aaff10a7 New translations strings.xml (Korean) 2023-06-25 02:07:13 +02:00
Eugen Rochko
6da4256adf New translations strings.xml (Japanese) 2023-06-25 02:07:12 +02:00
Eugen Rochko
da9c826791 New translations strings.xml (Italian) 2023-06-25 02:07:11 +02:00
Eugen Rochko
bed02e248e New translations strings.xml (Armenian) 2023-06-25 02:07:11 +02:00
Eugen Rochko
f53531889f New translations strings.xml (Hungarian) 2023-06-25 02:07:10 +02:00
Eugen Rochko
ef3246ae2a New translations strings.xml (Hebrew) 2023-06-25 02:07:09 +02:00
Eugen Rochko
5504b534f7 New translations strings.xml (Irish) 2023-06-25 02:07:08 +02:00
Eugen Rochko
0e553d7868 New translations strings.xml (Finnish) 2023-06-25 02:07:07 +02:00
Eugen Rochko
38be51367e New translations strings.xml (Basque) 2023-06-25 02:07:06 +02:00
Eugen Rochko
0d4af0970c New translations strings.xml (Greek) 2023-06-25 02:07:05 +02:00
Eugen Rochko
b51335ffd6 New translations strings.xml (German) 2023-06-25 02:07:04 +02:00
Eugen Rochko
444fa07984 New translations strings.xml (Danish) 2023-06-25 02:07:03 +02:00
Eugen Rochko
feb0f304fb New translations strings.xml (Catalan) 2023-06-25 02:07:02 +02:00
Eugen Rochko
ccd4a1aa9f New translations strings.xml (Belarusian) 2023-06-25 02:07:01 +02:00
Eugen Rochko
b3723c2977 New translations strings.xml (Arabic) 2023-06-25 02:07:00 +02:00
Eugen Rochko
c5b70a9ada New translations strings.xml (Spanish) 2023-06-25 02:06:59 +02:00
Eugen Rochko
82789179e7 New translations strings.xml (French) 2023-06-25 02:06:58 +02:00
Eugen Rochko
b0a309a817 New translations strings.xml (Romanian) 2023-06-25 02:06:57 +02:00
Eugen Rochko
32a27b6e59 New translations strings.xml (Chinese Simplified) 2023-06-25 02:06:56 +02:00
Eugen Rochko
f73a318dad New translations strings.xml (Czech) 2023-06-25 02:06:55 +02:00
Eugen Rochko
4fff2c5f5c New translations strings.xml (Persian) 2023-06-25 02:06:54 +02:00
Eugen Rochko
81abac657f New translations strings.xml (Indonesian) 2023-06-25 02:06:53 +02:00
Grishka
74decd3ec7 Merge branch 'm3_redesign' 2023-06-25 02:55:26 +03:00
Grishka
11d17d1f3f Fix ripple color again 2023-06-25 02:52:54 +03:00
Grishka
ca2384ba8c Remove unused resources and fix ripple colors 2023-06-25 02:41:45 +03:00
Grishka
ded23342db Fix layout in post edit history 2023-06-25 01:46:17 +03:00
Grishka
a35c14865f Refresh the notifications list when it's opened 2023-06-25 01:28:08 +03:00
Grishka
0952d97557 Unified account row 2023-06-25 01:18:38 +03:00
Grishka
e1db5f15ca M3 redesign: search/discover 2023-06-24 22:56:55 +03:00
Grishka
c9e467ac2f And again 2023-06-24 10:43:28 +03:00
Grishka
74a5e970d9 Fix #604 again 2023-06-23 23:07:28 +03:00
Eugen Rochko
935de7d02e New translations strings.xml (Chinese Simplified) 2023-06-23 13:54:49 +02:00
Grishka
a04522ff72 Fix #605 2023-06-20 22:56:37 +03:00
Grishka
90a93ffba6 Fix #604 2023-06-20 22:54:09 +03:00
Eugen Rochko
069c55d4b9 New translations strings.xml (Persian) 2023-06-17 20:19:46 +01:00
Eugen Rochko
1efaf4b605 New translations strings.xml (Persian) 2023-06-17 19:21:04 +01:00
Eugen Rochko
ff74516ce2 New translations strings.xml (Persian) 2023-06-17 17:57:39 +01:00
Eugen Rochko
aee4b7aaab New translations full_description.txt (Persian) 2023-06-17 16:23:41 +01:00
Eugen Rochko
1d94479bde New translations strings.xml (Persian) 2023-06-17 16:23:40 +01:00
Eugen Rochko
85c95d899e New translations full_description.txt (Persian) 2023-06-17 15:24:06 +01:00
Eugen Rochko
18d4210e7d New translations strings.xml (Persian) 2023-06-17 15:24:05 +01:00
Eugen Rochko
ac25bc6d42 New translations strings.xml (Persian) 2023-06-17 14:17:46 +01:00
Eugen Rochko
ec05ef3b4c New translations full_description.txt (Persian) 2023-06-16 21:57:49 +01:00
Eugen Rochko
9827d97374 New translations strings.xml (Persian) 2023-06-16 21:57:48 +01:00
Eugen Rochko
4fab92d516 New translations strings.xml (Persian) 2023-06-16 20:43:12 +01:00
Eugen Rochko
44bc9c4e40 New translations strings.xml (Persian) 2023-06-16 19:47:01 +01:00
Eugen Rochko
1030773ef6 New translations strings.xml (Persian) 2023-06-16 18:50:21 +01:00
Eugen Rochko
1a0cb4b8c8 New translations strings.xml (Persian) 2023-06-16 17:47:36 +01:00
Eugen Rochko
4295a3672c New translations strings.xml (Persian) 2023-06-16 16:32:57 +01:00
Eugen Rochko
fd2a8fe230 New translations strings.xml (Persian) 2023-06-16 15:14:44 +01:00
Eugen Rochko
e2d1eccfb9 New translations strings.xml (Persian) 2023-06-16 14:17:09 +01:00
Eugen Rochko
05d1d3e725 New translations full_description.txt (Persian) 2023-06-15 10:57:44 +01:00
Eugen Rochko
37261928c2 New translations full_description.txt (Persian) 2023-06-15 09:56:31 +01:00
Eugen Rochko
63132110d9 New translations full_description.txt (Persian) 2023-06-15 08:46:22 +01:00
Eugen Rochko
ffd7e415a2 New translations full_description.txt (Persian) 2023-06-15 07:42:12 +01:00
Eugen Rochko
d43664d018 New translations full_description.txt (Persian) 2023-06-15 06:36:15 +01:00
Eugen Rochko
8e06362ff8 New translations full_description.txt (Persian) 2023-06-14 20:11:35 +01:00
Eugen Rochko
625ccfd31f New translations strings.xml (Persian) 2023-06-14 20:11:34 +01:00
Eugen Rochko
d7b5d242ff New translations strings.xml (Persian) 2023-06-14 19:12:25 +01:00
Eugen Rochko
7973c87b9a New translations strings.xml (Persian) 2023-06-14 18:16:52 +01:00
Eugen Rochko
3085b1507b New translations strings.xml (Persian) 2023-06-14 16:46:46 +01:00
Eugen Rochko
f17ef17492 New translations strings.xml (Persian) 2023-06-14 15:49:02 +01:00
Eugen Rochko
8d0a31d0f9 New translations strings.xml (Persian) 2023-06-14 15:47:38 +02:00
Eugen Rochko
6c6d3fed05 New translations strings.xml (Persian) 2023-06-14 14:49:35 +02:00
Eugen Rochko
b616e2e11b New translations strings.xml (Persian) 2023-06-14 13:42:47 +02:00
Eugen Rochko
b18771bb79 New translations full_description.txt (Persian) 2023-06-14 12:38:20 +02:00
Eugen Rochko
40ef4c179a New translations strings.xml (Persian) 2023-06-13 18:31:58 +02:00
Eugen Rochko
62212dc6c9 New translations strings.xml (Persian) 2023-06-13 17:23:01 +02:00
Eugen Rochko
3ed2b67037 New translations strings.xml (Czech) 2023-06-13 15:55:50 +02:00
Eugen Rochko
d240750606 New translations strings.xml (Persian) 2023-06-13 14:44:08 +02:00
Eugen Rochko
ed009d3e2e New translations strings.xml (Persian) 2023-06-13 13:15:59 +02:00
Eugen Rochko
be2fa0f217 New translations strings.xml (Persian) 2023-06-13 12:09:09 +02:00
Eugen Rochko
a8d9b4538b New translations strings.xml (Persian) 2023-06-12 22:41:37 +02:00
Eugen Rochko
2b7b3de043 New translations strings.xml (Persian) 2023-06-12 21:44:04 +02:00
Eugen Rochko
64362968fc New translations strings.xml (Persian) 2023-06-12 19:10:22 +02:00
Grishka
aa9caefed1 Fix unread notifications 2023-06-12 18:54:46 +03:00
Grishka
14bbe1ffef Update the update icon 2023-06-12 18:50:06 +03:00
Eugen Rochko
b6976fb519 New translations strings.xml (Persian) 2023-06-12 15:36:23 +02:00
Eugen Rochko
040237de2b New translations strings.xml (Persian) 2023-06-12 14:30:32 +02:00
Eugen Rochko
fc307ff43f New translations strings.xml (Persian) 2023-06-11 21:52:27 +02:00
Eugen Rochko
04304b3397 New translations strings.xml (Persian) 2023-06-11 20:49:08 +02:00
Eugen Rochko
7260db6668 New translations full_description.txt (Persian) 2023-06-11 19:41:48 +02:00
Eugen Rochko
1dadc51ddf New translations strings.xml (Persian) 2023-06-11 19:41:48 +02:00
Eugen Rochko
fe85351869 New translations strings.xml (Persian) 2023-06-11 18:34:16 +02:00
Eugen Rochko
74a83c6ac4 New translations strings.xml (Persian) 2023-06-11 17:37:10 +02:00
Eugen Rochko
df77ba61ad New translations strings.xml (Persian) 2023-06-11 15:12:30 +02:00
Eugen Rochko
ed40f74d59 New translations strings.xml (Persian) 2023-06-11 13:57:15 +02:00
Eugen Rochko
42e26bef68 New translations strings.xml (Persian) 2023-06-11 12:54:18 +02:00
Eugen Rochko
af60adb55f New translations strings.xml (Indonesian) 2023-06-11 05:16:01 +02:00
Eugen Rochko
5dfa9237ad New translations short_description.txt (Persian) 2023-06-10 21:44:47 +02:00
Eugen Rochko
c890195567 New translations strings.xml (Swedish) 2023-06-08 22:57:52 +02:00
Eugen Rochko
b50a327b17 New translations strings.xml (Swedish) 2023-06-08 17:47:06 +02:00
Grishka
17957b69d1 Filtered posts in timelines (AND-8) 2023-06-07 04:47:54 +03:00
Eugen Rochko
c4ac4ee173 New translations strings.xml (Swedish) 2023-06-06 13:23:46 +02:00
Eugen Rochko
659b4e2fcd New translations strings.xml (Swedish) 2023-06-06 11:55:59 +02:00
Eugen Rochko
02b1ad8d7a New translations strings.xml (Swedish) 2023-06-06 00:56:52 +02:00
Eugen Rochko
47eeb01b75 New translations strings.xml (Turkish) 2023-06-05 21:08:28 +02:00
Eugen Rochko
3d5fb2dfea New translations strings.xml (Swedish) 2023-06-05 15:00:47 +02:00
Eugen Rochko
ef6238b593 New translations strings.xml (Swedish) 2023-06-05 13:45:02 +02:00
Eugen Rochko
bc9bec3d66 New translations strings.xml (Swedish) 2023-06-05 12:47:38 +02:00
Grishka
a24b4363d7 Bring toolbar appearance more in line with design 2023-06-05 04:46:21 +03:00
Grishka
02ddad22e7 Update profile menu (AND-34) 2023-06-05 04:32:18 +03:00
Grishka
a705512dc5 Update post options menu (AND-33) 2023-06-05 04:25:11 +03:00
Grishka
cfd6954755 Update launcher icon (AND-84) 2023-06-05 02:34:58 +03:00
Grishka
702ac43f86 Fix shortcut icon (AND-85) 2023-06-05 01:45:23 +03:00
Grishka
6ede2d22bb Fix #591 2023-06-04 23:02:48 +03:00
Grishka
315d26ad52 Settings tweaks 2023-06-04 05:32:27 +03:00
Grishka
a78b0687f7 Fix it again 2023-06-04 04:50:40 +03:00
Grishka
148b8e9369 Fix #583 2023-06-04 02:40:59 +03:00
Grishka
ae6ce0f9b0 Fix #548 2023-06-04 02:11:40 +03:00
Grishka
31c8665653 Settings M3 redesign wip 2023-06-04 02:04:55 +03:00
Eugen Rochko
d7f73e02c5 New translations strings.xml (German) 2023-06-02 20:22:32 +02:00
Eugen Rochko
e897b3af57 New translations strings.xml (German) 2023-06-02 19:23:23 +02:00
Eugen Rochko
fdbf331432 New translations strings.xml (Bengali) 2023-06-02 18:01:03 +02:00
Eugen Rochko
aed86ac6f0 New translations strings.xml (Bengali) 2023-06-02 16:50:10 +02:00
Eugen Rochko
3a13d4d6c0 New translations strings.xml (Bengali) 2023-06-02 05:45:51 +02:00
Eugen Rochko
f5336564d0 New translations strings.xml (Bengali) 2023-06-02 04:39:21 +02:00
Eugen Rochko
07ca5a8b77 New translations strings.xml (Bengali) 2023-06-01 19:07:10 +02:00
Eugen Rochko
6926a212f4 New translations strings.xml (Bengali) 2023-06-01 17:46:40 +02:00
Eugen Rochko
936f39161b New translations strings.xml (German) 2023-05-31 20:11:48 +02:00
Grishka
7c6ec2e3d7 Fix #587 2023-05-30 21:32:08 +03:00
Grishka
31c7116a15 Merge branch 'master' into m3_redesign
# Conflicts:
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java
2023-05-28 20:58:27 +03:00
Gregory K
47edc3180b Merge pull request #586 from sk22/fix/hashtags-crash-akkoma
Fix crash when searching for Hashtags on Akkoma servers
2023-05-28 20:55:04 +03:00
sk
8053e8bb05 fix hashtag search crash on akkoma servers
closes mastodon#468
closes sk22#523
2023-05-28 19:51:29 +02:00
Eugen Rochko
fe84dc4823 New translations strings.xml (Chinese Simplified) 2023-05-27 16:39:55 +02:00
Grishka
5c480b37b3 Notifications M3 redesign (+ read marker support) 2023-05-27 13:31:01 +03:00
Grishka
92335d8678 Merge branch 'master' into m3_redesign 2023-05-25 21:02:17 +03:00
Gregory K
285eb25706 Merge pull request #584 from sk22/fix-restored-tab-selection
Fix wrong tab being selected on restore
2023-05-25 21:00:12 +03:00
sk
85c3d9f65f fix wrong tab being selected on restore 2023-05-25 19:51:45 +02:00
Grishka
1dcb5717ea Fix #553 2023-05-23 12:56:49 +03:00
Grishka
36f1a557d7 Fix default tab in profile 2023-05-23 12:17:26 +03:00
Grishka
bd7157c172 Reporting M3 redesign 2023-05-22 17:08:04 +03:00
Eugen Rochko
2e1795dc6f New translations strings.xml (Russian) 2023-05-20 13:04:15 +02:00
Eugen Rochko
cd1be782fa New translations strings.xml (Armenian) 2023-05-16 18:44:56 +02:00
Eugen Rochko
67059f3d71 New translations strings.xml (Armenian) 2023-05-16 17:30:41 +02:00
Eugen Rochko
15f4d3326b New translations strings.xml (Russian) 2023-05-15 08:47:41 +02:00
Eugen Rochko
e65404a466 New translations strings.xml (Dutch) 2023-05-13 20:26:36 +02:00
Eugen Rochko
3d47d1b4db New translations full_description.txt (Dutch) 2023-05-13 19:05:00 +02:00
Eugen Rochko
d1749ab610 New translations strings.xml (Dutch) 2023-05-13 19:04:59 +02:00
Eugen Rochko
806c264686 New translations strings.xml (Vietnamese) 2023-05-13 17:46:17 +02:00
Eugen Rochko
34a9cb5a74 New translations strings.xml (Dutch) 2023-05-13 17:46:16 +02:00
Eugen Rochko
64fad2e871 New translations strings.xml (Vietnamese) 2023-05-13 16:36:00 +02:00
Grishka
34a2af8429 Compose M3 redesign: custom emoji keyboard 2023-05-13 04:27:12 +03:00
Eugen Rochko
9c89c26097 New translations strings.xml (Dutch) 2023-05-13 01:52:41 +02:00
Eugen Rochko
e3b6a5d389 New translations strings.xml (Dutch) 2023-05-13 00:57:05 +02:00
Eugen Rochko
0fb54efde5 New translations strings.xml (Dutch) 2023-05-12 23:58:21 +02:00
Eugen Rochko
a4a3f32dba New translations strings.xml (Dutch) 2023-05-12 22:45:52 +02:00
Grishka
15883f2138 Compose: language selection 2023-05-12 22:21:21 +03:00
Eugen Rochko
03a1e29e0c New translations strings.xml (Dutch) 2023-05-12 19:43:17 +02:00
Eugen Rochko
eda9ff272b New translations strings.xml (Dutch) 2023-05-12 18:35:16 +02:00
Grishka
89501271ce Compose autocomplete improvements 2023-05-12 03:39:46 +03:00
Eugen Rochko
b3728e06ac New translations strings.xml (Swedish) 2023-05-11 12:28:06 +02:00
Grishka
968a6ea9b3 Merge branch 'master' into m3_redesign
# Conflicts:
#	mastodon/build.gradle
2023-05-11 05:49:34 +03:00
Grishka
33cbd85e19 Bump version 2023-05-11 05:42:40 +03:00
Grishka
8cb1f3f387 Merge branch 'l10n_master' 2023-05-11 05:40:54 +03:00
Grishka
e253d8f4f3 Pull user row into a separate view holder & update its design 2023-05-11 03:45:23 +03:00
Eugen Rochko
3f0c6fcec5 New translations strings.xml (Icelandic) 2023-05-10 13:59:12 +02:00
Grishka
cfabe47e10 Select "timeline" tab in profiles by default 2023-05-10 02:10:19 +03:00
Grishka
d3fe7857b7 Move media & poll stuff out of ComposeFragment to separate classes 2023-05-09 23:59:56 +03:00
Grishka
642e96a439 Compose M3 redesign wip 2023-05-09 21:34:42 +03:00
Eugen Rochko
797cf893da New translations strings.xml (Italian) 2023-05-08 15:58:22 +02:00
Eugen Rochko
a3564b70e1 New translations strings.xml (Ukrainian) 2023-05-08 07:52:51 +02:00
Eugen Rochko
43004307b8 New translations strings.xml (Ukrainian) 2023-05-08 06:34:46 +02:00
Eugen Rochko
acd1e4ced3 New translations full_description.txt (Scottish Gaelic) 2023-05-06 07:17:18 +02:00
Eugen Rochko
6717070f93 New translations strings.xml (Scottish Gaelic) 2023-05-06 07:17:17 +02:00
Eugen Rochko
387499ae49 New translations strings.xml (Vietnamese) 2023-05-05 08:49:53 +02:00
Eugen Rochko
8ab140c55d New translations strings.xml (Japanese) 2023-05-04 22:50:38 +02:00
Eugen Rochko
914abb95dd New translations strings.xml (Japanese) 2023-05-04 21:34:45 +02:00
Eugen Rochko
5360c0f0f7 New translations strings.xml (Greek) 2023-05-04 01:44:50 +02:00
Eugen Rochko
243d803b51 New translations strings.xml (Turkish) 2023-05-03 23:31:14 +02:00
Eugen Rochko
b343fe3835 New translations strings.xml (Turkish) 2023-05-03 22:27:36 +02:00
Eugen Rochko
3c42c1120f New translations strings.xml (Japanese) 2023-05-03 17:42:48 +02:00
Eugen Rochko
ad840dcef6 New translations strings.xml (Japanese) 2023-05-03 16:47:22 +02:00
Eugen Rochko
f73072d95e New translations strings.xml (Spanish) 2023-05-03 01:08:32 +02:00
Eugen Rochko
95cb9b5079 New translations strings.xml (Thai) 2023-05-02 21:12:14 +02:00
Eugen Rochko
c6684d3c9b New translations strings.xml (Thai) 2023-05-02 19:41:48 +02:00
Eugen Rochko
5c5989d8c0 New translations strings.xml (Chinese Traditional) 2023-05-02 18:13:38 +02:00
Eugen Rochko
60e92d30b0 New translations strings.xml (Italian) 2023-05-02 14:58:27 +02:00
Eugen Rochko
8bf8e3f86b New translations strings.xml (Slovenian) 2023-05-02 00:17:51 +02:00
Eugen Rochko
891ee2d06b New translations strings.xml (Indonesian) 2023-05-01 21:04:03 +02:00
Eugen Rochko
b450bc7ae8 New translations strings.xml (Icelandic) 2023-05-01 21:04:02 +02:00
Eugen Rochko
4ca1e0d5db New translations strings.xml (Vietnamese) 2023-05-01 21:04:01 +02:00
Eugen Rochko
859213dd9e New translations strings.xml (Chinese Simplified) 2023-05-01 21:04:01 +02:00
Eugen Rochko
ad2857791d New translations strings.xml (Turkish) 2023-05-01 21:03:59 +02:00
Eugen Rochko
497827f2e2 New translations strings.xml (Slovenian) 2023-05-01 21:03:57 +02:00
Eugen Rochko
967e333022 New translations strings.xml (Japanese) 2023-05-01 21:03:54 +02:00
Eugen Rochko
8df1406006 New translations strings.xml (Dutch) 2023-05-01 21:03:46 +02:00
Eugen Rochko
4af42fafdc New translations strings.xml (Russian) 2023-05-01 21:03:45 +02:00
Eugen Rochko
a9e6a452c1 New translations strings.xml (Galician) 2023-05-01 21:03:43 +02:00
Eugen Rochko
a4a4632397 New translations strings.xml (Chinese Traditional) 2023-05-01 21:03:42 +02:00
Eugen Rochko
421f39e414 New translations strings.xml (Italian) 2023-05-01 21:03:41 +02:00
Eugen Rochko
f8121e2dc4 New translations strings.xml (German) 2023-05-01 21:03:41 +02:00
Eugen Rochko
b1784fc51c New translations strings.xml (Danish) 2023-05-01 21:03:40 +02:00
Eugen Rochko
96db0d7de7 New translations strings.xml (Greek) 2023-05-01 21:03:39 +02:00
Eugen Rochko
3837ed9cb1 New translations strings.xml (Thai) 2023-05-01 21:03:38 +02:00
Eugen Rochko
2be789a43c New translations strings.xml (Spanish) 2023-05-01 21:03:37 +02:00
Grishka
2b8451e045 Merge branch 'master' into m3_redesign 2023-05-01 21:38:31 +03:00
Grishka
fd8d96169a Update string 2023-05-01 21:38:16 +03:00
Eugen Rochko
1562dc32c1 New translations strings.xml (Dutch) 2023-05-01 13:44:56 +02:00
Grishka
62074e554a Account switcher M3 redesign 2023-05-01 13:43:00 +03:00
Grishka
0434cda2da Thread view M3 redesign 2023-04-30 23:04:45 +03:00
Eugen Rochko
38f377ca09 New translations full_description.txt (Armenian) 2023-04-28 20:21:00 +02:00
Eugen Rochko
cc28bba884 New translations strings.xml (Armenian) 2023-04-28 20:20:59 +02:00
Eugen Rochko
beb3081918 New translations strings.xml (Armenian) 2023-04-28 19:06:20 +02:00
Eugen Rochko
1b3c9106b5 New translations strings.xml (Russian) 2023-04-28 09:05:19 +02:00
Eugen Rochko
385b91761b New translations strings.xml (Portuguese, Brazilian) 2023-04-28 02:45:25 +02:00
Eugen Rochko
43600756c0 New translations strings.xml (Chinese Traditional) 2023-04-25 18:41:49 +02:00
Eugen Rochko
3c3e0633ad New translations strings.xml (Danish) 2023-04-25 12:23:29 +02:00
Eugen Rochko
f819ad6917 New translations strings.xml (Danish) 2023-04-25 11:26:19 +02:00
Eugen Rochko
e7ad396fc6 New translations strings.xml (Italian) 2023-04-22 19:15:42 +02:00
Eugen Rochko
b1cb4d4257 New translations strings.xml (German) 2023-04-22 16:44:30 +02:00
Eugen Rochko
100bd4b062 New translations strings.xml (Galician) 2023-04-19 07:36:52 +02:00
Eugen Rochko
7da09d9b37 New translations strings.xml (Spanish) 2023-04-19 07:36:51 +02:00
Eugen Rochko
f46eb07228 New translations strings.xml (Galician) 2023-04-19 06:04:49 +02:00
Eugen Rochko
7627b5eb25 New translations strings.xml (Greek) 2023-04-17 15:01:08 +02:00
Eugen Rochko
c710448c6b New translations strings.xml (Greek) 2023-04-17 13:59:43 +02:00
Grishka
4b4c88d44d Support opening mastodon.online and mastodon.social links 2023-04-17 06:47:49 +03:00
Eugen Rochko
1ad270b1d6 New translations strings.xml (Chinese Simplified) 2023-04-16 18:13:11 +02:00
Eugen Rochko
099e253b2b New translations strings.xml (Chinese Simplified) 2023-04-16 17:11:48 +02:00
Eugen Rochko
66de4a5b91 New translations strings.xml (Thai) 2023-04-16 09:19:23 +02:00
Eugen Rochko
41437d91d5 New translations strings.xml (Icelandic) 2023-04-15 11:54:07 +02:00
Eugen Rochko
d33d5a6efa New translations strings.xml (Icelandic) 2023-04-15 10:45:31 +02:00
Eugen Rochko
4f9248d040 New translations strings.xml (Vietnamese) 2023-04-15 05:15:40 +02:00
Eugen Rochko
f40c0e41f3 New translations strings.xml (Japanese) 2023-04-14 20:26:49 +02:00
Eugen Rochko
deeb03ff2b New translations strings.xml (Indonesian) 2023-04-13 21:36:15 +02:00
Eugen Rochko
5c2a09e243 New translations strings.xml (Indonesian) 2023-04-13 20:38:20 +02:00
Eugen Rochko
2473c999db New translations strings.xml (German) 2023-04-13 14:38:33 +02:00
Eugen Rochko
ea2cc265e3 New translations strings.xml (Turkish) 2023-04-13 05:12:38 +02:00
Eugen Rochko
a0cd2d42cf New translations strings.xml (Turkish) 2023-04-13 04:16:05 +02:00
Grishka
9e116bec97 Merge branch 'master' into m3_redesign
# Conflicts:
#	mastodon/build.gradle
2023-04-12 19:41:39 +03:00
Grishka
0a17ceb984 Merge branch 'l10n_master' 2023-04-12 19:20:42 +03:00
Grishka
4ef18f1f4a Add touch interaction for the SplashFragment art 2023-04-12 19:20:23 +03:00
Grishka
0de227ab9c Use fixed colors for SplashFragment
fixes #561
2023-04-12 18:47:02 +03:00
Eugen Rochko
19cb8703a6 New translations strings.xml (Kabyle) 2023-04-12 17:37:10 +02:00
Eugen Rochko
e18567dd82 New translations strings.xml (Filipino) 2023-04-12 17:37:06 +02:00
Eugen Rochko
bfb3bcdbfb New translations strings.xml (Portuguese, Brazilian) 2023-04-12 17:37:02 +02:00
Eugen Rochko
565cd14d88 New translations strings.xml (Galician) 2023-04-12 17:37:00 +02:00
Eugen Rochko
ebc37eac75 New translations strings.xml (Vietnamese) 2023-04-12 17:36:59 +02:00
Eugen Rochko
c3702db577 New translations strings.xml (Chinese Traditional) 2023-04-12 17:36:59 +02:00
Eugen Rochko
e15c4fa342 New translations strings.xml (Chinese Simplified) 2023-04-12 17:36:58 +02:00
Eugen Rochko
8330b9f1c5 New translations strings.xml (Turkish) 2023-04-12 17:36:57 +02:00
Eugen Rochko
f759150982 New translations strings.xml (Swedish) 2023-04-12 17:36:56 +02:00
Eugen Rochko
6af177b596 New translations strings.xml (Slovenian) 2023-04-12 17:36:55 +02:00
Eugen Rochko
657bb94975 New translations strings.xml (Russian) 2023-04-12 17:36:54 +02:00
Eugen Rochko
3c946212b1 New translations strings.xml (Polish) 2023-04-12 17:36:52 +02:00
Eugen Rochko
b4e80f7fca New translations strings.xml (Norwegian) 2023-04-12 17:36:51 +02:00
Eugen Rochko
c9efc2cb2b New translations strings.xml (Korean) 2023-04-12 17:36:50 +02:00
Eugen Rochko
0d62e33dc7 New translations strings.xml (Japanese) 2023-04-12 17:36:49 +02:00
Eugen Rochko
ac88b9e19c New translations strings.xml (Italian) 2023-04-12 17:36:48 +02:00
Eugen Rochko
871dfda79e New translations strings.xml (Hungarian) 2023-04-12 17:36:47 +02:00
Eugen Rochko
e0c2c208ae New translations strings.xml (Basque) 2023-04-12 17:36:44 +02:00
Eugen Rochko
22ac112bdb New translations strings.xml (German) 2023-04-12 17:36:43 +02:00
Eugen Rochko
afd0cca176 New translations strings.xml (Danish) 2023-04-12 17:36:42 +02:00
Eugen Rochko
c083c8bce5 New translations strings.xml (Arabic) 2023-04-12 17:36:41 +02:00
Eugen Rochko
63bde032b3 New translations strings.xml (French) 2023-04-12 17:36:40 +02:00
Eugen Rochko
49492c0788 New translations strings.xml (Belarusian) 2023-04-12 17:36:38 +02:00
Eugen Rochko
b439c64add New translations strings.xml (Greek) 2023-04-12 17:36:37 +02:00
Eugen Rochko
1868bfe8e3 New translations strings.xml (Thai) 2023-04-12 17:36:36 +02:00
Eugen Rochko
f240a3d996 New translations strings.xml (Indonesian) 2023-04-12 17:36:35 +02:00
Eugen Rochko
788e5bd12e New translations strings.xml (Icelandic) 2023-04-12 17:36:34 +02:00
Eugen Rochko
a55fed4502 New translations strings.xml (Ukrainian) 2023-04-12 17:36:33 +02:00
Eugen Rochko
a8fdaf1a47 New translations strings.xml (Dutch) 2023-04-12 17:36:32 +02:00
Eugen Rochko
4a758bd488 New translations strings.xml (Czech) 2023-04-12 17:36:31 +02:00
Eugen Rochko
2c9731ec2a New translations strings.xml (Spanish) 2023-04-12 17:36:30 +02:00
Grishka
eef33266fc Remove unused code and strings 2023-04-12 18:34:28 +03:00
Eugen Rochko
58d2c3e5a6 New translations strings.xml (Indonesian) 2023-04-11 17:28:22 +02:00
Eugen Rochko
9e6a355db0 New translations strings.xml (Slovenian) 2023-04-10 00:54:45 +02:00
Eugen Rochko
0d10e09fd6 New translations strings.xml (Slovenian) 2023-04-09 23:47:00 +02:00
Eugen Rochko
f85bb995ba New translations strings.xml (Greek) 2023-04-09 17:56:44 +02:00
Eugen Rochko
268e5639f6 New translations strings.xml (Greek) 2023-04-09 16:26:19 +02:00
Eugen Rochko
51809df8ca New translations strings.xml (Thai) 2023-04-09 07:25:51 +02:00
Eugen Rochko
94fb676b0c New translations strings.xml (Chinese Traditional) 2023-04-09 04:29:35 +02:00
Eugen Rochko
a47106594b New translations strings.xml (Chinese Traditional) 2023-04-09 03:10:02 +02:00
Grishka
d93d66f702 Prepare new release 2023-04-09 01:59:13 +03:00
Eugen Rochko
b2f9f7ae54 New translations strings.xml (Italian) 2023-04-09 00:51:05 +02:00
Grishka
de7b908c78 Merge branch 'l10n_master' 2023-04-09 01:50:55 +03:00
Grishka
dc25e16c00 fix 2023-04-09 01:48:54 +03:00
Eugen Rochko
75d3c2fdce New translations strings.xml (Italian) 2023-04-08 23:55:36 +02:00
Eugen Rochko
ea1b6c5835 New translations strings.xml (Greek) 2023-04-08 22:51:05 +02:00
Eugen Rochko
cb7887da41 New translations strings.xml (Greek) 2023-04-08 21:38:06 +02:00
Eugen Rochko
a80313ee6b New translations strings.xml (Thai) 2023-04-07 23:53:35 +02:00
Eugen Rochko
e1a821bc43 New translations strings.xml (Thai) 2023-04-07 22:56:50 +02:00
Grishka
42a7c324fa Merge branch 'master' into m3_redesign 2023-04-07 23:16:53 +03:00
Grishka
924ea2d03a Fix #557 2023-04-07 22:58:04 +03:00
Grishka
55270fe654 Fix 2023-04-07 22:55:29 +03:00
Eugen Rochko
a125fab57b New translations strings.xml (Kabyle) 2023-04-07 21:48:05 +02:00
Eugen Rochko
395ee0aa99 New translations strings.xml (Scottish Gaelic) 2023-04-07 21:48:03 +02:00
Eugen Rochko
0f50fa6ba1 New translations strings.xml (Bosnian) 2023-04-07 21:48:01 +02:00
Eugen Rochko
adb7df3c71 New translations strings.xml (Filipino) 2023-04-07 21:48:00 +02:00
Eugen Rochko
5d7bcb629b New translations strings.xml (Burmese) 2023-04-07 21:47:59 +02:00
Eugen Rochko
a00f1417d2 New translations strings.xml (Hindi) 2023-04-07 21:47:59 +02:00
Eugen Rochko
8efd7e8ebf New translations strings.xml (Croatian) 2023-04-07 21:47:58 +02:00
Eugen Rochko
b016d277e0 New translations strings.xml (Bengali) 2023-04-07 21:47:57 +02:00
Eugen Rochko
fdb39617d1 New translations strings.xml (Persian) 2023-04-07 21:47:56 +02:00
Eugen Rochko
89f83fbf62 New translations strings.xml (Portuguese, Brazilian) 2023-04-07 21:47:55 +02:00
Eugen Rochko
ecee9e01a6 New translations strings.xml (Galician) 2023-04-07 21:47:54 +02:00
Eugen Rochko
20dc9bb8b9 New translations strings.xml (Vietnamese) 2023-04-07 21:47:53 +02:00
Eugen Rochko
2c47d0e9ed New translations strings.xml (Chinese Traditional) 2023-04-07 21:47:52 +02:00
Eugen Rochko
8e13d52e51 New translations strings.xml (Chinese Simplified) 2023-04-07 21:47:51 +02:00
Eugen Rochko
cc40198c9e New translations strings.xml (Turkish) 2023-04-07 21:47:50 +02:00
Eugen Rochko
290897ea41 New translations strings.xml (Swedish) 2023-04-07 21:47:49 +02:00
Eugen Rochko
b9e1c84304 New translations strings.xml (Slovenian) 2023-04-07 21:47:48 +02:00
Eugen Rochko
3c44c80e2e New translations strings.xml (Russian) 2023-04-07 21:47:47 +02:00
Eugen Rochko
dffa4e4594 New translations strings.xml (Portuguese) 2023-04-07 21:47:46 +02:00
Eugen Rochko
fa2d9fec58 New translations strings.xml (Polish) 2023-04-07 21:47:45 +02:00
Eugen Rochko
09c1a2cfa0 New translations strings.xml (Norwegian) 2023-04-07 21:47:44 +02:00
Eugen Rochko
d1f90eb231 New translations strings.xml (Korean) 2023-04-07 21:47:42 +02:00
Eugen Rochko
1f7d97134b New translations strings.xml (Japanese) 2023-04-07 21:47:42 +02:00
Eugen Rochko
79be91784d New translations strings.xml (Italian) 2023-04-07 21:47:41 +02:00
Eugen Rochko
de2654def3 New translations strings.xml (Hungarian) 2023-04-07 21:47:39 +02:00
Eugen Rochko
56343dacff New translations strings.xml (Hebrew) 2023-04-07 21:47:38 +02:00
Eugen Rochko
0e677f8ce7 New translations strings.xml (Basque) 2023-04-07 21:47:36 +02:00
Eugen Rochko
aa911896d6 New translations strings.xml (German) 2023-04-07 21:47:35 +02:00
Eugen Rochko
c62a8635b9 New translations strings.xml (Danish) 2023-04-07 21:47:34 +02:00
Eugen Rochko
4e900247c5 New translations strings.xml (Catalan) 2023-04-07 21:47:33 +02:00
Eugen Rochko
b3bd62bc6c New translations strings.xml (Arabic) 2023-04-07 21:47:32 +02:00
Eugen Rochko
8e5fd48ecd New translations strings.xml (French) 2023-04-07 21:47:31 +02:00
Eugen Rochko
b2bca9dd2c New translations strings.xml (Romanian) 2023-04-07 21:47:30 +02:00
Eugen Rochko
b8c0dc3181 New translations strings.xml (Belarusian) 2023-04-07 21:47:29 +02:00
Eugen Rochko
cccdc5292e New translations strings.xml (Greek) 2023-04-07 21:47:28 +02:00
Eugen Rochko
76d77a0e7a New translations strings.xml (Thai) 2023-04-07 21:47:27 +02:00
Eugen Rochko
e737f4bf9a New translations strings.xml (Indonesian) 2023-04-07 21:47:26 +02:00
Eugen Rochko
391db2f1c9 New translations strings.xml (Icelandic) 2023-04-07 21:47:25 +02:00
Eugen Rochko
359d61183c New translations strings.xml (Ukrainian) 2023-04-07 21:47:24 +02:00
Eugen Rochko
46fd05d88e New translations strings.xml (Dutch) 2023-04-07 21:47:23 +02:00
Eugen Rochko
cde22a0945 New translations strings.xml (Czech) 2023-04-07 21:47:22 +02:00
Eugen Rochko
111b7e25c5 New translations strings.xml (Spanish) 2023-04-07 21:47:21 +02:00
Grishka
4f8d8f0c8d Welcome fragment redesign again
# Conflicts:
#	mastodon/src/main/res/values/strings.xml
#	mastodon/src/main/res/values/styles.xml
2023-04-07 22:44:28 +03:00
Grishka
915b0603d0 Reblog -> boost 2023-04-07 22:42:16 +03:00
Grishka
849888d128 Welcome fragment redesign again 2023-04-07 22:40:00 +03:00
Eugen Rochko
075aab8074 New translations strings.xml (Greek) 2023-04-07 16:16:35 +02:00
Eugen Rochko
6ebe4c86af New translations strings.xml (Greek) 2023-04-07 15:16:19 +02:00
Eugen Rochko
0925c8c582 New translations strings.xml (Spanish) 2023-04-06 21:52:50 +02:00
Eugen Rochko
a683fdce62 New translations strings.xml (Greek) 2023-04-06 18:46:34 +02:00
Eugen Rochko
b958299446 New translations strings.xml (Greek) 2023-04-06 17:45:19 +02:00
Eugen Rochko
3f80be8377 New translations strings.xml (Greek) 2023-04-05 16:29:26 +02:00
Eugen Rochko
ced0accde5 New translations strings.xml (Belarusian) 2023-04-05 12:06:47 +02:00
Eugen Rochko
b454ff5ec7 New translations strings.xml (Belarusian) 2023-04-05 11:07:06 +02:00
Eugen Rochko
45af198f32 New translations strings.xml (Greek) 2023-04-05 02:04:08 +02:00
Eugen Rochko
ff374f8899 New translations strings.xml (Greek) 2023-04-05 00:46:17 +02:00
Eugen Rochko
faecb3bc4b New translations strings.xml (Greek) 2023-04-04 20:21:01 +02:00
Eugen Rochko
6b893fadef New translations short_description.txt (Greek) 2023-04-04 17:47:01 +02:00
Eugen Rochko
c328467a41 New translations strings.xml (Greek) 2023-04-04 17:47:00 +02:00
Eugen Rochko
182325470b New translations full_description.txt (Greek) 2023-04-04 17:46:59 +02:00
Eugen Rochko
f330ad71ac New translations full_description.txt (Greek) 2023-04-04 16:06:32 +02:00
Eugen Rochko
ba0c064f36 New translations full_description.txt (Chinese Traditional) 2023-04-03 16:47:01 +02:00
Eugen Rochko
8d7aaee5b9 New translations strings.xml (Dutch) 2023-04-01 15:16:39 +02:00
Grishka
8a7e910e7c Reblog -> boost 2023-03-30 19:14:12 +03:00
Grishka
72d72d443e Limit aspect ratio to 9:20 2023-03-30 18:53:59 +03:00
Eugen Rochko
68cba2de63 New translations strings.xml (Icelandic) 2023-03-29 00:29:42 +02:00
Eugen Rochko
5a914f9c0e New translations strings.xml (Thai) 2023-03-28 21:06:33 +02:00
Eugen Rochko
b0e6805a20 New translations strings.xml (Czech) 2023-03-28 16:34:35 +02:00
Eugen Rochko
21e7e44c01 New translations strings.xml (Czech) 2023-03-28 15:36:30 +02:00
Grishka
7b0a3f0f96 More media layout optimizations 2023-03-27 04:04:16 +03:00
Eugen Rochko
f7df4abdae New translations strings.xml (Spanish) 2023-03-26 17:05:29 +02:00
Eugen Rochko
7674ceefe9 New translations strings.xml (Ukrainian) 2023-03-25 20:42:18 +01:00
Eugen Rochko
4be575c534 New translations strings.xml (Ukrainian) 2023-03-25 19:25:16 +01:00
Eugen Rochko
dd0f0a7d5a New translations strings.xml (Indonesian) 2023-03-25 04:22:02 +01:00
Eugen Rochko
759b44c224 New translations strings.xml (Indonesian) 2023-03-25 03:16:02 +01:00
Eugen Rochko
920384b26c New translations strings.xml (Dutch) 2023-03-24 16:26:08 +01:00
Grishka
cf4604e0d8 More media layout optimizations 2023-03-24 01:33:33 +03:00
Grishka
e873dd7d0a New sensitive media design 2023-03-24 00:33:02 +03:00
Grishka
4492e940e5 Media attachments minor fixes 2023-03-23 22:36:52 +03:00
Eugen Rochko
193a2c4f70 New translations strings.xml (German) 2023-03-23 13:42:28 +01:00
Eugen Rochko
793dec98b2 New translations strings.xml (Vietnamese) 2023-03-23 07:36:02 +01:00
Eugen Rochko
11ddf8015d New translations strings.xml (Galician) 2023-03-23 05:17:18 +01:00
Grishka
c833c03dc3 New play button & alt text support for gifs & videos 2023-03-23 03:15:15 +03:00
Grishka
30b0d226b5 Tab bar M3 design 2023-03-23 01:49:39 +03:00
Grishka
8afad21113 Add "error" M3 button style 2023-03-23 01:09:03 +03:00
Grishka
6b5e5b0f25 More minor tweaks 2023-03-23 00:58:38 +03:00
Grishka
3c0ab6822f Add elevation effect stuff to a base class for all fragments that need it 2023-03-23 00:31:17 +03:00
Eugen Rochko
1753bdbd8b New translations strings.xml (Thai) 2023-03-22 17:21:08 +01:00
Grishka
f7215d00ca Profile about tab redesign 2023-03-22 17:22:22 +03:00
Grishka
43bbe9be0f Fix LinkedTextView breaking XML layout editor 2023-03-22 17:01:13 +03:00
Grishka
477a691c9e Fix z-order and remove V.dp from more custom views 2023-03-22 16:55:09 +03:00
Grishka
e5d60050a2 Add filters to AccontTimelineFragment 2023-03-22 03:22:04 +03:00
Grishka
9a698fda18 Merge branch 'master' into m3_redesign 2023-03-22 02:47:13 +03:00
Grishka
d6e563486b Fix alt text button 2023-03-22 02:46:48 +03:00
Grishka
d5701c1073 Merge branch 'master' into m3_redesign 2023-03-22 02:41:26 +03:00
Grishka
0112bfa9c4 Fix #547 2023-03-22 02:38:58 +03:00
Grishka
5951611fb0 Fix #551 2023-03-22 02:34:25 +03:00
Grishka
955b9a4b2b Featured tab in profiles 2023-03-22 02:30:42 +03:00
Eugen Rochko
cc8c818e13 New translations strings.xml (Slovenian) 2023-03-21 11:52:06 +01:00
Eugen Rochko
2e1f273d78 New translations strings.xml (Chinese Traditional) 2023-03-21 06:38:54 +01:00
Eugen Rochko
35d6800877 New translations strings.xml (Chinese Traditional) 2023-03-21 05:17:21 +01:00
Eugen Rochko
91e154bbee New translations strings.xml (Italian) 2023-03-20 20:01:49 +01:00
Eugen Rochko
0c02b0cb68 New translations strings.xml (Dutch) 2023-03-20 18:49:16 +01:00
Grishka
09ffda2605 Merge branch 'master' into m3_redesign 2023-03-17 22:07:51 +03:00
Grishka
039fb0c505 Profile redesign: header 2023-03-17 22:07:28 +03:00
Grishka
20799ef1a8 Posts redesign wip 2023-03-14 19:17:37 +03:00
Eugen Rochko
a9b828001c New translations strings.xml (Burmese) 2023-03-13 15:08:41 +01:00
Eugen Rochko
be050abf7e New translations strings.xml (Burmese) 2023-03-13 14:01:13 +01:00
Eugen Rochko
2d071ca252 New translations strings.xml (Burmese) 2023-03-13 12:53:22 +01:00
Eugen Rochko
abf94e1e70 New translations strings.xml (Burmese) 2023-03-13 10:01:18 +01:00
Eugen Rochko
2991c7421c New translations strings.xml (Burmese) 2023-03-13 08:59:13 +01:00
Eugen Rochko
834f84f995 New translations strings.xml (Burmese) 2023-03-13 07:58:41 +01:00
Eugen Rochko
6bf713c96c New translations strings.xml (Polish) 2023-03-13 00:35:08 +01:00
Eugen Rochko
72b16be297 New translations strings.xml (Swedish) 2023-03-12 21:53:57 +01:00
Eugen Rochko
0c7f27b3ac New translations strings.xml (Swedish) 2023-03-12 20:47:06 +01:00
Eugen Rochko
ca101748eb New translations strings.xml (Belarusian) 2023-03-11 23:37:17 +01:00
Eugen Rochko
3584c123d6 New translations strings.xml (Belarusian) 2023-03-11 22:33:13 +01:00
Eugen Rochko
a2fd4be339 New translations title.txt (Urdu (India)) 2023-03-11 14:05:49 +01:00
Eugen Rochko
656364db60 New translations short_description.txt (Urdu (India)) 2023-03-11 14:05:48 +01:00
Eugen Rochko
884347da12 New translations full_description.txt (Urdu (India)) 2023-03-11 14:05:47 +01:00
Eugen Rochko
675c49a922 New translations strings.xml (Urdu (India)) 2023-03-11 14:05:46 +01:00
Eugen Rochko
a3146d6cdd New translations full_description.txt (Belarusian) 2023-03-11 09:12:18 +01:00
Eugen Rochko
d769f757ed New translations full_description.txt (Belarusian) 2023-03-11 08:04:43 +01:00
Eugen Rochko
654e3eb36b New translations strings.xml (Japanese) 2023-03-09 23:29:35 +01:00
Eugen Rochko
f62ba685d1 New translations strings.xml (Japanese) 2023-03-09 14:07:24 +01:00
Eugen Rochko
8cc5678b38 New translations strings.xml (Japanese) 2023-03-08 11:02:08 +01:00
Eugen Rochko
592eb2d589 New translations strings.xml (Japanese) 2023-03-08 02:55:58 +01:00
Eugen Rochko
0bf31de1f1 New translations strings.xml (Japanese) 2023-03-06 21:23:36 +01:00
Eugen Rochko
7fd32cab3d New translations strings.xml (Japanese) 2023-03-06 19:38:06 +01:00
Eugen Rochko
0c63b99e6c New translations strings.xml (Japanese) 2023-03-05 02:15:44 +01:00
Eugen Rochko
5881f5fa05 New translations strings.xml (Japanese) 2023-03-05 01:20:11 +01:00
Eugen Rochko
575ca6251c New translations strings.xml (Arabic) 2023-03-03 20:35:51 +01:00
Eugen Rochko
9c83a5aaeb New translations strings.xml (Portuguese, Brazilian) 2023-03-02 15:21:36 +01:00
Eugen Rochko
bc3e46eae1 New translations strings.xml (Portuguese, Brazilian) 2023-03-02 14:20:26 +01:00
Eugen Rochko
0303e59fc1 New translations strings.xml (Bengali) 2023-02-28 16:38:40 +01:00
Eugen Rochko
0c0d36da62 New translations strings.xml (Bengali) 2023-02-28 13:32:43 +01:00
Eugen Rochko
e7e0b8841c New translations strings.xml (Bengali) 2023-02-28 12:21:52 +01:00
Eugen Rochko
79d9abe7f7 New translations full_description.txt (Turkish) 2023-02-27 17:26:40 +01:00
Eugen Rochko
684fbc0050 New translations strings.xml (Turkish) 2023-02-27 17:26:39 +01:00
Eugen Rochko
03973d41be New translations strings.xml (Turkish) 2023-02-27 16:25:31 +01:00
Eugen Rochko
06c533bf5a New translations full_description.txt (Japanese) 2023-02-27 08:35:37 +01:00
Eugen Rochko
cc64a20e53 New translations full_description.txt (Japanese) 2023-02-27 07:28:37 +01:00
Eugen Rochko
d633b6f1d4 New translations strings.xml (Bengali) 2023-02-25 16:06:21 +01:00
Eugen Rochko
7b68baef0d New translations strings.xml (Bengali) 2023-02-25 13:06:26 +01:00
Eugen Rochko
fb5ef921f7 New translations strings.xml (Spanish) 2023-02-24 21:22:35 +01:00
Eugen Rochko
da44e50679 New translations strings.xml (Spanish) 2023-02-24 19:50:53 +01:00
Eugen Rochko
93d89f93b2 New translations strings.xml (Bengali) 2023-02-24 09:17:38 +01:00
Eugen Rochko
cce64f9a76 New translations strings.xml (Bengali) 2023-02-24 08:19:59 +01:00
Eugen Rochko
afb396acd8 New translations strings.xml (Chinese Traditional) 2023-02-22 18:07:34 +01:00
Eugen Rochko
0943908173 New translations strings.xml (Kabyle) 2023-02-20 08:34:11 +01:00
Eugen Rochko
b018788cd1 New translations strings.xml (Galician) 2023-02-19 22:35:48 +01:00
Eugen Rochko
4a315e73eb New translations strings.xml (Galician) 2023-02-19 21:34:24 +01:00
Eugen Rochko
b01ef6d5a7 New translations short_description.txt (Russian) 2023-02-17 10:01:43 +01:00
Eugen Rochko
604e581139 New translations full_description.txt (Russian) 2023-02-17 10:01:42 +01:00
Eugen Rochko
082f697b40 New translations strings.xml (Russian) 2023-02-17 10:01:41 +01:00
Eugen Rochko
8c61660cfc New translations strings.xml (Arabic) 2023-02-15 15:17:22 +01:00
Eugen Rochko
d6933be3cd New translations strings.xml (Arabic) 2023-02-15 13:12:38 +01:00
Eugen Rochko
ca6cfd2b91 New translations strings.xml (French) 2023-02-14 20:11:52 +01:00
Eugen Rochko
20960bdd57 New translations strings.xml (Belarusian) 2023-02-14 14:14:56 +01:00
741 changed files with 27929 additions and 12891 deletions

View File

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

View File

@@ -1,8 +1,8 @@
ماستودون هي أكبر شبكة اجتماعية لا مركزيَّة على الإنترنت. بدلاً من كونها على موقع ويب واحد مركزي، هي عبارة عن شبكة من ملايين المستخدمين في مجتمعات مُستقلَّة يمكنهم جميعًا التفاعل مع بعضهم البعض بسلاسة. بغض النظر عن اهتماماتك، يمكنك مقابلة أشخاص متحمسين ينشرون عنها في ماستودون!
ماستدون هي أكبر شبكة اجتماعية لا مركزيَّة على الإنترنت. بدلاً من كونها على موقع ويب واحد مركزي، هي عبارة عن شبكة من ملايين المستخدمين في مجتمعات مُستقلَّة يمكنهم جميعًا التفاعل مع بعضهم البعض بسلاسة. بغض النظر عن اهتماماتك، يمكنك مقابلة أشخاص متحمسين ينشرون عنها في ماستودون!
اِنضم إلَى مُجتَمع وأنشئ مِلَفَّكَ التَّعريفِيّ. ابحث عن أشخاص رائعين، تابعهم واقرأ منشوراتهم في خطٍّ زمني خالٍ من الإعلانات. عبِّر عَن نَفسِكَ باِستخدام رُموزٍ تَعبيرِيَّةٍ مُخصَّصَة، أو صُوَر، أو صُوَرٍ مُتحَرِّكَة، أو مَقاطِعٍ مَرئِّيَة أو مَقاطِعٍ صَوتِيَّةٍ فِي مَنشوراتٍ ذَاتُ خَمسِمائَة حَرف. رُدّ على سَلاسِلِ المَنشوراتِ، وأعِد تَدوينَ مَنشُوراتِ أيِّ شَخصٍ لِمُشارَكَةِ الأُمُورِ الرَّائِعَة. اِبحَث عَن حِساباتٍ جَديدَةٍ لِمُتابَعَتِها، وَعَن وُسُومٍ شَائِعَةٍ لِتَوسيعِ شَبَكَتِك.
ماستودون مبني بتركيز على الأمان والخصوصيَّة. حدِّد ما إذا أردتَ مُشارَكَةَ مَنشُوراتِكَ مَعَ مُتابِعيك، أو الأشخاصِ الَّذينَ أشَرتَ إليهِم فَقَط أو العالَمَ بأسرِه. تتيح لك تحذيرات المحتوى إخفاء المنشورات التي تحتوي على مواد حساسة أو محفِّزَة حتى تكون مستعد للتفاعل مع محتواها. لكل مجتمع إرشاداته الخاصة ومشرفيه الخاصين للحفاظ على أمان أعضائه، كما تُساعد أدوات الحظر والإبلاغ القوية في منع إساءة الاستخدام.
ماستدون مبني بتركيزٍ على الأمان والخصوصيَّة. حدِّد ما إذا أردتَ مُشارَكَةَ مَنشُوراتِكَ مَعَ مُتابِعيك، أو الأشخاصِ الَّذينَ أشَرتَ إليهِم فَقَط أو العالَمَ بأسرِه. تتيح لك تحذيرات المحتوى إخفاء المنشورات التي تحتوي على مواد حساسة أو محفِّزَة حتى تكون مستعد للتفاعل مع محتواها. لكل مجتمع إرشاداته الخاصة ومشرفيه الخاصين للحفاظ على أمان أعضائه، كما تُساعد أدوات الحظر والإبلاغ القوية في منع إساءة الاستخدام.
مَزيدٌ مِنَ المَزايَا:

View File

@@ -2,15 +2,15 @@ Mastodon - гэта найбуйнейшая дэцэнтралізаваная
Далучайцеся да супольнасці і стварыце свой профіль. Знаходзьце цікавых людзей, сачыце за імі і чытайце іх допісы ў храналагічнай стужцы без рэкламы. Выяўляйце сябе з дапамогай карыстальніцкіх эмодзі, малюнкаў, GIF-файлаў, відэа і аўдыя ў допісах на 500 знакаў. Удзельнічайце ў абмеркаваннях і прасоўвайце выдатныя допісы ад іншых людзей. Знаходзьце новыя ўліковыя запісы, на якія можна падпісацца, і папулярныя хэштэгі, каб пашырыць сваю сетку.
Mastodon створаны з акцэнтам на прыватнасць і бяспеку. Вырашайце з кім абагульваць вашы допісы: з вашымі падпісчыкамі, толькі з людзьмі, якіх вы згадваеце, або з усім светам. Папярэджанні аб змесце дазваляюць схаваць допісы, якія змяшчаюць канфідэнцыяльны або правакацыйны матэрыял, пакуль вы не будзеце гатовыя да яго. Кожная суполка мае ўласныя правілы і мадэратараў, якія забяспечваюць бяспеку яе ўдзельнікаў, а надзейныя інструменты блакіроўкі і сістэма скарг дапамагаюць прадухіліць злоўжыванні.
Mastodon створаны з акцэнтам на прыватнасць і бяспеку. Вырашайце з кім абагульваць вашы допісы: з вашымі падпісчыкамі, толькі з людзьмі, якіх вы згадваеце або з усім светам. Папярэджанні аб змесціве дазваляюць схаваць допісы, якія змяшчаюць канфідэнцыяльны або правакацыйны матэрыял, пакуль вы не будзеце гатовыя да яго. Кожная суполка мае ўласныя правілы і мадэратараў, якія забяспечваюць бяспеку яе ўдзельнікаў, а надзейныя інструменты блакіроўкі і сістэма скарг дапамагаюць прадухіліць злоўжыванні.
Больш функцый:
• Цёмны рэжым: чытайце допісы ў светлым, цёмным або сапраўдным чорным рэжыме
• Апытанні: пытайцеся ў падпісчыкаў іх меркаванне і падлічвайце галасы
• Даследуйце: папулярныя хэштэгі і ўліковыя запісы даступныя адным націскам
• Даследуйце: папулярныя хэштэгі і ўліковыя запісы адным дотыкам
• Апавяшчэнні: атрымлівайце апавяшчэнні аб новых падпісчыках, адказах і рэпостах
• Абагульванне: публікуйце непасрэдна ў Mastodon з любога меню абагульвання ў любой праграме
• Прывабнасць: наш талісман - чароўны слон, і вы будзеце бачыць, як ён з'яўляецца час ад часу
• Прывабнасць: нашым талісманам з'яўляецца чароўны слон, і час ад часу ён будзеце з'яўляцца перад вамі
Mastodon з'яўляецца зарэгістраванай некамерцыйнай арганізацыяй і яго развіццё падтрымліваецца непасрэдна вашымі ахвяраваннямі. У нас няма рэкламы, манетызацыі і венчурнага капіталу, і мы плануем так і працягваць.

View File

@@ -1,16 +1,16 @@
Mastodon er det største decentraliserede sociale netværk på internettet. I stedet for et enkelt website, er det et netværk af millioner af brugere i uafhængige fællesskaber som alle sammen kan interagere med hinanden. Uanset hvad du interesserer dig for, kan du møde engagerede mennesker som skriver om det på Mastodon!
Mastodon er det største decentraliserede sociale netværk på internet. Frem for ét enkelt website, er det i stedet et netværk af millioner af brugere i uafhængige fællesskaber, som alle kan interagere med hinanden. Uanset interessesfære, kan man møde engagerede personer, som skriver herom på Mastodon!
Find et fællesskab og opret din profil. Find og følg fascinerende mennesker og læs deres indlæg i en reklamefri, kronologisk tidslinje. Udtryk dig selv med emojis, billeder, GIFs, videoer og lyd i indlæg på op til 500 tegn. Svar på tråde og del alt det gode ved at booste indlæg fra andre. Find nye brugere at følge og aktuelle hashtags dit netværk udvides.
Find et fællesskab og opret din profil. Find og følg fascinerende folk og læs deres indlæg i en reklamefri, kronologisk tidslinje. Udtryk dig selv med tilpassede emojis, billeder, GIF'er, videoer og lyd i 500-tegns indlæg. Svar på tråde og genpost indlæg fra enhver for dele alt det gode. Find nye konti at følge, og populære hashtags, for at udvide dit netværk.
Mastodon er bygget med fokus på privatliv og sikkerhed. Beslut om dine indlæg skal deles med dine følgere, bare dem du nævner eller hele verden. Indholdsadvarsler giver dig mulighed for at gemme indlæg med sensitivt eller triggende indhold indtil du er klar til at læse dem. Hvert fællesskab har sine egne regler og moderatorer som holder øje og sikrer medlemmerne mod spam og trolde. De har robuste blokerings- og rapporteringsredskaber til deres rådighed.
Mastodon er bygget med fokus på fortrolighed og sikkerhed. Afgør, hvorvidt dine indlæg skal deles med Følgere, blot dem du nævner eller hele verden. Indholdsadvarsler muliggør at skjule indlæg med sensitivt eller udløsende indhold, indtil du er klar til at læse dem. Hvert fællesskab har deres egne retningslinjer og moderatorer til at holde deres medlemmer sikre, og robuste blokerings- og anmeldelsesværktøjer hjælper med at forhindre misbrug.
Flere funktioner:
• Mørk Mode: Læs indlæg i lys, mørk eller ægte sort tilstand
• Afstemninger: Spørg tilhængere om deres mening og stemme
• Udforsk: Populære hashtags og konti er et tryk væk
• Notifikationer: Få besked om nye følgere, svar og boosts
• Deling: Send direkte til Mastodon fra en hvilken som app
• Nuttethedsfaktor: Vores maskot er en yndig elefant, og du vil se dem dukke op fra tid til anden
• Mørk tilstand: Læs indlæg i lys, mørk eller ægte sort tilstand
• Afstemninger: Spørg Følgere om deres mening og stemme
• Udforsk: Populære hashtags og konti er ét tryk væk
• Notifikationer: Få besked om nye Følgere, svar og genpostninger
• Deling: Post direkte til Mastodon fra enhver apps delingsfunktion
• Nuttethed: Vores maskot er en yndig elefant, som du vil se dukke op fra tid til anden
Mastodon er en registreret nonprofit og udvikling understøttes direkte af dine donationer. Der er ingen reklame, ingen indtægtsgenerering og ingen risikovillig kapital, og sådan forbliver det.
Mastodon er en registreret nonprofit, hvis udvikling direkte understøttes af dine donationer. Der er ingen annoncering, ingen indtægtsgenerering og ingen risikovillig kapital, og intentionen er, at det forbliver sådan.

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Το Mastodon είναι το μεγαλύτερο αποκεντρωμένο κοινωνικό δίκτυο στο διαδίκτυο. Αντί για μία ενιαία ιστοσελίδα, είναι ένα δίκτυο εκατομμυρίων χρηστών σε ανεξάρτητες κοινότητες που μπορούν όλοι να αλληλεπιδράσουν μεταξύ τους, απρόσκοπτα. Δεν έχει σημασία τί σας αρέσει, μπορείτε να συναντήσετε παθιασμένους ανθρώπους να μιλάνε γι' αυτό στο Mastodon!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Εγγραφείτε σε μια κοινότητα και δημιουργήστε το προφίλ σας. Βρείτε και ακολουθήστε συναρπαστικούς ανθρώπους και διαβάσετε τις αναρτήσεις τους σε μία χωρίς διαφημίσεις, χρονολογική ροή. Εκφραστείτε με προσαρμοσμένα emoji, εικόνες, GIF, βίντεο και ήχο σε αναρτήσεις 500 χαρακτήρων. Απαντήστε σε νήματα και παραθέστε αναρτήσεις από οποιονδήποτε για να μοιραστείτε μεγάλα πράγματα. Βρείτε νέους λογαριασμούς για να ακολουθήσετε και δημοφιλείς ετικέτες για να επεκτείνετε το δίκτυό σας.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Το Mastodon είναι κατασκευασμένο με έμφαση στην ιδιωτικότητα και την ασφάλεια. Αποφασίστε αν οι αναρτήσεις σας μοιράζονται με τους ακόλουθούς σας, μόνο τα άτομα που αναφέρετε ή ολόκληρο τον κόσμο. Προειδοποιήσεις περιεχομένου σας επιτρέπουν να κρύψετε αναρτήσεις που περιέχουν ευαίσθητο ή ανησυχητικό υλικό μέχρι να είστε έτοιμοι να συμμετάσχετε μαζί τους. Κάθε κοινότητα έχει τις δικές της οδηγίες και συντονιστές για να κρατήσει τα μέλη της ασφαλή και ισχυρά εργαλεία αποκλεισμού και αναφοράς βοηθούν στην πρόληψη της κατάχρησης.
More features:
Περισσότερα χαρακτηριστικά:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
Explore: Trending hashtags and accounts are a tap away
Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Σκοτεινή λειτουργία: Διαβάστε αναρτήσεις σε φωτεινή, σκοτεινή ή την απόλυτα μαύρη λειτουργία
Δημοσκοπήσεις: Ρωτήστε τους ακόλουθους για τη γνώμη τους και συγκεντρώστε ψήφους
Εξερεύνηση: Δημοφιλείς ετικέτες και λογαριασμοί είναι ένα άγγιγμα μακριά
Ειδοποιήσεις: Ενημερωθείτε για νέους ακόλουθους, απαντήσεις και παραθέσεις
Κοινοποίηση: Αναρτήστε απευθείας στο Mastodon από οποιαδήποτε καρτέλα κοινής χρήσης σε οποιαδήποτε εφαρμογή
Χάρη: Η μασκότ μας είναι ένας αξιολάτρευτος ελέφαντας και θα τον δείτε να εμφανίζεται πού και πού
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Το Mastodon είναι μη κερδοσκοπική και η ανάπτυξη υποστηρίζεται άμεσα από τις δωρεές σας. Δεν υπάρχουν διαφημίσεις, δεν γίνεται δημιουργία εσόδων και χωρίς επιχειρηματικά κεφάλαια και σκοπεύουμε να το διατηρήσουμε έτσι.

View File

@@ -1 +1 @@
Decentralized social network
Αποκεντρωμένο κοινωνικό δίκτυο

View File

@@ -1,6 +1,6 @@
Mastodon Interneteko sare sozial deszentralizatu handiena da. Webgune bakar bat izan ordez, beren artean elkarreragin dezaketen komunitate independenteetako milioika erabiltzailek osatutako sarea da. Zure interesak direnak direla ere, jende interesgarria aurkituko duzu Mastodonen!
Batu komunitate batera eta sortu zure profila. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Adierazi nahi duzuna 500 karaktereko bidalketetan emoji pertsonalizatuak, irudiak, GIFak, bideoak eta audioak erabiliz. Erantzun edozeinen hariak eta eman bultzada bidalketei edukiak partekatzeko. Bilatu jarraitzeko kontu berriak eta traolen joerak zure sarea zabaltzeko.
Batu komunitate batera eta sortu zure profila. Bilatu eta jarraitu jende zoragarria eta irakurri beren bidalketak, publizitaterik gabeko denbora-lerro kronologikoan. Adierazi nahi duzuna 500 karaktereko bidalketetan emoji pertsonalizatuak, irudiak, GIFak, bideoak eta audioak erabiliz. Erantzun edozeinen hariak eta eman bultzada bidalketei edukiak partekatzeko. Bilatu jarraitzeko kontu berriak eta traolen joerak zure sarea zabaltzeko.
Mastodon pribatutasunean eta segurtasunean arreta jarriz eraikia dago. Erabaki zure bidalketak norekin partekatu: zure jarraitzaileekin, aipatzen dituzunekin edo mundu osoarekin. Edukiaren abisuek aukera ematen dute eduki sentibera edo zuregan eragina izan dezaketen bidalketak zuk erabaki arte ezkutatzeko. Komunitate bakoitzak bere gidalerro eta moderatzaileak ditu, bertako kideak seguru mantentzeko. Baita blokeatzeko eta salatzeko tresna sendoak ere abusuak galarazteko.

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
ماستودون بزرگترین شبکه اجتماعی غیرمتمرکز در اینترنت است. به جای یک وب سایت واحد، شبکه ای متشکل از میلیون ها کاربر در جوامع مستقل است که همگی می توانند به صورت یکپارچه با یکدیگر تعامل داشته باشند. مهم نیست که به چه چیزی علاقه دارید، می توانید با افراد پرشوری که درباره آن در ماستودون فرسته ارسال می‌کنند صحبت کنید!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
به یک اجتماع بپیوندید و نمایه خود را ایجاد کنید. افراد جذاب را پیدا کنید و پی‌گیری کنید و فرسته‌های آنها را در یک خط زمانی بدون تبلیغات و زمانی بخوانید. با ایموجی‌های سفارشی، تصاویر، گیف‌ها، ویدیوها و صدا در فرسته‌های 500 کاراکتری، خود را بیان کنید. برای به اشتراک گذاشتن مطالب عالی، به موضوعات پاسخ دهید و فرسته‌های هر کسی را مجددا ری‌بلاگ کنید. اکانت های جدیدی را برای پی‌گیری پیدا کنید و برچسب‌های پرطرفدار را برای گسترش شبکه خود پیدا کنید.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
ماستودون با تمرکز بر حریم خصوصی و ایمنی ساخته شده است. تصمیم بگیرید که آیا پست‌های شما با پیگیرهایتان، فقط افرادی که نام می‌برید، یا کل دنیا به اشتراک گذاشته شود. هشدارهای محتوا به شما امکان می‌دهند پست‌های حاوی مطالب حساس یا محرک را پنهان کنید تا زمانی که آماده تعامل با آنها باشید. هر اجتماع‌ای دستورالعمل‌ها و ناظران خود را دارد تا اعضای خود را ایمن نگه دارد و ابزارهای قوی مسدود کردن و گزارش‌دهی به جلوگیری از سوء استفاده کمک می‌کند.
More features:
قابلیت های دیگر:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
Explore: Trending hashtags and accounts are a tap away
Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
حالت تاریک: فرسته‌ها را در حالت روشن، تاریک، یا سیاه بخوانید
نظرسنجی: از پی‌گیران خود نظر بخواهید و رای‌ها را جمع‌آوری کنید
کاوش: برچسب‌ها و حساب‌های پرطرفدار یک ضربه فاصله دارند
آگاهی‌ها: از پی‌گیری‌ها، پاسخ‌های جدید و ری‌بلاگ‌ها مطلع شوید
اشتراک گذاری: از هر برگه اشتراکی در هر برنامه، مستقیماً به ماستودون ارسال کنید
ناز: طلسم ما یک فیل شایان ستایش است، و شما هر از گاهی آنها را خواهید دید
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
ماستودون یک سازمان غیرانتفاعی ثبت شده است و توسعه مستقیماً توسط اعانه های مالی شما پشتیبانی می شود. نه تبلیغاتی وجود دارد،نه کسب درآمدی و نه سرمایه گذاری خطرپذیری و ما قصد داریم آن را به همین شکل حفظ کنیم.

View File

@@ -1 +1 @@
Decentralized social network
شبکه اجتماعی نامتمرکز

View File

@@ -1,6 +1,6 @@
S e an lìonra sòisealta sgaoilte as motha air an eadar-lìon a th ann am Mastodon. Seach aon làrach-lìn a-mhàin, s e lìonra de mhilleanan de dhaoine ann an coimhearsnachdan neo-eisimeileach a th ann agus s urrainn dhan a h-uile duine bruidhinn ri chèile fhathast gun duilgheadas. Ge b e dè na rudan a tha ùidh agad annta, coinnichidh tu ri daoine a sgrìobhas mun dèidhinn air Mastodon!
Faigh ballrachd ann an coimhearsnachd s cruthaich pròifil dhut. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Cuir thu fhèin an cèill le Emojis gnàthaichte, dealbhan, GIFs, videothan is fuaimean ann am postaichean le 500 caractar. Freagair ri snàithleanan is brosnaich postaichean le neach sam bith airson deagh rudan a cho-roinneadh. Lorg cunntasan ùra ri leantainn is tagaichean hais a treandadh airson an lìonra agad a leudachadh.
Faigh ballrachd ann an coimhearsnachd s cruthaich pròifil dhut. Lorg is lean daoine inntinneach leugh na postaichean aca air loidhne-ama cheart gun sanasachd. Cuir thu fhèin an cèill le Emojis gnàthaichte, dealbhan, GIFs, videothan is fuaimean ann am postaichean le 500 caractar. Freagair ri snàithleanan is brosnaich postaichean le neach sam bith airson deagh rudan a cho-roinneadh. Lorg cunntasan ùra ri leantainn is tagaichean hais a treandadh airson an lìonra agad a leudachadh.
Chaidh Mastodon a thogail leis an aire air prìobhaideachd is sàbhailteachd. Tha e an urra riut fhèin an co-roinn thu post leis an luchd-leantainn agad, leis na daoine air an doir thu iomradh a-mhàin no leis an t-saoghal mhòr. Leigidh rabhaidhean susbainte leat postaichean sa bheil susbaint fhrionasach fhalach is cha leig daoine leas coimhead air ach nuair a bhios iad deònach. Tha riaghailtean is maoir fa leth aig gach coimhearsnachd airson a buill a chumail sàbhailte agus cuidichidh innealan bacaidh is gearain le dìon o dhroch-dhìol.

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Մաստոդոնը համացանցի ամենամեծ ապակենտրոնացված սոցցանցն է։ Այն մի կայք չէ, այլ իրար հետ կապակցված անկախ համայնքների միլիոնավոր օգտատերերից կազմված ցանց։ No matter what youre into, you can meet passionate people posting about it on Mastodon!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Միացեք համայնքին և ստեղծեք հաշիվ։ Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Զգայուն կամ հրահրող թեմաներով գրառումները կարելի է թաքցնել նախազգուշացումներով։ Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:
Ավելին՝
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
Մուգ տարբերակ՝ կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
Հարցումներ՝ իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Մաստոդոնը գրանցված շահույթ չհետապնդող կազմակերպություն է, և աջակցվում է ձեր նվիրաբերություններով։ Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -1 +1 @@
Decentralized social network
Ապակենտրոնացված սոցիալական ցանց

View File

@@ -1,6 +1,6 @@
Mastodon adalah jejaring sosial terdesentralisasi terbesar di internet. Daripada sebuah satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat berinteraksi antar sesama, tanpa masalah. Tanpa memedulikan apa yang Anda minat, Anda dapat bertemu orang-orang yang mengirimkan apa yang mereka minat di Mastodon!
Mastodon dalah jaringan sosial terdesentralisasi terbesar di internet. Bukan hanya satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat saling interaksi antar sesama, tanpa batasan. Apapun yang kamu minati, kamu dapat bertemu orang-orang baru yang mengirimkan apa yang mereka minati di Mastodon!
Bergabung sebuah komunitas dan buat profil Anda. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam lini masa yang kronologis dan bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
Bergabunglah dalam sebuah komunitas dan buat profil kalian. Temukan dan ikuti orang-orang yang menarik dan baca postingan mereka dalam linimasa yang kronologis serta bebas iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.

View File

@@ -1,16 +1,16 @@
Mastodonは、インターネット上で最大の分散型ソーシャルネットワークです。 Mastodonは単一のウェブサイトではなく、それぞれ独立したコミュニティに参加している何百万人ものユーザーによって構成されたネットワークなのです。ユーザーたちはその中で、誰もがお互いとシームレスにやり取りできます。 あなたの興味関心がどんな分野にあっても、きっとMastodonのどこかで同じ情熱を投稿している仲間がいますよ!
Mastodon は、インターネット上で最大の分散型ソーシャルネットワークです。 Mastodon は単一のウェブサイトではなく、それぞれ独立したコミュニティに参加している何百万人ものユーザーによって構成されたネットワークなのです。ユーザーたちはその中で、誰もがお互いとシームレスにやり取りできます。 あなたの興味関心がどんな分野にあっても、きっと Mastodon のどこかで同じ情熱を投稿している仲間がいることでしょう。
まずはコミュニティに参加して、自分のプロフィールを作成しましょう。 Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. あるいは、500文字まで使える投稿で自分を表現してみましょう。カスタム絵文字や画像、GIF、動画、音声も使用できます。 スレッドに返事したり、他の誰かの面白い投稿をブーストして共有したりすることもできます。 新しいアカウントとホットなタグを見つけて、あなた自身のネットワークを広げていきましょう
まずはコミュニティに参加して、自分のプロフィールを作成しましょう。 魅力的なユーザーを見つけたら、フォローしてタイムラインで投稿を見てみましょう。タイムラインに広告は出てきません。順番も時系列順です。 そして、500 文字まで使える投稿で自分を表現してみましょう。カスタム絵文字や画像、GIF、動画、音声も使用できます。 スレッドに返事したり、他の誰かの面白い投稿をブーストして共有したりすることもできます。 興味のあるアカウントとホットなハッシュタグをどんどん見つけて、あなた自身のネットワークを広げていきましょう
Mastondonはプライバシーと安全性を重視しています。 自分の投稿をフォロワー限定公開にするのか、メンションした特定のユーザーだけ共有するのか、全世界に放流するのかは、すべてあなた次第。 また、入力中の投稿について「ちょっとセンシティブな内容だな」と思ったら、閲覧注意機能で内容を伏せることで、見たくない人に配慮した投稿が作成できます。 そして、各コミュニティにはそれぞれのガイドラインと管理者・モデレーターが存在し、コミュニティメンバーの安全を守っています。強力なブロック・通報機能も、不正利用の防止をお手伝いします。
Mastondon はプライバシーと安全性を重視しています。 自分の投稿をフォロワーだけに限定公開にするのか、メンションした特定のユーザーだけ共有するのか、全世界に放流するのか、自由に決められます。 また、入力中の投稿について「ちょっとセンシティブな内容だな」と思ったら、閲覧注意機能で内容を伏せることで、見たくない人に配慮した投稿が作成できます。 そして、各コミュニティにはそれぞれのガイドラインと管理者・モデレーターが存在し、コミュニティメンバーの安全を守っています。強力なブロック・通報機能も、不正利用の防止をお手伝いします。
その他の機能
その他の機能:
• ダークモード対応:ライトモードだけでなく、ダークモードや「真っ黒」モードで投稿を閲覧
投票機能:フォロワーたちの意見を投票形式で集計
• 探索話題のハッシュタグやアカウントに1タップでアクセス
• 通知設定:新しいフォローやリプライ、ブーストがあった時に通知
• 共有どのアプリからでも「共有」メニューを通じてMastodonへ直接投稿
• 癒しMastodonが誇る象のマスコットかわいいが、画面にお邪魔したり、しなかったり
• ダークモード: ライトモードだけでなく、ダークモードや「真っ黒」モードで投稿を閲覧
アンケート: フォロワーたちの意見を投票形式で集計
• 探索: 話題のハッシュタグやアカウントに 1 タップでアクセス
• 通知: 新しいフォローやリプライ、ブーストがあった時に通知
• 共有: どのアプリからでも「共有」メニューを通じて Mastodon へ直接投稿
• 癒し: Mastodon が誇る象のマスコット(かわいい)が、画面にお邪魔したり、しなかったり
Mastodonは公認の非営利アプリです。開発は全てユーザーの寄付から成り立っています。 広告なし、アフィリエイトなし、第三者組織による出資なし。今でも、そしてこれからもそんなアプリであり続けるために、我々は日々努力し続けています。
Mastodon は公認の非営利アプリです。開発は全てユーザーの寄付から成り立っています。 広告なし、アフィリエイトなし、第三者組織による出資なし。今でも、そしてこれからもそんなアプリであり続けるために、我々は日々努力し続けています。

View File

@@ -1,4 +1,4 @@
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke gemeenschappen die allemaal naadloos met elkaar kunnen communiceren. Wat je ook leuk vindt, je kunt gepassioneerde mensen volgen die het met je willen delen op Mastodon!
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke communities die allemaal naadloos met elkaar kunnen communiceren. Wat je ook leuk vindt, je kunt gepassioneerde mensen volgen die het met je willen delen op Mastodon!
Meld je aan bij een community en maak je profiel aan. Vind en volg fascinerende mensen en lees hun berichten in een advertentievrije, chronologische tijdlijn. Druk jezelf uit met aangepaste emoji, afbeeldingen, GIFs, videos en audio in berichten van 500 karakters. Reageer op discussies en boost berichten van anderen om geweldige dingen te delen. Vind nieuwe accounts om te volgen en hashtags om je netwerk uit te breiden.

View File

@@ -1,16 +1,16 @@
Mastodon — это крупнейшая распределённая социальная сеть в интернете. Вместо одного сайта, это сеть из независимых сообществ с миллионами пользователей, которые могут бесшовно взаимодействовать друг с другом. Вне зависимости от того, чем вы увлекаетесь, вы всегда найдёте себе единомышленников в Mastodon!
Вступите в сообщество по интересу и создайте свой профиль. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Выражайте себя в 500-символьных постах, дополняя их пользовательскими эмодзи, изображениями, гифками, видео и аудио. Участвуйте в обсуждениях и продвигайте отличные посты от других людей. Расширяйте свой кругозор, находя новых интересных людей и следя за актуальными хэштегами.
Вступите в сообщество по интересу и создайте свой профиль. Ищите и подписывайтесь на увлекательных пользователей, читайте их посты без рекламы в хронологической ленте. Выражайте себя в 500-символьных постах, дополняя их пользовательскими эмодзи, изображениями, гифками, видео и аудио. Участвуйте в обсуждениях и продвигайте отличные посты от других людей. Расширяйте свой кругозор, находя новых интересных людей и следя за актуальными хэштегами.
Mastodon создан с акцентом на конфиденциальность и безопасность. Решайте с кем вы хотите поделиться своими постами: своими подписчиками, только упомянутыми людьми или же вообще со всем миром. Предупреждения о содержимом позволят вам скрыть посты содержащие материалы деликатного или шокирующего характера. В каждом сообществе свои правила и модераторы, следящие за порядком, а надёжные инструменты блокировки и система жалоб помогают предотвращать злоупотребление.
Ещё больше возможностей:
• Темы на любой вкус: читайте посты в светлом, тёмном или OLED режимах
Спрашивайте мнение подписчиков и подсчитывайте их голоса с опросами
Найдите актуальные хэштеги, интересные посты и профили во вкладке «Обзор»
Будьте в курсе происходящего с уведомлениями о новых подписчиках, ответах и продвижениях
Опросы: спрашивайте мнение подписчиков и подсчитывайте их голоса
Обзор: в одно касание найдите актуальные хэштеги, посты и профили
Уведомления: узнавайте о новых подписках, ответах и продвижениях
• Делитесь в Mastodon содержимым из любого приложения
Умиляйтесь с нашим талисманом, восхитительным слонёнком, которого можно встретить и тут и там
Милота: Наш талисман является восхитительным слоном, и вы будете видеть его время от времени
Mastodon является зарегистрированной некоммерческой организацией, его разработка поддерживается непосредственно вашими пожертвованиями. У нас нет рекламы, монетизации и венчурного капитала, и мы не планируем это менять.

View File

@@ -1 +1 @@
Распределённая социальная сеть
Децентрализованная социальная сеть

View File

@@ -1,16 +1,16 @@
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbiri ile kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğun önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsin!
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbiri ile kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğunuz önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsiniz!
Bir topluluğa katıl ve profilini oluştur. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Gönderilerinde 500 karakter sınırlamasıyla kendini emojiler, görseller, GIFler, videolar ve sesler ile ifade et. Harika içerikler paylaşmak için başlıklara yanıt yaz, insanların gönderilerini yeniden paylaş. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bul.
Bir topluluğa katılın ve profilinizi oluşturun. Olağanüstü kişileri bulun ve takip edin; gönderilerini reklamsız ve kronolojik bir zaman çizelgesinde okuyun. Gönderilerinizde şimdilik 500 karakter sınırlamasıyla kendinizi emojiler, görseller, GIFler, videolar ve sesler ile ifade edin. Harika içerikler paylaşmak için başlıklara yanıt yazın, insanların gönderilerini yineleyin. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bulun.
Mastodon gizlilik ve güvenlik odaklı yapılmıştır. Her postunuz için takipçilerinizle mi, bahsettiğiniz kişilerle mi ya da tüm dünyayla mı paylaşılacağına karar verin. Gönderi uyarıları, hassas ve tetikleyici olabilecek içerikleri kişi görmeyi hazır olana kadar gizler. Her topluluk, üyelerini güvende tutmak için kendi kurallarına ve moderatörlerine; istismarı önlemek için de güçlü engelleme ve bildirme araçlarına sahiptir.
Mastodon, mahremiyet ve güvenliğe odaklanılarak inşa edilmiştir. Gönderilerinizi takipçilerinizle mi, sadece bahsettiğiniz kişilerle mi yoksa tüm dünyayla mı paylaşılacağına karar verin. İçerik uyarıları, hassas veya tetikleyici materyal içeren gönderileri, siz onlarla etkileşim kurmaya hazır olana kadar gizlemenize olanak tanır. Her topluluk, üyelerini güvende tutmak için kendi kurallarına ve moderatörlerine; istismarı önlemek için de güçlü engelleme ve bildirme araçlarına sahiptir.
Diğer özellikler:
• Koyu Mod: Gönderileri aydınlık, karanlık ya da gerçek karanlık modunda okuyabilirsin
• Anketler: Takipçilerine görüşlerini sor ve oylarını gör
• Keşfet: Trend hashtagler ve hesaplar bir tık uzağında
• Bildirimler: Yeni takipçilerden, yanıtlardan ve yeniden paylaşımlardan haberin olsun
• Paylaşım: Doğrudan Mastodon'a herhangi bir tipte gönderi paylaş
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksin
• Karanlık Mod: Gönderileri aydınlık, karanlık ya da gerçek karanlık modunda okuyabilirsin
• Anketler: Takipçilere fikirlerini sorun ve oylarını görün
• Keşfet: Öne çıkan etiketlerler ve hesaplar bir tık uzağınızda
• Bildirimler: Yeni takipçilerden, yanıtlardan ve yeniden paylaşımlardan haberiniz olsun
• Paylaşım: Doğrudan Mastodon'a herhangi bir türde gönderi paylaş
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksiniz
Mastodon kar amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam, para kazanma amacı, risk sermayesi yoktur ve bunu böyle tutmayı planlıyoruz.
Mastodon kar amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam yok, para kazanma güdüsü yok, risk sermayesi yok ve bu şekilde devam etmeyi planlıyoruz.

View File

@@ -1 +1 @@
Merkezsizleştirilmiş sosyal ağ
Merkezi olmayan sosyal ağ

View File

@@ -1,16 +1,16 @@
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Замість одного сайту це мережа мільйонів користувачів у незалежних спільнотах, які можуть взаємодіяти один з одним. Незалежно від того, чим ви займаєтеся, ви можете зустріти захоплених людей, які пишуть про це на Mastodon!
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Це не один сайт, а мережа з мільйонів користувачів у незалежних спільнотах, які взаємодіють одна з одною. Незалежно від того, чим ви займаєтеся, ви можете зустріти людей зі спільними інтересами, які пишуть про це на Mastodon!
Приєднуйтесь до спільноти і створіть свій профіль. Знайдіть і підпишіться на цікавих людей і читайте пости у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо з 500-символьними постами. Відповідайте на теми й робіть репости постів від будь-кого, щоб ділитися гарними матеріалами. Знаходьте нові облікові записи, щоб підписатися і популярні хештеги для розширення вашої мережі.
Приєднуйтесь до спільноти та створіть свій профіль. Знайдіть і підпишіться на цікавих людей та читайте дописи у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо у дописах, обмеженими 500 символами. Відповідайте на теми та поширюйте дописи від будь-кого, щоб ділитися цікавим. Знаходьте нових користувачів, щоб підписатися та слідкуйте за популярними хештегами, щоб розширити свій кругозір.
Mastodon будується з акцентом на конфіденційність та безпеці. Вирішіть, чи будуть ваші пости тільки для підписників, або для людей, яких ви згадали, чи для цілого світу. Попередження щодо вмісту дозволяють приховати публікації, що містять конфіденційний або провокаційний матеріал, доки ви не будете готові до нього. Кожна спільнота має свої правила і модераторів, щоб залишити учасників в безпеці, а також надійне блокування та інструменти для скарг, щоб запобігти зловживання.
Mastodon будується з акцентом на конфіденційність та безпеку. Вирішіть, чи будуть ваші дописи доступні лише підписникам, або для людей, яких ви згадали, чи взагалі для всіх. Попередження про вміст дозволяють приховати дописи, що містять чутливі або провокаційні матеріали, доки ви не будете готові взаємодіяти з ними. Кожна спільнота має власні правила та модераторів, які забезпечують безпеку учасників, а надійні інструменти блокування та система скарг допомагають запобігти зловживанням.
Більше можливостей:
• Темна Тема: Читайте у світлій, темній, або справжній чорній темі
• Опитування: запитуйте думку підписникіна та підраховуйте голоси
Досліджуйте: Популярні Хештеги й Користувачі за одним дотиком
• Сповіщення: отримуйте сповіщення про нових підписників, відповіді та репости
Діліться: Публікуйте безпосередньо в Mastodon з будь-якого меню "поділитися" в будь-якому додатку
Привабливість: Нашим талісманом є чарівний слон, і ви побачите, як він з'являється час від часу
• Темна тема: Читайте дописи у світлому, темному або повністю чорному режимі
• Опитування: Запитайте у підписників їхню думку та підраховуйте голоси
Досліджуйте: Популярні хештеги та користувачі на відстані одного дотику
• Сповіщення: Отримуйте сповіщення про нові підписки, відповіді та поширення
Діліться: Публікуйте безпосередньо в Mastodon через будь-яке меню "поділитися" у будь-якому додатку
Миле: Наш талісман - чарівне слоненя, яке час від часу ви помічатимете
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу, і плануємо так тримати.
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу. І ми плануємо, що так буде й надалі.

View File

@@ -0,0 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:
• Dark Mode: Read posts in light, dark, or true black mode
• Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -0,0 +1 @@
Decentralized social network

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -1 +1 @@
Mạng xã hội phi tập trung
Mạng xã hội liên hợp

View File

@@ -1,16 +1,16 @@
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你关注什么话题,你都能在 Mastodon 上找到兴趣相投的人进行交流。
加入一个社区节点并创建你的账户。 Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. 用自定义表情、图像、GIF、视频和音频 500 个字符的帖子里写下你的想法。 回复转发其他人的帖子来共同分享美好的事物。 寻找新的账户以及热门话题来拓展你的网络。
加入一个社区节点并创建你的账户。 查找、关注有趣的同好,无广告、无时间线干扰地阅读他们的帖子。 借助自定义 emoji、图像、GIF、视频和音频,在最多 500 字的帖文中表达自我。 通过回复转发其他人的帖文来分享美好的事物。 通过准寻新账户并关注热门话题标签来扩展你的社交网络。
Mastodon 以隐私和安全为首要目标。 自由选择你的帖子是分享给关注者,或是你提到的人,亦或是整个世界。 内容警告允许让你隐藏可能剧透的内容,让其他人在交互之前做好充分准备。 每个社区都有自己的规则和管理员,让其用户保持安全。通过强有力的屏蔽和举报工具防止滥用。
Mastodon 以隐私和安全为首要目标。 你可以自主决定帖文的分享分享对象,可以是你的关注者、你提到的人或是整个世界。 在你做好充足的互动准备之前,内容警告可以隐藏包含敏感或刺激内容的帖文。 每个社区都有自己的规则和管理员来保证其成员安全,同时还有强力的屏蔽和举报工具来避免滥用。
更多功能:
• 暗色模式:浅色、深色纯黑模式下进行阅读
• 暗色模式:浅色、深色纯黑模式下阅读帖文
• 投票:询问关注者的意见并统计他们的投票
• 探索:热门的话题及账号
• 通知:获得新关注、回复和转发的通知
• 探索:热门的话题标签及账号只有一触之遥
• 通知:获关注、回复和转发相关的通知提醒
• 分享:从其他应用中的分享菜单中直接发布到 Mastodon
• 吉祥物:你会不时地看到我们可爱的长毛象
Mastodon 是一个注册非营利开发项目,直接由您的捐赠支持。 没有广告没有商业化,也没有风险资本,我们计划保持这种方式。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。
Mastodon 是一个直接由用户捐赠支持、已注册非营利开发项目。 没有广告没有商业化,也没有风险资本,并且我们也计划保持这种方式。

View File

@@ -1,4 +1,4 @@
Mastodon 是網際網路上最大的去中心化社網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能在 Mastodon 上遇到充滿熱情的人們討論該話題。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
Mastodon 是網際網路上最大的去中心化社網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能在 Mastodon 上遇到充滿熱情的人們討論該話題。
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並在無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 在 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊來表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤來拓展您的網路。

View File

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

279
mastodon/.editorconfig Normal file
View File

@@ -0,0 +1,279 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = false
max_line_length = 300
tab_width = 4
ij_continuation_indent_size = 8
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = false
ij_smart_tabs = false
ij_visual_guides = none
ij_wrap_on_typing = false
[*.java]
ij_java_align_consecutive_assignments = false
ij_java_align_consecutive_variable_declarations = false
ij_java_align_group_field_declarations = false
ij_java_align_multiline_annotation_parameters = false
ij_java_align_multiline_array_initializer_expression = false
ij_java_align_multiline_assignment = false
ij_java_align_multiline_binary_operation = false
ij_java_align_multiline_chained_methods = false
ij_java_align_multiline_extends_list = false
ij_java_align_multiline_for = true
ij_java_align_multiline_method_parentheses = false
ij_java_align_multiline_parameters = true
ij_java_align_multiline_parameters_in_calls = false
ij_java_align_multiline_parenthesized_expression = false
ij_java_align_multiline_records = true
ij_java_align_multiline_resources = true
ij_java_align_multiline_ternary_operation = false
ij_java_align_multiline_text_blocks = false
ij_java_align_multiline_throws_list = false
ij_java_align_subsequent_simple_methods = false
ij_java_align_throws_keyword = false
ij_java_align_types_in_multi_catch = true
ij_java_annotation_parameter_wrap = off
ij_java_array_initializer_new_line_after_left_brace = false
ij_java_array_initializer_right_brace_on_new_line = false
ij_java_array_initializer_wrap = off
ij_java_assert_statement_colon_on_next_line = false
ij_java_assert_statement_wrap = off
ij_java_assignment_wrap = off
ij_java_binary_operation_sign_on_next_line = false
ij_java_binary_operation_wrap = off
ij_java_blank_lines_after_anonymous_class_header = 0
ij_java_blank_lines_after_class_header = 0
ij_java_blank_lines_after_imports = 1
ij_java_blank_lines_after_package = 1
ij_java_blank_lines_around_class = 1
ij_java_blank_lines_around_field = 0
ij_java_blank_lines_around_field_in_interface = 0
ij_java_blank_lines_around_initializer = 1
ij_java_blank_lines_around_method = 1
ij_java_blank_lines_around_method_in_interface = 1
ij_java_blank_lines_before_class_end = 0
ij_java_blank_lines_before_imports = 1
ij_java_blank_lines_before_method_body = 0
ij_java_blank_lines_before_package = 0
ij_java_block_brace_style = end_of_line
ij_java_block_comment_add_space = false
ij_java_block_comment_at_first_column = true
ij_java_builder_methods = none
ij_java_call_parameters_new_line_after_left_paren = false
ij_java_call_parameters_right_paren_on_new_line = false
ij_java_call_parameters_wrap = off
ij_java_case_statement_on_separate_line = true
ij_java_catch_on_new_line = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_line
ij_java_class_count_to_use_import_on_demand = 99
ij_java_class_names_in_javadoc = 1
ij_java_do_not_indent_top_level_class_members = false
ij_java_do_not_wrap_after_single_annotation = false
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
ij_java_do_while_brace_force = never
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = true
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = false
ij_java_doc_keep_empty_lines = true
ij_java_doc_keep_empty_parameter_tag = true
ij_java_doc_keep_empty_return_tag = true
ij_java_doc_keep_empty_throws_tag = true
ij_java_doc_keep_invalid_tags = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_preserve_line_breaks = false
ij_java_doc_use_throws_not_exception_tag = true
ij_java_else_on_new_line = false
ij_java_enum_constants_wrap = off
ij_java_extends_keyword_wrap = off
ij_java_extends_list_wrap = off
ij_java_field_annotation_wrap = split_into_lines
ij_java_finally_on_new_line = false
ij_java_for_brace_force = never
ij_java_for_statement_new_line_after_left_paren = false
ij_java_for_statement_right_paren_on_new_line = false
ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = never
ij_java_imports_layout = android.**,|,com.**,|,junit.**,|,net.**,|,org.**,|,java.**,|,javax.**,|,*,|,$*,|
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
ij_java_keep_blank_lines_before_right_brace = 2
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
ij_java_keep_blank_lines_in_code = 2
ij_java_keep_blank_lines_in_declarations = 2
ij_java_keep_builder_methods_indents = false
ij_java_keep_control_statement_in_one_line = true
ij_java_keep_first_column_comment = true
ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false
ij_java_label_indent_size = 0
ij_java_lambda_brace_style = end_of_line
ij_java_layout_static_imports_separately = true
ij_java_line_comment_add_space = false
ij_java_line_comment_add_space_on_reformat = false
ij_java_line_comment_at_first_column = true
ij_java_method_annotation_wrap = split_into_lines
ij_java_method_brace_style = end_of_line
ij_java_method_call_chain_wrap = off
ij_java_method_parameters_new_line_after_left_paren = false
ij_java_method_parameters_right_paren_on_new_line = false
ij_java_method_parameters_wrap = off
ij_java_modifier_list_wrap = false
ij_java_multi_catch_types_wrap = normal
ij_java_names_count_to_use_import_on_demand = 99
ij_java_new_line_after_lparen_in_annotation = false
ij_java_new_line_after_lparen_in_record_header = false
ij_java_parameter_annotation_wrap = off
ij_java_parentheses_expression_new_line_after_left_paren = false
ij_java_parentheses_expression_right_paren_on_new_line = false
ij_java_place_assignment_sign_on_next_line = false
ij_java_prefer_longer_names = true
ij_java_prefer_parameters_wrap = false
ij_java_record_components_wrap = normal
ij_java_repeat_synchronized = true
ij_java_replace_instanceof_and_cast = false
ij_java_replace_null_check = true
ij_java_replace_sum_lambda_with_method_ref = true
ij_java_resource_list_new_line_after_left_paren = false
ij_java_resource_list_right_paren_on_new_line = false
ij_java_resource_list_wrap = off
ij_java_rparen_on_new_line_in_annotation = false
ij_java_rparen_on_new_line_in_record_header = false
ij_java_space_after_closing_angle_bracket_in_type_argument = false
ij_java_space_after_colon = true
ij_java_space_after_comma = true
ij_java_space_after_comma_in_type_arguments = true
ij_java_space_after_for_semicolon = true
ij_java_space_after_quest = true
ij_java_space_after_type_cast = true
ij_java_space_before_annotation_array_initializer_left_brace = false
ij_java_space_before_annotation_parameter_list = false
ij_java_space_before_array_initializer_left_brace = false
ij_java_space_before_catch_keyword = false
ij_java_space_before_catch_left_brace = false
ij_java_space_before_catch_parentheses = false
ij_java_space_before_class_left_brace = false
ij_java_space_before_colon = true
ij_java_space_before_colon_in_foreach = true
ij_java_space_before_comma = false
ij_java_space_before_do_left_brace = false
ij_java_space_before_else_keyword = false
ij_java_space_before_else_left_brace = false
ij_java_space_before_finally_keyword = false
ij_java_space_before_finally_left_brace = false
ij_java_space_before_for_left_brace = false
ij_java_space_before_for_parentheses = false
ij_java_space_before_for_semicolon = false
ij_java_space_before_if_left_brace = false
ij_java_space_before_if_parentheses = false
ij_java_space_before_method_call_parentheses = false
ij_java_space_before_method_left_brace = false
ij_java_space_before_method_parentheses = false
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
ij_java_space_before_quest = true
ij_java_space_before_switch_left_brace = false
ij_java_space_before_switch_parentheses = false
ij_java_space_before_synchronized_left_brace = false
ij_java_space_before_synchronized_parentheses = false
ij_java_space_before_try_left_brace = false
ij_java_space_before_try_parentheses = false
ij_java_space_before_type_parameter_list = false
ij_java_space_before_while_keyword = false
ij_java_space_before_while_left_brace = false
ij_java_space_before_while_parentheses = false
ij_java_space_inside_one_line_enum_braces = false
ij_java_space_within_empty_array_initializer_braces = false
ij_java_space_within_empty_method_call_parentheses = false
ij_java_space_within_empty_method_parentheses = false
ij_java_spaces_around_additive_operators = false
ij_java_spaces_around_annotation_eq = true
ij_java_spaces_around_assignment_operators = false
ij_java_spaces_around_bitwise_operators = false
ij_java_spaces_around_equality_operators = false
ij_java_spaces_around_lambda_arrow = false
ij_java_spaces_around_logical_operators = true
ij_java_spaces_around_method_ref_dbl_colon = false
ij_java_spaces_around_multiplicative_operators = false
ij_java_spaces_around_relational_operators = false
ij_java_spaces_around_shift_operators = false
ij_java_spaces_around_type_bounds_in_type_parameters = true
ij_java_spaces_around_unary_operator = false
ij_java_spaces_within_angle_brackets = false
ij_java_spaces_within_annotation_parentheses = false
ij_java_spaces_within_array_initializer_braces = false
ij_java_spaces_within_braces = false
ij_java_spaces_within_brackets = false
ij_java_spaces_within_cast_parentheses = false
ij_java_spaces_within_catch_parentheses = false
ij_java_spaces_within_for_parentheses = false
ij_java_spaces_within_if_parentheses = false
ij_java_spaces_within_method_call_parentheses = false
ij_java_spaces_within_method_parentheses = false
ij_java_spaces_within_parentheses = false
ij_java_spaces_within_record_header = false
ij_java_spaces_within_switch_parentheses = false
ij_java_spaces_within_synchronized_parentheses = false
ij_java_spaces_within_try_parentheses = false
ij_java_spaces_within_while_parentheses = false
ij_java_special_else_if_treatment = true
ij_java_subclass_name_suffix = Impl
ij_java_ternary_operation_signs_on_next_line = false
ij_java_ternary_operation_wrap = off
ij_java_test_name_suffix = Test
ij_java_throws_keyword_wrap = off
ij_java_throws_list_wrap = off
ij_java_use_external_annotations = false
ij_java_use_fq_class_names = false
ij_java_use_relative_indents = false
ij_java_use_single_class_imports = true
ij_java_variable_annotation_wrap = off
ij_java_visibility = public
ij_java_while_brace_force = never
ij_java_while_on_new_line = false
ij_java_wrap_comments = false
ij_java_wrap_first_method_in_call_chain = false
ij_java_wrap_long_lines = false
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4
ij_xml_align_attributes = false
ij_xml_align_text = false
ij_xml_attribute_wrap = normal
ij_xml_block_comment_add_space = false
ij_xml_block_comment_at_first_column = true
ij_xml_keep_blank_lines = 2
ij_xml_keep_indents_on_empty_lines = false
ij_xml_keep_line_breaks = false
ij_xml_keep_line_breaks_in_text = true
ij_xml_keep_whitespaces = false
ij_xml_keep_whitespaces_around_cdata = preserve
ij_xml_keep_whitespaces_inside_cdata = false
ij_xml_line_comment_at_first_column = true
ij_xml_space_after_tag_name = false
ij_xml_space_around_equals_in_attribute = false
ij_xml_space_inside_empty_tag = true
ij_xml_text_wrap = normal
ij_xml_use_custom_settings = true

View File

@@ -9,10 +9,10 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 50
versionName "1.2.0"
versionCode 64
versionName "2.0.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -37,6 +37,9 @@ android {
githubRelease{
initWith release
}
githubDebug{
initWith debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -53,6 +56,9 @@ android {
githubRelease{
setRoot "src/github"
}
githubDebug{
setRoot "src/github"
}
}
lintOptions{
checkReleaseBuilds false
@@ -69,14 +75,15 @@ 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.7'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.9'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'
implementation 'org.parceler:parceler-api:1.1.12'
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
def appCenterSdkVersion = "4.4.2"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
@@ -84,8 +91,8 @@ dependencies {
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'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View File

@@ -65,6 +65,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.*;
@LargeTest
public class StoreScreenshotsGenerator{
private static final String PHOTO_FILE="IMG_1010.jpg";
private static final long LOAD_WAIT_TIMEOUT=20_000;
@Rule
public ActivityScenarioRule<MainActivity> activityScenarioRule=new ActivityScenarioRule<>(MainActivity.class);
@@ -84,14 +85,14 @@ public class StoreScreenshotsGenerator{
AccountSession session=AccountSessionManager.getInstance().getAccount(AccountSessionManager.getInstance().getLastActiveAccountID());
MastodonApp.context.deleteDatabase(session.getID()+".db");
onView(isRoot()).perform(waitId(R.id.more, 5000));
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
Thread.sleep(500);
takeScreenshot("HomeTimeline");
GlobalUserPreferences.theme=GlobalUserPreferences.ThemePreference.DARK;
activityScenarioRule.getScenario().recreate();
onView(isRoot()).perform(waitId(R.id.more, 5000));
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT));
Thread.sleep(500);
takeScreenshot("HomeTimeline_Dark");
@@ -100,8 +101,8 @@ public class StoreScreenshotsGenerator{
activityScenarioRule.getScenario().onActivity(activity->UiUtils.openProfileByID(activity, session.getID(), args.getString("profileAccountID")));
Thread.sleep(500);
onView(isRoot()).perform(waitId(R.id.avatar_border, 5000)); // wait for profile to load
onView(isRoot()).perform(waitId(R.id.more, 5000)); // wait for timeline to load
onView(isRoot()).perform(waitId(R.id.avatar_border, LOAD_WAIT_TIMEOUT)); // wait for profile to load
onView(isRoot()).perform(waitId(R.id.more, LOAD_WAIT_TIMEOUT)); // wait for timeline to load
Thread.sleep(500);
takeScreenshot("Profile");

View File

@@ -95,7 +95,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
return;
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
if(timeSinceLastCheck>CHECK_PERIOD){
if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
@@ -109,23 +109,26 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
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+)");
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)(?:\\.(\\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));
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
if(!matcher.find()){
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=matcher.group(3)!=null ? Integer.parseInt(matcher.group(3)) : 0;
Matcher curMatcher=pattern.matcher(BuildConfig.VERSION_NAME);
if(!curMatcher.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));
int curMajor=Integer.parseInt(curMatcher.group(1)), curMinor=Integer.parseInt(curMatcher.group(2)), curRevision=matcher.group(3)!=null ? Integer.parseInt(curMatcher.group(3)) : 0;
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
if(newVersion>curVersion || BuildConfig.DEBUG){
String version=newMajor+"."+newMinor+"."+newRevision;
if(newVersion>curVersion || forceUpdate){
forceUpdate=false;
String version=newMajor+"."+newMinor;
if(matcher.group(3)!=null)
version+="."+newRevision;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
@@ -295,6 +298,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
}
}
@Override
public void reset(){
getPrefs().edit().clear().apply();
File apk=getUpdateApkFile();
if(apk.exists())
apk.delete();
state=UpdateState.NO_UPDATE;
}
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
@Override

View File

@@ -16,6 +16,9 @@
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="android.intent.action.TRANSLATE" />
</intent>
</queries>
<application
@@ -25,7 +28,6 @@
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:largeHeap="true">
@@ -34,6 +36,18 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https" android:host="mastodon.social" android:pathPrefix="/@"/>
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https" android:host="mastodon.online" android:pathPrefix="/@"/>
</intent-filter>
</activity>
<activity android:name=".OAuthActivity" android:exported="true" android:configChanges="orientation|screenSize" android:launchMode="singleTask">
<intent-filter>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<style>
*{
box-sizing: border-box;
overflow-wrap: break-word;
}
body{
background: {{colorSurface}};
padding: 16px 16px 0 16px;
margin: 0;
color: {{colorOnSurface}};
font-family: Roboto, sans-serif;
font-size: 14px;
line-height: 20px;
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
}
a{
text-decoration: none;
color: {{colorPrimary}};
}
p, h1, h2, h3, h4, h5, h6, ul, ol{
margin-bottom: 8px;
margin-top: 0;
}
h1, h2{
font-size: 16px;
line-height: 24px;
font-weight: 500;
}
h3, h4, h5, h6{
font-size: 14px;
line-height: 20px;
font-weight: 500;
}
b, strong{
font-weight: 500;
}
ul, ol{
padding-inline-start: 16px;
}
ul>li, ol>li{
padding-inline-start: 4px;
}
</style>
</head>
<body>
{{content}}
</body>
</html>

View File

@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcels;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import androidx.annotation.Nullable;
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
private static HashSet<Callback> callbacks=new HashSet<>();
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
private boolean resumeAfterAudioFocusGain;
private boolean isBuffering=true;
private BroadcastReceiver receiver=new BroadcastReceiver(){
@Override
@@ -176,6 +176,7 @@ public class AudioPlayerService extends Service{
player.setOnErrorListener(this::onPlayerError);
player.setOnCompletionListener(this::onPlayerCompletion);
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
player.setOnInfoListener(this::onPlayerInfo);
try{
player.setDataSource(this, Uri.parse(attachment.url));
player.prepareAsync();
@@ -187,7 +188,9 @@ public class AudioPlayerService extends Service{
}
private void onPlayerPrepared(MediaPlayer mp){
Log.i(TAG, "onPlayerPrepared");
playerReady=true;
isBuffering=false;
player.start();
updateSessionState(false);
}
@@ -205,6 +208,21 @@ public class AudioPlayerService extends Service{
stopSelf();
}
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
switch(what){
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
isBuffering=true;
updateSessionState(false);
}
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
isBuffering=false;
updateSessionState(false);
}
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
}
return true;
}
private void onAudioFocusChanged(int change){
switch(change){
case AudioManager.AUDIOFOCUS_LOSS -> {
@@ -212,7 +230,7 @@ public class AudioPlayerService extends Service{
pause(false);
}
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
resumeAfterAudioFocusGain=true;
resumeAfterAudioFocusGain=isPlaying();
pause(false);
}
case AudioManager.AUDIOFOCUS_GAIN -> {
@@ -232,12 +250,16 @@ public class AudioPlayerService extends Service{
private void updateSessionState(boolean removeNotification){
session.setPlaybackState(new PlaybackState.Builder()
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f)
.setState(switch(getPlayState()){
case PLAYING -> PlaybackState.STATE_PLAYING;
case PAUSED -> PlaybackState.STATE_PAUSED;
case BUFFERING -> PlaybackState.STATE_BUFFERING;
}, player.getCurrentPosition(), 1f)
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
.build());
updateNotification(!player.isPlaying(), removeNotification);
for(Callback cb:callbacks)
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition());
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
}
private void updateNotification(boolean dismissable, boolean removeNotification){
@@ -310,6 +332,12 @@ public class AudioPlayerService extends Service{
return attachment.id;
}
public PlayState getPlayState(){
if(isBuffering)
return PlayState.BUFFERING;
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
}
public static void registerCallback(Callback cb){
callbacks.add(cb);
}
@@ -333,7 +361,13 @@ public class AudioPlayerService extends Service{
}
public interface Callback{
void onPlayStateChanged(String attachmentID, boolean playing, int position);
void onPlayStateChanged(String attachmentID, PlayState state, int position);
void onPlaybackStopped(String attachmentID);
}
public enum PlayState{
PLAYING,
PAUSED,
BUFFERING
}
}

View File

@@ -6,7 +6,7 @@ import android.content.SharedPreferences;
public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
public static boolean trueBlackTheme;
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
public static ThemePreference theme;
private static SharedPreferences getPrefs(){
@@ -17,7 +17,10 @@ public class GlobalUserPreferences{
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
altTextReminders=prefs.getBoolean("altTextReminders", false);
confirmUnfollow=prefs.getBoolean("confirmUnfollow", false);
confirmBoost=prefs.getBoolean("confirmBoost", false);
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
}
@@ -25,8 +28,11 @@ public class GlobalUserPreferences{
getPrefs().edit()
.putBoolean("playGifs", playGifs)
.putBoolean("useCustomTabs", useCustomTabs)
.putBoolean("trueBlackTheme", trueBlackTheme)
.putInt("theme", theme.ordinal())
.putBoolean("altTextReminders", altTextReminders)
.putBoolean("confirmUnfollow", confirmUnfollow)
.putBoolean("confirmBoost", confirmBoost)
.putBoolean("confirmDeletePost", confirmDeletePost)
.apply();
}

View File

@@ -4,13 +4,15 @@ import android.Manifest;
import android.app.Application;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
@@ -20,6 +22,7 @@ import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
@@ -28,6 +31,9 @@ import java.lang.reflect.InvocationTargetException;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity{
@Override
@@ -64,6 +70,8 @@ public class MainActivity extends FragmentStackActivity{
showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}else{
maybeRequestNotificationsPermission();
}
@@ -105,11 +113,55 @@ public class MainActivity extends FragmentStackActivity{
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData(), null);
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
}*/
}
public void handleURL(Uri uri, String accountID){
if(uri==null)
return;
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
return;
AccountSession session;
if(accountID==null)
session=AccountSessionManager.getInstance().getLastActiveAccount();
else
session=AccountSessionManager.get(accountID);
if(session==null || !session.activated)
return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
}
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
Bundle args=new Bundle();
args.putString("account", accountID);
if(result.statuses!=null && !result.statuses.isEmpty()){
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
Nav.go(MainActivity.this, ThreadFragment.class, args);
}else if(result.accounts!=null && !result.accounts.isEmpty()){
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
Nav.go(MainActivity.this, ProfileFragment.class, args);
}else{
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(ErrorResponse error){
error.showToast(MainActivity.this);
}
})
.wrapProgress(this, progressText, true)
.exec(accountID);
}
private void showFragmentForNotification(Notification notification, String accountID){
Fragment fragment;
Bundle args=new Bundle();

View File

@@ -3,11 +3,10 @@ package org.joinmastodon.android;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.webkit.WebView;
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;
@@ -30,5 +29,8 @@ public class MastodonApp extends Application{
PushSubscriptionManager.tryRegisterFCM();
GlobalUserPreferences.load();
if(BuildConfig.DEBUG){
WebView.setWebContentsDebuggingEnabled(true);
}
}
}

View File

@@ -69,6 +69,10 @@ public class PushNotificationReceiver extends BroadcastReceiver{
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
return;
}
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
return;
}
String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
new GetNotificationByID(pn.notificationId+"")

View File

@@ -15,19 +15,17 @@ 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.FilterContext;
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;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -35,13 +33,15 @@ 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());
private final String accountID;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
private boolean loadingNotifications;
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
private static final int POST_FLAG_GAP_AFTER=1;
@@ -57,28 +57,23 @@ public class CacheController{
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", "flags"}, 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, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
String newMaxID;
outer:
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))
continue outer;
}
result.add(status);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
return;
}
@@ -90,7 +85,9 @@ public class CacheController{
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
ArrayList<Status> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null);
}
@@ -113,7 +110,7 @@ public class CacheController{
runOnDbThread((db)->{
if(clear)
db.delete("home_timeline", null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Status s:posts){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
@@ -121,6 +118,7 @@ public class CacheController{
if(s.hasGapAfter)
flags|=POST_FLAG_GAP_AFTER;
values.put("flags", flags);
values.put("time", s.createdAt.getEpochSecond());
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -130,28 +128,27 @@ public class CacheController{
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(!onlyMentions && loadingNotifications){
synchronized(pendingNotificationsCallbacks){
pendingNotificationsCallbacks.add(callback);
}
return;
}
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+"")){
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, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
String newMaxID;
outer:
do{
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
ntf.postprocess();
newMaxID=ntf.id;
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status))
continue outer;
}
}
result.add(ntf);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
return;
}
@@ -159,26 +156,40 @@ public class CacheController{
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
}
}
if(!onlyMentions)
loadingNotifications=true;
new GetNotifications(maxID, count, 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)){
return false;
}
}
}
return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
ArrayList<Notification> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
callback.onSuccess(res);
putNotifications(result, onlyMentions, maxID==null);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
}
}
@Override
public void onError(ErrorResponse error){
callback.onError(error);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
cb.onError(error);
}
pendingNotificationsCallbacks.clear();
}
}
}
})
.exec(accountID);
@@ -196,7 +207,7 @@ public class CacheController{
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
db.delete(table, null, null);
ContentValues values=new ContentValues(3);
ContentValues values=new ContentValues(4);
for(Notification n:notifications){
if(n.type==null){
continue;
@@ -204,6 +215,7 @@ public class CacheController{
values.put("id", n.id);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
values.put("time", n.createdAt.getEpochSecond());
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
@@ -300,30 +312,36 @@ public class CacheController{
CREATE TABLE `home_timeline` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0
`flags` INTEGER NOT NULL DEFAULT 0,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_all` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
db.execSQL("""
CREATE TABLE `notifications_mentions` (
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL
)""");
createRecentSearchesTable(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
if(oldVersion==1){
if(oldVersion<2){
createRecentSearchesTable(db);
}
if(oldVersion<3){
addTimeColumns(db);
}
}
private void createRecentSearchesTable(SQLiteDatabase db){
@@ -334,6 +352,15 @@ public class CacheController{
`time` INTEGER NOT NULL
)""");
}
private void addTimeColumns(SQLiteDatabase db){
db.execSQL("DELETE FROM `home_timeline`");
db.execSQL("DELETE FROM `notifications_all`");
db.execSQL("DELETE FROM `notifications_mentions`");
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
}
}
@FunctionalInterface

View File

@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -86,6 +87,9 @@ public class MastodonAPIController{
synchronized(req){
req.okhttpCall=call;
}
if(req.timeout>0){
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
}
if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
@@ -122,13 +126,17 @@ public class MastodonAPIController{
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
else
else if(req.respClass!=null)
respObj=gson.fromJson(respJson, req.respClass);
else
respObj=null;
}else{
if(req.respTypeToken!=null)
respObj=gson.fromJson(reader, req.respTypeToken.getType());
else
else if(req.respClass!=null)
respObj=gson.fromJson(reader, req.respClass);
else
respObj=null;
}
}catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG)

View File

@@ -45,6 +45,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Token token;
boolean canceled;
Map<String, String> headers;
long timeout;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@@ -127,6 +128,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
headers.put(key, value);
}
protected void setTimeout(long timeout){
this.timeout=timeout;
}
protected String getPathPrefix(){
return "/api/v1";
}

View File

@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
private String accountID;
private PrivateKey privateKey;
private PublicKey publicKey;
private PublicKey serverKey;
private byte[] authKey;
public PushSubscriptionManager(String accountID){
@@ -162,10 +161,6 @@ public class PushSubscriptionManager{
@Override
public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null)
return;

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.api;
import com.google.gson.reflect.TypeToken;
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
super(method, path, (Class<Void>)null);
}
}

View File

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

View File

@@ -27,6 +27,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
addQueryParameter("exclude_reblogs", "true");
}
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
case PINNED -> addQueryParameter("pinned", "true");
}
}
@@ -35,6 +36,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
INCLUDE_REPLIES,
MEDIA,
NO_REBLOGS,
OWN_POSTS_AND_REPLIES
OWN_POSTS_AND_REPLIES,
PINNED
}
}

View File

@@ -4,21 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{
public RegisterAccount(String username, String email, String password, String locale, String reason){
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
super(HttpMethod.POST, "/accounts", Token.class);
setRequestBody(new Body(username, email, password, locale, reason));
setRequestBody(new Body(username, email, password, locale, reason, timezone));
}
private static class Body{
public String username, email, password, locale, reason;
public String username, email, password, locale, reason, timeZone;
public boolean agreement=true;
public Body(String username, String email, String password, String locale, String reason){
public Body(String username, String email, String password, String locale, String reason, String timeZone){
this.username=username;
this.email=email;
this.password=password;
this.locale=locale;
this.reason=reason;
this.timeZone=timeZone;
}
}
}

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.StatusPrivacy;
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
}
private static class Request{
public Boolean locked, discoverable;
public RequestSource source;
public Request(Boolean locked, Boolean discoverable, RequestSource source){
this.locked=locked;
this.discoverable=discoverable;
this.source=source;
}
}
private static class RequestSource{
public StatusPrivacy privacy;
public String language;
public RequestSource(StatusPrivacy privacy, String language){
this.privacy=privacy;
this.language=language;
}
}
}

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.catalog;
import android.net.Uri;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import java.util.List;
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
public GetCatalogDefaultInstances(){
super(HttpMethod.GET, null, new TypeToken<>(){});
setTimeout(500);
}
@Override
public Uri getURL(){
return Uri.parse("https://api.joinmastodon.org/default-servers");
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
public class CreateFilter extends MastodonAPIRequest<Filter>{
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
super(HttpMethod.POST, "/filters", Filter.class);
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class DeleteFilter extends ResultlessMastodonAPIRequest{
public DeleteFilter(String id){
super(HttpMethod.DELETE, "/filters/"+id);
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,26 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.Keep;
@Keep
class FilterRequest{
public String title;
public EnumSet<FilterContext> context;
public FilterAction filterAction;
public Integer expiresIn;
public List<KeywordAttribute> keywordsAttributes;
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
this.title=title;
this.context=context;
this.filterAction=filterAction;
this.expiresIn=expiresIn;
this.keywordsAttributes=keywordsAttributes;
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.api.requests.accounts;
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken;
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
import java.util.List;
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{
public GetWordFilters(){
public class GetFilters extends MastodonAPIRequest<List<Filter>>{
public GetFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.LegacyFilter;
import java.util.List;
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
public GetLegacyFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
}
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.annotations.SerializedName;
class KeywordAttribute{
public String id;
@SerializedName("_destroy")
public Boolean delete;
public String keyword;
public Boolean wholeWord;
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
this.id=id;
this.delete=delete;
this.keyword=keyword;
this.wholeWord=wholeWord;
}
}

View File

@@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class UpdateFilter extends MastodonAPIRequest<Filter>{
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
List<KeywordAttribute> attrs=Stream.of(
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
).flatMap(Function.identity()).collect(Collectors.toList());
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.time.Instant;
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
public GetInstanceExtendedDescription(){
super(HttpMethod.GET, "/instance/extended_description", Response.class);
}
public static class Response{
public Instant updatedAt;
public String content;
}
}

View File

@@ -0,0 +1,12 @@
package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.TimelineMarkers;
public class GetMarkers extends MastodonAPIRequest<TimelineMarkers>{
public GetMarkers(){
super(HttpMethod.GET, "/markers", TimelineMarkers.class);
addQueryParameter("timeline[]", "home");
addQueryParameter("timeline[]", "notifications");
}
}

View File

@@ -2,11 +2,11 @@ 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;
import org.joinmastodon.android.model.TimelineMarkers;
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
super(HttpMethod.POST, "/markers", Response.class);
super(HttpMethod.POST, "/markers", TimelineMarkers.class);
JsonObjectBuilder builder=new JsonObjectBuilder();
if(lastSeenHomePostID!=null)
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
@@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
setRequestBody(builder.build());
}
public static class Response{
public Marker home, notifications;
}
}

View File

@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
addQueryParameter("resolve", "true");
}
public GetSearchResults limit(int limit){
addQueryParameter("limit", String.valueOf(limit));
return this;
}
@Override
protected String getPathPrefix(){
return "/api/v2";

View File

@@ -17,6 +17,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public static class Request{
public String status;
public List<MediaAttribute> mediaAttributes;
public List<String> mediaIds;
public Poll poll;
public String inReplyToId;
@@ -32,5 +33,17 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public boolean multiple;
public boolean hideTotals;
}
public static class MediaAttribute{
public String id;
public String description;
public String focus;
public MediaAttribute(String id, String description, String focus){
this.id=id;
this.description=description;
this.focus=focus;
}
}
}
}

View File

@@ -0,0 +1,40 @@
package org.joinmastodon.android.api.session;
import android.content.SharedPreferences;
public class AccountLocalPreferences{
private final SharedPreferences prefs;
public boolean showInteractionCounts;
public boolean customEmojiInNames;
public boolean showCWs;
public boolean hideSensitiveMedia;
public boolean serverSideFiltersSupported;
public AccountLocalPreferences(SharedPreferences prefs){
this.prefs=prefs;
showInteractionCounts=prefs.getBoolean("interactionCounts", true);
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
showCWs=prefs.getBoolean("showCWs", true);
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
}
public long getNotificationsPauseEndTime(){
return prefs.getLong("notificationsPauseTime", 0L);
}
public void setNotificationsPauseEndTime(long time){
prefs.edit().putLong("notificationsPauseTime", time).apply();
}
public void save(){
prefs.edit()
.putBoolean("interactionCounts", showInteractionCounts)
.putBoolean("emojiInNames", customEmojiInNames)
.putBoolean("showCWs", showCWs)
.putBoolean("hideSensitive", hideSensitiveMedia)
.putBoolean("serverSideFilters", serverSideFiltersSupported)
.apply();
}
}

View File

@@ -1,19 +1,48 @@
package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class AccountSession{
private static final String TAG="AccountSession";
public Token token;
public Account self;
public String domain;
@@ -26,13 +55,17 @@ public class AccountSession{
public PushSubscription pushSubscription;
public boolean needUpdatePushSettings;
public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>();
public List<LegacyFilter> wordFilters=new ArrayList<>();
public String pushAccountID;
public AccountActivationInfo activationInfo;
public Preferences preferences;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
private transient SharedPreferences prefs;
private transient boolean preferencesNeedSaving;
private transient AccountLocalPreferences localPreferences;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
@@ -73,4 +106,159 @@ public class AccountSession{
pushSubscriptionManager=new PushSubscriptionManager(getID());
return pushSubscriptionManager;
}
public String getFullUsername(){
return '@'+self.username+'@'+domain;
}
public void reloadPreferences(Consumer<Preferences> callback){
new GetPreferences()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Preferences result){
preferences=result;
if(callback!=null)
callback.accept(result);
AccountSessionManager.getInstance().writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
}
})
.exec(getID());
}
public SharedPreferences getRawLocalPreferences(){
if(prefs==null)
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
return prefs;
}
public void reloadNotificationsMarker(Consumer<String> callback){
new GetMarkers()
.setCallback(new Callback<>(){
@Override
public void onSuccess(TimelineMarkers result){
if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){
String id=result.notifications.lastReadId;
String lastKnown=getLastKnownNotificationsMarker();
if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){
// Marker moved back -- previous marker update must have failed.
// Pretend it didn't happen and repeat the request.
id=lastKnown;
new SaveMarkers(null, id).exec(getID());
}
callback.accept(id);
setNotificationsMarker(id, false);
}
}
@Override
public void onError(ErrorResponse error){}
})
.exec(getID());
}
public String getLastKnownNotificationsMarker(){
return getRawLocalPreferences().getString("notificationsMarker", null);
}
public void setNotificationsMarker(String id, boolean clearUnread){
getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
}
public void logOut(Activity activity, Runnable onDone){
new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
AccountSessionManager.getInstance().removeAccount(getID());
onDone.run();
}
@Override
public void onError(ErrorResponse error){
AccountSessionManager.getInstance().removeAccount(getID());
onDone.run();
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(getID());
}
public void savePreferencesLater(){
preferencesNeedSaving=true;
}
public void savePreferencesIfPending(){
if(preferencesNeedSaving){
new UpdateAccountCredentialsPreferences(preferences, null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
preferencesNeedSaving=false;
self=result;
AccountSessionManager.getInstance().writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
Log.e(TAG, "failed to save preferences: "+error);
}
})
.exec(getID());
}
}
public AccountLocalPreferences getLocalPreferences(){
if(localPreferences==null)
localPreferences=new AccountLocalPreferences(getRawLocalPreferences());
return localPreferences;
}
public void filterStatuses(List<Status> statuses, FilterContext context){
filterStatusContainingObjects(statuses, Function.identity(), context);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
if(getLocalPreferences().serverSideFiltersSupported){
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
if(s.filtered==null)
return false;
for(FilterResult filter:s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true;
}
return false;
});
return;
}
if(wordFilters==null)
return;
for(T obj:objects){
Status s=extractor.apply(obj);
if(s!=null && s.filtered!=null){
getLocalPreferences().serverSideFiltersSupported=true;
getLocalPreferences().save();
return;
}
}
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
for(LegacyFilter filter:wordFilters){
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
return true;
}
return false;
});
}
}

View File

@@ -13,8 +13,6 @@ import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
@@ -22,7 +20,7 @@ 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.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance;
@@ -32,7 +30,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
@@ -142,6 +140,10 @@ public class AccountSessionManager{
return session;
}
public static AccountSession get(String id){
return getInstance().getAccount(id);
}
@Nullable
public AccountSession tryGetAccount(String id){
return sessions.get(id);
@@ -174,12 +176,19 @@ public class AccountSessionManager{
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db");
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
MastodonApp.context.deleteSharedPreferences(id);
}else{
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
}
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty())
lastActiveAccountID=null;
else
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
}
writeAccountsFile();
String domain=session.domain.toLowerCase();
@@ -251,7 +260,7 @@ public class AccountSessionManager{
if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionLocalInfo(session);
}
if(now-session.filtersLastUpdated>3600_000L){
if(!session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session);
}
}
@@ -289,10 +298,10 @@ public class AccountSessionManager{
}
private void updateSessionWordFilters(AccountSession session){
new GetWordFilters()
new GetLegacyFilters()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Filter> result){
public void onSuccess(List<LegacyFilter> result){
session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
writeAccountsFile();

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -10,28 +12,36 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class AccountTimelineFragment extends StatusListFragment{
private Account user;
private GetAccountStatuses.Filter filter;
private HorizontalScrollView filtersBar;
private FilterChipView defaultFilter, withRepliesFilter, mediaFilter;
public AccountTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_no_refresh);
}
public static AccountTimelineFragment newInstance(String accountID, Account profileAccount, GetAccountStatuses.Filter filter, boolean load){
public static AccountTimelineFragment newInstance(String accountID, Account profileAccount, boolean load){
AccountTimelineFragment f=new AccountTimelineFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
args.putString("filter", filter.toString());
if(!load)
args.putBoolean("noAutoLoad", true);
args.putBoolean("__is_tab", true);
@@ -41,22 +51,22 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter"));
filter=GetAccountStatuses.Filter.DEFAULT;
super.onAttach(activity);
}
@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());
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.ACCOUNT);
onDataLoaded(result, !empty);
}
})
.exec(accountID);
@@ -65,6 +75,7 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackground(null); // prevents unnecessary overdraw
}
@Override
@@ -74,22 +85,95 @@ public class AccountTimelineFragment extends StatusListFragment{
loadData();
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
protected void onStatusCreated(Status status){
if(!AccountSessionManager.getInstance().isSelf(accountID, status.account))
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))
if(status.inReplyToAccountId!=null && !status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
return;
}else if(filter==GetAccountStatuses.Filter.MEDIA){
if(ev.status.mediaAttachments.isEmpty())
if(status.mediaAttachments.isEmpty())
return;
}
prependItems(Collections.singletonList(ev.status), true);
prependItems(Collections.singletonList(status), true);
}
@Override
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
@Override
protected RecyclerView.Adapter getAdapter(){
filtersBar=new HorizontalScrollView(getActivity());
LinearLayout filtersLayout=new LinearLayout(getActivity());
filtersBar.addView(filtersLayout);
filtersLayout.setOrientation(LinearLayout.HORIZONTAL);
filtersLayout.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(8));
filtersLayout.setDividerDrawable(new EmptyDrawable(V.dp(8), 1));
filtersLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
defaultFilter=new FilterChipView(getActivity());
defaultFilter.setText(R.string.posts);
defaultFilter.setTag(GetAccountStatuses.Filter.DEFAULT);
defaultFilter.setSelected(filter==GetAccountStatuses.Filter.DEFAULT);
defaultFilter.setOnClickListener(this::onFilterClick);
filtersLayout.addView(defaultFilter);
withRepliesFilter=new FilterChipView(getActivity());
withRepliesFilter.setText(R.string.posts_and_replies);
withRepliesFilter.setTag(GetAccountStatuses.Filter.INCLUDE_REPLIES);
withRepliesFilter.setSelected(filter==GetAccountStatuses.Filter.INCLUDE_REPLIES);
withRepliesFilter.setOnClickListener(this::onFilterClick);
filtersLayout.addView(withRepliesFilter);
mediaFilter=new FilterChipView(getActivity());
mediaFilter.setText(R.string.media);
mediaFilter.setTag(GetAccountStatuses.Filter.MEDIA);
mediaFilter.setSelected(filter==GetAccountStatuses.Filter.MEDIA);
mediaFilter.setOnClickListener(this::onFilterClick);
filtersLayout.addView(mediaFilter);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(filtersBar));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
protected int getMainAdapterOffset(){
return super.getMainAdapterOffset()+1;
}
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
return switch(filter){
case DEFAULT -> defaultFilter;
case INCLUDE_REPLIES -> withRepliesFilter;
case MEDIA -> mediaFilter;
default -> throw new IllegalStateException("Unexpected value: "+filter);
};
}
private void onFilterClick(View v){
GetAccountStatuses.Filter newFilter=(GetAccountStatuses.Filter) v.getTag();
if(newFilter==filter)
return;
// TODO maybe cache the filtered timelines that were already loaded?
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
getViewForFilter(filter).setSelected(false);
filter=newFilter;
v.setSelected(true);
data.clear();
preloadedData.clear();
int size=displayItems.size();
displayItems.clear();
adapter.notifyItemRangeRemoved(0, size);
loaded=false;
dataLoading=true;
doLoadData();
}
}

View File

@@ -5,14 +5,9 @@ import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -29,25 +24,26 @@ import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
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.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -56,15 +52,15 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
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.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends MastodonRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@@ -267,7 +263,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
list.getDecoratedBoundsWithMargins(view, outRect);
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
list.getDecoratedBoundsWithMargins(view, outRect);
}else{
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
}
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
@@ -312,6 +312,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
protected int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}
return 0;
}
@@ -323,6 +326,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
c.drawLine(0, y, parent.getWidth(), y, paint);
}
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return false;
}
public abstract void onItemClick(String id);
protected void updatePoll(String itemID, Status status, Poll poll){
@@ -405,54 +412,27 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
.exec(accountID);
}
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID());
toggleSpoiler(status, holder.getItemID());
}
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID());
}
protected void revealSpoiler(Status status, String itemID){
status.spoilerRevealed=true;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null)
header.rebind();
updateImagesSpoilerState(status, itemID);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
protected void toggleSpoiler(Status status, String itemID){
status.spoilerRevealed=!status.spoilerRevealed;
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
}
holder.rebind();
updateImagesSpoilerState(status, holder.getItemID());
}
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null)
spoiler.rebind();
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
mediaGrid.setRevealed(status.spoilerRevealed);
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
int index=displayItems.indexOf(spoilerItem);
if(status.spoilerRevealed){
displayItems.addAll(index+1, spoilerItem.contentItems);
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
}else{
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
}
list.invalidateItemDecorations();
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
@@ -580,6 +560,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return attachmentViewsPool;
}
public void rebuildAllDisplayItems(){
displayItems.clear();
for(T item:data){
displayItems.addAll(buildDisplayItems(item));
}
adapter.notifyDataSetChanged();
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){
@@ -589,7 +579,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@NonNull
@Override
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent);
BindableViewHolder<StatusDisplayItem> holder=(BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent, BaseStatusListFragment.this);
onModifyItemViewHolder(holder);
return holder;
}
@Override
@@ -620,15 +612,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
private Paint dividerPaint=new Paint(), hiddenMediaPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Typeface mediumTypeface=Typeface.create("sans-serif-medium", Typeface.NORMAL);
private Layout mediaHiddenTitleLayout, mediaHiddenTextLayout, tapToRevealTextLayout;
private int currentMediaHiddenLayoutsWidth=0;
private Paint dividerPaint=new Paint();
{
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
dividerPaint.setStyle(Paint.Style.STROKE);
dividerPaint.setStrokeWidth(V.dp(1));
dividerPaint.setStrokeWidth(V.dp(0.5f));
}
@Override
@@ -638,79 +627,22 @@ 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<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if(needDrawDivider(holder, siblingHolder)){
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
hiddenMediaPaint.setColor(0x80000000);
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
}
}
private boolean needDrawDivider(RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
if(needDividerForExtraItem(holder.itemView, siblingHolder.itemView, holder, siblingHolder))
return true;
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh){
// Do not draw dividers between hashtag and/or account rows
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
return false;
return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
}
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH));
if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32));
c.save();
float totalHeight;
boolean hiddenByAuthor=imgHolder.getItem().status.sensitive;
if(hiddenByAuthor)
totalHeight=mediaHiddenTitleLayout.getHeight()+mediaHiddenTextLayout.getHeight()+V.dp(8);
else
totalHeight=tapToRevealTextLayout.getHeight();
c.translate(child.getX()+V.dp(16), child.getY()+child.getHeight()/2f-totalHeight/2f);
if(hiddenByAuthor){
mediaHiddenTitleLayout.draw(c);
c.translate(0, mediaHiddenTitleLayout.getHeight()+V.dp(8));
mediaHiddenTextLayout.draw(c);
}else{
tapToRevealTextLayout.draw(c);
}
c.restore();
}
}
}
}
}
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));
titlePaint.setTextSize(V.dp(22));
titlePaint.setTypeface(mediumTypeface);
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
String tapToReveal=getString(R.string.tap_to_reveal);
tapToRevealTextLayout=StaticLayout.Builder.obtain(tapToReveal, 0, tapToReveal.length(), titlePaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(getResources().getColor(R.color.gray_200));
textPaint.setTextSize(V.dp(16));
String text=getString(R.string.sensitive_content_explain);
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(V.dp(5), 1f)
.build();
return false;
}
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status;
@@ -34,4 +35,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
})
.exec(accountID);
}
@Override
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
}

View File

@@ -1,10 +1,18 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.res.TypedArray;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -12,28 +20,34 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Attachment;
import org.parceler.Parcels;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import java.util.Collections;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
private static final String TAG="ComposeImageDescription";
private String accountID, attachmentID;
private EditText edit;
private Button saveButton;
private FixedAspectRatioImageView image;
private ContextThemeWrapper themeWrapper;
private PhotoViewer photoViewer;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -46,7 +60,13 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.edit_image);
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
setTitle(R.string.add_alt_text);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
return super.onCreateView(themeWrapper.getSystemService(LayoutInflater.class), container, savedInstanceState);
}
@Override
@@ -54,14 +74,48 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
View view=inflater.inflate(R.layout.fragment_image_description, container, false);
edit=view.findViewById(R.id.edit);
ImageView image=view.findViewById(R.id.photo);
image=view.findViewById(R.id.photo);
int width=getArguments().getInt("width", 0);
int height=getArguments().getInt("height", 0);
if(width>0 && height>0){
image.setAspectRatio(Math.max(1f, (float)width/height));
}
image.setOnClickListener(v->openPhotoViewer());
Uri uri=getArguments().getParcelable("uri");
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
Attachment.Type type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
if(type==Attachment.Type.IMAGE)
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
else
loadVideoThumbIntoView(image, uri);
edit.setText(getArguments().getString("existingDescription"));
return view;
}
private void loadVideoThumbIntoView(ImageView target, Uri uri){
MastodonAPIController.runInBackground(()->{
Context context=getActivity();
if(context==null)
return;
try{
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
mmr.setDataSource(context, uri);
Bitmap frame=mmr.getFrameAtTime(3_000_000);
mmr.release();
int size=Math.max(frame.getWidth(), frame.getHeight());
int maxSize=V.dp(250);
if(size>maxSize){
float factor=maxSize/(float)size;
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
}
Bitmap finalFrame=frame;
target.post(()->target.setImageBitmap(finalFrame));
}catch(Exception x){
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
}
});
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
@@ -71,43 +125,114 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle});
int buttonStyle=ta.getResourceId(0, 0);
ta.recycle();
saveButton=new Button(getActivity(), null, 0, buttonStyle);
saveButton.setText(R.string.save);
saveButton.setOnClickListener(this::onSaveClick);
FrameLayout wrap=new FrameLayout(getActivity());
wrap.addView(saveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(R.string.publish);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
inflater.inflate(R.menu.compose_image_description, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text)
.setMessage(msg)
.setPositiveButton(R.string.ok, null)
.show();
}
return true;
}
private void onSaveClick(View v){
new UpdateAttachment(attachmentID, edit.getText().toString().trim())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Attachment result){
Bundle r=new Bundle();
r.putParcelable("attachment", Parcels.wrap(result));
setResult(true, r);
Nav.finish(ComposeImageDescriptionFragment.this);
}
@Override
public boolean onBackPressed(){
deliverResult();
return false;
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.saving, false)
.exec(accountID);
@Override
protected LayoutInflater getToolbarLayoutInflater(){
return LayoutInflater.from(themeWrapper);
}
private void deliverResult(){
Bundle r=new Bundle();
r.putString("text", edit.getText().toString().trim());
r.putString("attachment", attachmentID);
setResult(true, r);
}
private void openPhotoViewer(){
Attachment fakeAttachment=new Attachment();
fakeAttachment.id="local";
fakeAttachment.type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
int width=getArguments().getInt("width", 0);
int height=getArguments().getInt("height", 0);
Uri uri=getArguments().getParcelable("uri");
fakeAttachment.url=uri.toString();
fakeAttachment.meta=new Attachment.Metadata();
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
int[] pos={0, 0};
image.getLocationOnScreen(pos);
outRect.set(pos[0], pos[1], pos[0]+image.getWidth(), pos[1]+image.getHeight());
image.setElevation(1f);
return true;
}
@Override
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
image.setTranslationX(translateX);
image.setTranslationY(translateY);
image.setScaleX(scale);
image.setScaleY(scale);
}
@Override
public void endPhotoViewTransition(){
Drawable d=image.getDrawable();
image.setImageDrawable(null);
image.setImageDrawable(d);
image.setTranslationX(0f);
image.setTranslationY(0f);
image.setScaleX(1f);
image.setScaleY(1f);
image.setElevation(0f);
}
@Nullable
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
return image.getDrawable();
}
@Override
public void photoViewerDismissed(){
photoViewer=null;
}
@Override
public void onRequestPermissions(String[] permissions){
}
});
photoViewer.removeMenu();
}
}

View File

@@ -0,0 +1,57 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
private Account account;
private String accountID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
onDataLoaded(getArguments().getParcelableArrayList("hashtags").stream().map(p->(Hashtag)Parcels.unwrap(p)).collect(Collectors.toList()), false);
setTitle(R.string.hashtags);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Hashtag s){
return Collections.singletonList(new HashtagStatusDisplayItem(s.name, this, s));
}
@Override
protected void addAccountToKnown(Hashtag s){
}
@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, id);
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
// no-op
}
}

View File

@@ -66,6 +66,6 @@ public class HashtagTimelineFragment extends StatusListFragment{
@Override
protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
}
}

View File

@@ -1,38 +1,51 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.app.NotificationManager;
import android.graphics.Outline;
import android.os.Build;
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.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.PushNotificationReceiver;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
@@ -44,7 +57,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
private FragmentRootLinearLayout content;
private HomeTimelineFragment homeTimelineFragment;
private NotificationsFragment notificationsFragment;
private NotificationsListFragment notificationsFragment;
private DiscoverFragment searchFragment;
private ProfileFragment profileFragment;
private TabBar tabBar;
@@ -52,6 +65,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private ImageView tabBarAvatar;
@IdRes
private int currentTab=R.id.tab_home;
private TextView notificationsBadge;
private String accountID;
@@ -73,7 +87,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment();
searchFragment.setArguments(args);
notificationsFragment=new NotificationsFragment();
notificationsFragment=new NotificationsListFragment();
notificationsFragment.setArguments(args);
args=new Bundle(args);
args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self));
@@ -82,6 +96,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
profileFragment.setArguments(args);
}
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Nullable
@@ -100,15 +121,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
tabBarWrap=content.findViewById(R.id.tabbar_wrap);
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
tabBarAvatar.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
tabBarAvatar.setOutlineProvider(OutlineProviders.OVAL);
tabBarAvatar.setClipToOutline(true);
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
notificationsBadge.setVisibility(View.GONE);
if(savedInstanceState==null){
getChildFragmentManager().beginTransaction()
@@ -142,9 +161,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
return;
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
notificationsFragment=(NotificationsListFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab");
tabBar.selectTab(currentTab);
Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction()
.hide(homeTimelineFragment)
@@ -164,7 +184,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public boolean wantsLightStatusBar(){
return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme();
return !UiUtils.isDarkTheme();
}
@Override
@@ -176,7 +196,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(24)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
@@ -201,6 +221,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
throw new IllegalArgumentException();
}
public void setCurrentTab(@IdRes int tab){
if(tab==currentTab)
return;
tabBar.selectTab(tab);
onTabSelected(tab);
}
private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){
@@ -220,9 +247,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
lf.loadData();
}else if(newFragment instanceof DiscoverFragment){
((DiscoverFragment) newFragment).loadData();
}else if(newFragment instanceof NotificationsFragment){
((NotificationsFragment) newFragment).loadData();
// TODO make an interface?
}
if(newFragment instanceof NotificationsListFragment){
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID);
}
@@ -234,10 +260,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new AccountSwitcherSheet(getActivity()).show();
new AccountSwitcherSheet(getActivity(), this).show();
return true;
}
if(tab==R.id.tab_home){
if(tab==R.id.tab_home && BuildConfig.DEBUG){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
@@ -263,4 +289,72 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
}
@Override
protected void onShown(){
super.onShown();
reloadNotificationsForUnreadCount();
}
private void reloadNotificationsForUnreadCount(){
List<Notification>[] notifications=new List[]{null};
String[] marker={null};
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
marker[0]=m;
if(notifications[0]!=null){
updateUnreadCount(notifications[0], marker[0]);
}
});
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, true, new Callback<>(){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
notifications[0]=result.items;
if(marker[0]!=null)
updateUnreadCount(notifications[0], marker[0]);
}
@Override
public void onError(ErrorResponse error){}
});
}
@SuppressLint("DefaultLocale")
private void updateUnreadCount(List<Notification> notifications, String marker){
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
notificationsBadge.setVisibility(View.GONE);
}else{
notificationsBadge.setVisibility(View.VISIBLE);
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
notificationsBadge.setText(String.format("%d+", notifications.size()));
}else{
int count=0;
for(Notification n:notifications){
if(n.id.equals(marker))
break;
count++;
}
notificationsBadge.setText(String.format("%d", count));
}
}
}
@Subscribe
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
if(ev.clearUnread)
notificationsBadge.setVisibility(View.GONE);
}
@Subscribe
public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
if(!ev.accountID.equals(accountID))
return;
if(homeTimelineFragment.loaded)
homeTimelineFragment.rebuildAllDisplayItems();
if(notificationsFragment.loaded)
notificationsFragment.rebuildAllDisplayItems();
}
}

View File

@@ -30,20 +30,20 @@ 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.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
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.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -122,7 +122,7 @@ public class HomeTimelineFragment extends StatusListFragment{
public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
Nav.go(getActivity(), SettingsMainFragment.class, args);
return true;
}
@@ -154,7 +154,7 @@ public class HomeTimelineFragment extends StatusListFragment{
new SaveMarkers(topPostID, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SaveMarkers.Response result){
public void onSuccess(TimelineMarkers result){
}
@Override
@@ -167,8 +167,8 @@ public class HomeTimelineFragment extends StatusListFragment{
}
}
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
public void onStatusCreated(Status status){
prependItems(Collections.singletonList(status), true);
}
private void onFabClick(View v){
@@ -199,10 +199,7 @@ public class HomeTimelineFragment extends StatusListFragment{
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!filters.isEmpty()){
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
}
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
showNewPostsButton();
@@ -273,19 +270,13 @@ public class HomeTimelineFragment extends StatusListFragment{
}else{
result=result.subList(0, endIndex);
}
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
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);
}
@@ -440,7 +431,7 @@ public class HomeTimelineFragment extends StatusListFragment{
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);
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
}
@Subscribe

View File

@@ -0,0 +1,60 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.View;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.Collections;
import java.util.List;
import androidx.annotation.CallSuper;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T>{
protected ElevationOnScrollListener elevationOnScrollListener;
public MastodonRecyclerFragment(int perPage){
super(perPage);
}
public MastodonRecyclerFragment(int layout, int perPage){
super(layout, perPage);
}
protected List<View> getViewsForElevationEffect(){
Toolbar toolbar=getToolbar();
return toolbar!=null ? Collections.singletonList(toolbar) : Collections.emptyList();
}
@Override
@CallSuper
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if(wantsElevationOnScrollEffect())
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
if(refreshLayout!=null){
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
refreshLayout.setColorSchemeColors(colorPrimary);
}
}
@Override
@CallSuper
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(elevationOnScrollListener!=null){
elevationOnScrollListener.setViews(getViewsForElevationEffect());
}
}
protected boolean wantsElevationOnScrollEffect(){
return true;
}
}

View File

@@ -11,6 +11,15 @@ import androidx.annotation.CallSuper;
import me.grishka.appkit.fragments.ToolbarFragment;
public abstract class MastodonToolbarFragment extends ToolbarFragment{
public MastodonToolbarFragment(){
super();
}
protected MastodonToolbarFragment(int layout){
super(layout);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);

View File

@@ -1,187 +0,0 @@
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.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{
private TabLayout tabLayout;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private String accountID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
accountID=getArguments().getString("account");
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.notifications);
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false);
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[2];
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;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
tabViews[i]=tabView;
}
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
scrollToTop();
}
});
pager.setOffscreenPageLimit(4);
pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageSelected(int position){
if(position==0)
return;
Fragment _page=getFragmentForPage(position);
if(_page instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
}
});
if(allNotificationsFragment==null){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
allNotificationsFragment=new NotificationsListFragment();
allNotificationsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyMentions", true);
mentionsFragment=new NotificationsListFragment();
mentionsFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.notifications_all, allNotificationsFragment)
.add(R.id.notifications_mentions, mentionsFragment)
.commit();
}
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
tab.setText(switch(position){
case 0 -> R.string.all_notifications;
case 1 -> R.string.mentions;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
}
});
tabLayoutMediator.attach();
return view;
}
@Override
public void scrollToTop(){
getFragmentForPage(pager.getCurrentItem()).scrollToTop();
}
public void loadData(){
if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading)
allNotificationsFragment.loadData();
}
@Override
protected void updateToolbar(){
super.updateToolbar();
getToolbar().setOutlineProvider(null);
getToolbar().setOnClickListener(v->scrollToTop());
}
private NotificationsListFragment getFragmentForPage(int page){
return switch(page){
case 0 -> allNotificationsFragment;
case 1 -> mentionsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
};
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=tabViews[viewType];
((ViewGroup)view.getParent()).removeView(view);
view.setVisibility(View.VISIBLE);
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){}
@Override
public int getItemCount(){
return 2;
}
@Override
public int getItemViewType(int position){
return position;
}
}
}

View File

@@ -1,7 +1,15 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -15,33 +23,45 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.utils.ObjectIdComparator;
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;
import me.grishka.appkit.utils.V;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private boolean onlyMentions;
private boolean onlyMentions=true;
private String maxID;
private View tabBar;
private View mentionsTab, allTab;
private View endMark;
private String unreadMarker, realUnreadMarker;
private MenuItem markAllReadItem;
private boolean reloadingFromCache;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setLayout(R.layout.fragment_notifications);
E.register(this);
if(savedInstanceState!=null){
onlyMentions=savedInstanceState.getBoolean("onlyMentions", true);
}
setHasOptionsMenu(true);
}
@Override
@@ -53,28 +73,29 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
onlyMentions=getArguments().getBoolean("onlyMentions", false);
setTitle(R.string.notifications);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
String extraText=switch(n.type){
case FOLLOW -> getString(R.string.user_followed_you);
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
case MENTION, STATUS -> null;
case REBLOG -> getString(R.string.notification_boosted);
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
return Arrays.asList(titleItem, card);
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
@@ -90,46 +111,41 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && !reloadingFromCache)
endMark.setVisibility(View.GONE);
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
if(getActivity()==null)
return;
if(refreshing)
relationships.clear();
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
Set<String> needRelationships=result.items.stream()
.filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id))
.map(ntf->ntf.account.id)
.collect(Collectors.toSet());
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset==0 && !result.items.isEmpty()){
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
}
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
reloadingFromCache=false;
}
});
}
@Override
protected void onRelationshipsLoaded(){
if(getActivity()==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
accountHolder.rebind();
protected void onShown(){
super.onShown();
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
if(!dataLoading){
if(onlyMentions){
refresh();
}else{
reloadingFromCache=true;
refresh();
}
}
}
@Override
protected void onShown(){
super.onShown();
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
// loadData();
protected void onHidden(){
super.onHidden();
resetUnreadBackground();
}
@Override
@@ -139,7 +155,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
@@ -153,8 +169,62 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
tabBar=view.findViewById(R.id.tabbar);
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
View tabBarItself=view.findViewById(R.id.tabbar_inner);
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
tabBarItself.setClipToOutline(true);
mentionsTab=view.findViewById(R.id.mentions_tab);
allTab=view.findViewById(R.id.all_tab);
mentionsTab.setOnClickListener(this::onTabClick);
allTab.setOnClickListener(this::onTabClick);
mentionsTab.setSelected(onlyMentions);
allTab.setSelected(!onlyMentions);
NestedRecyclerScrollView scroller=view.findViewById(R.id.scroller);
scroller.setScrollableChildSupplier(()->list);
scroller.setTakePriorityOverChildViews(true);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
private Rect tmpRect=new Rect();
{
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3SurfaceVariant));
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(TextUtils.isEmpty(unreadMarker))
return;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
String itemID=holder.getItemID();
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
c.drawRect(tmpRect, paint);
}
}
}
}
}, 0);
}
@Override
protected List<View> getViewsForElevationEffect(){
ArrayList<View> views=new ArrayList<>(super.getViewsForElevationEffect());
views.add(tabBar);
return views;
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putBoolean("onlyMentions", onlyMentions);
}
private Notification getNotificationByID(String id){
@@ -211,4 +281,87 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
private void onTabClick(View v){
boolean newOnlyMentions=v.getId()==R.id.mentions_tab;
if(newOnlyMentions==onlyMentions)
return;
onlyMentions=newOnlyMentions;
mentionsTab.setSelected(onlyMentions);
allTab.setSelected(!onlyMentions);
maxID=null;
showProgress();
loadData(0, 20);
refreshing=true;
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
markAllReadItem=menu.findItem(R.id.mark_all_read);
updateMarkAllReadButton();
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.mark_all_read){
markAsRead();
resetUnreadBackground();
}
return true;
}
private void markAsRead(){
String id=data.get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
updateMarkAllReadButton();
}
}
private void resetUnreadBackground(){
unreadMarker=realUnreadMarker;
list.invalidate();
}
@Override
public void onRefresh(){
super.onRefresh();
resetUnreadBackground();
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
unreadMarker=realUnreadMarker=m;
});
}
private void updateMarkAllReadButton(){
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
}
@Override
public void onAppendItems(List<Notification> items){
super.onAppendItems(items);
if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker))
return;
for(Notification n:items){
if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0){
markAsRead();
break;
}
}
}
}

View File

@@ -0,0 +1,36 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class PinnedPostsListFragment extends StatusListFragment{
private Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
setTitle(R.string.posts);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, false);
}
}).exec(accountID);
}
}

View File

@@ -13,6 +13,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -48,11 +49,10 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public UsableRecyclerView list;
private List<AccountField> fields=Collections.emptyList();
private AboutAdapter adapter;
private Paint dividerPaint=new Paint();
private boolean isInEditMode;
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private RecyclerView.ViewHolder draggedViewHolder;
private ListImageLoaderWrapper imgLoader;
private boolean editDirty;
public void setFields(List<AccountField> fields){
this.fields=fields;
@@ -74,27 +74,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
list.setLayoutManager(new LinearLayoutManager(getActivity()));
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
list.setAdapter(adapter=new AboutAdapter());
int pad=V.dp(16);
list.setPadding(pad, pad, pad, pad);
list.setPadding(0, V.dp(16), 0, 0);
list.setClipToPadding(false);
dividerPaint.setStyle(Paint.Style.STROKE);
dividerPaint.setStrokeWidth(V.dp(1));
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){
for(int i=0;i<parent.getChildCount();i++){
View item=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(item);
int draggedPos=draggedViewHolder==null ? -1 : draggedViewHolder.getAbsoluteAdapterPosition();
if(pos<adapter.getItemCount()-1 && pos!=draggedPos && pos!=draggedPos-1){
float y=item.getY()+item.getHeight();
dividerPaint.setAlpha(Math.round(255*item.getAlpha()));
c.drawLine(item.getLeft(), y, item.getRight(), y, dividerPaint);
}
}
}
});
return list;
}
@@ -103,12 +84,17 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
fields=editableFields;
adapter.notifyDataSetChanged();
dragHelper.attachToRecyclerView(list);
editDirty=false;
}
public List<AccountField> getFields(){
return fields;
}
public boolean isEditDirty(){
return editDirty;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
@@ -183,36 +169,25 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
protected ShapeDrawable background=new ShapeDrawable();
public BaseViewHolder(int layout){
super(getActivity(), layout, list);
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
itemView.setBackground(background);
}
@Override
public void onBind(AccountField item){
boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==adapter.getItemCount()-1;
float radius=V.dp(10);
float[] rad=new float[8];
if(first)
rad[0]=rad[1]=rad[2]=rad[3]=radius;
if(last)
rad[4]=rad[5]=rad[6]=rad[7]=radius;
background.setShape(new RoundRectShape(rad, null, null));
itemView.invalidateOutline();
}
}
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
private TextView title;
private LinkedTextView value;
private final TextView title;
private final LinkedTextView value;
private final ImageView verifiedIcon;
public AboutViewHolder(){
super(R.layout.item_profile_about);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
verifiedIcon=findViewById(R.id.verified_icon);
}
@Override
@@ -220,20 +195,7 @@ 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);
}
verifiedIcon.setVisibility(item.verifiedAt!=null ? View.VISIBLE : View.GONE);
}
@Override
@@ -251,27 +213,38 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
private class EditableAboutViewHolder extends BaseViewHolder{
private EditText title;
private EditText value;
private final EditText title;
private final EditText value;
private boolean ignoreTextChange;
public EditableAboutViewHolder(){
super(R.layout.item_profile_about_editable);
super(R.layout.onboarding_profile_field);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
value=findViewById(R.id.content);
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
dragHelper.startDrag(this);
return true;
});
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick);
title.addTextChangedListener(new SimpleTextWatcher(e->{
item.name=e.toString();
if(!ignoreTextChange)
editDirty=true;
}));
value.addTextChangedListener(new SimpleTextWatcher(e->{
item.value=e.toString();
if(!ignoreTextChange)
editDirty=true;
}));
findViewById(R.id.delete).setOnClickListener(this::onRemoveRowClick);
}
@Override
public void onBind(AccountField item){
super.onBind(item);
ignoreTextChange=true;
title.setText(item.name);
value.setText(item.value);
ignoreTextChange=false;
}
private void onRemoveRowClick(View v){
@@ -323,8 +296,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
}
adapter.notifyItemMoved(fromPosition, toPosition);
((BindableViewHolder)viewHolder).rebind();
((BindableViewHolder)target).rebind();
((BindableViewHolder<?>)viewHolder).rebind();
((BindableViewHolder<?>)target).rebind();
return true;
}
@@ -339,7 +312,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=viewHolder;
}
}
@@ -347,7 +319,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=null;
}
@Override

View File

@@ -0,0 +1,204 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFeaturedHashtags;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SectionHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult>{
private Account profileAccount;
private List<Hashtag> featuredTags;
// private List<Account> endorsedAccounts;
private List<Status> pinnedStatuses;
private boolean tagsLoaded, statusesLoaded;
public ProfileFeaturedFragment(){
setListLayoutId(R.layout.recycler_fragment_no_refresh);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
profileAccount=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){
ArrayList<StatusDisplayItem> items=switch(s.type){
case ACCOUNT -> new ArrayList<>(Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)));
case HASHTAG -> new ArrayList<>(Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
};
if(s.firstInSection){
items.add(0, new SectionHeaderStatusDisplayItem(this, getString(switch(s.type){
case ACCOUNT -> R.string.profile_endorsed_accounts;
case HASHTAG -> R.string.hashtags;
case STATUS -> R.string.posts;
}), getString(R.string.view_all), switch(s.type){
case ACCOUNT -> (Runnable)this::showAllEndorsedAccounts;
case HASHTAG -> (Runnable)this::showAllFeaturedHashtags;
case STATUS -> (Runnable)this::showAllPinnedPosts;
}));
}
return items;
}
@Override
protected void addAccountToKnown(SearchResult s){
Account acc=switch(s.type){
case ACCOUNT -> s.account;
case STATUS -> s.status.account;
case HASHTAG -> null;
};
if(acc!=null && !knownAccounts.containsKey(acc.id))
knownAccounts.put(acc.id, acc);
}
@Override
public void onItemClick(String id){
SearchResult res=getResultByID(id);
if(res==null)
return;
switch(res.type){
case ACCOUNT -> {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}
}
}
@Override
protected void doLoadData(int offset, int count){
if(!statusesLoaded){
new GetAccountStatuses(profileAccount.id, null, null, 2, GetAccountStatuses.Filter.PINNED)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
pinnedStatuses=result;
statusesLoaded=true;
onOneApiRequestCompleted();
}
})
.exec(accountID);
}
if(!tagsLoaded){
new GetAccountFeaturedHashtags(profileAccount.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Hashtag> result){
featuredTags=result;
tagsLoaded=true;
onOneApiRequestCompleted();
}
})
.exec(accountID);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onRefresh(){
statusesLoaded=false;
tagsLoaded=false;
super.onRefresh();
}
private void onOneApiRequestCompleted(){
if(getActivity()==null)
return;
if(tagsLoaded && statusesLoaded){
ArrayList<SearchResult> results=new ArrayList<>();
for(int i=0;i<Math.min(2, pinnedStatuses.size());i++){
SearchResult res=new SearchResult(pinnedStatuses.get(i));
res.firstInSection=(i==0);
results.add(res);
}
for(int i=0;i<Math.min(5, featuredTags.size());i++){
SearchResult res=new SearchResult(featuredTags.get(i));
res.firstInSection=(i==0);
results.add(res);
}
onDataLoaded(results, false);
}
}
protected SearchResult getResultByID(String id){
for(SearchResult s:data){
if(s.id.equals(id)){
return s;
}
}
return null;
}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
if(holder instanceof FooterStatusDisplayItem.Holder && siblingHolder instanceof StatusDisplayItem.Holder<?> sdi && sdi.getItemID().startsWith("post_")){
super.drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, paint);
}
}
private void showAllPinnedPosts(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
Nav.go(getActivity(), PinnedPostsListFragment.class, args);
}
private void showAllFeaturedHashtags(){
Bundle args=new Bundle();
args.putString("account", accountID);
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("hashtags", tags);
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);
}
private void showAllEndorsedAccounts(){
}
}

View File

@@ -10,16 +10,21 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
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.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -29,12 +34,11 @@ import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
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;
@@ -43,7 +47,6 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
@@ -55,18 +58,22 @@ 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.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
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;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.CustomDrawingOrderLinearLayout;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.time.LocalDateTime;
@@ -92,6 +99,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
private static final int AVATAR_RESULT=722;
@@ -100,31 +108,34 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private ProfileFeaturedFragment featuredFragment;
private AccountTimelineFragment timelineFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
private CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable();
private float titleTransY;
private View postsBtn, followersBtn, followingBtn;
private View followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
private LinearLayout countersLayout;
private View nameEditWrap, bioEditWrap;
private View tabsDivider;
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private Account account;
private String accountID;
private Relationship relationship;
private int statusBarHeight;
private boolean isOwnProfile;
private ArrayList<AccountField> fields=new ArrayList<>();
private boolean isInEditMode;
private boolean isInEditMode, editDirty;
private Uri editNewAvatar, editNewCover;
private String profileAccountID;
private boolean refreshing;
@@ -132,10 +143,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar);
}
private ElevationOnScrollListener onScrollListener;
private Drawable tabsColorBackground;
private boolean tabBarIsAtTop;
private Animator tabBarColorAnim;
private MenuItem editSaveMenuItem;
private boolean savingEdits;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -180,9 +193,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingCount=content.findViewById(R.id.following_count);
followingLabel=content.findViewById(R.id.following_label);
followingBtn=content.findViewById(R.id.following_btn);
postsCount=content.findViewById(R.id.posts_count);
postsLabel=content.findViewById(R.id.posts_label);
postsBtn=content.findViewById(R.id.posts_btn);
actionButton=content.findViewById(R.id.profile_action_btn);
pager=content.findViewById(R.id.pager);
scrollView=content.findViewById(R.id.scroller);
@@ -190,36 +200,34 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
refreshLayout=content.findViewById(R.id.refresh_layout);
nameEdit=content.findViewById(R.id.name_edit);
bioEdit=content.findViewById(R.id.bio_edit);
nameEditWrap=content.findViewById(R.id.name_edit_wrap);
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
actionProgress=content.findViewById(R.id.action_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
countersLayout=content.findViewById(R.id.profile_counters);
tabsDivider=content.findViewById(R.id.tabs_divider);
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
avatar.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(25));
}
});
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
Toolbar toolbar=getToolbar();
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-toolbar.getLayoutParams().height-statusBarHeight-V.dp(38);
coverGradient.setTopPadding(statusBarHeight+toolbar.getLayoutParams().height);
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
tabViews=new FrameLayout[4];
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.profile_posts;
case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media;
case 3 -> R.id.profile_about;
case 0 -> R.id.profile_featured;
case 1 -> R.id.profile_timeline;
case 2 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -235,22 +243,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
sizeWrapper.addView(content);
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabbar.setTabTextSize(V.dp(16));
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabbar.setTabTextSize(V.dp(14));
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
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 0 -> R.string.profile_featured;
case 1 -> R.string.profile_timeline;
case 2 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
});
cover.setForeground(coverGradient);
cover.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
@@ -264,6 +270,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
refreshLayout.setOnRefreshListener(this);
fab.setOnClickListener(this::onFabClick);
if(savedInstanceState!=null){
featuredFragment=(ProfileFeaturedFragment) getChildFragmentManager().getFragment(savedInstanceState, "featured");
timelineFragment=(AccountTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "timeline");
aboutFragment=(ProfileAboutFragment) getChildFragmentManager().getFragment(savedInstanceState, "about");
}
if(loaded){
bindHeaderView();
dataLoaded();
@@ -287,6 +299,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return true;
});
scrollableContent.setDrawingOrderCallback((count, pos)->{
// The header is the first child, draw it last to overlap everything for the photo viewer transition to look nice
if(pos==count-1)
return 0;
// Offset the order of other child views to compensate
return pos+1;
});
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
refreshLayout.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
refreshLayout.setColorSchemeColors(colorPrimary);
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
return sizeWrapper;
}
@@ -309,12 +337,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(refreshing){
refreshing=false;
refreshLayout.setRefreshing(false);
if(postsFragment.loaded)
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
if(timelineFragment.loaded)
timelineFragment.onRefresh();
if(featuredFragment.loaded)
featuredFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
@@ -334,14 +360,32 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public void dataLoaded(){
if(getActivity()==null)
return;
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
args.putBoolean("__is_tab", true);
args.putBoolean("noAutoLoad", true);
if(featuredFragment==null){
featuredFragment=new ProfileFeaturedFragment();
featuredFragment.setArguments(args);
}
if(timelineFragment==null){
timelineFragment=AccountTimelineFragment.newInstance(accountID, account, true);
}
if(aboutFragment==null){
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
}
pager.getAdapter().notifyDataSetChanged();
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
pager.getViewTreeObserver().removeOnPreDrawListener(this);
pager.setCurrentItem(1, false);
tabbar.selectTab(tabbar.getTabAt(1));
return true;
}
});
super.dataLoaded();
}
@@ -357,10 +401,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageSelected(int position){
if(position==0)
return;
Fragment _page=getFragmentForPage(position);
if(_page instanceof BaseRecyclerFragment<?> page){
if(_page instanceof BaseRecyclerFragment<?> page && page.isAdded()){
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
@@ -368,6 +410,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onPageScrollStateChanged(int state){
if(isInEditMode)
return;
refreshLayout.setEnabled(state!=ViewPager2.SCROLL_STATE_DRAGGING);
}
});
@@ -375,17 +419,36 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
});
tabsColorBackground=((LayerDrawable)tabbar.getBackground()).findDrawableByLayerId(R.id.color_overlay);
onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar());
scrollView.setOnScrollChangeListener(this::onScrollChanged);
titleTransY=getToolbar().getLayoutParams().height;
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
scrollView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
scrollView.getViewTreeObserver().removeOnPreDrawListener(this);
tabBarIsAtTop=!scrollView.canScrollVertically(1) && scrollView.getHeight()>0;
tabsColorBackground.setAlpha(tabBarIsAtTop ? 20 : 0);
tabbar.setTranslationZ(tabBarIsAtTop ? V.dp(3) : 0);
tabsDivider.setAlpha(tabBarIsAtTop ? 0 : 1);
return true;
}
});
}
@Override
public void onDestroyView(){
super.onDestroyView();
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
if(featuredFragment==null)
return;
if(featuredFragment.isAdded())
getChildFragmentManager().putFragment(outState, "featured", featuredFragment);
if(timelineFragment.isAdded())
getChildFragmentManager().putFragment(outState, "timeline", timelineFragment);
if(aboutFragment.isAdded())
getChildFragmentManager().putFragment(outState, "about", aboutFragment);
}
@Override
@@ -396,28 +459,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onApplyWindowInsets(WindowInsets insets){
statusBarHeight=insets.getSystemWindowInsetTop();
if(contentView!=null){
((ViewGroup.MarginLayoutParams) getToolbar().getLayoutParams()).topMargin=statusBarHeight;
refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24));
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
int insetBottom=insets.getSystemWindowInsetBottom();
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+insetBottom;
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+insetBottom;
applyChildWindowInsets();
insets=insets.inset(0, 0, 0, insetBottom);
}else{
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24);
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16);
}
}
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
super.onApplyWindowInsets(insets);
}
private void applyChildWindowInsets(){
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
if(timelineFragment!=null && timelineFragment.isAdded() && childInsets!=null){
timelineFragment.onApplyWindowInsets(childInsets);
featuredFragment.onApplyWindowInsets(childInsets);
}
}
@@ -427,7 +486,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
@@ -441,7 +501,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_fluent_lock_closed_20_filled, getActivity().getTheme()).mutate();
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, 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_BOTTOM), 0);
@@ -459,16 +519,20 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
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, (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);
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
actionButton.setText(R.string.edit_profile);
TypedArray ta=actionButton.getContext().obtainStyledAttributes(R.style.Widget_Mastodon_M3_Button_Tonal, new int[]{android.R.attr.background});
actionButton.setBackground(ta.getDrawable(0));
ta.recycle();
ta=actionButton.getContext().obtainStyledAttributes(R.style.Widget_Mastodon_M3_Button_Tonal, new int[]{android.R.attr.textColor});
actionButton.setTextColor(ta.getColorStateList(0));
ta.recycle();
}else{
actionButton.setVisibility(View.GONE);
}
@@ -503,47 +567,34 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void updateToolbar(){
getToolbar().setBackgroundColor(0);
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
getToolbar().setOnClickListener(v->scrollToTop());
getToolbar().setNavigationContentDescription(R.string.back);
}
@Override
public boolean wantsLightStatusBar(){
return false;
if(onScrollListener!=null){
onScrollListener.setViews(getToolbar());
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){
Button cancelButton=new Button(getActivity(), null, 0, R.style.Widget_Mastodon_Button_Secondary_LightOnDark);
cancelButton.setText(R.string.cancel);
cancelButton.setOnClickListener(v->exitEditMode());
FrameLayout wrap=new FrameLayout(getActivity());
wrap.addView(cancelButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(R.string.cancel);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
editSaveMenuItem=menu.add(0, R.id.save, 0, R.string.save_changes);
editSaveMenuItem.setIcon(R.drawable.ic_save_24px);
editSaveMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
editSaveMenuItem.setVisible(!isActionButtonInView());
return;
}
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
menu.findItem(R.id.share).setTitle(R.string.share_user);
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()));
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.displayName));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.displayName));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.displayName));
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()));
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.displayName));
else
menu.findItem(R.id.hide_boosts).setVisible(false);
if(!account.isLocal())
@@ -568,6 +619,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
args.putParcelable("relationship", Parcels.wrap(relationship));
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);
@@ -599,15 +651,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
}else if(id==R.id.save){
if(isInEditMode)
saveAndExitEditMode();
}
return true;
}
@Override
protected int getToolbarResource(){
return R.layout.profile_toolbar;
}
private void loadRelationship(){
new GetAccountRelationships(Collections.singletonList(account.id))
.setCallback(new Callback<>(){
@@ -630,49 +680,71 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void updateRelationship(){
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatarBorder.getTop()-topBarsH){
float avaAlpha=Math.max(1f-((scrollY-(avatarBorder.getTop()-topBarsH))/(float)V.dp(38)), 0f);
avatarBorder.setAlpha(avaAlpha);
}else{
avatarBorder.setAlpha(1f);
}
if(scrollY>cover.getHeight()-topBarsH){
cover.setTranslationY(scrollY-(cover.getHeight()-topBarsH));
if(scrollY>cover.getHeight()){
cover.setTranslationY(scrollY-(cover.getHeight()));
cover.setTranslationZ(V.dp(10));
cover.setTransform(cover.getHeight()/2f-topBarsH/2f, 1f);
cover.setTransform(cover.getHeight()/2f);
}else{
cover.setTranslationY(0f);
cover.setTranslationZ(0f);
cover.setTransform(scrollY/2f, 1f);
cover.setTransform(scrollY/2f);
}
coverGradient.setTopOffset(scrollY);
cover.invalidate();
titleTransY=getToolbar().getHeight();
if(scrollY>name.getTop()-topBarsH){
titleTransY=Math.max(0f, titleTransY-(scrollY-(name.getTop()-topBarsH)));
}
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
if(currentPhotoViewer!=null){
currentPhotoViewer.offsetView(0, oldScrollY-scrollY);
}
onScrollListener.onScrollChange(v, scrollX, scrollY, oldScrollX, oldScrollY);
boolean newTabBarIsAtTop=!scrollView.canScrollVertically(1);
if(newTabBarIsAtTop!=tabBarIsAtTop){
tabBarIsAtTop=newTabBarIsAtTop;
if(tabBarIsAtTop){
// ScrollView would sometimes leave 1 pixel unscrolled, force it into the correct scrollY
int maxY=scrollView.getChildAt(0).getHeight()-scrollView.getHeight();
if(scrollView.getScrollY()!=maxY)
scrollView.scrollTo(0, maxY);
}
if(tabBarColorAnim!=null)
tabBarColorAnim.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(tabsColorBackground, "alpha", tabBarIsAtTop ? 20 : 0),
ObjectAnimator.ofFloat(tabbar, View.TRANSLATION_Z, tabBarIsAtTop ? V.dp(3) : 0),
ObjectAnimator.ofFloat(getToolbar(), View.TRANSLATION_Z, tabBarIsAtTop ? 0 : V.dp(3)),
ObjectAnimator.ofFloat(tabsDivider, View.ALPHA, tabBarIsAtTop ? 0 : 1)
);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
tabBarColorAnim=null;
}
});
tabBarColorAnim=set;
set.start();
}
if(isInEditMode && editSaveMenuItem!=null){
boolean buttonInView=isActionButtonInView();
if(buttonInView==editSaveMenuItem.isVisible()){
editSaveMenuItem.setVisible(!buttonInView);
}
}
}
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
case 3 -> aboutFragment;
case 0 -> featuredFragment;
case 1 -> timelineFragment;
case 2 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@@ -695,6 +767,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
if(visible)
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
actionButton.setClickable(!visible);
}
@@ -732,40 +806,42 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
isInEditMode=true;
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.done);
pager.setCurrentItem(3);
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.save_changes);
pager.setCurrentItem(2);
for(int i=0;i<3;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false);
}
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
avatar.setForeground(overlay);
animators.add(ObjectAnimator.ofInt(overlay, "alpha", 0, 255));
nameEdit.setVisibility(View.VISIBLE);
Toolbar toolbar=getToolbar();
Drawable close=getToolbarContext().getDrawable(R.drawable.ic_baseline_close_24).mutate();
close.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
toolbar.setNavigationIcon(close);
toolbar.setNavigationContentDescription(R.string.discard);
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
name.setVisibility(View.GONE);
username.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
nameEditWrap.setVisibility(View.VISIBLE);
nameEdit.setText(account.displayName);
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
lp.addRule(RelativeLayout.BELOW, R.id.name_edit);
username.getParent().requestLayout();
animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f, 1f));
bioEdit.setVisibility(View.VISIBLE);
bioEditWrap.setVisibility(View.VISIBLE);
bioEdit.setText(account.source.note);
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f, 1f));
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 0f));
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, .3f));
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, .3f));
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, .3f));
AnimatorSet set=new AnimatorSet();
set.playTogether(animators);
set.setDuration(300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.start();
aboutFragment.enterEditMode(account.source.fields);
refreshLayout.setEnabled(false);
editDirty=false;
}
private void exitEditMode(){
@@ -774,39 +850,38 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
isInEditMode=false;
invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
tabbar.getTabAt(i).view.setEnabled(true);
}
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f));
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f));
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 1f));
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, 1f));
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, 1f));
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, 1f));
pager.setUserInputEnabled(true);
avatar.setForeground(null);
AnimatorSet set=new AnimatorSet();
set.playTogether(animators);
set.setDuration(200);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
for(int i=0;i<3;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
nameEdit.setVisibility(View.GONE);
bioEdit.setVisibility(View.GONE);
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
lp.addRule(RelativeLayout.BELOW, R.id.name);
username.getParent().requestLayout();
avatar.setForeground(null);
}
});
set.start();
Toolbar toolbar=getToolbar();
if(canGoBack()){
Drawable back=getToolbarContext().getDrawable(R.drawable.ic_arrow_back).mutate();
back.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
toolbar.setNavigationIcon(back);
toolbar.setNavigationContentDescription(0);
}else{
toolbar.setNavigationIcon(null);
}
editSaveMenuItem=null;
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
nameEditWrap.setVisibility(View.GONE);
bioEditWrap.setVisibility(View.GONE);
name.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
refreshLayout.setEnabled(true);
bindHeaderView();
}
@@ -815,10 +890,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(!isInEditMode)
throw new IllegalStateException();
setActionProgressVisible(true);
savingEdits=true;
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, aboutFragment.getFields())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
savingEdits=false;
account=result;
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
exitEditMode();
@@ -827,6 +904,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onError(ErrorResponse error){
savingEdits=false;
error.showToast(getActivity());
setActionProgressVisible(false);
}
@@ -850,7 +928,17 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public boolean onBackPressed(){
if(isInEditMode){
exitEditMode();
if(savingEdits)
return true;
if(editDirty || aboutFragment.isEditDirty()){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_changes)
.setPositiveButton(R.string.discard, (dlg, btn)->exitEditMode())
.setNegativeButton(R.string.cancel, null)
.show();
}else{
exitEditMode();
}
return true;
}
return false;
@@ -901,9 +989,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void startImagePicker(int requestCode){
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
Intent intent=UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1);
startActivityForResult(intent, requestCode);
}
@@ -912,10 +998,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(resultCode==Activity.RESULT_OK){
if(requestCode==AVATAR_RESULT){
editNewAvatar=data.getData();
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
editDirty=true;
}else if(requestCode==COVER_RESULT){
editNewCover=data.getData();
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
editDirty=true;
}
}
}
@@ -940,6 +1028,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
Nav.go(getActivity(), cls, args);
}
private boolean isActionButtonInView(){
return actionButton.getVisibility()==View.VISIBLE && actionButtonWrap.getTop()+actionButtonWrap.getHeight()>scrollView.getScrollY();
}
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
@@ -959,6 +1051,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
getChildFragmentManager().executePendingTransactions();
if(fragment.isAdded()){
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
applyChildWindowInsets();
@@ -971,7 +1064,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public int getItemCount(){
return loaded ? 4 : 0;
return loaded ? 3 : 0;
}
@Override

View File

@@ -1,761 +0,0 @@
package org.joinmastodon.android.fragments;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
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.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.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
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;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SettingsFragment extends MastodonToolbarFragment{
private UsableRecyclerView list;
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private PushSubscription pushSubscription;
private ImageView themeTransitionWindowView;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
setTitle(R.string.settings);
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 HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
GlobalUserPreferences.playGifs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_custom_tabs, R.drawable.ic_fluent_link_24_regular, GlobalUserPreferences.useCustomTabs, i->{
GlobalUserPreferences.useCustomTabs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
PushSubscription pushSubscription=getPushSubscription();
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new 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));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
if(BuildConfig.DEBUG){
items.add(new RedHeaderItem("Debug options"));
items.add(new TextItem("Test e-mail confirmation flow", ()->{
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false;
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("debug", true);
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
}));
}
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
if(themeTransitionWindowView!=null){
// Activity has finished recreating. Remove the overlay.
MastodonApp.context.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
themeTransitionWindowView=null;
}
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
list=new UsableRecyclerView(getActivity());
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(new SettingsAdapter());
list.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
list.setPadding(0, V.dp(16), 0, V.dp(12));
list.setClipToPadding(false);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
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()>1)
outRect.top=V.dp(32);
}
});
return list;
}
@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(12)+insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}
super.onApplyWindowInsets(insets);
}
@Override
public void onDestroy(){
super.onDestroy();
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 onTrueBlackThemeChanged(SwitchItem item){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
RecyclerView.ViewHolder themeHolder=list.findViewHolderForAdapterPosition(items.indexOf(themeItem));
if(themeHolder!=null){
((ThemeViewHolder)themeHolder).bindSubitems();
}else{
list.getAdapter().notifyItemChanged(items.indexOf(themeItem));
}
if(UiUtils.isDarkTheme()){
restartActivityToApplyNewTheme();
}
}
private void restartActivityToApplyNewTheme(){
// Calling activity.recreate() causes a black screen for like half a second.
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
// As a bonus, we can fade it out to make it even smoother.
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
View activityDecorView=getActivity().getWindow().getDecorView();
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
activityDecorView.draw(new Canvas(bitmap));
themeTransitionWindowView=new ImageView(MastodonApp.context);
themeTransitionWindowView.setImageBitmap(bitmap);
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
lp.token=getActivity().getWindow().getAttributes().token;
lp.windowAnimations=R.style.window_fade_out;
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
}
getActivity().recreate();
}
private PushSubscription getPushSubscription(){
if(pushSubscription!=null)
return pushSubscription;
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(session.pushSubscription==null){
pushSubscription=new PushSubscription();
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
}else{
pushSubscription=session.pushSubscription.clone();
}
return pushSubscription;
}
private void onNotificationsChanged(PushNotification.Type type, boolean enabled){
PushSubscription subscription=getPushSubscription();
switch(type){
case FAVORITE -> subscription.alerts.favourite=enabled;
case FOLLOW -> subscription.alerts.follow=enabled;
case REBLOG -> subscription.alerts.reblog=enabled;
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
}
needUpdateNotificationSettings=true;
}
private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
PushSubscription subscription=getPushSubscription();
PushSubscription.Policy prevPolicy=subscription.policy;
if(prevPolicy==policy)
return;
subscription.policy=policy;
int index=items.indexOf(notificationPolicyItem);
RecyclerView.ViewHolder policyHolder=list.findViewHolderForAdapterPosition(index);
if(policyHolder!=null){
((NotificationPolicyViewHolder)policyHolder).rebind();
}else{
list.getAdapter().notifyItemChanged(index);
}
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
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)
((BindableViewHolder<?>)holder).rebind();
else
list.getAdapter().notifyItemChanged(index);
index++;
}
}
needUpdateNotificationSettings=true;
}
private void confirmLogOut(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.log_out)
.setMessage(R.string.confirm_log_out)
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut())
.setNegativeButton(R.string.cancel, null)
.show();
}
private void logOut(){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
onLoggedOut();
}
@Override
public void onError(ErrorResponse error){
onLoggedOut();
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
private void onLoggedOut(){
AccountSessionManager.getInstance().removeAccount(accountID);
getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}
private void clearImageCache(){
MastodonAPIController.runInBackground(()->{
Activity activity=getActivity();
ImageCache.getInstance(getActivity()).clear();
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
});
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
if(items.get(0) instanceof UpdateItem item){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
if(holder instanceof UpdateViewHolder uvh){
uvh.bind(item);
}
}
}
private static abstract class Item{
public abstract int getViewType();
}
private class HeaderItem extends Item{
private String text;
public HeaderItem(@StringRes int text){
this.text=getString(text);
}
public HeaderItem(String text){
this.text=text;
}
@Override
public int getViewType(){
return 0;
}
}
private class SwitchItem extends Item{
private String text;
private int icon;
private boolean checked;
private Consumer<SwitchItem> onChanged;
private boolean enabled=true;
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged){
this.text=getString(text);
this.icon=icon;
this.checked=checked;
this.onChanged=onChanged;
}
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
this.text=getString(text);
this.icon=icon;
this.checked=checked;
this.onChanged=onChanged;
this.enabled=enabled;
}
@Override
public int getViewType(){
return 1;
}
}
private static class ThemeItem extends Item{
@Override
public int getViewType(){
return 2;
}
}
private static class NotificationPolicyItem extends Item{
@Override
public int getViewType(){
return 3;
}
}
private class TextItem extends Item{
private String text;
private Runnable onClick;
public TextItem(@StringRes int text, Runnable onClick){
this.text=getString(text);
this.onClick=onClick;
}
public TextItem(String text, Runnable onClick){
this.text=text;
this.onClick=onClick;
}
@Override
public int getViewType(){
return 4;
}
}
private class RedHeaderItem extends HeaderItem{
public RedHeaderItem(int text){
super(text);
}
public RedHeaderItem(String text){
super(text);
}
@Override
public int getViewType(){
return 5;
}
}
private class FooterItem extends Item{
private String text;
public FooterItem(String text){
this.text=text;
}
@Override
public int getViewType(){
return 6;
}
}
private class UpdateItem extends Item{
@Override
public int getViewType(){
return 7;
}
}
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
@NonNull
@Override
public BindableViewHolder<Item> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
//noinspection unchecked
return (BindableViewHolder<Item>) switch(viewType){
case 0 -> new HeaderViewHolder(false);
case 1 -> new SwitchViewHolder();
case 2 -> new ThemeViewHolder();
case 3 -> new NotificationPolicyViewHolder();
case 4 -> new TextViewHolder();
case 5 -> new HeaderViewHolder(true);
case 6 -> new FooterViewHolder();
case 7 -> new UpdateViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@Override
public void onBindViewHolder(@NonNull BindableViewHolder<Item> holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
@Override
public int getItemViewType(int position){
return items.get(position).getViewType();
}
}
private class HeaderViewHolder extends BindableViewHolder<HeaderItem>{
private final TextView text;
public HeaderViewHolder(boolean red){
super(getActivity(), R.layout.item_settings_header, list);
text=(TextView) itemView;
if(red)
text.setTextColor(getResources().getColor(UiUtils.isDarkTheme() ? R.color.error_400 : R.color.error_700));
}
@Override
public void onBind(HeaderItem item){
text.setText(item.text);
}
}
private class SwitchViewHolder extends BindableViewHolder<SwitchItem> implements UsableRecyclerView.DisableableClickable{
private final TextView text;
private final ImageView icon;
private final Switch checkbox;
public SwitchViewHolder(){
super(getActivity(), R.layout.item_settings_switch, list);
text=findViewById(R.id.text);
icon=findViewById(R.id.icon);
checkbox=findViewById(R.id.checkbox);
}
@Override
public void onBind(SwitchItem item){
text.setText(item.text);
icon.setImageResource(item.icon);
checkbox.setChecked(item.checked && item.enabled);
checkbox.setEnabled(item.enabled);
}
@Override
public void onClick(){
item.checked=!item.checked;
checkbox.setChecked(item.checked);
item.onChanged.accept(item);
}
@Override
public boolean isEnabled(){
return item.enabled;
}
}
private class ThemeViewHolder extends BindableViewHolder<ThemeItem>{
private SubitemHolder autoHolder, lightHolder, darkHolder;
public ThemeViewHolder(){
super(getActivity(), R.layout.item_settings_theme, list);
autoHolder=new SubitemHolder(findViewById(R.id.theme_auto));
lightHolder=new SubitemHolder(findViewById(R.id.theme_light));
darkHolder=new SubitemHolder(findViewById(R.id.theme_dark));
}
@Override
public void onBind(ThemeItem item){
bindSubitems();
}
public void bindSubitems(){
autoHolder.bind(R.string.theme_auto, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_auto_trueblack : R.drawable.theme_auto, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO);
lightHolder.bind(R.string.theme_light, R.drawable.theme_light, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.LIGHT);
darkHolder.bind(R.string.theme_dark, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_dark_trueblack : R.drawable.theme_dark, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK);
}
private void onSubitemClick(View v){
GlobalUserPreferences.ThemePreference pref;
if(v.getId()==R.id.theme_auto)
pref=GlobalUserPreferences.ThemePreference.AUTO;
else if(v.getId()==R.id.theme_light)
pref=GlobalUserPreferences.ThemePreference.LIGHT;
else if(v.getId()==R.id.theme_dark)
pref=GlobalUserPreferences.ThemePreference.DARK;
else
return;
onThemePreferenceClick(pref);
}
private class SubitemHolder{
public TextView text;
public ImageView icon;
public RadioButton checkbox;
public SubitemHolder(View view){
text=view.findViewById(R.id.text);
icon=view.findViewById(R.id.icon);
checkbox=view.findViewById(R.id.checkbox);
view.setOnClickListener(ThemeViewHolder.this::onSubitemClick);
icon.setClipToOutline(true);
icon.setOutlineProvider(OutlineProviders.roundedRect(4));
}
public void bind(int text, int icon, boolean checked){
this.text.setText(text);
this.icon.setImageResource(icon);
checkbox.setChecked(checked);
}
public void setChecked(boolean checked){
checkbox.setChecked(checked);
}
}
}
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
private final Button button;
private final PopupMenu popupMenu;
@SuppressLint("ClickableViewAccessibility")
public NotificationPolicyViewHolder(){
super(getActivity(), R.layout.item_settings_notification_policy, list);
button=findViewById(R.id.button);
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.notification_policy);
popupMenu.setOnMenuItemClickListener(item->{
PushSubscription.Policy policy;
int id=item.getItemId();
if(id==R.id.notify_anyone)
policy=PushSubscription.Policy.ALL;
else if(id==R.id.notify_followed)
policy=PushSubscription.Policy.FOLLOWED;
else if(id==R.id.notify_follower)
policy=PushSubscription.Policy.FOLLOWER;
else if(id==R.id.notify_none)
policy=PushSubscription.Policy.NONE;
else
return false;
onNotificationsPolicyChanged(policy);
return true;
});
UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
}
@Override
public void onBind(NotificationPolicyItem item){
button.setText(switch(getPushSubscription().policy){
case ALL -> R.string.notify_anyone;
case FOLLOWED -> R.string.notify_followed;
case FOLLOWER -> R.string.notify_follower;
case NONE -> R.string.notify_none;
});
}
}
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text=(TextView) itemView;
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
}
@Override
public void onClick(){
item.onClick.run();
}
}
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
private final TextView text;
public FooterViewHolder(){
super(getActivity(), R.layout.item_settings_footer, list);
text=(TextView) itemView;
}
@Override
public void onBind(FooterItem item){
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.UpdateInfo info=updater.getUpdateInfo();
GithubSelfUpdater.UpdateState state=updater.getState();
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

@@ -1,43 +1,59 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import org.parceler.Parcels;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class SplashFragment extends AppKitFragment{
private static final String DEFAULT_SERVER="mastodon.social";
private SizeListenerFrameLayout contentView;
private View artContainer, blueFill, greenFill;
private ViewPager2 pager;
private ViewGroup pagerDots;
private InterpolatingMotionEffect motionEffect;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
private ProgressBarButton defaultServerButton;
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
loadAndChooseDefaultServer();
}
@Nullable
@@ -46,44 +62,31 @@ public class SplashFragment extends AppKitFragment{
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
defaultServerButton=contentView.findViewById(R.id.btn_join_default_server);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
defaultServerButton.setOnClickListener(this::onJoinDefaultServerClick);
defaultServerProgress=contentView.findViewById(R.id.action_progress);
if(loadingDefaultServer){
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
contentView.findViewById(R.id.btn_learn_more).setOnClickListener(this::onLearnMoreClick);
artClouds=contentView.findViewById(R.id.art_clouds);
artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant);
artRightHill=contentView.findViewById(R.id.art_right_hill);
artLeftHill=contentView.findViewById(R.id.art_left_hill);
artCenterHill=contentView.findViewById(R.id.art_center_hill);
pager=contentView.findViewById(R.id.pager);
pagerDots=contentView.findViewById(R.id.pager_dots);
pager.setAdapter(new PagerAdapter());
pager.setOffscreenPageLimit(3);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
for(int i=0;i<pagerDots.getChildCount();i++){
float alpha;
if(i==position){
alpha=0.3f+0.7f*(1f-positionOffset);
}else if(i==position+1){
alpha=0.3f+0.7f*positionOffset;
}else{
alpha=0.3f;
}
pagerDots.getChildAt(i).setAlpha(alpha);
}
float parallaxProgress=(position+positionOffset)/2f;
artClouds.setTranslationX(V.dp(-27)*(position>=1 ? 1f : positionOffset));
artPlaneElephant.setTranslationX(V.dp(101.55f)*parallaxProgress);
artLeftHill.setTranslationX(V.dp(-88)*parallaxProgress);
artLeftHill.setTranslationY(V.dp(24)*parallaxProgress);
artRightHill.setTranslationX(V.dp(-88)*parallaxProgress);
artRightHill.setTranslationY(V.dp(-24)*parallaxProgress);
artCenterHill.setTranslationX(V.dp(-40)*parallaxProgress);
}
});
artContainer=contentView.findViewById(R.id.art_container);
blueFill=contentView.findViewById(R.id.blue_fill);
greenFill=contentView.findViewById(R.id.green_fill);
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artClouds, V.dp(-5), V.dp(5), V.dp(-5), V.dp(5)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artRightHill, V.dp(-15), V.dp(25), V.dp(-10), V.dp(10)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artLeftHill, V.dp(-25), V.dp(15), V.dp(-15), V.dp(15)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artCenterHill, V.dp(-14), V.dp(14), V.dp(-5), V.dp(25)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(artPlaneElephant, V.dp(-20), V.dp(12), V.dp(-20), V.dp(12)));
artContainer.setOnTouchListener(motionEffect);
contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){
@Override
@@ -109,6 +112,48 @@ public class SplashFragment extends AppKitFragment{
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
}
private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer)
return;
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
if(!result.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(chosenDefaultServer);
}
private void onLearnMoreClick(View v){
View sheetView=getActivity().getLayoutInflater().inflate(R.layout.intro_bottom_sheet, null);
BottomSheet sheet=new BottomSheet(getActivity());
sheet.setContentView(sheetView);
sheet.setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface),
UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
sheet.show();
}
private void updateArtSize(int w, int h){
float scale=w/(float)V.dp(360);
artContainer.setScaleX(scale);
@@ -139,60 +184,65 @@ public class SplashFragment extends AppKitFragment{
return true;
}
private class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder>{
@NonNull
@Override
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new PagerViewHolder(viewType);
}
@Override
public void onBindViewHolder(@NonNull PagerViewHolder holder, int position){}
@Override
public int getItemCount(){
return 3;
}
@Override
public int getItemViewType(int position){
return position;
}
@Override
protected void onShown(){
super.onShown();
motionEffect.activate();
}
private class PagerViewHolder extends RecyclerView.ViewHolder{
public PagerViewHolder(int page){
super(new LinearLayout(getActivity()));
LinearLayout ll=(LinearLayout) itemView;
ll.setOrientation(LinearLayout.VERTICAL);
int pad=V.dp(16);
ll.setPadding(pad, pad, pad, pad);
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
@Override
protected void onHidden(){
super.onHidden();
motionEffect.deactivate();
}
TextView title=new TextView(getActivity());
title.setTextAppearance(R.style.m3_headline_medium);
title.setText(switch(page){
case 0 -> getString(R.string.welcome_page1_title);
case 1 -> getString(R.string.welcome_page2_title);
case 2 -> getString(R.string.welcome_page3_title);
default -> throw new IllegalStateException("Unexpected value: "+page);
});
title.setTextColor(0xFF17063B);
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(page==0 ? 46 : 36));
lp.bottomMargin=V.dp(page==0 ? 4 : 14);
ll.addView(title, lp);
private void loadAndChooseDefaultServer(){
loadingDefaultServer=true;
new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogDefaultInstance> result){
if(result.isEmpty()){
setChosenDefaultServer(DEFAULT_SERVER);
return;
}
float sum=0f;
for(CatalogDefaultInstance inst:result){
sum+=inst.weight;
}
if(sum<=0)
sum=1f;
for(CatalogDefaultInstance inst:result){
inst.weight/=sum;
}
float rand=ThreadLocalRandom.current().nextFloat();
float prev=0f;
for(CatalogDefaultInstance inst:result){
if(rand>=prev && rand<prev+inst.weight){
setChosenDefaultServer(inst.domain);
return;
}
prev+=inst.weight;
}
// Just in case something didn't add up
setChosenDefaultServer(result.get(result.size()-1).domain);
}
TextView text=new TextView(getActivity());
text.setTextAppearance(R.style.m3_body_medium);
text.setText(switch(page){
case 0 -> R.string.welcome_page1_text;
case 1 -> R.string.welcome_page2_text;
case 2 -> R.string.welcome_page3_text;
default -> throw new IllegalStateException("Unexpected value: "+page);
});
text.setTextColor(0xFF17063B);
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
@Override
public void onError(ErrorResponse error){
setChosenDefaultServer(DEFAULT_SERVER);
}
})
.execNoAuth("");
}
private void setChosenDefaultServer(String domain){
chosenDefaultServer=domain;
loadingDefaultServer=false;
if(defaultServerButton!=null && getActivity()!=null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
}
}
}

View File

@@ -57,32 +57,32 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}
protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusCreated(Status status){}
protected void onStatusUpdated(StatusUpdatedEvent ev){
protected void onStatusUpdated(Status status){
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;
if(s.reblog!=null && s.reblog.id.equals(status.id)){
s.reblog=status.clone();
statusesForDisplayItems.add(s);
}else if(s.id.equals(ev.status.id)){
data.set(i, ev.status);
statusesForDisplayItems.add(ev.status);
}else if(s.id.equals(status.id)){
data.set(i, status);
statusesForDisplayItems.add(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(s.reblog!=null && s.reblog.id.equals(status.id)){
s.reblog=status.clone();
}else if(s.id.equals(status.id)){
preloadedData.set(i, status);
}
}
@@ -143,7 +143,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
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)))
.filter(s->s.account.id.equals(ev.postsByAccountID) || (!ev.isUnfollow && s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID)))
.collect(Collectors.toList());
for(Status s:toRemove){
removeStatus(s);
@@ -209,12 +209,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
public void onStatusCreated(StatusCreatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
StatusListFragment.this.onStatusCreated(ev);
StatusListFragment.this.onStatusCreated(ev.status.clone());
}
@Subscribe
public void onStatusUpdated(StatusUpdatedEvent ev){
StatusListFragment.this.onStatusUpdated(ev);
StatusListFragment.this.onStatusUpdated(ev.status);
}
@Subscribe
@@ -224,7 +224,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
for(Status status:data){
Status contentStatus=status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(status.id, status, ev.poll);
updatePoll(status.id, contentStatus, ev.poll);
}
}
}

View File

@@ -1,14 +1,18 @@
package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
@@ -23,10 +27,15 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment{
private Status mainStatus;
private ImageView endMark;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -37,7 +46,10 @@ public class ThreadFragment extends StatusListFragment{
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
else
setTitle(getString(R.string.post_from_user, mainStatus.account.displayName));
}
@Override
@@ -69,8 +81,8 @@ public class ThreadFragment extends StatusListFragment{
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
}
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
filterStatuses(result.descendants);
filterStatuses(result.ancestors);
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
@@ -91,17 +103,8 @@ public class ThreadFragment extends StatusListFragment{
.exec(accountID);
}
private List<Status> filterStatuses(List<Status> statuses){
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
if(filters.isEmpty())
return statuses;
return statuses.stream().filter(status->{
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
}).collect(Collectors.toList());
private void filterStatuses(List<Status> statuses){
AccountSessionManager.get(accountID).filterStatuses(statuses, FilterContext.THREAD);
}
@Override
@@ -122,9 +125,10 @@ public class ThreadFragment extends StatusListFragment{
footerProgress.setVisibility(View.VISIBLE);
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
onAppendItems(Collections.singletonList(ev.status));
protected void onStatusCreated(Status status){
if(status.inReplyToId!=null && getStatusByID(status.inReplyToId)!=null){
onAppendItems(Collections.singletonList(status));
data.add(status);
}
}
@@ -132,4 +136,36 @@ public class ThreadFragment extends StatusListFragment{
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id);
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter a=new MergeRecyclerAdapter();
a.addAdapter(super.getAdapter());
endMark=new ImageView(getActivity());
endMark.setScaleType(ImageView.ScaleType.CENTER);
endMark.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant)));
endMark.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(25)));
endMark.setImageResource(R.drawable.thread_end_mark);
a.addAdapter(new SingleViewRecyclerAdapter(endMark));
return a;
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return bottomSibling==endMark;
}
@Override
protected void onErrorRetryClick(){
if(preloadingFailed){
preloadingFailed=false;
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
V.setVisibilityAnimated(footerError, View.GONE);
doLoadData();
return;
}
super.onErrorRetryClick();
}
}

View File

@@ -1,38 +1,21 @@
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.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
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 org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import java.util.ArrayList;
import java.util.HashMap;
@@ -43,20 +26,15 @@ 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>{
public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<AccountViewModel>{
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -65,6 +43,10 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
super(40);
}
public BaseAccountListFragment(int layout, int perPage){
super(layout, perPage);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -72,7 +54,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
}
@Override
protected void onDataLoaded(List<AccountItem> d, boolean more){
protected void onDataLoaded(List<AccountViewModel> d, boolean more){
if(refreshing){
relationships.clear();
}
@@ -89,7 +71,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
super.onRefresh();
}
protected void loadRelationships(List<AccountItem> accounts){
protected void loadRelationships(List<AccountViewModel> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req);
@@ -125,9 +107,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
@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();
}
@@ -160,6 +140,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
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());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, V.dp(16), 0, V.dp(16));
@@ -167,6 +149,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
super.onApplyWindowInsets(insets);
}
protected void onConfigureViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@@ -175,7 +159,9 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
onConfigureViewHolder(holder);
return holder;
}
@Override
@@ -196,199 +182,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountItem item=data.get(position);
AccountViewModel 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()));
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)
.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,99 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private String currentQuery;
private boolean resultDelivered;
private SearchViewHelper searchViewHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRefreshEnabled(false);
setEmptyText("");
dataLoaded();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
searchViewHelper.setListeners(this::onQueryChanged, null);
searchViewHelper.addDivider(contentView);
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
setStatusBarColor(color);
setNavigationBarColor(color);
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setOnClickListener(this::onItemClick);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
}
private void onItemClick(AccountViewHolder holder){
if(resultDelivered)
return;
resultDelivered=true;
Bundle res=new Bundle();
res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account));
setResult(true, res);
Nav.finish(this, false);
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(!TextUtils.isEmpty(currentQuery))
loadData();
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.account_list;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import java.util.stream.Collectors;
@@ -23,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);

View File

@@ -1,80 +1,39 @@
package org.joinmastodon.android.fragments.discover;
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.GetFollowSuggestions;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
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 org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
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;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop{
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
public DiscoverAccountsFragment(){
super(20);
}
public class DiscoverAccountsFragment extends BaseAccountListFragment implements ScrollableToTop{
private DiscoverInfoBannerHelper bannerHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.ACCOUNTS, accountID);
}
@Override
protected void doLoadData(int offset, int count){
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
currentRequest=new GetFollowSuggestions(count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false);
loadRelationships();
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
onDataLoaded(accounts, false);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
@@ -82,219 +41,14 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
@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;
}
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@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);
}
}
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;
private final ProgressBar actionProgress;
private final View actionWrap;
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);
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);
}
@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){
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
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 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

@@ -3,22 +3,19 @@ package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
@@ -28,22 +25,24 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
private static final int QUERY_RESULT=937;
private TabLayout tabLayout;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private EditText searchEdit;
private boolean searchActive;
private FrameLayout searchView;
private ImageButton searchBack, searchClear;
private ProgressBar searchProgress;
private ImageButton searchBack;
private TextView searchText;
private View tabsDivider;
private DiscoverPostsFragment postsFragment;
private TrendingHashtagsFragment hashtagsFragment;
@@ -53,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private LocalTimelineFragment localTimelineFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private String currentQuery;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -88,8 +87,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabViews[i]=tabView;
}
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabLayout.setTabTextSize(V.dp(14));
pager.setOffscreenPageLimit(4);
pager.setAdapter(new DiscoverPagerAdapter());
@@ -146,7 +145,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 4 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
}
});
tabLayoutMediator.attach();
@@ -163,58 +161,39 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.VISIBLE);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.INVISIBLE);
}
}
});
searchView=view.findViewById(R.id.search_fragment);
if(searchFragment==null){
searchFragment=new SearchFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
searchFragment.setArguments(args);
searchFragment.setProgressVisibilityListener(this::onSearchProgressVisibilityChanged);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
}
searchBack=view.findViewById(R.id.search_back);
searchClear=view.findViewById(R.id.search_clear);
searchProgress=view.findViewById(R.id.search_progress);
searchText=view.findViewById(R.id.search_text);
searchBack.setEnabled(searchActive);
searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
searchBack.setOnClickListener(v->exitSearch());
if(searchActive){
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
searchBack.setImageResource(R.drawable.ic_arrow_back);
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
}
searchClear.setOnClickListener(v->{
searchEdit.setText("");
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
View searchWrap=view.findViewById(R.id.search_wrap);
searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
searchWrap.setClipToOutline(true);
searchText.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
if(!TextUtils.isEmpty(currentQuery)){
args.putString("query", currentQuery);
}
Nav.goForResult(getActivity(), SearchQueryFragment.class, args, QUERY_RESULT, DiscoverFragment.this);
});
tabsDivider=view.findViewById(R.id.tabs_divider);
return view;
}
@@ -233,35 +212,32 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment.loadData();
}
private void onSearchEditFocusChanged(View v, boolean hasFocus){
if(!searchActive && hasFocus){
private void enterSearch(){
if(!searchActive){
searchActive=true;
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
searchBack.setImageResource(R.drawable.ic_arrow_back);
searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
}
}
private void exitSearch(){
if(!searchActive)
return;
searchActive=false;
pager.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE);
searchView.setVisibility(View.GONE);
searchEdit.clearFocus();
searchEdit.setText("");
searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular);
searchText.setText(R.string.search_mastodon);
searchBack.setImageResource(R.drawable.ic_search_24px);
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
tabsDivider.setVisibility(View.VISIBLE);
currentQuery=null;
}
private Fragment getFragmentForPage(int page){
@@ -284,23 +260,20 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
return false;
}
private void onSearchChangedDebounced(){
searchFragment.setQuery(searchEdit.getText().toString());
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
return true;
}
private void onSearchProgressVisibilityChanged(boolean visible){
V.setVisibilityAnimated(searchProgress, visible ? View.VISIBLE : View.INVISIBLE);
if(searchEdit.length()>0)
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==QUERY_RESULT && success){
enterSearch();
currentQuery=result.getString("query");
SearchResult.Type type;
if(result.containsKey("filter")){
type=SearchResult.Type.values()[result.getInt("filter")];
}else{
type=null;
}
searchFragment.setQuery(currentQuery, type);
searchText.setText(currentQuery);
}
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
@@ -12,32 +13,43 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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.ListImageLoaderAdapter;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
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.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop{
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
private DiscoverInfoBannerHelper bannerHelper;
private MergeRecyclerAdapter mergeAdapter;
private UsableRecyclerView cardsList;
private ArrayList<CardViewModel> top3=new ArrayList<>();
private CardLinksAdapter cardsAdapter;
public DiscoverNewsFragment(){
super(10);
@@ -47,6 +59,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID);
}
@Override
@@ -55,10 +68,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Card> result){
imageRequests=result.stream()
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150)))
.collect(Collectors.toList());
onDataLoaded(result, false);
top3.clear();
top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
cardsAdapter.notifyDataSetChanged();
onDataLoaded(result.subList(top3.size(), result.size()).stream()
.map(card->new CardViewModel(card, 56, 56))
.collect(Collectors.toList()), false);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
@@ -66,14 +83,27 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
@Override
protected RecyclerView.Adapter getAdapter(){
return new LinksAdapter();
}
cardsList=new UsableRecyclerView(getActivity());
cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
cardsList.setClipToPadding(false);
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.right=V.dp(16);
}
});
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
cardsList.setDrawSelectorOnTop(true);
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0));
bannerHelper.maybeAddBanner(contentWrap);
mergeAdapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, mergeAdapter);
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
return mergeAdapter;
}
@Override
@@ -81,14 +111,17 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
smoothScrollRecyclerViewToTop(list);
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(){
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
private final List<CardViewModel> data;
public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader);
this.data=data;
}
@NonNull
@Override
public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkViewHolder();
}
@@ -98,46 +131,51 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
}
@Override
public void onBindViewHolder(LinkViewHolder holder, int position){
holder.bind(data.get(position));
public void onBindViewHolder(BaseLinkViewHolder holder, int position){
holder.bind(data.get(position).card);
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return imageRequests.get(position)==null ? 0 : 1;
return data.get(position).imageRequest==null ? 0 : 1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return imageRequests.get(position);
return data.get(position).imageRequest;
}
}
private class LinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
private final TextView name, title, subtitle;
private final ImageView photo;
private class CardLinksAdapter extends LinksAdapter{
public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader, data);
}
@NonNull
@Override
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkCardViewHolder();
}
}
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
protected final TextView name, title;
protected final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
public LinkViewHolder(){
super(getActivity(), R.layout.item_trending_link, list);
public BaseLinkViewHolder(int layout){
super(getActivity(), layout, list);
name=findViewById(R.id.name);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
photo=findViewById(R.id.photo);
photo.setOutlineProvider(OutlineProviders.roundedRect(2));
photo.setClipToOutline(true);
}
@Override
public void onBind(Card item){
name.setText(item.providerName);
title.setText(item.title);
int num=item.history.get(0).uses;
if(item.history.size()>1)
num+=item.history.get(1).uses;
subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num));
crossfadeDrawable.setSize(item.width, item.height);
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
@@ -164,4 +202,20 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
UiUtils.launchWebBrowser(getActivity(), item.url);
}
}
private class LinkViewHolder extends BaseLinkViewHolder{
public LinkViewHolder(){
super(R.layout.item_trending_link);
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
photo.setClipToOutline(true);
}
}
private class LinkCardViewHolder extends BaseLinkViewHolder{
public LinkCardViewHolder(){
super(R.layout.item_trending_link_card);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
}
}
}

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.StatusListFragment;
@@ -10,10 +9,18 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverPostsFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
private DiscoverInfoBannerHelper bannerHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
}
@Override
protected void doLoadData(int offset, int count){
@@ -22,13 +29,16 @@ public class DiscoverPostsFragment extends StatusListFragment{
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
bannerHelper.onBannerBecameVisible();
}
}).exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
}

View File

@@ -4,21 +4,29 @@ import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class LocalTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
private DiscoverInfoBannerHelper bannerHelper;
private String maxID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@@ -27,15 +35,20 @@ public class LocalTimelineFragment extends StatusListFragment{
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
}

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.discover;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -22,7 +21,6 @@ import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -33,12 +31,9 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
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.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@@ -46,12 +41,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
private List<SearchResult> unfilteredResults=Collections.emptyList();
private HideableSingleViewRecyclerAdapter headerAdapter;
private ProgressVisibilityListener progressVisibilityListener;
private InputMethodManager imm;
private TabLayout tabLayout;
public SearchFragment(){
setLayout(R.layout.fragment_search);
}
@@ -61,6 +52,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
setEmptyText(R.string.no_search_results);
loadData();
}
@@ -119,51 +111,52 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@Override
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
if(getActivity()==null)
return;
unfilteredResults=sr;
prevDisplayItems=new ArrayList<>(displayItems);
onDataLoaded(sr, false);
});
GetSearchResults.Type type;
if(currentFilter.size()==1){
type=switch(currentFilter.iterator().next()){
case ACCOUNT -> GetSearchResults.Type.ACCOUNTS;
case HASHTAG -> GetSearchResults.Type.HASHTAGS;
case STATUS -> GetSearchResults.Type.STATUSES;
};
}else{
progressVisibilityListener.onProgressVisibilityChanged(true);
currentRequest=new GetSearchResults(currentQuery, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
results.add(new SearchResult(acc));
}
if(result.hashtags!=null){
for(Hashtag tag:result.hashtags)
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
})
.exec(accountID);
type=null;
}
if(currentQuery==null){
dataLoaded();
return;
}
currentRequest=new GetSearchResults(currentQuery, type, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
results.add(new SearchResult(acc));
}
if(result.hashtags!=null){
for(Hashtag tag:result.hashtags)
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
}
})
.exec(accountID);
}
@Override
@@ -173,86 +166,30 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return;
}
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.forceUpdateImages();
prevDisplayItems=null;
}
@Override
protected void onDataLoaded(List<SearchResult> d, boolean more){
super.onDataLoaded(d, more);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
tabLayout=view.findViewById(R.id.tabbar);
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_all));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_people));
tabLayout.addTab(tabLayout.newTab().setText(R.string.hashtags));
tabLayout.addTab(tabLayout.newTab().setText(R.string.posts));
for(int i=0;i<tabLayout.getTabCount();i++){
tabLayout.getTabAt(i).view.textView.setAllCaps(true);
}
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
setFilter(switch(tab.getPosition()){
case 0 -> EnumSet.allOf(SearchResult.Type.class);
case 1 -> EnumSet.of(SearchResult.Type.ACCOUNT);
case 2 -> EnumSet.of(SearchResult.Type.HASHTAG);
case 3 -> EnumSet.of(SearchResult.Type.STATUS);
default -> throw new IllegalStateException("Unexpected value: "+tab.getPosition());
});
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
}
@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.item_recent_searches_header, list, false);
header.findViewById(R.id.clear).setOnClickListener(this::onClearRecentClick);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(headerAdapter=new HideableSingleViewRecyclerAdapter(header));
adapter.addAdapter(super.getAdapter());
headerAdapter.setVisible(isInRecentMode());
return adapter;
}
public void setQuery(String q){
if(Objects.equals(q, currentQuery) || q.isBlank())
public void setQuery(String q, SearchResult.Type filter){
if(q.isBlank())
return;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
currentQuery=q;
if(filter==null)
currentFilter=EnumSet.allOf(SearchResult.Type.class);
else
currentFilter=EnumSet.of(filter);
refreshing=true;
doLoadData(0, 0);
loadData();
}
private void setFilter(EnumSet<SearchResult.Type> filter){
if(filter.equals(currentFilter))
return;
currentFilter=filter;
if(isInRecentMode())
return;
// This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
@@ -274,23 +211,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return null;
}
private void onClearRecentClick(View v){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
onDataLoaded(unfilteredResults=Collections.emptyList(), false);
}
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
public void setProgressVisibilityListener(ProgressVisibilityListener progressVisibilityListener){
this.progressVisibilityListener=progressVisibilityListener;
}
@Override
public void onScrollStarted(){
super.onScrollStarted();

View File

@@ -0,0 +1,564 @@
package org.joinmastodon.android.fragments.discover;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Fragment;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.RoundedCorner;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowInsets;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.SearchResultViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
private MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
private HideableSingleViewRecyclerAdapter recentsHeader;
private ListItem<Void> openUrlItem, goToHashtagItem, goToAccountItem, goToStatusSearchItem, goToAccountSearchItem;
private ArrayList<ListItem<Void>> topOptions=new ArrayList<>();
private GenericListItemsAdapter<Void> topOptionsAdapter;
private String accountID;
private SearchViewHelper searchViewHelper;
private String currentQuery;
private LayerDrawable navigationIcon;
private Drawable searchIcon, backIcon;
public SearchQueryFragment(){
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setRefreshEnabled(false);
setEmptyText("");
openUrlItem=new ListItem<>(R.string.search_open_url, 0, R.drawable.ic_link_24px, this::onOpenURLClick);
goToHashtagItem=new ListItem<>("", null, R.drawable.ic_tag_24px, this::onGoToHashtagClick);
goToAccountItem=new ListItem<>("", null, R.drawable.ic_person_24px, this::onGoToAccountClick);
goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_search_24px, this::onGoToStatusSearchClick);
goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_group_24px, this::onGoToAccountSearchClick);
currentQuery=getArguments().getString("query");
dataLoaded();
doLoadData(0, 0);
}
@Override
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(results->{
if(getActivity()==null)
return;
onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
}).collect(Collectors.toList()), false);
recentsHeader.setVisible(!data.isEmpty());
});
}else{
currentRequest=new GetSearchResults(currentQuery, null, false)
.limit(2)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
.flatMap(Function.identity())
.map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
})
.collect(Collectors.toList()), false);
recentsHeader.setVisible(false);
}
})
.exec(accountID);
}
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.display_item_section_header, list, false);
TextView title=header.findViewById(R.id.title);
Button action=header.findViewById(R.id.action_btn);
title.setText(R.string.recent_searches);
action.setText(R.string.clear_all);
action.setOnClickListener(v->onClearRecentClick());
recentsHeader=new HideableSingleViewRecyclerAdapter(header);
mergeAdapter.addAdapter(recentsHeader);
mergeAdapter.addAdapter(topOptionsAdapter=new GenericListItemsAdapter<>(topOptions));
mergeAdapter.addAdapter(new SearchResultsAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_mastodon));
searchViewHelper.setListeners(this::onQueryChanged, this::onQueryChangedNoDebounce);
searchViewHelper.addDivider(contentView);
searchViewHelper.setEnterCallback(this::onSearchViewEnter);
navigationIcon=new LayerDrawable(new Drawable[]{
searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_search_24px, getToolbarContext().getTheme()).mutate(),
backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_arrow_back, getToolbarContext().getTheme()).mutate()
}){
@Override
public Drawable mutate(){
return this;
}
};
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
setStatusBarColor(color);
setNavigationBarColor(color);
if(currentQuery!=null){
searchViewHelper.setQuery(currentQuery);
searchIcon.setAlpha(0);
}
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=V.dp(8);
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
refreshing=true;
doLoadData(0, 0);
}
private void onQueryChangedNoDebounce(String q){
updateTopOptions(q);
if(!TextUtils.isEmpty(q)){
recentsHeader.setVisible(false);
}
data.clear();
mergeAdapter.notifyDataSetChanged();
}
private void updateTopOptions(String q){
topOptions.clear();
// https://github.com/mastodon/mastodon/blob/a985d587e13494b78ef2879e4d97f78a2df693db/app/javascript/mastodon/features/compose/components/search.jsx#L233
String trimmedValue=q.trim();
if(trimmedValue.length()>0){
boolean couldBeURL=trimmedValue.startsWith("https://") && !trimmedValue.contains(" ");
if(couldBeURL){
topOptions.add(openUrlItem);
}
boolean couldBeHashtag=(trimmedValue.startsWith("#") && trimmedValue.length()>1 && !trimmedValue.contains(" ")) || HASHTAG_REGEX.matcher(trimmedValue).find();
if(couldBeHashtag){
String tag=trimmedValue.startsWith("#") ? trimmedValue.substring(1) : trimmedValue;
goToHashtagItem.title=getString(R.string.posts_matching_hashtag, "#"+tag);
topOptions.add(goToHashtagItem);
}
Matcher usernameMatcher=USERNAME_REGEX.matcher(trimmedValue);
if(usernameMatcher.find()){
String username="@"+usernameMatcher.group(1);
String atDomain=usernameMatcher.group(2);
if(atDomain==null){
username+="@"+AccountSessionManager.get(accountID).domain;
}
goToAccountItem.title=getString(R.string.search_go_to_account, username);
topOptions.add(goToAccountItem);
}
goToStatusSearchItem.title=getString(R.string.posts_matching_string, trimmedValue);
topOptions.add(goToStatusSearchItem);
goToAccountSearchItem.title=getString(R.string.accounts_matching_string, trimmedValue);
topOptions.add(goToAccountSearchItem);
}
topOptionsAdapter.notifyDataSetChanged();
}
@Override
public Animator onCreateEnterTransition(View prev, View container){
return createTransition(prev, container, true);
}
@Override
public Animator onCreateExitTransition(View prev, View container){
return createTransition(prev, container, false);
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
protected Drawable getNavigationIconDrawable(){
return navigationIcon;
}
@Override
protected void onShown(){
super.onShown();
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchViewHelper.getSearchEdit(), 0);
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0);
}
@RequiresApi(api = Build.VERSION_CODES.S)
private float getScreenCornerRadius(WindowInsets insets, int pos){
RoundedCorner corner=insets.getRoundedCorner(pos);
if(corner==null)
return 0;
return corner.getRadius();
}
private Animator createTransition(View prev, View container, boolean enter){
int[] loc={0, 0};
View searchBtn=prev.findViewById(R.id.search_wrap);
searchBtn.getLocationInWindow(loc);
int btnLeft=loc[0], btnTop=loc[1];
container.getLocationInWindow(loc);
int offX=btnLeft-loc[0], offY=btnTop-loc[1];
float screenRadius;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
WindowInsets insets=container.getRootWindowInsets();
screenRadius=Math.min(
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT)),
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT))
);
}else{
screenRadius=0;
}
float buttonRadius=V.dp(26);
Rect buttonBounds=new Rect(offX, offY, offX+searchBtn.getWidth(), offY+searchBtn.getHeight());
Rect containerBounds=new Rect(0, 0, container.getWidth(), container.getHeight());
AnimatableOutlineProvider outlineProvider=new AnimatableOutlineProvider(enter ? buttonBounds : containerBounds, enter ? containerBounds : buttonBounds, enter ? buttonRadius : screenRadius);
container.setOutlineProvider(outlineProvider);
container.setClipToOutline(true);
AnimatorSet set=new AnimatorSet();
ObjectAnimator boundsAnim;
Toolbar toolbar=getToolbar();
float toolbarTX=offX-toolbar.getX();
float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f;
ArrayList<Animator> anims=new ArrayList<>();
anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f));
anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY));
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getSearchLayout(), View.TRANSLATION_X, enter ? V.dp(-4) : 0, enter ? 0 : V.dp(-4)));
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
View parentContent=prev.findViewById(R.id.discover_content);
View parentContentParent=(View) parentContent.getParent();
parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
if(enter){
anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0));
}else{
}
anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
for(Animator anim:anims){
anim.setDuration(enter ? 700 : 300);
}
if(TextUtils.isEmpty(currentQuery)){
anims.add(ObjectAnimator.ofInt(searchIcon, "alpha", enter ? 255 : 0, enter ? 0 : 255).setDuration(200));
anims.add(ObjectAnimator.ofInt(backIcon, "alpha", enter ? 0 : 255, enter ? 255 : 0).setDuration(200));
}
ObjectAnimator parentContentFade;
anims.add(parentContentFade=ObjectAnimator.ofFloat(parentContent, View.ALPHA, enter ? 1 : 0, enter ? 0 : 1).setDuration(enter ? 350 : 250));
if(!enter){
parentContentFade.setStartDelay(50);
ObjectAnimator parentContentTY;
anims.add(parentContentTY=ObjectAnimator.ofFloat(parentContent, View.TRANSLATION_Y, V.dp(16), 0).setDuration(250));
parentContentTY.setStartDelay(50);
}
set.playTogether(anims);
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_emphasized_decelerate));
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
container.setOutlineProvider(null);
container.setClipToOutline(false);
parentContentParent.setBackground(null);
}
});
boundsAnim.addUpdateListener(animation->{
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
return set;
}
private void openHashtag(SearchResult res){
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
private void onSearchViewEnter(){
deliverResult(currentQuery, null);
}
private void onOpenURLClick(){
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
}
private void onGoToHashtagClick(){
String q=searchViewHelper.getQuery();
if(q.startsWith("#"))
q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q);
}
private void onGoToAccountClick(){
String q=searchViewHelper.getQuery();
if(!q.startsWith("@")){
q="@"+q;
}
if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain;
}
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
}
private void onGoToStatusSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
}
private void onGoToAccountSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
}
private void onClearRecentClick(){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
data.clear();
recentsHeader.setVisible(false);
mergeAdapter.notifyDataSetChanged();
}
}
private void deliverResult(String query, SearchResult.Type typeFilter){
Bundle res=new Bundle();
res.putString("query", query);
if(typeFilter!=null)
res.putInt("filter", typeFilter.ordinal());
setResult(true, res);
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;
private AnimatableOutlineProvider(Rect boundsFrom, Rect boundsTo, float radius){
this.boundsFrom=boundsFrom;
this.boundsTo=boundsTo;
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(
UiUtils.lerp(boundsFrom.left, boundsTo.left, boundsFraction),
UiUtils.lerp(boundsFrom.top, boundsTo.top, boundsFraction),
UiUtils.lerp(boundsFrom.right, boundsTo.right, boundsFraction),
UiUtils.lerp(boundsFrom.bottom, boundsTo.bottom, boundsFraction),
radius
);
}
@Keep
public float getBoundsFraction(){
return boundsFraction;
}
@Keep
public void setBoundsFraction(float boundsFraction){
this.boundsFraction=boundsFraction;
}
@Keep
public float getRadius(){
return radius;
}
@Keep
public void setRadius(float radius){
this.radius=radius;
}
}
private class SearchResultsAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
public SearchResultsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
if(viewType==R.id.list_item_account){
return new CustomAccountViewHolder(SearchQueryFragment.this, parent, null);
}else if(viewType==R.id.list_item_simple){
return new SimpleListItemViewHolder(parent.getContext(), parent);
}
throw new IllegalArgumentException();
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position){
if(holder instanceof CustomAccountViewHolder avh){
avh.bind(data.get(position).account);
avh.searchResult=data.get(position).result;
}else if(holder instanceof SimpleListItemViewHolder ivh){
ivh.bind(data.get(position).hashtagItem);
}
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getItemViewType(int position){
return switch(data.get(position).result.type){
case ACCOUNT -> R.id.list_item_account;
case HASHTAG -> R.id.list_item_simple;
default -> throw new IllegalStateException("Unexpected value: "+data.get(position).result.type);
};
}
@Override
public int getImageCountForItem(int position){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null)
return vm.account.emojiHelper.getImageCount()+1;
return 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null){
if(image==0)
return vm.account.avaRequest;
return vm.account.emojiHelper.getImageRequest(image-1);
}
return null;
}
}
private class CustomAccountViewHolder extends AccountViewHolder{
public SearchResult searchResult;
public CustomAccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment, list, relationships);
setStyle(AccessoryType.NONE, false);
}
@Override
public void onClick(){
super.onClick();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(searchResult);
}
}
}

View File

@@ -25,7 +25,6 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop{
private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public TrendingHashtagsFragment(){
super(10);
@@ -54,13 +53,6 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
return new HashtagsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16));
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);
@@ -98,6 +90,12 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
@Override
public void onBind(Hashtag item){
title.setText('#'+item.name);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
return;
}
chart.setVisibility(View.VISIBLE);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends ToolbarFragment{
private String accountID;
@@ -50,6 +47,7 @@ public class AccountActivationFragment extends ToolbarFragment{
private APIRequest currentRequest;
private Runnable resendTimer=this::updateResendTimer;
private long lastResendTime;
private boolean visible;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -70,7 +68,7 @@ public class AccountActivationFragment extends ToolbarFragment{
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
Nav.go(getActivity(), SettingsMainFragment.class, args);
return true;
});
resendBtn=view.findViewById(R.id.btn_resend);
@@ -89,13 +87,6 @@ public class AccountActivationFragment extends ToolbarFragment{
return !UiUtils.isDarkTheme();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
@@ -110,29 +101,25 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity()).show();
new AccountSwitcherSheet(getActivity(), null).show();
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
contentView.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(contentView, insets));
}
@Override
protected void onShown(){
super.onShown();
visible=true;
tryGetAccount();
}
@Override
protected void onHidden(){
super.onHidden();
visible=false;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
@@ -245,6 +232,8 @@ public class AccountActivationFragment extends ToolbarFragment{
}
private void proceed(){
if(!visible)
return;
Bundle args=new Bundle();
args.putString("account", accountID);
// Nav.goClearingStack(getActivity(), HomeFragment.class, args);

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