Compare commits

...

695 Commits

Author SHA1 Message Date
Grishka
357041b995 Prepare for release 2024-11-15 01:15:59 +03:00
Grishka
6754f004f2 Merge branch 'l10n_master' 2024-11-15 01:13:48 +03:00
Eugen Rochko
3487b0b70d New translations strings.xml (Italian) 2024-11-14 00:11:39 +01:00
Gregory K
f91b887586 Merge pull request #921 from owo-uwu-nyaa/master
Make sure cursor stays under 1MB size limit
2024-11-13 20:30:23 +03:00
Jonas
62cd754f3a Merge branch 'mastodon:master' into master 2024-11-13 18:08:48 +01:00
likeazir
be82274bee consistent naming 2024-11-13 17:52:01 +01:00
likeazir
6779b5cc43 single queries for domains to prevent running into the cursor limit 2024-11-13 17:49:30 +01:00
likeazir
86e369201a remove extra query to get emoji row length 2024-11-13 17:32:46 +01:00
Eugen Rochko
6b2fd26961 New translations strings.xml (Lithuanian) 2024-11-13 11:33:32 +01:00
Eugen Rochko
423718b4f2 New translations strings.xml (Lithuanian) 2024-11-13 09:33:41 +01:00
Eugen Rochko
f6ed37f80f New translations strings.xml (Chinese Traditional) 2024-11-13 01:46:55 +01:00
Eugen Rochko
71a986b280 New translations strings.xml (Swedish) 2024-11-12 22:45:23 +01:00
Eugen Rochko
12375609e3 New translations strings.xml (Interlingua) 2024-11-12 20:42:20 +01:00
Eugen Rochko
02659407f8 New translations strings.xml (Kabyle) 2024-11-12 20:42:18 +01:00
Eugen Rochko
a4f0e577f5 New translations strings.xml (Scottish Gaelic) 2024-11-12 20:42:16 +01:00
Eugen Rochko
9bc4d81321 New translations strings.xml (Filipino) 2024-11-12 20:42:13 +01:00
Eugen Rochko
72dd2e1b65 New translations strings.xml (Welsh) 2024-11-12 20:42:12 +01:00
Eugen Rochko
58e74708f5 New translations strings.xml (Thai) 2024-11-12 20:42:09 +01:00
Eugen Rochko
77855154d3 New translations strings.xml (Persian) 2024-11-12 20:42:07 +01:00
Eugen Rochko
d77fcff253 New translations strings.xml (Indonesian) 2024-11-12 20:42:05 +01:00
Eugen Rochko
5358c889e4 New translations strings.xml (Portuguese, Brazilian) 2024-11-12 20:42:04 +01:00
Eugen Rochko
bcbf3412a7 New translations strings.xml (Galician) 2024-11-12 20:42:03 +01:00
Eugen Rochko
17a481e797 New translations strings.xml (Vietnamese) 2024-11-12 20:42:01 +01:00
Eugen Rochko
dce8d38237 New translations strings.xml (Chinese Traditional) 2024-11-12 20:42:00 +01:00
Eugen Rochko
1cf250765b New translations strings.xml (Chinese Simplified) 2024-11-12 20:41:59 +01:00
Eugen Rochko
d136060b6f New translations strings.xml (Ukrainian) 2024-11-12 20:41:57 +01:00
Eugen Rochko
6dd7d779dd New translations strings.xml (Turkish) 2024-11-12 20:41:56 +01:00
Eugen Rochko
aca2843991 New translations strings.xml (Swedish) 2024-11-12 20:41:55 +01:00
Eugen Rochko
04f46cbecc New translations strings.xml (Slovenian) 2024-11-12 20:41:54 +01:00
Eugen Rochko
9fbca3a7d6 New translations strings.xml (Russian) 2024-11-12 20:41:52 +01:00
Eugen Rochko
a6a71ce8b6 New translations strings.xml (Portuguese) 2024-11-12 20:41:50 +01:00
Eugen Rochko
31d8a85e35 New translations strings.xml (Polish) 2024-11-12 20:41:49 +01:00
Eugen Rochko
63a3069041 New translations strings.xml (Norwegian) 2024-11-12 20:41:48 +01:00
Eugen Rochko
96750bb80a New translations strings.xml (Dutch) 2024-11-12 20:41:47 +01:00
Eugen Rochko
e53df3d1b1 New translations strings.xml (Lithuanian) 2024-11-12 20:41:45 +01:00
Eugen Rochko
8a5a681b86 New translations strings.xml (Korean) 2024-11-12 20:41:44 +01:00
Eugen Rochko
06d2df4773 New translations strings.xml (Japanese) 2024-11-12 20:41:42 +01:00
Eugen Rochko
723bdb9fed New translations strings.xml (Italian) 2024-11-12 20:41:41 +01:00
Eugen Rochko
774601bc6c New translations strings.xml (Armenian) 2024-11-12 20:41:40 +01:00
Eugen Rochko
909fbcb54a New translations strings.xml (Hungarian) 2024-11-12 20:41:38 +01:00
Eugen Rochko
1dd422918a New translations strings.xml (Finnish) 2024-11-12 20:41:36 +01:00
Eugen Rochko
7c433ccea9 New translations strings.xml (Greek) 2024-11-12 20:41:34 +01:00
Eugen Rochko
56602a88e1 New translations strings.xml (German) 2024-11-12 20:41:33 +01:00
Eugen Rochko
dd838689c5 New translations strings.xml (Danish) 2024-11-12 20:41:32 +01:00
Eugen Rochko
8b54f2960b New translations strings.xml (Czech) 2024-11-12 20:41:31 +01:00
Eugen Rochko
782f828073 New translations strings.xml (Catalan) 2024-11-12 20:41:29 +01:00
Eugen Rochko
32c0a3985e New translations strings.xml (Belarusian) 2024-11-12 20:41:28 +01:00
Eugen Rochko
6bfee114db New translations strings.xml (Arabic) 2024-11-12 20:41:27 +01:00
Eugen Rochko
fb0bbe3b11 New translations strings.xml (Spanish) 2024-11-12 20:41:25 +01:00
Eugen Rochko
64d0adc3ff New translations strings.xml (French) 2024-11-12 20:41:23 +01:00
Eugen Rochko
942c9998ba New translations strings.xml (Frisian) 2024-11-12 20:41:21 +01:00
Eugen Rochko
864d6fb7a5 New translations strings.xml (Icelandic) 2024-11-12 20:41:19 +01:00
Eugen Rochko
9286de4580 New translations strings.xml (Basque) 2024-11-12 20:41:18 +01:00
Grishka
6e4590caf2 Fix #820, probably 2024-11-12 22:40:48 +03:00
Grishka
ea1216b352 Show the direction on gaps + bug fix 2024-11-12 22:12:05 +03:00
Eugen Rochko
c2cd886844 New translations strings.xml (Swedish) 2024-11-12 19:38:17 +01:00
Eugen Rochko
83e5a041a3 New translations strings.xml (Swedish) 2024-11-12 18:38:19 +01:00
Eugen Rochko
a10bbab6d2 New translations strings.xml (Interlingua) 2024-11-12 16:55:50 +01:00
Eugen Rochko
ed36774f16 New translations strings.xml (Urdu (India)) 2024-11-12 16:55:49 +01:00
Eugen Rochko
6663c1c2be New translations strings.xml (Kabyle) 2024-11-12 16:55:48 +01:00
Eugen Rochko
6d448fbb5c New translations strings.xml (Igbo) 2024-11-12 16:55:47 +01:00
Eugen Rochko
25a49971bb New translations strings.xml (Occitan) 2024-11-12 16:55:45 +01:00
Eugen Rochko
3300660321 New translations strings.xml (Scottish Gaelic) 2024-11-12 16:55:44 +01:00
Eugen Rochko
39cf4b6678 New translations strings.xml (Sinhala) 2024-11-12 16:55:43 +01:00
Eugen Rochko
0edf1a5653 New translations strings.xml (Bosnian) 2024-11-12 16:55:42 +01:00
Eugen Rochko
52473e69cc New translations strings.xml (Filipino) 2024-11-12 16:55:41 +01:00
Eugen Rochko
0981ef5d68 New translations strings.xml (Welsh) 2024-11-12 16:55:40 +01:00
Eugen Rochko
4ed29cde81 New translations strings.xml (Burmese) 2024-11-12 16:55:39 +01:00
Eugen Rochko
25c05dcc51 New translations strings.xml (Hindi) 2024-11-12 16:55:38 +01:00
Eugen Rochko
afab3a04ca New translations strings.xml (Croatian) 2024-11-12 16:55:36 +01:00
Eugen Rochko
8a6e00d2bb New translations strings.xml (Thai) 2024-11-12 16:55:35 +01:00
Eugen Rochko
678dc12595 New translations strings.xml (Bengali) 2024-11-12 16:55:34 +01:00
Eugen Rochko
7390550cd2 New translations strings.xml (Persian) 2024-11-12 16:55:32 +01:00
Eugen Rochko
e4d193eb6c New translations strings.xml (Indonesian) 2024-11-12 16:55:31 +01:00
Eugen Rochko
ebe19a4393 New translations strings.xml (Portuguese, Brazilian) 2024-11-12 16:55:30 +01:00
Eugen Rochko
fe0ae9a425 New translations strings.xml (Galician) 2024-11-12 16:55:29 +01:00
Eugen Rochko
9b82c8b066 New translations strings.xml (Vietnamese) 2024-11-12 16:55:27 +01:00
Eugen Rochko
0a1df389e7 New translations strings.xml (Chinese Traditional) 2024-11-12 16:55:26 +01:00
Eugen Rochko
1735a3ba9a New translations strings.xml (Chinese Simplified) 2024-11-12 16:55:25 +01:00
Eugen Rochko
7ba6d37ebf New translations strings.xml (Ukrainian) 2024-11-12 16:55:23 +01:00
Eugen Rochko
7d59ca27fc New translations strings.xml (Turkish) 2024-11-12 16:55:22 +01:00
Eugen Rochko
323ede05a9 New translations strings.xml (Swedish) 2024-11-12 16:55:21 +01:00
Eugen Rochko
2c2dcf3c28 New translations strings.xml (Slovenian) 2024-11-12 16:55:20 +01:00
Eugen Rochko
1f5b1ba3f9 New translations strings.xml (Slovak) 2024-11-12 16:55:18 +01:00
Eugen Rochko
ec81a53e88 New translations strings.xml (Russian) 2024-11-12 16:55:17 +01:00
Eugen Rochko
324ef1cce9 New translations strings.xml (Portuguese) 2024-11-12 16:55:16 +01:00
Eugen Rochko
f4cae9c51f New translations strings.xml (Polish) 2024-11-12 16:55:15 +01:00
Eugen Rochko
0e92626755 New translations strings.xml (Norwegian) 2024-11-12 16:55:13 +01:00
Eugen Rochko
0d6658ca72 New translations strings.xml (Dutch) 2024-11-12 16:55:12 +01:00
Eugen Rochko
ac812704f0 New translations strings.xml (Lithuanian) 2024-11-12 16:55:10 +01:00
Eugen Rochko
5dbcf68aca New translations strings.xml (Korean) 2024-11-12 16:55:09 +01:00
Eugen Rochko
6a2395a3c5 New translations strings.xml (Georgian) 2024-11-12 16:55:07 +01:00
Eugen Rochko
be020bd742 New translations strings.xml (Japanese) 2024-11-12 16:55:06 +01:00
Eugen Rochko
1e19fbe4f9 New translations strings.xml (Italian) 2024-11-12 16:55:05 +01:00
Eugen Rochko
b18fecf9d8 New translations strings.xml (Armenian) 2024-11-12 16:55:04 +01:00
Eugen Rochko
3cde5435c0 New translations strings.xml (Hungarian) 2024-11-12 16:55:02 +01:00
Eugen Rochko
b988a455e3 New translations strings.xml (Hebrew) 2024-11-12 16:55:01 +01:00
Eugen Rochko
8c3ebaf784 New translations strings.xml (Irish) 2024-11-12 16:55:00 +01:00
Eugen Rochko
a381d338ef New translations strings.xml (Finnish) 2024-11-12 16:54:59 +01:00
Eugen Rochko
c04733411a New translations strings.xml (Greek) 2024-11-12 16:54:57 +01:00
Eugen Rochko
9218cb728e New translations strings.xml (German) 2024-11-12 16:54:56 +01:00
Eugen Rochko
e00bb3f6e9 New translations strings.xml (Danish) 2024-11-12 16:54:54 +01:00
Eugen Rochko
6727ec5119 New translations strings.xml (Czech) 2024-11-12 16:54:53 +01:00
Eugen Rochko
4d7a6d9476 New translations strings.xml (Catalan) 2024-11-12 16:54:52 +01:00
Eugen Rochko
858cc34298 New translations strings.xml (Belarusian) 2024-11-12 16:54:51 +01:00
Eugen Rochko
5ac37290df New translations strings.xml (Arabic) 2024-11-12 16:54:50 +01:00
Eugen Rochko
efd9690d10 New translations strings.xml (Spanish) 2024-11-12 16:54:48 +01:00
Eugen Rochko
2985c5490b New translations strings.xml (French) 2024-11-12 16:54:47 +01:00
Eugen Rochko
f70101279d New translations strings.xml (Romanian) 2024-11-12 16:54:46 +01:00
Eugen Rochko
9fe52e7853 New translations strings.xml (Frisian) 2024-11-12 16:54:44 +01:00
Eugen Rochko
f8059b2810 New translations strings.xml (Icelandic) 2024-11-12 16:54:43 +01:00
Eugen Rochko
372add3cf8 New translations strings.xml (Basque) 2024-11-12 16:54:42 +01:00
Grishka
134bd13d60 Show followers/following number when blocking a server (AND-233) 2024-11-12 18:49:05 +03:00
Eugen Rochko
a53959d707 New translations strings.xml (Lithuanian) 2024-11-12 12:36:31 +01:00
Eugen Rochko
2a6d87a513 New translations strings.xml (Italian) 2024-11-12 12:36:30 +01:00
Eugen Rochko
b2a9ce998d New translations strings.xml (Italian) 2024-11-12 10:31:58 +01:00
Eugen Rochko
7c63943814 New translations strings.xml (Italian) 2024-11-12 01:19:56 +01:00
likeazir
4b5f84d781 remove stray newline 2024-11-11 17:18:15 +01:00
likeazir
5c75ada632 rudimentary pagination fix to keep emoji size under 1MB cursor limit 2024-11-11 17:05:12 +01:00
Eugen Rochko
964b84ec79 New translations full_description.txt (Icelandic) 2024-11-11 13:02:37 +01:00
Eugen Rochko
55db777906 New translations strings.xml (Chinese Traditional) 2024-11-11 10:19:36 +01:00
Eugen Rochko
16b8724045 New translations strings.xml (Icelandic) 2024-11-10 15:35:26 +01:00
Eugen Rochko
87c425b89c New translations strings.xml (Spanish) 2024-11-10 01:29:53 +01:00
Grishka
20ed47032e Add filters to search (AND-106) 2024-11-08 20:13:31 +03:00
Grishka
91d65b4e27 Fix search view state restoration & animation 2024-11-08 19:53:30 +03:00
Eugen Rochko
5a0ed9b7c1 New translations strings.xml (Italian) 2024-11-07 20:36:16 +01:00
Eugen Rochko
b62963c4f3 New translations strings.xml (Italian) 2024-11-07 19:31:50 +01:00
Eugen Rochko
f0a00f6919 New translations strings.xml (Swedish) 2024-11-07 13:25:16 +01:00
Grishka
8ea049c956 Fix refresh 2024-11-07 14:04:40 +03:00
Grishka
ff5e281db7 Minor fixes in profiles 2024-11-07 14:02:13 +03:00
Eugen Rochko
65189753eb New translations strings.xml (Interlingua) 2024-11-07 11:53:47 +01:00
Eugen Rochko
0944682bef New translations strings.xml (Kabyle) 2024-11-07 11:53:46 +01:00
Eugen Rochko
aa41b9bef2 New translations strings.xml (Scottish Gaelic) 2024-11-07 11:53:43 +01:00
Eugen Rochko
668286215b New translations strings.xml (Welsh) 2024-11-07 11:53:40 +01:00
Eugen Rochko
bf1d709892 New translations strings.xml (Thai) 2024-11-07 11:53:36 +01:00
Eugen Rochko
4ae49029d0 New translations strings.xml (Persian) 2024-11-07 11:53:35 +01:00
Eugen Rochko
90a1860a1c New translations strings.xml (Indonesian) 2024-11-07 11:53:27 +01:00
Eugen Rochko
f3e8a0c6c9 New translations strings.xml (Portuguese, Brazilian) 2024-11-07 11:53:25 +01:00
Eugen Rochko
661a54f006 New translations strings.xml (Vietnamese) 2024-11-07 11:53:23 +01:00
Eugen Rochko
9f9e3f8eba New translations strings.xml (Chinese Traditional) 2024-11-07 11:53:22 +01:00
Eugen Rochko
6a9fb76677 New translations strings.xml (Chinese Simplified) 2024-11-07 11:53:21 +01:00
Eugen Rochko
c81288e694 New translations strings.xml (Ukrainian) 2024-11-07 11:53:20 +01:00
Eugen Rochko
594f044068 New translations strings.xml (Turkish) 2024-11-07 11:53:19 +01:00
Eugen Rochko
147485633c New translations strings.xml (Swedish) 2024-11-07 11:53:17 +01:00
Eugen Rochko
4498a0f226 New translations strings.xml (Slovenian) 2024-11-07 11:53:16 +01:00
Eugen Rochko
1df56fc320 New translations strings.xml (Russian) 2024-11-07 11:53:14 +01:00
Eugen Rochko
1e2f47dd2a New translations strings.xml (Polish) 2024-11-07 11:53:12 +01:00
Eugen Rochko
26f5fbf3b8 New translations strings.xml (Norwegian) 2024-11-07 11:53:11 +01:00
Eugen Rochko
ecd2cecfa6 New translations strings.xml (Dutch) 2024-11-07 11:53:10 +01:00
Eugen Rochko
f6ecd25e1d New translations strings.xml (Lithuanian) 2024-11-07 11:53:09 +01:00
Eugen Rochko
1ac088f5a3 New translations strings.xml (Japanese) 2024-11-07 11:53:06 +01:00
Eugen Rochko
0030a06cd3 New translations strings.xml (Italian) 2024-11-07 11:53:05 +01:00
Eugen Rochko
6b7d1b0981 New translations strings.xml (Armenian) 2024-11-07 11:53:04 +01:00
Eugen Rochko
f1140a0197 New translations strings.xml (Hungarian) 2024-11-07 11:53:03 +01:00
Eugen Rochko
179534e931 New translations strings.xml (Greek) 2024-11-07 11:52:59 +01:00
Eugen Rochko
be2db34586 New translations strings.xml (German) 2024-11-07 11:52:58 +01:00
Eugen Rochko
50b81c3b98 New translations strings.xml (Danish) 2024-11-07 11:52:57 +01:00
Eugen Rochko
31ae0b6e08 New translations strings.xml (Czech) 2024-11-07 11:52:56 +01:00
Eugen Rochko
36fdecd22e New translations strings.xml (Belarusian) 2024-11-07 11:52:53 +01:00
Eugen Rochko
d33f1d59ca New translations strings.xml (Spanish) 2024-11-07 11:52:52 +01:00
Eugen Rochko
b6a978f376 New translations strings.xml (French) 2024-11-07 11:52:50 +01:00
Eugen Rochko
a1da9be14f New translations strings.xml (Frisian) 2024-11-07 11:52:48 +01:00
Eugen Rochko
692ebed8fb New translations strings.xml (Icelandic) 2024-11-07 11:52:47 +01:00
Eugen Rochko
000294d2fa New translations strings.xml (Basque) 2024-11-07 11:52:46 +01:00
Grishka
6406d25938 oops 2024-11-07 13:46:25 +03:00
Grishka
9c95d5f6e5 Make favorites and bookmarks a tab in profile (AND-135) 2024-11-07 13:24:40 +03:00
Eugen Rochko
c15ef07d53 New translations strings.xml (Russian) 2024-11-06 12:56:23 +01:00
Eugen Rochko
7182647574 New translations strings.xml (Icelandic) 2024-11-06 12:56:22 +01:00
Eugen Rochko
2a08b27667 New translations strings.xml (Swedish) 2024-11-06 10:06:02 +01:00
Grishka
56a2510564 Show mutual followers in profile (AND-187) 2024-11-06 11:56:04 +03:00
Grishka
ddaab49976 Support "new post" push notifications
fixes #910
2024-11-06 10:25:21 +03:00
Eugen Rochko
508ea32701 New translations strings.xml (Portuguese, Brazilian) 2024-11-05 22:46:34 +01:00
Grishka
1b17600835 Group settings by account (AND-170) 2024-11-05 23:05:33 +03:00
Grishka
c60d06950f Crash fixes 2024-11-05 20:45:49 +03:00
Eugen Rochko
bbdc3d4038 New translations strings.xml (Italian) 2024-11-05 17:49:45 +01:00
Eugen Rochko
5e2a292aeb New translations strings.xml (Thai) 2024-11-03 20:20:52 +01:00
Eugen Rochko
1f6c48edf8 New translations strings.xml (Thai) 2024-11-03 19:12:56 +01:00
Eugen Rochko
a72acf7d32 New translations strings.xml (German) 2024-11-03 12:51:41 +01:00
Grishka
162bc86ebe Merge branch 'l10n_master' 2024-11-02 11:20:36 +03:00
Grishka
af8f042f10 Add changelog 2024-11-02 11:20:18 +03:00
Eugen Rochko
7aae8b03af New translations strings.xml (Vietnamese) 2024-11-01 15:38:16 +01:00
Grishka
b28c095226 Fix dynamic colors setting 2024-11-01 10:45:41 +03:00
Eugen Rochko
9432fc9b8c New translations strings.xml (Chinese Traditional) 2024-11-01 03:09:45 +01:00
Eugen Rochko
37df47c7cd New translations strings.xml (Russian) 2024-10-31 13:22:11 +01:00
Grishka
62602839db Merge branch 'l10n_master' 2024-10-31 15:13:13 +03:00
Eugen Rochko
52c56db1ce New translations strings.xml (Russian) 2024-10-31 10:26:24 +01:00
Eugen Rochko
cdc3b37ee4 New translations strings.xml (Swedish) 2024-10-31 09:15:57 +01:00
Grishka
46bd36b65d And more colors 2024-10-31 10:22:55 +03:00
Grishka
70eb5bf68c Fix more colors 2024-10-31 10:18:34 +03:00
Grishka
c2f6b16aff Support grouping follow notifications (AND-230) 2024-10-31 10:09:42 +03:00
Grishka
603c058ec9 Allow disabling dynamic colors on Android 12+ (AND-143) 2024-10-31 10:05:25 +03:00
Grishka
3582d7bdad Fix colors in high-contrast modes 2024-10-31 09:24:26 +03:00
Grishka
d988e1aecf Assorted minor fixes 2024-10-31 08:55:28 +03:00
Grishka
7bf322d48a Crash fix 2024-10-31 08:28:14 +03:00
Eugen Rochko
5c4a450ef0 New translations strings.xml (Belarusian) 2024-10-30 20:51:38 +01:00
Eugen Rochko
c370fab1b4 New translations strings.xml (Belarusian) 2024-10-30 19:55:52 +01:00
Eugen Rochko
c79cba96ec New translations strings.xml (Chinese Traditional) 2024-10-30 17:05:32 +01:00
Eugen Rochko
ca4aed3dc2 New translations full_description.txt (Scottish Gaelic) 2024-10-30 13:33:29 +01:00
Eugen Rochko
c20237d32c New translations strings.xml (Scottish Gaelic) 2024-10-30 13:33:28 +01:00
Grishka
6082a0bcd8 Provide URLs for the link button thing in system app switcher
closes #632
2024-10-30 14:27:44 +03:00
Grishka
3de494f9e9 Avatar cropping (AND-203)
closes #76
2024-10-30 13:39:06 +03:00
Grishka
1b6c299251 Fix custom emoji keyboard 2024-10-30 09:20:35 +03:00
Grishka
01ae5b915d oops I forgot the copy link thing 2024-10-30 05:55:04 +03:00
Eugen Rochko
d0ca465194 New translations strings.xml (Spanish) 2024-10-29 18:25:05 +01:00
Eugen Rochko
07564f2964 New translations strings.xml (Italian) 2024-10-29 11:13:11 +01:00
Grishka
eb45b59cac Fix gap loading for real this time 2024-10-29 05:48:48 +03:00
Eugen Rochko
6e4c4c86f6 New translations strings.xml (Chinese Traditional) 2024-10-28 19:07:33 +01:00
Eugen Rochko
b52dd603a1 New translations full_description.txt (Icelandic) 2024-10-28 14:51:47 +01:00
Eugen Rochko
573e13f39f New translations strings.xml (Icelandic) 2024-10-28 14:51:45 +01:00
Grishka
5848dc0e67 oops 2024-10-28 11:53:42 +03:00
Eugen Rochko
7ae5546113 New translations strings.xml (Interlingua) 2024-10-28 09:41:58 +01:00
Eugen Rochko
a126a078b4 New translations strings.xml (Kabyle) 2024-10-28 09:41:56 +01:00
Eugen Rochko
808dab6f50 New translations strings.xml (Occitan) 2024-10-28 09:41:54 +01:00
Eugen Rochko
70dc5aece0 New translations strings.xml (Scottish Gaelic) 2024-10-28 09:41:53 +01:00
Eugen Rochko
80f76d0f05 New translations strings.xml (Sinhala) 2024-10-28 09:41:51 +01:00
Eugen Rochko
d2a96af886 New translations strings.xml (Bosnian) 2024-10-28 09:41:50 +01:00
Eugen Rochko
6b6e720ca5 New translations strings.xml (Filipino) 2024-10-28 09:41:49 +01:00
Eugen Rochko
9eacb7b067 New translations strings.xml (Welsh) 2024-10-28 09:41:48 +01:00
Eugen Rochko
36cce87ffc New translations strings.xml (Burmese) 2024-10-28 09:41:46 +01:00
Eugen Rochko
b028c3ad38 New translations strings.xml (Hindi) 2024-10-28 09:41:45 +01:00
Eugen Rochko
eada060f57 New translations strings.xml (Croatian) 2024-10-28 09:41:44 +01:00
Eugen Rochko
53cfbcb5b0 New translations strings.xml (Thai) 2024-10-28 09:41:43 +01:00
Eugen Rochko
23c624f575 New translations strings.xml (Bengali) 2024-10-28 09:41:42 +01:00
Eugen Rochko
669f3a50c8 New translations strings.xml (Persian) 2024-10-28 09:41:41 +01:00
Eugen Rochko
fa3f4f6eda New translations strings.xml (Indonesian) 2024-10-28 09:41:40 +01:00
Eugen Rochko
d97ffc32aa New translations strings.xml (Portuguese, Brazilian) 2024-10-28 09:41:38 +01:00
Eugen Rochko
1aecf4021f New translations strings.xml (Galician) 2024-10-28 09:41:37 +01:00
Eugen Rochko
f3d76a26f7 New translations strings.xml (Vietnamese) 2024-10-28 09:41:35 +01:00
Eugen Rochko
e5dc62db6f New translations strings.xml (Chinese Traditional) 2024-10-28 09:41:34 +01:00
Eugen Rochko
6008368045 New translations strings.xml (Chinese Simplified) 2024-10-28 09:41:33 +01:00
Eugen Rochko
83af61a758 New translations strings.xml (Ukrainian) 2024-10-28 09:41:31 +01:00
Eugen Rochko
eefcc34277 New translations strings.xml (Turkish) 2024-10-28 09:41:30 +01:00
Eugen Rochko
4a65976eea New translations strings.xml (Swedish) 2024-10-28 09:41:29 +01:00
Eugen Rochko
cb8aea258a New translations strings.xml (Slovenian) 2024-10-28 09:41:28 +01:00
Eugen Rochko
e5297b023d New translations strings.xml (Slovak) 2024-10-28 09:41:26 +01:00
Eugen Rochko
21c6f41013 New translations strings.xml (Russian) 2024-10-28 09:41:25 +01:00
Eugen Rochko
19f8d908c7 New translations strings.xml (Portuguese) 2024-10-28 09:41:24 +01:00
Eugen Rochko
fae7f73f7a New translations strings.xml (Polish) 2024-10-28 09:41:22 +01:00
Eugen Rochko
41da8cc1d7 New translations strings.xml (Norwegian) 2024-10-28 09:41:21 +01:00
Eugen Rochko
39d5a32494 New translations strings.xml (Dutch) 2024-10-28 09:41:20 +01:00
Eugen Rochko
c885b5a85e New translations strings.xml (Lithuanian) 2024-10-28 09:41:19 +01:00
Eugen Rochko
76f2b63171 New translations strings.xml (Korean) 2024-10-28 09:41:17 +01:00
Eugen Rochko
58e35d8da3 New translations strings.xml (Georgian) 2024-10-28 09:41:16 +01:00
Eugen Rochko
a5e03357df New translations strings.xml (Japanese) 2024-10-28 09:41:15 +01:00
Eugen Rochko
5a7ab6be70 New translations strings.xml (Italian) 2024-10-28 09:41:14 +01:00
Eugen Rochko
e977b46392 New translations strings.xml (Armenian) 2024-10-28 09:41:13 +01:00
Eugen Rochko
66de2f4b87 New translations strings.xml (Hungarian) 2024-10-28 09:41:11 +01:00
Eugen Rochko
1a48277cf2 New translations strings.xml (Finnish) 2024-10-28 09:41:08 +01:00
Eugen Rochko
6a55bd2248 New translations strings.xml (Greek) 2024-10-28 09:41:07 +01:00
Eugen Rochko
3a82395428 New translations strings.xml (German) 2024-10-28 09:41:06 +01:00
Eugen Rochko
a98b93feec New translations strings.xml (Danish) 2024-10-28 09:41:04 +01:00
Eugen Rochko
0cff2658f3 New translations strings.xml (Czech) 2024-10-28 09:41:03 +01:00
Eugen Rochko
d3d95d49de New translations strings.xml (Catalan) 2024-10-28 09:41:02 +01:00
Eugen Rochko
71ca9b6f3d New translations strings.xml (Belarusian) 2024-10-28 09:41:01 +01:00
Eugen Rochko
bf9c3d4d8d New translations strings.xml (Arabic) 2024-10-28 09:40:59 +01:00
Eugen Rochko
4b304629b7 New translations strings.xml (Spanish) 2024-10-28 09:40:58 +01:00
Eugen Rochko
c4314f100e New translations strings.xml (French) 2024-10-28 09:40:57 +01:00
Eugen Rochko
14c250446c New translations strings.xml (Frisian) 2024-10-28 09:40:55 +01:00
Eugen Rochko
8bc1c8e79c New translations strings.xml (Icelandic) 2024-10-28 09:40:54 +01:00
Eugen Rochko
5e6781817d New translations strings.xml (Basque) 2024-10-28 09:40:53 +01:00
Grishka
5b5e4fbbd2 Remove unused resources 2024-10-28 11:30:48 +03:00
Grishka
3dcc6d0013 Media viewer redesign (AND-196) 2024-10-28 11:26:40 +03:00
Grishka
2ad50cd972 Allow returning to previous scroll position in home timeline (AND-189) 2024-10-27 05:38:15 +03:00
Grishka
ced5fe4ee0 Revert "fix"
This reverts commit db4afd4c8f.
2024-10-27 04:33:48 +03:00
Grishka
db4afd4c8f fix 2024-10-26 07:21:45 +03:00
Grishka
a0d3bd83f2 Fix possible post duplicates in home timeline 2024-10-26 07:11:31 +03:00
Grishka
2092a6b8fe Fix new posts button dismiss gesture 2024-10-26 06:47:43 +03:00
Grishka
ad04433944 Load timeline gaps depending on scroll direction (AND-190)
Closes #70, closes #154, closes #147, closes #281
2024-10-26 06:45:01 +03:00
Grishka
57da77b642 Disable overscrolling in account switcher sheet (AND-207) 2024-10-25 05:17:40 +03:00
Grishka
f80e3771d1 Make toolbar scroll away in home timeline (AND-208) 2024-10-25 05:10:07 +03:00
Grishka
6a18d8ef03 Replace characters Android doesn't like (AND-193), fix #890 2024-10-25 04:37:11 +03:00
Grishka
a312018441 Open old file picker by long-pressing "add media" button (AND-194) 2024-10-25 04:31:54 +03:00
Grishka
3026bd5c51 Support for Android 15's color contrast setting WIP 2024-10-25 02:40:33 +03:00
Eugen Rochko
5afde48052 New translations strings.xml (Icelandic) 2024-10-24 17:29:09 +02:00
Grishka
14209dc785 Assorted crash fixes 2024-10-24 02:31:05 +03:00
Grishka
f1b30f251c Improve rendering of block elements in rich text
fixes #907
2024-10-24 01:37:28 +03:00
Eugen Rochko
6a849d654f New translations strings.xml (Frisian) 2024-10-19 16:50:00 +02:00
Eugen Rochko
2f9c5fe210 New translations strings.xml (Basque) 2024-10-18 00:48:18 +02:00
Eugen Rochko
43f096420f New translations strings.xml (Icelandic) 2024-10-16 12:12:44 +02:00
Eugen Rochko
5b848ed1ca New translations strings.xml (Russian) 2024-10-13 10:05:10 +02:00
Eugen Rochko
9e1cf330d7 New translations strings.xml (Greek) 2024-10-12 09:39:28 +02:00
Grishka
1ad2d08e27 Merge branch 'l10n_master' 2024-10-10 04:09:47 +03:00
Grishka
42658add38 Fix opening links in non-browser apps 2024-10-10 04:09:20 +03:00
Grishka
b211789847 Fix colors for quotes and code blocks 2024-10-10 01:29:21 +03:00
Eugen Rochko
9c88183366 New translations strings.xml (Russian) 2024-10-09 20:16:06 +02:00
Eugen Rochko
c76dba3a8c New translations strings.xml (Basque) 2024-10-09 18:07:53 +02:00
Eugen Rochko
29bee87f2a New translations strings.xml (Basque) 2024-10-09 16:54:04 +02:00
Grishka
c139f85b99 Fix wrong unread notification count for some accounts on 4.3 2024-10-09 06:09:53 +03:00
Grishka
3247d4f2f5 Fix notifications loading on pre-4.3 servers
fixes #897
2024-10-09 05:28:01 +03:00
Grishka
77b2f98f17 Quotes in text formatting (AND-222) 2024-10-09 05:20:58 +03:00
Grishka
82c6c8076a Lists in text formatting (AND-221) 2024-10-09 03:00:37 +03:00
Grishka
4177faa553 Monospace text formatting (AND-223) 2024-10-09 01:21:50 +03:00
Eugen Rochko
92ec125661 New translations strings.xml (Indonesian) 2024-10-08 12:59:00 +02:00
Grishka
513a57663b Display bold, italic, and strikethrough formatting (AND-220, AND-224) 2024-10-07 18:48:06 +03:00
Eugen Rochko
20e7f716f1 New translations strings.xml (Chinese Simplified) 2024-10-07 08:00:50 +02:00
Grishka
71f92cb66c Bump version 2024-10-06 01:25:21 +03:00
Grishka
77b2abd0cb Merge branch 'l10n_master' 2024-10-06 01:24:29 +03:00
Grishka
15385dd924 Make fastlane create draft releases 2024-10-06 01:22:25 +03:00
Grishka
08847ec641 Always reset notifications marker on "mark as read"
fixes #897
2024-10-06 01:11:21 +03:00
Grishka
805fc5d8c7 Crash fix 2024-10-06 01:06:45 +03:00
Eugen Rochko
3d7a95d336 New translations strings.xml (German) 2024-10-05 09:01:18 +02:00
Eugen Rochko
c1869386ff New translations strings.xml (German) 2024-10-05 07:34:09 +02:00
Eugen Rochko
7a728c52cf New translations strings.xml (German) 2024-10-04 21:23:57 +02:00
Eugen Rochko
22f3aad538 New translations strings.xml (Frisian) 2024-10-04 18:40:52 +02:00
Eugen Rochko
42da6dcf48 New translations strings.xml (Dutch) 2024-10-04 18:40:50 +02:00
Eugen Rochko
c0f18b1f61 New translations strings.xml (Frisian) 2024-10-04 17:34:12 +02:00
Grishka
492d851d89 Merge branch 'l10n_master' 2024-10-04 00:26:56 +03:00
Grishka
853f9dc8e4 Assorted crash fixes
fixes #896
2024-10-04 00:26:23 +03:00
Eugen Rochko
0e2ae4d3c0 New translations strings.xml (Italian) 2024-10-03 23:12:01 +02:00
Eugen Rochko
3ea1412faa New translations full_description.txt (Persian) 2024-10-02 21:38:56 +02:00
Eugen Rochko
e83f0749ee New translations full_description.txt (Persian) 2024-10-02 19:59:42 +02:00
Eugen Rochko
343d958677 New translations strings.xml (Persian) 2024-10-02 19:59:41 +02:00
Eugen Rochko
2f32378978 New translations strings.xml (Dutch) 2024-10-02 19:59:40 +02:00
Eugen Rochko
cfc5683f75 New translations strings.xml (Dutch) 2024-10-02 18:41:01 +02:00
Eugen Rochko
823e2f6ac6 New translations short_description.txt (Turkish) 2024-10-02 15:19:46 +02:00
Eugen Rochko
5f5fcdde46 New translations full_description.txt (Turkish) 2024-10-02 15:19:45 +02:00
Eugen Rochko
cf74e252ce New translations strings.xml (Turkish) 2024-10-02 15:19:44 +02:00
Eugen Rochko
010ca587d2 New translations strings.xml (Turkish) 2024-10-02 13:21:57 +02:00
Eugen Rochko
2957ac813f New translations full_description.txt (Turkish) 2024-10-02 12:08:45 +02:00
Eugen Rochko
5aa117e2e3 New translations strings.xml (Turkish) 2024-10-02 12:08:44 +02:00
Eugen Rochko
f1df4e72d2 New translations strings.xml (Turkish) 2024-10-02 09:45:42 +02:00
Eugen Rochko
afee257211 New translations strings.xml (Turkish) 2024-10-02 08:32:27 +02:00
Grishka
74986a10f6 Fix crash. This is getting annoying. 2024-10-01 23:48:35 +03:00
Grishka
77b6344032 Fix account data migration for real 2024-10-01 23:19:48 +03:00
Grishka
cb0d7e73d4 Fix proguard shit again 2024-10-01 21:50:38 +03:00
Grishka
1b714e5815 Prepare a new release 2024-10-01 21:12:59 +03:00
Grishka
e5dd97f992 Merge branch 'l10n_master' 2024-10-01 21:11:45 +03:00
Grishka
2a7838f2a7 Crash fix 2024-10-01 20:46:11 +03:00
Grishka
25112c1fe3 Refresh FCM token every 30 days
Fixes #892
2024-10-01 20:36:25 +03:00
Grishka
e7850dfcfb Fix Token field requirements 2024-10-01 20:29:09 +03:00
Eugen Rochko
bbdece33ea New translations strings.xml (Lithuanian) 2024-10-01 18:11:26 +02:00
Eugen Rochko
f861eefe78 New translations strings.xml (Lithuanian) 2024-10-01 16:41:14 +02:00
Eugen Rochko
07e4c6d0a9 New translations full_description.txt (Lithuanian) 2024-10-01 14:16:23 +02:00
Eugen Rochko
fd1afc8c02 New translations strings.xml (Lithuanian) 2024-10-01 14:16:22 +02:00
Eugen Rochko
a008c025f5 New translations strings.xml (Lithuanian) 2024-10-01 12:53:42 +02:00
Eugen Rochko
c4768e7f87 New translations strings.xml (Spanish) 2024-10-01 00:25:26 +02:00
Eugen Rochko
812ea1023e New translations strings.xml (Spanish) 2024-09-30 22:10:15 +02:00
Eugen Rochko
e74e45f315 New translations strings.xml (Spanish) 2024-09-29 22:05:21 +02:00
Eugen Rochko
b590b23439 New translations strings.xml (Spanish) 2024-09-29 21:02:44 +02:00
Eugen Rochko
a8208b4c05 New translations strings.xml (Thai) 2024-09-28 22:35:55 +02:00
Eugen Rochko
c742b695e3 New translations full_description.txt (Japanese) 2024-09-28 20:36:01 +02:00
Eugen Rochko
9a8ff82f2d New translations strings.xml (Thai) 2024-09-28 20:36:00 +02:00
Eugen Rochko
3b2f495a50 New translations full_description.txt (Japanese) 2024-09-28 19:27:45 +02:00
Eugen Rochko
773d5a104b New translations strings.xml (Basque) 2024-09-27 04:13:48 +02:00
Eugen Rochko
c4ac325c14 New translations strings.xml (Basque) 2024-09-27 03:10:49 +02:00
Eugen Rochko
8964f22c0c New translations strings.xml (Basque) 2024-09-26 21:58:40 +02:00
Eugen Rochko
6b99cc48a4 New translations full_description.txt (Persian) 2024-09-26 20:42:25 +02:00
Eugen Rochko
448949d5c2 New translations full_description.txt (Persian) 2024-09-26 19:32:09 +02:00
Eugen Rochko
e94f508680 New translations full_description.txt (Persian) 2024-09-25 21:38:40 +02:00
Eugen Rochko
a3472d0e1c New translations full_description.txt (Persian) 2024-09-25 19:14:57 +02:00
Eugen Rochko
3ebb058a62 New translations strings.xml (Persian) 2024-09-25 19:14:56 +02:00
Eugen Rochko
32fd14b3ea New translations short_description.txt (Persian) 2024-09-25 17:17:32 +02:00
Eugen Rochko
49c1f14a20 New translations full_description.txt (Persian) 2024-09-25 17:17:31 +02:00
Eugen Rochko
7dea64676e New translations strings.xml (Persian) 2024-09-25 17:17:30 +02:00
Eugen Rochko
a77142a9ee New translations strings.xml (Slovak) 2024-09-25 13:32:51 +02:00
Eugen Rochko
898ef9a560 New translations strings.xml (Japanese) 2024-09-24 17:38:33 +02:00
Eugen Rochko
47539ac47e New translations strings.xml (Japanese) 2024-09-24 15:54:13 +02:00
Eugen Rochko
5aac227ad6 New translations strings.xml (Chinese Traditional) 2024-09-23 15:22:48 +02:00
Eugen Rochko
38c8990f4e New translations strings.xml (Italian) 2024-09-23 15:22:47 +02:00
Eugen Rochko
797a967cb1 New translations strings.xml (Italian) 2024-09-23 12:18:00 +02:00
Eugen Rochko
0d7a5e55fe New translations strings.xml (Spanish) 2024-09-22 15:14:22 +02:00
Eugen Rochko
87de48c957 New translations strings.xml (Vietnamese) 2024-09-22 12:25:22 +02:00
Eugen Rochko
3489d8d5b3 New translations strings.xml (Vietnamese) 2024-09-22 11:28:35 +02:00
Eugen Rochko
aa6c3c56bb New translations strings.xml (Portuguese, Brazilian) 2024-09-21 11:48:22 +02:00
Eugen Rochko
3fdf3dcdab New translations strings.xml (Ukrainian) 2024-09-21 11:48:21 +02:00
Eugen Rochko
2f716d63de New translations strings.xml (Interlingua) 2024-09-21 10:23:51 +02:00
Eugen Rochko
bbe71fff51 New translations strings.xml (Urdu (India)) 2024-09-21 10:23:49 +02:00
Eugen Rochko
74ae04e0d2 New translations strings.xml (Kabyle) 2024-09-21 10:23:49 +02:00
Eugen Rochko
bb293ec319 New translations strings.xml (Igbo) 2024-09-21 10:23:48 +02:00
Eugen Rochko
bf6f8a7137 New translations strings.xml (Occitan) 2024-09-21 10:23:47 +02:00
Eugen Rochko
8080986e5b New translations strings.xml (Scottish Gaelic) 2024-09-21 10:23:46 +02:00
Eugen Rochko
5ed24a227e New translations strings.xml (Sinhala) 2024-09-21 10:23:44 +02:00
Eugen Rochko
2d89b4b539 New translations strings.xml (Bosnian) 2024-09-21 10:23:43 +02:00
Eugen Rochko
378a675157 New translations strings.xml (Filipino) 2024-09-21 10:23:42 +02:00
Eugen Rochko
127662967a New translations strings.xml (Burmese) 2024-09-21 10:23:41 +02:00
Eugen Rochko
76b52dc06e New translations strings.xml (Hindi) 2024-09-21 10:23:40 +02:00
Eugen Rochko
55d3b127e7 New translations strings.xml (Croatian) 2024-09-21 10:23:39 +02:00
Eugen Rochko
0f9e0fe140 New translations strings.xml (Thai) 2024-09-21 10:23:38 +02:00
Eugen Rochko
2b462f7289 New translations strings.xml (Bengali) 2024-09-21 10:23:37 +02:00
Eugen Rochko
977ca60655 New translations strings.xml (Persian) 2024-09-21 10:23:36 +02:00
Eugen Rochko
309b8089b8 New translations strings.xml (Indonesian) 2024-09-21 10:23:35 +02:00
Eugen Rochko
0563068ec3 New translations strings.xml (Portuguese, Brazilian) 2024-09-21 10:23:34 +02:00
Eugen Rochko
e33282d686 New translations strings.xml (Icelandic) 2024-09-21 10:23:33 +02:00
Eugen Rochko
0b83b9b5e0 New translations strings.xml (Galician) 2024-09-21 10:23:31 +02:00
Eugen Rochko
8abb4d25f9 New translations strings.xml (Chinese Traditional) 2024-09-21 10:23:30 +02:00
Eugen Rochko
aadf404142 New translations strings.xml (Chinese Simplified) 2024-09-21 10:23:29 +02:00
Eugen Rochko
8a608a1e71 New translations strings.xml (Turkish) 2024-09-21 10:23:28 +02:00
Eugen Rochko
5571220b20 New translations strings.xml (Swedish) 2024-09-21 10:23:27 +02:00
Eugen Rochko
05c831a8b1 New translations strings.xml (Slovenian) 2024-09-21 10:23:25 +02:00
Eugen Rochko
c237b36f8b New translations strings.xml (Slovak) 2024-09-21 10:23:24 +02:00
Eugen Rochko
2ed7fdecb4 New translations strings.xml (Russian) 2024-09-21 10:23:23 +02:00
Eugen Rochko
1dce9f1928 New translations strings.xml (Portuguese) 2024-09-21 10:23:22 +02:00
Eugen Rochko
4a6a707bbc New translations strings.xml (Polish) 2024-09-21 10:23:21 +02:00
Eugen Rochko
989095a13d New translations strings.xml (Norwegian) 2024-09-21 10:23:20 +02:00
Eugen Rochko
34556b0c45 New translations strings.xml (Korean) 2024-09-21 10:23:19 +02:00
Eugen Rochko
7aa4926ac3 New translations strings.xml (Georgian) 2024-09-21 10:23:18 +02:00
Eugen Rochko
189cee1395 New translations strings.xml (Italian) 2024-09-21 10:23:16 +02:00
Eugen Rochko
5a7b18983d New translations strings.xml (Armenian) 2024-09-21 10:23:15 +02:00
Eugen Rochko
72f01267ae New translations strings.xml (Hungarian) 2024-09-21 10:23:14 +02:00
Eugen Rochko
e5df4a3886 New translations strings.xml (Hebrew) 2024-09-21 10:23:13 +02:00
Eugen Rochko
3827047f05 New translations strings.xml (Irish) 2024-09-21 10:23:12 +02:00
Eugen Rochko
0f262c41ec New translations strings.xml (Finnish) 2024-09-21 10:23:11 +02:00
Eugen Rochko
2d2d405c73 New translations strings.xml (Basque) 2024-09-21 10:23:10 +02:00
Eugen Rochko
9b82c9bbe5 New translations strings.xml (Greek) 2024-09-21 10:23:09 +02:00
Eugen Rochko
8573276a79 New translations strings.xml (German) 2024-09-21 10:23:08 +02:00
Eugen Rochko
9f5f0d594c New translations strings.xml (Danish) 2024-09-21 10:23:07 +02:00
Eugen Rochko
a7bfaf732f New translations strings.xml (Czech) 2024-09-21 10:23:06 +02:00
Eugen Rochko
9b0af7d60d New translations strings.xml (Catalan) 2024-09-21 10:23:04 +02:00
Eugen Rochko
1dd937f047 New translations strings.xml (Belarusian) 2024-09-21 10:23:03 +02:00
Eugen Rochko
e135b61dd0 New translations strings.xml (Arabic) 2024-09-21 10:23:02 +02:00
Eugen Rochko
8c3687ca4a New translations strings.xml (Romanian) 2024-09-21 10:23:01 +02:00
Eugen Rochko
3ceea6a83b New translations strings.xml (Lithuanian) 2024-09-21 10:23:00 +02:00
Eugen Rochko
cc22ccfb66 New translations strings.xml (Frisian) 2024-09-21 10:22:59 +02:00
Eugen Rochko
d1c4ca38ff New translations strings.xml (Dutch) 2024-09-21 10:22:58 +02:00
Eugen Rochko
52fb35e5e9 New translations strings.xml (French) 2024-09-21 10:22:56 +02:00
Eugen Rochko
2b0faf0e23 New translations strings.xml (Ukrainian) 2024-09-21 10:22:55 +02:00
Eugen Rochko
bc74a72369 New translations strings.xml (Vietnamese) 2024-09-21 10:22:54 +02:00
Eugen Rochko
0c2e4e34fe New translations strings.xml (Spanish) 2024-09-21 10:22:53 +02:00
Eugen Rochko
67bd2e9629 New translations strings.xml (Japanese) 2024-09-21 10:22:52 +02:00
Eugen Rochko
d9dfd6e1bb New translations strings.xml (Welsh) 2024-09-21 10:22:51 +02:00
Grishka
1dc9adafc7 Add moderation_warning notifications (AND-175) 2024-09-21 11:09:36 +03:00
Grishka
9e201f3c00 fix 2024-09-21 10:43:24 +03:00
Grishka
16e2632d9b Add severed_relationships notifications (AND-174) 2024-09-21 10:40:04 +03:00
Grishka
9533b4f45d Fix layout for single-line notification header + inline status 2024-09-21 09:47:50 +03:00
Grishka
de83c1ea96 Add update notifications (AND-173) 2024-09-21 09:22:41 +03:00
Grishka
18b5c8ed50 Add "just posted" header for new status notifications 2024-09-21 07:41:45 +03:00
Eugen Rochko
14f7f5a1f4 New translations strings.xml (Chinese Traditional) 2024-09-21 06:18:56 +02:00
Eugen Rochko
4960589bef New translations strings.xml (Swedish) 2024-09-21 01:34:11 +02:00
Eugen Rochko
e2c7f8c7f0 New translations strings.xml (Italian) 2024-09-20 23:52:20 +02:00
Eugen Rochko
1371260fdf New translations strings.xml (German) 2024-09-20 15:47:30 +02:00
Eugen Rochko
01c816814a New translations strings.xml (Portuguese, Brazilian) 2024-09-20 13:06:18 +02:00
Eugen Rochko
bbb61e7a87 New translations strings.xml (Interlingua) 2024-09-20 11:13:12 +02:00
Eugen Rochko
30b24384d5 New translations strings.xml (Scottish Gaelic) 2024-09-20 11:13:08 +02:00
Eugen Rochko
506df71b37 New translations strings.xml (Hindi) 2024-09-20 11:13:04 +02:00
Eugen Rochko
bb323ab08f New translations strings.xml (Thai) 2024-09-20 11:13:02 +02:00
Eugen Rochko
6d8cb8cbb4 New translations strings.xml (Persian) 2024-09-20 11:13:00 +02:00
Eugen Rochko
90dc391b99 New translations strings.xml (Indonesian) 2024-09-20 11:12:58 +02:00
Eugen Rochko
8b974c2e96 New translations strings.xml (Portuguese, Brazilian) 2024-09-20 11:12:57 +02:00
Eugen Rochko
baa0f2ba33 New translations strings.xml (Icelandic) 2024-09-20 11:12:56 +02:00
Eugen Rochko
697fc713a0 New translations strings.xml (Galician) 2024-09-20 11:12:55 +02:00
Eugen Rochko
1704f8079f New translations strings.xml (Chinese Traditional) 2024-09-20 11:12:54 +02:00
Eugen Rochko
7c1fd70569 New translations strings.xml (Chinese Simplified) 2024-09-20 11:12:52 +02:00
Eugen Rochko
dd19d1408b New translations strings.xml (Turkish) 2024-09-20 11:12:51 +02:00
Eugen Rochko
f36bd866f0 New translations strings.xml (Swedish) 2024-09-20 11:12:50 +02:00
Eugen Rochko
6311920b4c New translations strings.xml (Slovenian) 2024-09-20 11:12:49 +02:00
Eugen Rochko
5cf20658bb New translations strings.xml (Slovak) 2024-09-20 11:12:48 +02:00
Eugen Rochko
c5c1015231 New translations strings.xml (Russian) 2024-09-20 11:12:46 +02:00
Eugen Rochko
2fb45b5384 New translations strings.xml (Portuguese) 2024-09-20 11:12:45 +02:00
Eugen Rochko
b6e1304df0 New translations strings.xml (Polish) 2024-09-20 11:12:43 +02:00
Eugen Rochko
f0ed251438 New translations strings.xml (Norwegian) 2024-09-20 11:12:42 +02:00
Eugen Rochko
0f2d218956 New translations strings.xml (Georgian) 2024-09-20 11:12:40 +02:00
Eugen Rochko
0756883af9 New translations strings.xml (Italian) 2024-09-20 11:12:39 +02:00
Eugen Rochko
d76612f0b9 New translations strings.xml (Armenian) 2024-09-20 11:12:38 +02:00
Eugen Rochko
3c85421474 New translations strings.xml (Hungarian) 2024-09-20 11:12:36 +02:00
Eugen Rochko
cba8050989 New translations strings.xml (Finnish) 2024-09-20 11:12:34 +02:00
Eugen Rochko
5060409597 New translations strings.xml (Basque) 2024-09-20 11:12:33 +02:00
Eugen Rochko
e80d711738 New translations strings.xml (Greek) 2024-09-20 11:12:31 +02:00
Eugen Rochko
903a5477a7 New translations strings.xml (German) 2024-09-20 11:12:30 +02:00
Eugen Rochko
b7940df425 New translations strings.xml (Danish) 2024-09-20 11:12:29 +02:00
Eugen Rochko
6b2685618d New translations strings.xml (Czech) 2024-09-20 11:12:27 +02:00
Eugen Rochko
40c52c7df7 New translations strings.xml (Catalan) 2024-09-20 11:12:25 +02:00
Eugen Rochko
5b460fec39 New translations strings.xml (Belarusian) 2024-09-20 11:12:24 +02:00
Eugen Rochko
f0085cb240 New translations strings.xml (Arabic) 2024-09-20 11:12:22 +02:00
Eugen Rochko
477dd0b219 New translations strings.xml (Lithuanian) 2024-09-20 11:12:20 +02:00
Eugen Rochko
9b585e5955 New translations strings.xml (Frisian) 2024-09-20 11:12:19 +02:00
Eugen Rochko
9e7e540068 New translations strings.xml (Dutch) 2024-09-20 11:12:18 +02:00
Eugen Rochko
c1e5daec3d New translations strings.xml (French) 2024-09-20 11:12:16 +02:00
Eugen Rochko
ccc2c9d6d3 New translations strings.xml (Ukrainian) 2024-09-20 11:12:15 +02:00
Eugen Rochko
fbcad38d71 New translations strings.xml (Vietnamese) 2024-09-20 11:12:14 +02:00
Eugen Rochko
d487b3c114 New translations strings.xml (Spanish) 2024-09-20 11:12:13 +02:00
Eugen Rochko
62a5e36eec New translations strings.xml (Japanese) 2024-09-20 11:12:11 +02:00
Grishka
c6130e4d29 Add a debug option to force re-register FCM 2024-09-20 11:46:31 +03:00
Grishka
26f7a75628 Grouped notifications 2024-09-20 11:39:50 +03:00
Eugen Rochko
4110eb064a New translations short_description.txt (Russian) 2024-09-20 05:35:32 +02:00
Eugen Rochko
3e8c4a8443 New translations full_description.txt (Russian) 2024-09-20 05:35:31 +02:00
Eugen Rochko
b8fc674fe8 New translations strings.xml (Russian) 2024-09-20 05:35:30 +02:00
Eugen Rochko
cb123a178a New translations strings.xml (Russian) 2024-09-20 03:26:46 +02:00
Grishka
80323f8236 Add support for /api/v2/instance 2024-09-19 02:37:19 +03:00
Grishka
edc03642cc Fix corner rounding 2024-09-19 01:50:35 +03:00
Grishka
114c77a665 Inline statuses 2024-09-18 19:25:59 +03:00
Grishka
0627644019 Fix spoilers on retoots 2024-09-18 06:05:42 +03:00
Grishka
1874267b32 Fix paddings 2024-09-18 05:07:07 +03:00
Grishka
de763ab6f7 Reply lines 2024-09-18 04:59:12 +03:00
Grishka
ace0072118 Post redesign part 2 (AND-18) 2024-09-18 03:57:11 +03:00
Grishka
3820eee174 Post redesign part 1 2024-09-17 22:06:31 +03:00
Eugen Rochko
754cd807c0 New translations strings.xml (Icelandic) 2024-09-16 12:22:01 +02:00
Eugen Rochko
e3f4951f95 New translations full_description.txt (French) 2024-09-15 23:47:41 +02:00
Eugen Rochko
b321c4e479 New translations full_description.txt (French) 2024-09-15 22:09:51 +02:00
Grishka
192c634755 Migrate account and instance data to SQLite 2024-09-15 20:15:01 +03:00
Eugen Rochko
ef27200764 New translations strings.xml (Chinese Traditional) 2024-09-15 11:09:52 +02:00
Eugen Rochko
aa4932077b New translations strings.xml (Greek) 2024-09-15 10:14:33 +02:00
Eugen Rochko
6c87c4aa7c New translations strings.xml (Thai) 2024-09-15 09:09:37 +02:00
Eugen Rochko
59e6f6033a New translations strings.xml (Basque) 2024-09-14 19:50:14 +02:00
Eugen Rochko
82a0d046a7 New translations strings.xml (Spanish) 2024-09-14 18:39:20 +02:00
Eugen Rochko
c98f1d32e8 New translations full_description.txt (Portuguese) 2024-09-13 18:19:28 +02:00
Eugen Rochko
34698a5aa2 New translations strings.xml (Vietnamese) 2024-09-13 16:52:11 +02:00
Grishka
ee1a0cc666 Allow "new posts" button to be swiped away 2024-09-13 01:21:23 +03:00
Grishka
eda1526830 Show an overlay on image attachments that failed to load 2024-09-13 00:47:04 +03:00
Grishka
f5961c8077 Update appkit image loader 2024-09-13 00:12:13 +03:00
Eugen Rochko
7f03bdae2b New translations strings.xml (Frisian) 2024-09-12 14:33:18 +02:00
Eugen Rochko
38ac8f14fb New translations strings.xml (Frisian) 2024-09-12 13:11:41 +02:00
Eugen Rochko
7d7bfad3c0 New translations strings.xml (Frisian) 2024-09-10 15:38:43 +02:00
Grishka
81ac72e4eb Trim strings in donation campaigns 2024-09-10 13:43:48 +03:00
Grishka
e915aab2fb Add tooltips to emoji keyboard (closes #884) 2024-09-10 13:37:28 +03:00
Grishka
a92c903ffb Fix #876 2024-09-10 13:32:07 +03:00
Grishka
e174a7efd7 Force intent to default browser when opening things in browser
fixes #881
2024-09-10 13:23:57 +03:00
Grishka
9f4575f349 Handle the case when a post with CW is also filtered 2024-09-10 13:08:58 +03:00
Gregory K
9cbfb1a7f8 Merge pull request #867 from adillari/profile_tab_margin_fix
Adjust margins on profile tab to center icons together
2024-09-08 09:11:23 +03:00
Eugen Rochko
6db20a2cc1 New translations full_description.txt (Lithuanian) 2024-09-07 18:37:14 +02:00
Eugen Rochko
fe067cba13 New translations strings.xml (Lithuanian) 2024-09-07 17:42:10 +02:00
Eugen Rochko
c0b5d34aae New translations strings.xml (Lithuanian) 2024-09-07 15:14:38 +02:00
Eugen Rochko
ee15655fc7 New translations strings.xml (Lithuanian) 2024-09-07 14:15:30 +02:00
Eugen Rochko
34a9cd0614 New translations strings.xml (Lithuanian) 2024-09-07 13:15:44 +02:00
Eugen Rochko
44b46bd83c New translations strings.xml (Welsh) 2024-09-06 18:07:09 +02:00
Eugen Rochko
8fbe1ef444 New translations strings.xml (Welsh) 2024-09-06 16:28:05 +02:00
Eugen Rochko
61e8615907 New translations strings.xml (French) 2024-09-05 03:31:00 +02:00
Eugen Rochko
4eef3a3243 New translations full_description.txt (Frisian) 2024-09-03 18:05:20 +02:00
Eugen Rochko
8585dc2cd0 New translations strings.xml (Frisian) 2024-09-03 18:05:19 +02:00
Eugen Rochko
3647fcaa0b New translations full_description.txt (Frisian) 2024-09-03 16:50:49 +02:00
Eugen Rochko
184a211819 New translations strings.xml (Frisian) 2024-09-02 16:22:10 +02:00
Eugen Rochko
7d36b67652 New translations short_description.txt (Frisian) 2024-09-02 14:44:25 +02:00
Eugen Rochko
7e0f786a43 New translations full_description.txt (Frisian) 2024-09-02 14:44:24 +02:00
Eugen Rochko
0a8cec46c0 New translations strings.xml (Frisian) 2024-09-02 14:44:23 +02:00
Eugen Rochko
3cd558c44f New translations strings.xml (Dutch) 2024-09-02 13:04:53 +02:00
Eugen Rochko
4076067b3e New translations short_description.txt (French) 2024-09-01 14:10:37 +02:00
Eugen Rochko
b3593a0e30 New translations full_description.txt (French) 2024-09-01 14:10:36 +02:00
Eugen Rochko
1daede2c62 New translations strings.xml (French) 2024-09-01 12:29:56 +02:00
Eugen Rochko
fefe529438 New translations strings.xml (Ukrainian) 2024-08-31 17:18:03 +02:00
Eugen Rochko
ba59aa6147 New translations strings.xml (Vietnamese) 2024-08-31 06:33:49 +02:00
Eugen Rochko
c42edb3b94 New translations strings.xml (Spanish) 2024-08-30 01:50:06 +02:00
Eugen Rochko
d647d9edd1 New translations full_description.txt (Japanese) 2024-08-29 12:52:24 +02:00
Eugen Rochko
ed2d03b5de New translations strings.xml (Japanese) 2024-08-29 12:52:23 +02:00
Eugen Rochko
5e1b39bfd7 New translations short_description.txt (Welsh) 2024-08-27 01:29:11 +02:00
Eugen Rochko
dc60f4bb51 New translations full_description.txt (Welsh) 2024-08-27 01:29:10 +02:00
Eugen Rochko
6a5c564b99 New translations strings.xml (Welsh) 2024-08-27 01:29:09 +02:00
Eugen Rochko
8745e5d034 New translations strings.xml (Welsh) 2024-08-27 00:28:37 +02:00
Eugen Rochko
448a12f2ed New translations short_description.txt (Welsh) 2024-08-26 00:50:10 +02:00
Eugen Rochko
e6cc15879e New translations full_description.txt (Welsh) 2024-08-26 00:50:09 +02:00
Eugen Rochko
76fd8686b3 New translations strings.xml (Welsh) 2024-08-26 00:50:08 +02:00
Eugen Rochko
2dbc282769 New translations full_description.txt (Thai) 2024-08-25 11:26:34 +02:00
Eugen Rochko
4fca4ca332 New translations full_description.txt (Thai) 2024-08-25 09:27:24 +02:00
Eugen Rochko
a18ed46151 New translations strings.xml (Lithuanian) 2024-08-22 18:11:54 +02:00
Eugen Rochko
07d9ee00ff New translations strings.xml (Lithuanian) 2024-08-22 16:50:59 +02:00
Eugen Rochko
5789c3af29 New translations strings.xml (Norwegian) 2024-08-22 00:45:26 +02:00
Eugen Rochko
d9a1ef059d New translations strings.xml (Norwegian) 2024-08-21 23:49:42 +02:00
Eugen Rochko
7148d95830 New translations strings.xml (Greek) 2024-08-19 13:26:10 +02:00
Eugen Rochko
9bfaa5db34 New translations short_description.txt (Indonesian) 2024-08-19 11:23:00 +02:00
Eugen Rochko
54a4d327ec New translations strings.xml (Indonesian) 2024-08-19 11:22:59 +02:00
Eugen Rochko
90bf1113f9 New translations full_description.txt (Indonesian) 2024-08-19 11:22:57 +02:00
Eugen Rochko
7cc23856f7 New translations full_description.txt (Indonesian) 2024-08-19 09:35:09 +02:00
Eugen Rochko
09158f7036 New translations strings.xml (Swedish) 2024-08-18 22:10:20 +02:00
Eugen Rochko
687880bbd5 New translations strings.xml (Slovenian) 2024-08-18 18:11:15 +02:00
Eugen Rochko
598f1fc3b3 New translations strings.xml (Danish) 2024-08-18 14:30:58 +02:00
Eugen Rochko
7989fa2853 New translations strings.xml (Lithuanian) 2024-08-18 12:30:21 +02:00
Eugen Rochko
f81f14fb5d New translations full_description.txt (Thai) 2024-08-18 10:11:25 +02:00
Eugen Rochko
4acf7a13f2 New translations short_description.txt (Thai) 2024-08-18 09:09:12 +02:00
Eugen Rochko
c004db7ac9 New translations full_description.txt (Thai) 2024-08-18 09:09:11 +02:00
Eugen Rochko
e39ac3ac18 New translations strings.xml (Danish) 2024-08-18 00:28:02 +02:00
Eugen Rochko
2011b08a2b New translations strings.xml (Danish) 2024-08-17 23:28:55 +02:00
Eugen Rochko
5afd82585e New translations strings.xml (Slovak) 2024-08-17 17:05:00 +02:00
Eugen Rochko
5daaf15195 New translations strings.xml (Slovak) 2024-08-17 15:50:17 +02:00
Eugen Rochko
d8c4247a2c New translations strings.xml (Lithuanian) 2024-08-17 12:10:13 +02:00
Eugen Rochko
627870098f New translations strings.xml (Danish) 2024-08-17 01:34:42 +02:00
Eugen Rochko
2a7a88466d New translations strings.xml (Danish) 2024-08-17 00:02:53 +02:00
Eugen Rochko
4d02d659e7 New translations strings.xml (Danish) 2024-08-16 22:43:38 +02:00
Eugen Rochko
86ab70757e New translations full_description.txt (Swedish) 2024-08-16 20:58:02 +02:00
Eugen Rochko
0161f71d63 New translations strings.xml (Swedish) 2024-08-16 20:58:01 +02:00
Eugen Rochko
7b5664cc8f New translations strings.xml (Slovak) 2024-08-16 20:58:00 +02:00
Eugen Rochko
6992ab4f02 New translations strings.xml (Slovak) 2024-08-16 12:35:19 +02:00
Eugen Rochko
88d8f7afc8 New translations strings.xml (Slovak) 2024-08-16 11:36:02 +02:00
Eugen Rochko
1a344f777d New translations strings.xml (Slovak) 2024-08-16 09:10:45 +02:00
Eugen Rochko
ff158c28cf New translations full_description.txt (Slovak) 2024-08-16 07:39:05 +02:00
Eugen Rochko
c39d2a8ec1 New translations strings.xml (Slovak) 2024-08-16 07:39:04 +02:00
Eugen Rochko
07d722dca3 New translations full_description.txt (Slovak) 2024-08-16 06:31:26 +02:00
Eugen Rochko
5db7a9df18 New translations strings.xml (Danish) 2024-08-16 04:11:10 +02:00
Eugen Rochko
ce475516ad New translations strings.xml (Danish) 2024-08-16 03:14:32 +02:00
Eugen Rochko
fb0f15b844 New translations short_description.txt (Danish) 2024-08-16 00:49:38 +02:00
Eugen Rochko
3ca9d7b792 New translations strings.xml (Danish) 2024-08-16 00:49:37 +02:00
Eugen Rochko
a8732fcd20 New translations full_description.txt (Danish) 2024-08-16 00:49:36 +02:00
Eugen Rochko
e7530993a8 New translations full_description.txt (Danish) 2024-08-15 23:54:18 +02:00
Eugen Rochko
fa319023f6 New translations strings.xml (Russian) 2024-08-15 22:48:23 +02:00
Eugen Rochko
84a2585c63 New translations strings.xml (Armenian) 2024-08-15 22:48:22 +02:00
Eugen Rochko
a18fc769bb New translations short_description.txt (Slovak) 2024-08-15 21:28:01 +02:00
Eugen Rochko
b3a4572f6d New translations full_description.txt (Slovak) 2024-08-15 21:28:00 +02:00
Eugen Rochko
12d0ce8ff0 New translations strings.xml (Slovak) 2024-08-15 21:27:59 +02:00
Eugen Rochko
d21ca7c203 New translations strings.xml (Armenian) 2024-08-15 21:27:58 +02:00
Eugen Rochko
075780fe3f New translations full_description.txt (Basque) 2024-08-12 14:08:21 +02:00
Eugen Rochko
223b506284 New translations strings.xml (Basque) 2024-08-12 14:08:20 +02:00
Eugen Rochko
a4dda389c7 New translations strings.xml (Czech) 2024-08-10 18:18:50 +02:00
Eugen Rochko
6e2eaf10fa New translations strings.xml (Thai) 2024-08-09 18:48:08 +02:00
Eugen Rochko
73230643fc New translations strings.xml (Chinese Simplified) 2024-08-07 08:01:50 +02:00
Eugen Rochko
e70cebe2b8 New translations strings.xml (Interlingua) 2024-08-07 00:09:10 +02:00
Eugen Rochko
84e4a750bf New translations strings.xml (Icelandic) 2024-08-05 18:39:51 +02:00
Eugen Rochko
df36166a9e New translations full_description.txt (German) 2024-08-05 04:45:19 +02:00
Eugen Rochko
bb7831483a New translations strings.xml (German) 2024-08-05 04:45:18 +02:00
Eugen Rochko
87f96e6259 New translations strings.xml (Lithuanian) 2024-08-04 19:49:21 +01:00
Eugen Rochko
0db409eb97 New translations strings.xml (Vietnamese) 2024-08-03 17:29:31 +01:00
Eugen Rochko
b4b1f2cade New translations short_description.txt (Portuguese, Brazilian) 2024-08-03 04:17:08 +01:00
Eugen Rochko
884aa8377a New translations full_description.txt (Portuguese, Brazilian) 2024-08-03 04:17:07 +01:00
Eugen Rochko
7f021bc958 New translations full_description.txt (Portuguese, Brazilian) 2024-08-03 02:30:19 +01:00
Eugen Rochko
f4bab5a12e New translations strings.xml (Portuguese, Brazilian) 2024-08-03 02:30:18 +01:00
Eugen Rochko
2bbe5da955 New translations full_description.txt (Belarusian) 2024-08-03 01:22:11 +01:00
Eugen Rochko
819478854d New translations strings.xml (Belarusian) 2024-08-03 00:03:40 +01:00
Eugen Rochko
a58c17e844 New translations strings.xml (Italian) 2024-08-02 21:28:23 +01:00
Eugen Rochko
bf0b91ca92 New translations full_description.txt (Scottish Gaelic) 2024-08-01 15:59:14 +01:00
Eugen Rochko
ed716794d6 New translations strings.xml (Scottish Gaelic) 2024-08-01 15:59:13 +01:00
Eugen Rochko
a2aabe38ec New translations strings.xml (Chinese Traditional) 2024-07-31 17:12:33 +01:00
Grishka
2c9e92a254 Increase limit on explore -> news 2024-07-31 15:00:55 +03:00
Eugen Rochko
390a28ad0e New translations strings.xml (Scottish Gaelic) 2024-07-31 01:33:10 +02:00
Eugen Rochko
2b19135118 New translations strings.xml (Hindi) 2024-07-31 01:33:06 +02:00
Eugen Rochko
3bca29aee2 New translations strings.xml (Thai) 2024-07-31 01:33:05 +02:00
Eugen Rochko
a9cd8954d1 New translations strings.xml (Indonesian) 2024-07-31 01:33:03 +02:00
Eugen Rochko
d9bbd3b243 New translations strings.xml (Portuguese, Brazilian) 2024-07-31 01:33:02 +02:00
Eugen Rochko
0670ac53dc New translations strings.xml (Galician) 2024-07-31 01:33:01 +02:00
Eugen Rochko
f41972bda7 New translations strings.xml (Vietnamese) 2024-07-31 01:33:00 +02:00
Eugen Rochko
994360e52d New translations strings.xml (Chinese Traditional) 2024-07-31 01:32:58 +02:00
Eugen Rochko
a27ae00a5b New translations strings.xml (Turkish) 2024-07-31 01:32:57 +02:00
Eugen Rochko
84d6f162ae New translations strings.xml (Swedish) 2024-07-31 01:32:56 +02:00
Eugen Rochko
031a4e1d28 New translations strings.xml (Slovenian) 2024-07-31 01:32:55 +02:00
Eugen Rochko
934a35b1d3 New translations strings.xml (Russian) 2024-07-31 01:32:54 +02:00
Eugen Rochko
b4b60cee32 New translations strings.xml (Polish) 2024-07-31 01:32:52 +02:00
Eugen Rochko
0a887528ac New translations strings.xml (Norwegian) 2024-07-31 01:32:51 +02:00
Eugen Rochko
c2e5888d7a New translations strings.xml (Dutch) 2024-07-31 01:32:50 +02:00
Eugen Rochko
b51fcfea84 New translations strings.xml (Japanese) 2024-07-31 01:32:47 +02:00
Eugen Rochko
751f0a2726 New translations strings.xml (Italian) 2024-07-31 01:32:46 +02:00
Eugen Rochko
9880a26636 New translations strings.xml (Armenian) 2024-07-31 01:32:44 +02:00
Eugen Rochko
0398dfd1c1 New translations strings.xml (Hungarian) 2024-07-31 01:32:43 +02:00
Eugen Rochko
69cc090a2b New translations strings.xml (Finnish) 2024-07-31 01:32:41 +02:00
Eugen Rochko
9d4df68f02 New translations strings.xml (Danish) 2024-07-31 01:32:40 +02:00
Eugen Rochko
cf621930a8 New translations strings.xml (Czech) 2024-07-31 01:32:38 +02:00
Eugen Rochko
150f7c7137 New translations strings.xml (Catalan) 2024-07-31 01:32:37 +02:00
Eugen Rochko
b4a7828e6d New translations strings.xml (Arabic) 2024-07-31 01:32:36 +02:00
Eugen Rochko
8226e57597 New translations strings.xml (Spanish) 2024-07-31 01:32:35 +02:00
Eugen Rochko
97d52b625a New translations strings.xml (French) 2024-07-31 01:32:34 +02:00
Eugen Rochko
33d0cfd89c New translations strings.xml (Interlingua) 2024-07-31 01:32:32 +02:00
Eugen Rochko
585100f2a0 New translations strings.xml (Persian) 2024-07-31 01:32:31 +02:00
Eugen Rochko
87485beddc New translations strings.xml (Icelandic) 2024-07-31 01:32:30 +02:00
Eugen Rochko
da44b45af5 New translations strings.xml (Chinese Simplified) 2024-07-31 01:32:29 +02:00
Eugen Rochko
884bf2e9b4 New translations strings.xml (Basque) 2024-07-31 01:32:28 +02:00
Eugen Rochko
6efa929e21 New translations strings.xml (Greek) 2024-07-31 01:32:27 +02:00
Eugen Rochko
9f497724f5 New translations strings.xml (German) 2024-07-31 01:32:26 +02:00
Eugen Rochko
21b55c18fb New translations strings.xml (Belarusian) 2024-07-31 01:32:24 +02:00
Eugen Rochko
7d846d7862 New translations strings.xml (Ukrainian) 2024-07-31 01:32:23 +02:00
Eugen Rochko
67496023f7 New translations strings.xml (Lithuanian) 2024-07-31 01:32:22 +02:00
Grishka
30fcb19f9b AND-156 2024-07-31 02:20:43 +03:00
Eugen Rochko
506aa36017 New translations full_description.txt (Italian) 2024-07-31 00:29:16 +02:00
Eugen Rochko
0789753c28 New translations full_description.txt (Icelandic) 2024-07-29 20:38:04 +02:00
Eugen Rochko
bfeb230f3f New translations full_description.txt (Thai) 2024-07-29 16:30:46 +02:00
Eugen Rochko
f8e3b295ef New translations full_description.txt (Icelandic) 2024-07-29 16:30:44 +02:00
Eugen Rochko
0dbe21cb9c New translations strings.xml (German) 2024-07-29 16:30:42 +02:00
Eugen Rochko
4fc36693be New translations full_description.txt (Thai) 2024-07-29 14:23:40 +02:00
Eugen Rochko
fa3366141e New translations strings.xml (Chinese Simplified) 2024-07-29 08:12:30 +02:00
Eugen Rochko
5583948e73 New translations full_description.txt (Interlingua) 2024-07-28 14:32:39 +02:00
Eugen Rochko
a71fccecec New translations strings.xml (Interlingua) 2024-07-28 14:32:38 +02:00
Eugen Rochko
47a70dd648 New translations full_description.txt (Japanese) 2024-07-27 07:40:38 +02:00
Eugen Rochko
3507043494 New translations short_description.txt (Ukrainian) 2024-07-26 17:43:39 +02:00
Eugen Rochko
827185b394 New translations full_description.txt (Ukrainian) 2024-07-26 17:43:38 +02:00
Eugen Rochko
1173e0d44a New translations full_description.txt (Ukrainian) 2024-07-26 16:42:18 +02:00
Eugen Rochko
cca0428f35 New translations strings.xml (Ukrainian) 2024-07-26 16:42:17 +02:00
Eugen Rochko
bfe3c0316c New translations strings.xml (Persian) 2024-07-26 14:33:25 +02:00
Eugen Rochko
2e3a035109 New translations full_description.txt (Chinese Simplified) 2024-07-26 11:45:57 +02:00
Eugen Rochko
8ed73e0f03 New translations strings.xml (Chinese Simplified) 2024-07-26 11:45:56 +02:00
Eugen Rochko
3479618077 New translations full_description.txt (Chinese Simplified) 2024-07-26 10:39:52 +02:00
Eugen Rochko
a429a90ce5 New translations strings.xml (Chinese Simplified) 2024-07-26 10:39:51 +02:00
Eugen Rochko
7c4d8577a6 New translations short_description.txt (Chinese Simplified) 2024-07-26 06:50:37 +02:00
Eugen Rochko
d5bdb1afdb New translations full_description.txt (Chinese Simplified) 2024-07-26 06:50:36 +02:00
Eugen Rochko
e4504e3d54 New translations strings.xml (Chinese Simplified) 2024-07-26 05:31:57 +02:00
Eugen Rochko
e679b17aa7 New translations full_description.txt (Basque) 2024-07-26 03:34:41 +02:00
Eugen Rochko
19b5586e09 New translations strings.xml (Basque) 2024-07-26 03:34:40 +02:00
Eugen Rochko
3b3158bc68 New translations short_description.txt (Basque) 2024-07-26 02:32:36 +02:00
Eugen Rochko
15d21f4eb5 New translations full_description.txt (Basque) 2024-07-26 02:32:36 +02:00
Eugen Rochko
c76c27684f New translations strings.xml (Basque) 2024-07-26 02:32:35 +02:00
Eugen Rochko
77c5dbf7f5 New translations full_description.txt (Japanese) 2024-07-25 18:12:25 +02:00
Eugen Rochko
0e5fd46254 New translations full_description.txt (Japanese) 2024-07-25 16:27:40 +02:00
Eugen Rochko
db69fd76f4 New translations full_description.txt (Japanese) 2024-07-25 15:19:14 +02:00
Eugen Rochko
3723b275c6 New translations short_description.txt (Japanese) 2024-07-25 08:14:41 +02:00
Eugen Rochko
07d7e2ff10 New translations full_description.txt (Japanese) 2024-07-25 08:14:40 +02:00
Eugen Rochko
cac52e13f6 New translations full_description.txt (Icelandic) 2024-07-24 21:12:32 +02:00
Eugen Rochko
169a84ad93 New translations strings.xml (Lithuanian) 2024-07-23 23:58:41 +02:00
Eugen Rochko
b16da68f7b New translations full_description.txt (Italian) 2024-07-23 02:08:46 +02:00
Eugen Rochko
84b241e27e New translations full_description.txt (Japanese) 2024-07-22 09:20:18 +02:00
Eugen Rochko
22041e43ed New translations full_description.txt (Japanese) 2024-07-22 02:55:05 +02:00
Eugen Rochko
75a1ff5eb7 New translations full_description.txt (Japanese) 2024-07-22 01:48:10 +02:00
Adil Lari
4b831d1ba0 Add margins to center icons *together* 2024-07-21 18:23:27 -05:00
Eugen Rochko
b326acb018 New translations full_description.txt (Dutch) 2024-07-21 19:40:52 +02:00
Eugen Rochko
160ed459e6 New translations full_description.txt (Dutch) 2024-07-21 18:29:19 +02:00
Eugen Rochko
13c51ba464 New translations short_description.txt (Dutch) 2024-07-21 17:16:04 +02:00
Eugen Rochko
99b78659ec New translations short_description.txt (Icelandic) 2024-07-21 14:52:01 +02:00
Eugen Rochko
9d5965725e New translations full_description.txt (Icelandic) 2024-07-21 14:52:00 +02:00
Eugen Rochko
e89fc80ca1 New translations strings.xml (Greek) 2024-07-21 14:51:59 +02:00
Eugen Rochko
70e9fcbc72 New translations strings.xml (Lithuanian) 2024-07-21 14:51:58 +02:00
Eugen Rochko
f4412fdec1 New translations strings.xml (Greek) 2024-07-21 12:40:04 +02:00
Gregory K
1d25494274 Merge pull request #866 from nikclayton/patch-1
fix: Remove charset from application/json content type
2024-07-20 16:08:48 +03:00
Nik Clayton
74042dee06 fix: Remove charset from application/json content type
Per https://datatracker.ietf.org/doc/html/rfc8259, application/json does not have a charset parameter
2024-07-20 15:01:52 +02:00
Eugen Rochko
0e630d6506 New translations strings.xml (Belarusian) 2024-07-17 14:24:34 +02:00
339 changed files with 11474 additions and 3480 deletions

View File

@@ -28,7 +28,7 @@ platform :android do
build_type: "release",
)
upload_to_play_store(
changes_not_sent_for_review: true,
release_status: "draft",
skip_upload_images: true,
skip_upload_screenshots: true
)

View File

@@ -2,60 +2,60 @@ Mastodon - лепшы спосаб быць у курсе ўсяго, што а
Гэта афіцыйная праграма для Android ад Mastodon. Ён вокамгненна хуткі і прыгожы, распрацаваны, каб быць не толькі функцыянальным, але і простым у выкарыстанні. У нашай праграме вы можаце:
АГЛЕДЗЕЦЬ
ДАСЛЕДАВАЦЬ
■ Адкрыйце для сябе новых пісьменнікаў, журналістаў, мастакоў, фатографаў, навукоўцаў і многае іншае
■ Даведайцеся, што адбываецца ў свеце
READ
ЧЫТАЦЬ
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Сачыце за людзьмі, якія вам цікавыя, у храналагічнай стужцы без перапыненняў
Сачыце за хэштэгамі, каб быць у курсе канкрэтных тэм у рэжыме рэальнага часу
CREATE
СТВАРАЙЦЕ
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
Адпраўце паведамленне сваім падпісчыкам ці ўсім свеце, выкарыстоўваючы апытанні, высакаякасныя відарысы і відэа
Удзельнічайце ў цікавых гутарках з іншымі людзьмі
CURATE
КУРЫРУЙЦЕ
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
Стварайце спісы людзей, каб не прапусціць ні аднаго допісу
Фільтруйце словы і фразы, каб кантраляваць тое, што вы хочаце і не хочаце бачыць
AND MORE!
І БОЛЬШ!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Прыгожая тэма, якая адаптуецца да вашай індывідуальнай каляровай схемы, светлай ці цёмнай
Абагульвайце і сканіруйце QR-коды, каб хутка абменьвацца профілямі Mastodon з іншымі
Уваход і пераключэнне паміж некалькімі ўліковымі запісамі
Атрымлівайце апавяшчэнне, калі хтосьці публікуе допіс з дапамогай кнопкі званочка
Без спойлераў! Вы можаце размясціць свае допісы за папярэджаннямі аб змесце
A POWERFUL PUBLISHING PLATFORM
МАГУТНАЯ ПУБЛІКАЦЫЙНАЯ ПЛАТФОРМА
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Вам больш не трэба спрабаваць супакоіць непразрысты алгарытм, які вырашае, ці ўбачаць вашы сябры тое, што вы апублікавалі. Калі яны пойдуць за вамі, яны гэта ўбачаць.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Калі вы публікуеце яго ў адкрытым Інтэрнэце, ён становіцца даступным у адкрытым Інтэрнэце. Вы можаце смела дзяліцца спасылкамі на Mastodon, ведаючы, што кожны зможа прачытаць іх без уваходу ў сістэму.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Акрамя стужак, апытанняў, высакаякасных відарысаў, відэа, аўдыя і папярэджанняў аб змесце, Mastodon прапануе мноства спосабаў выказаць сябе так, як вам зручна.
A POWERFUL READING PLATFORM
МАГУТНАЯ ПЛАТФОРМА ДЛЯ ЧЫТАННЯ
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Нам не трэба паказваць вам рэкламу, таму нам не трэба трымаць вас у нашай праграме. Mastodon мае найбагацейшы выбар старонніх праграм і інтэграцый, так што вы можаце выбраць тое, што вам больш за ўсё падыходзіць.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Дзякуючы храналагічнай хатняй стужцы лёгка вызначыць, калі вы прагледзелі ўсе абнаўленні і можаце перайсці да чагосьці іншага.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Не трэба турбавацца аб тым, што няправільны клік сапсуе вашы рэкамендацыі назаўжды. Мы не адгадваем, што вы хочаце бачыць, мы дазваляем вам кіраваць гэтым.
PROTOCOLS, NOT PLATFORMS
ПРАТАКОЛЫ, А НЕ ПЛАТФОРМЫ
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
У адрозненне ад традыцыйнай платформы сацыяльных сетак, Mastodon пабудаваны на дэцэнтралізаваным пратаколе. Вы можаце зарэгістравацца на нашым афіцыйным серверы або выбраць трэцюю асобу для размяшчэння вашых даных і мадэрацыі вашага вопыту.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
З агульным пратаколам, незалежна ад таго, што вы абралі, вы можаце бесперашкодна мець зносіны з людзьмі на іншых серверах Mastodon. Але ёсць і больш: толькі з адным уліковым запісам вы можаце размаўляць з людзьмі з іншых платформаў fediverse.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Не задаволены сваім выбарам? Вы заўсёды можаце пераключыцца на іншы сервер Mastodon, узяўшы з сабой падпісчыкаў. Для прасунутых карыстальнікаў вы нават можаце размясціць свае даныя ў сваёй інфраструктуры, паколькі Mastodon з'яўляецца адкрытым зыходным кодам.
NON-PROFIT IN NATURE
НЕКАМЕРЦЫЙНЫ ХАРАКТАРАТ
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon з'яўляецца зарэгістраванай некамерцыйнай арганізацыяй у ЗША і Германіі. Нас матывуе не здабыванне грашовай каштоўнасці з платформы, а тое, што лепш для платформы.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
РЭКАМЕНТАВАНЫЯ Ў: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com і інш.

View File

@@ -0,0 +1,61 @@
Mastodon yw'r ffordd orau o gadw i fyny â'r hyn sy'n digwydd. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
DARGANFOD
■ Discover new writers, journalists, artists, photographers, scientists and more
■ See whats happening in the world
DARLLEN
■ Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
CREU
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
CURADU
■ Create lists of people to never miss a post
■ Filter words or phrases to control what you do and dont want to see
A MWY!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
■ Login and switch between multiple accounts
■ Get notified when a specific person posts with the bell button
■ No spoilers! You can put your posts behind content warnings
A POWERFUL PUBLISHING PLATFORM
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
A POWERFUL READING PLATFORM
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
PROTOCOLAU, NID PLATFFORMAU
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
NID-ER-ELW
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
FEL Y GWELWYD YN: Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, a mwy.

View File

@@ -0,0 +1 @@
Lle mae sgyrsiau yn digwydd

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon er den bedste måde at holde sig ajour med, hvad der sker. Følg enhver på tværs af fediverset og se alt i kronologisk rækkefølge. Ingen algoritmer, annoncer eller clickbait i syne.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Dette er den officielle Android-app til Mastodon. Den er lynhurtigt og forbløffende smuk, designet til at være ikke blot kraftfuld, men også nem at bruge. I vores app kan man:
EXPLORE
UDFORSKE
■ Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
● Opdag nye forfattere, journalister, kunstnere, fotografer, forskere med mere
Se, hvad der sker i verden
READ
LÆSE
■ Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
● Bliv holdt ajour med folk, man holder af, i et kronologisk feed uden afbrydelser
Følg hashtags for at være i tråd med bestemte emner i realtid
CREATE
OPRETTE
Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
Post til følgere eller hele verden, med meningsmålinger, højkvalitets billeder og videoer
● Deltag i interessante samtaler med andre mennesker
CURATE
KURATERE
■ Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
● Opret lister over personer for aldrig at gå glip af et indlæg
Filtr ord eller sætninger for at styre, hvad man ønsker, og ikke ønsker, at se
AND MORE!
OG MERE!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
■ Get notified when a specific person posts with the bell button
■ No spoilers! You can put your posts behind content warnings
● Et smukt tema, der tilpasser sig den personlige farvesammensætning, lys eller mørk
● Del og skan QR-koder for hurtigt at udveksle Mastodon-profiler med andre
Log ind og skift mellem flere konti
● Få besked med klokkeknappen, når en bestemt person poster
● Ingen spoilers! Man kan placere sine indlæg bag indholdsadvarsler
A POWERFUL PUBLISHING PLATFORM
EN KRAFTFULD UDGIVELSESPLATFORM
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Man behøver ikke længere at forsøge at formilde en uigennemsigtig algoritme, der beslutter, om vennerne vil se, hvad man har postet. Følges man af dem, vil de se det.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Udgiver man det til det åbne net, er det tilgængeligt på det åbne net. Man kan sikkert dele links via Mastodon med den viden, at alle vil være i stand til at læse dem uden at logge ind.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Mellem tråde, afstemninger, højkvalitets billeder, videoer, lyd og indholdsadvarsler, tilbyder Mastodon masser af måder at udtrykke sig på en måde, man finder passende.
A POWERFUL READING PLATFORM
EN KRAFTFULD LÆSEPLATFORM
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Vi behøver ikke at vise brugere annoncer, så vi behøver ikke at holde dem i vores app. Mastodon har det rigeste udvalg af 3. parts apps og integrationer, så man kan vælge mest passende oplevelse.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Takket være det kronologiske hjemme-feed er det nemt at vide, når man er i tråd med alle opdateringer og kan gå videre til noget andet.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Ingen grund til bekymringer over, at man får sine anbefalinger ødelagt for evigt med et fejlklik. Vi gætter ikke, hvad brugerne ønsker at se, vi lader dem styre det.
PROTOCOLS, NOT PLATFORMS
PROTOKOLLER, IKKE PLATFORME
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon er ikke som en traditionel social medieplatform, men baserer sig på en decentraliseret protokol. Man kan tilmelde sig på vores officielle server, eller vælge en 3. part til at være vært for sine data og moderere ens oplevelse.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Takket være den fælles protokol, kan man, uanset hvad man vælger, kommunikere problemfrit med folk på andre Mastodon-servere. Der er mere endnu: Med kun én konto kan man kommunikere med folk fra andre fediverse-platforme.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Ikke tilfreds med valget? Man kan altid skifte til en anden Mastodon-server, mens man tager sine tilhængere med sig. For avancerede brugere, man kan endda være vært for sine data på sin egen infrastruktur, da Mastodon er open-source.
NON-PROFIT IN NATURE
NON-PROFIT AF NATUR
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon er registreret som non-profit i USA og Tyskland. Vi er ikke motiveret af at udvinde pengeværdi fra platformen, men ved hvad der er bedst for den.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
SOM OMTALT I: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com og flere.

View File

@@ -1 +1 @@
Where conversations happen
Hvor samtaler finder sted

View File

@@ -5,34 +5,34 @@ Das ist die offizielle Android-App für Mastodon. Blitzschnell und atemberaubend
ENTDECKEN
■ Neue Autoren, Journalisten, Künstler, Fotografen, Wissenschaftler und viele andere entdecken
■ See whats happening in the world
■ Sehen Sie, was in der Welt passiert
READ
LESEN
Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
Bleiben Sie in einem chronologischen Feed ohne Unterbrechungen über die Menschen auf dem Laufenden, die Ihnen wichtig sind
■ Folgen Sie Hashtags, um in Echtzeit über bestimmte Themen auf dem Laufenden zu bleiben
KREIEREN
■ Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
■ Posten Sie für Ihre Follower oder die ganze Welt mit Umfragen, hochwertigen Bildern und Videos
Nehmen Sie an interessanten Gesprächen mit anderen Menschen teil
GESTALTEN
Create lists of people to never miss a post
■ Filter words or phrases to control what you do and dont want to see
Erstellen Sie Listen mit Personen, um nie einen Beitrag zu verpassen
■ Filtern Sie Wörter oder Ausdrücke, um zu steuern, was Sie sehen möchten und was nicht
UND MEHR!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
Ein schönes Design, das sich an Ihr persönliches Farbschema anpasst, ob hell oder dunkel
Teilen und scannen Sie QR-Codes, um Mastodon-Profile schnell mit anderen auszutauschen
Anmelden und zwischen mehreren Konten wechseln
Lassen Sie sich benachrichtigen, wenn eine bestimmte Person mit der Klingeltaste postet
■ Keine Spoiler! Du kannst deine Beträge hinter Inhaltswarnungen stellen
EINE MÄCHTIGE PLATTFORM ZUM VERÖFFENTLICHEN
Du musst nicht länger versuchen, einen undurchsichtigen Algorithmus dir wohlgesinnt zu stimmen, der darüber entscheidet, ob deine Freunde sehen, was du postest. If they follow you, theyll see it.
Du musst nicht länger versuchen, einen undurchsichtigen Algorithmus dir wohlgesinnt zu stimmen, der darüber entscheidet, ob deine Freunde sehen, was du postest. Wenn Sie Ihnen folgen, werden Sie es sehen.
Wenn du es im offenen Web veröffentlichst, ist es auch zugänglich im offenen Web. Du kannst ganz unbekümmert Links auf Mastodon teilen in dem Wissen, dass jeder sie ohne Einloggen wird lesen können.

View File

@@ -0,0 +1,4 @@
- We're rolling out grouped notifications for servers that support the feature
- You can now click "show results" on polls without having to submit a vote
- You will now see lines connecting reply chains in threads
- Bug fixes and improvements, obviously!

View File

@@ -0,0 +1,7 @@
- New option to use our color theme instead of Material You
- Support for the color contrast setting in Android 15
- You can now crop your profile picture when editing your profile
- You can now long-press "Add media" in the composer to bring up the file picker
- New look for the media viewer
- New follower notifications will now appear grouped
- Directional loading for gaps in timelines: load newer posts when scrolling up, older when down

View File

@@ -0,0 +1,6 @@
- Settings are now split into global settings and account-specific settings for each account you're logged into
- Profiles now show who of the people you follow also follows this person
- More easily swap between posts, profiles and hashtags when using search
- We now tell you exactly how many followers you'd lose when blocking a whole server so you can avoid costly mistakes
- Bookmarks and favourites have moved to a new tab on your profile
- The button that loads missing posts is now more upfront about which direction it's going to load in

View File

@@ -1 +1 @@
110.txt
128.txt

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon gertatzen ari denari buruz egunean egoteko modurik onena da. Jarraitu edonor fedibertsoan eta ikusi dena ordena kronologikoan. Ez dago algoritmorik, iragarkirik edo clickbait-ik bistan.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Hau Android gailuentzako Mastodon aplikazio ofiziala da. Ikaragarri azkarra eta ikaragarri ederra da, ahaltsua ez ezik, erabilerraza ere izateko diseinatua. Gure aplikazioan, hauek egin ditzakezu:
EXPLORE
ESPLORATU
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Ezagutu idazle, kazetari, artista, argazkilari, zientzialari eta gehiago
Ikusi zer ari den gertatzen munduan
READ
IRAKURRI
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Axola zaizkizun pertsonei buruz egunean egotea, etenik gabeko horma kronologikoarekin
Jarraitu hashtag-ak gai espezifikoekin denbora errealean informatuta egoteko
CREATE
SORTU
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
Bidali bidalketak zure jarraitzaileei edo mundu guztiari, inkestekin, kalitate handiko irudiekin eta bideoekin
Beste pertsona batzuekin elkarrizketa interesgarrietan parte hartu
CURATE
ONDU
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
Pertsonen zerrendak sortu elkarrizketa bat inoiz ez galtzeko
Hitzak edo esaldiak iragazi ikusi nahi duzuna eta ikusi nahi ez duzuna kontrolatzeko
AND MORE!
ETA GEHIAGO!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Zure kolore eskema pertsonalizatura egokitzen den gai ederra, argia edo iluna
Partekatu eta eskaneatu QR kodeak Mastodon profilak beste batzuekin azkar trukatzeko
Saioa hasi eta aldatu hainbat konturen artean
Jakinarazpenak jaso pertsona jakin batek argitaratzen duenean kanpaiaren botoiarekin
Izorrakirik ez! Zure mezuak eduki abisuekin babes ditzakezu
A POWERFUL PUBLISHING PLATFORM
ARGITARATZEKO PLATAFORMA INDARTSUA
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Jada ez duzu algoritmo opako bat baretzen saiatu behar, zure lagunek argitaratu zenuena ikusiko duten ala ez erabakitzen duena. Jarraitzen badizute, ikusiko dute.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Web irekian argitaratzen baduzu, web irekian eskuragarri dago. Mastodonerako loturak segurtasunez parteka ditzakezu, jakinda edozeinek irakurri ahal izango dituela saioa hasi gabe.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Harien, inkesten, irudien, bideoen, audioaren eta edukien ohartarazpenen artean, Mastodonek modu asko eskaintzen ditu komeni zaizun moduan adierazteko.
A POWERFUL READING PLATFORM
IRAKURTZEKO PLATAFORMA INDARTSUA
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Ez dizugu iragarkirik erakutsi behar; beraz, ez dizugu eutsi behar gure aplikazioan. Mastodonek hirugarrenen aplikazio eta integrazioen aukeraketa aberatsena du, gehien komeni zaizun esperientzia aukera dezazun.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Orri nagusiko jario kronologikoari esker, erraza da jakitea noiz aurkitu dituzun eguneraketa guztiak eta beste gauza batera pasa zaitezke.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Ez dago kezkatu beharrik okerreko klik batek betiko zure gomendioak hondatuko dituela. Ez dugu asmatzen zer ikusi nahi duzun, zuk kontrola dezazun uzten dugu.
PROTOCOLS, NOT PLATFORMS
PROTOKOLOAK, PLATAFORMARIK EZ
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon ez da sare sozialen ohiko plataforma bat, protokolo deszentralizatu batean oinarritzen da. Gure zerbitzari ofizialean izena eman dezakezu edo hirugarren bat aukeratu zure datuak ostatatzeko eta zure esperientzia moderatzeko.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Protokolo komunari esker, ez du axola zer aukeratzen duzun, arazorik gabe komunika zaitezke jendearekin beste Mastodon zerbitzari batzuetan badaude ere. Are gehiago: kontu bakar batekin, fedibertsoko beste plataforma batzuetako jendearekin komunika zaitezke.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Ez al zaude pozik zure aukerarekin? Beti alda dezakezu Mastodon zerbitzari desberdin batera zure jarraitzaileak zurekin eramaten dituzun bitartean. Erabiltzaile aurreratuentzat, zure datuak zure azpiegituran bertan ostatatu ditzakezu, Mastodon software librea baita.
NON-PROFIT IN NATURE
IRABAZI ASMORIK GABE IZAERAZ
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon Estatu Batuetan eta Alemanian erregistratutako irabazi-asmorik gabeko erakundea da. Ez gaude motibatuta plataformaren diru-balioa ateratzeagatik, baizik eta plataformarentzat hobea denagatik.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
HEMEN AGERTZEN DEN BEZALA: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo PCMAG.com, eta gehiago.

View File

@@ -1 +1 @@
Where conversations happen
Elkarrizketak gertatzen diren lekua

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
ماستودون بهترین راه برای پیگیری اتفاقات است. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
این کاره رسمی اندروید برای ماستودون است. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. در کاره ما، شما می‌توانید:
EXPLORE
گشت و گذار
■ Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
ببینید در دنیا چه رخ می‌دهد
READ
خواندن
■ Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
CREATE
ایجاد کردن
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
CURATE
Create lists of people to never miss a post
سیاهه‌ای از افراد ایجاد کنید تا هرگز فرسته‌ای را از دست ندهید
■ Filter words or phrases to control what you do and dont want to see
AND MORE!
و بیشتر!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
وارد شوید و بین چند حساب جابجا شوید
■ Get notified when a specific person posts with the bell button
■ No spoilers! You can put your posts behind content warnings
A POWERFUL PUBLISHING PLATFORM
یک سکوی انتشار قدرتمند
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. اگر آنها شما را پی بگیرند، آن را خواهند دید.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
A POWERFUL READING PLATFORM
یک سکوی خواندن قدرتمند
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
ما نیازی به نمایش تبلیغات به شما نداریم، بنابراین لازم نیست شما را در کاره‌مان نگه داریم. ماستودون غنی‌ترین مجموعه از برنامه‌ها و ادغام‌های شخص ثالث را دارد، بنابراین می‌توانید تجربه‌ای را انتخاب کنید که مناسب شما است.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
به لطف خوراک زمانی خانه، تشخیص اینکه چه زمانی از همه به‌روزرسانی‌ها مطلع شده‌اید و می‌توانید به چیز دیگری بروید، آسان است.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
لازم نیست نگران باشید که یک کلیک اشتباه توصیه های شما را برای همیشه خراب می کند. ما حدس نمی‌زنیم که چه چیزی می‌خواهید ببینید، به شما اجازه می‌دهیم آن را کنترل کنید.
PROTOCOLS, NOT PLATFORMS
شیوه‌نامه‌ها، بدون سکوها
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
به لطف پروتکل مشترک، مهم نیست که چه چیزی را انتخاب می کنید، می‌توانید به طور یکپارچه با افراد در سایر کارسازهای ماستودون ارتباط برقرار کنید. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
از انتخاب خود خوشحال نیستید؟ You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
NON-PROFIT IN NATURE
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
ماستودون یک سازمان غیرانتفاعی ثبت شده در ایالات متحده و آلمان است. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.

View File

@@ -1 +1 @@
Where conversations happen
کجا گفتگوها اتفاق می‌افتد

View File

@@ -1,44 +1,44 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon est le meilleur endroit pour se tenir au courant de ce qui se passe. Suivez n'importe qui à travers le fédivers et découvrez tout dans un ordre chronologique. Pas d'algorithmes, de publicités ou de pièges à clics dans le coin.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Ceci est l'application Android officielle de Mastodon. Elle est extrêmement rapide et d'une beauté époustouflante, conçue pour être non seulement puissante, mais aussi facile d'utilisation. Dans notre application, vous pouvez :
EXPLORE
EXPLORER
■ Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
■ Découvrir de nouveaux et nouvelles écrivain·e·s, journalistes, artistes, photographes, scientifiques et plus encore
Voir ce qui se passe dans le monde
READ
LIRE
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Suivez les gens qui vous intéressent dans un flux chronologique sans interruption
Suivez les hashtags pour suivre des sujets spécifiques en temps réel
CREATE
CRÉER
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
■ Participer à des conversations intéressantes avec d'autres personnes
CURATE
ACTUALISER
■ Create lists of people to never miss a post
■ Filter words or phrases to control what you do and dont want to see
■ Créez des listes de personnes pour ne jamais manquer un post
■ Filtrez les mots ou les phrases pour contrôler ce que vous voulez et ne voulez pas voir
AND MORE!
ET PLUS !
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Un beau thème qui sadapte à vos couleurs personnalisées, claires ou sombres
Partagez et numérisez des codes QR pour échanger rapidement des profils Mastodon avec d'autres personnes
Connexion et bascule entre plusieurs comptes
■ Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Pas de spoilers ! Vous pouvez mettre vos messages derrière des avertissements de contenu
A POWERFUL PUBLISHING PLATFORM
UNE PUISSANTE PLATEFORME DE PUBLICATION
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Vous n'avez plus à dépendre d'un algorithme opaque qui décide si vos amis vont voir ce que vous avez posté. S'ils vous suivent, ils le verront.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
A POWERFUL READING PLATFORM
UNE PUISSANTE PLATEFORME DE LECTURE
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
@@ -46,9 +46,9 @@ Thanks to the chronological home feed, its easy to tell when youve caught
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
PROTOCOLS, NOT PLATFORMS
PROTOCOLS, ET NON PLATEFORMES
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
« Mastodon » n'est pas un média social traditionnel, il est conçu sur la base d'un protocole décentralisé. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
@@ -56,6 +56,6 @@ Not happy with your choice? You can always switch to a different Mastodon server
NON-PROFIT IN NATURE
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon est une organisation à but non lucratif immatriculée aux États-Unis et en Allemagne. Nous ne sommes pas motivés par des considérations monétaires, mais par ce qu'il y a de mieux pour la plateforme.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.

View File

@@ -1 +1 @@
Where conversations happen
Là où les discussions se déroulent

View File

@@ -0,0 +1,61 @@
Mastodon is de beste manier om by te hâlden wat der bart. Folgje elkenien yn de fediverse en sjoch alles yn gronologyske folchoarder. Gjin algoritmen, advertinsjes of clickbait te bekennen.
Dit is de offisjele Android-app foar Mastodon. It is razendfluch en bjusterbaarlik moai. Untwurpen om net allinnich krêftich, mar ek brûkersfreonlik te wêzen. Yn ús app kinne jo:
UNTDEKKE
■ Nije skriuwers, sjoernalisten, keunstners, fotografen, wittenskippers en mear ûntdekke
■ Sjoch wat der yn de wrâld bart
LÊZE
■ Op de hichte bliuwe fan minsken om wat jo jouwe, op in gronologyske tiidline en sûnder ûnderbrekkingen
■ Hashtags yn realtime folgje om op de hichte te bliuwen fan spesifike ûnderwerpen
KREËARJE
■ Berjochten nei allinnich dyn folgers stjoere of nei de hiele wrâld, mei peilingen, ôfbyldingen fan hege kwaliteit en fideos
■ Oan ynteressante petearen mei oare minsken dielnimme
ONTWERPEN
■ Listen meitsje mei minsken dyt jo folgje, om sa nea in berjocht te missen
■ Wurden of sinnen filterje om te bepalen wat jo wol en net sjen wolle
EN MEAR!
■ In prachtich tema dat har oanpast oan jo persoanlike kleureskema, ljocht of donker
■ QR-koaden scanne om gau Mastodon-profilen mei oare minsken te dielen
■ Meardere accounts brûke
■ Meldingen fan spesifike persoanen ûntfange, wanneart dizze berjochten pleatse
■ Gjin spoilers! ■ Jo kinne jo berjochten efter ynhâldswarskôgingen pleatse
IN KRÊFTICH PUBLIKAASJEPLATFOARM
Jo hoege net langer te probearjen in ûntrochsichtich algoritme tefreden te stellen, dat beslist oft jo freonen wol of net sjen meie wat jo pleatst hawwe. As se jo folgje, sille se it sjen.
Wanneart jo it yn de fediverse pleatse, is it ek tagonklik yn de fediverse. Jo kinne feilich keppelingen nei Mastodon diele, yn de wittenskip dat elkenien se lêze kin sûnder oan hoege te melden.
Mei petearen, peilingen, ôfbyldingen fan hege kwaliteit, fideos, audio en ynhâld s warskôgingen, biedt Mastodon genôch manieren om josels uterje te kinnen, op in manier dyt by jo past.
IN KRÊFTICH LÊSPLATFOARM
Wy hoege jo gjin advertinsjes sjen te litten, dus wy hoege jo ek net foar ús app te behâlden. Mastodon hat de rykste kar oan troch tredden ûntwikkele apps en yntegraasjes, sadat jo de brûkersûnderfining kieze kinne dyt it beste by jo past.
Mei tank oan de gronologyske tiidline is it maklik om te sjen wanneart jo alle updates besjoen hawwe en mei wat oars troch gean kinne.
Jo hoege net bang te wêzen dat, wanneart jo op wat ferkeards klikke, jo oanrekommandaasjes foar altyd ferpest binne. We riede net nei wat jo sjen wolle, wy litte it oan jo oer.
PROTOKOLLEN, GJIN PLATFOARMS
Mastodon is net lykas in tradisjoneel sosjaal-mediaplatfoarm, mar is boud op in desintralisearre protool. Jo kinne foar ús offisjele server of foar ien fan in tredde partij kieze om jo gegevens te hosten en brûkers te moderearjen.
Mei tank oan it mienskiplike protokol, kinne jo, likefolle wat jo kieze, naadleas mei minsken op oare Mastodon-servers kommunisearje. Mar der is mear: Mei mar ien account kinne jo mei minsken fan oare fediverse-platfoarms kommunisearje.
Net tefreden mei jo kar? Jo kinne altyd nei ien oare Mastodon-server oerstappe en jo folgers meinimme. Betûfte brûkers kinne harren gegevens sels op harren eigen ynfrastruktuer hoste, omdat Mastodon iepen-boarne is.
NON-PROFIT FAN AARD
Mastodon is in registrearre non-profitorganisaasje yn de Ferienige Steaten en Dútslân. Wy helje ús motivaasje net út it kommersjeel misbrûk meitsje fan Mastodon, mar út wat it beste is foar Mastodon.
FERMELD YN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com en mear.

View File

@@ -0,0 +1 @@
Dêrt petearen ûntstean

View File

@@ -1,36 +1,36 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Is Mastodon an dòigh as fheàrr airson sùil a chumail air na tha a dol. Lean duine sam bith air a cho-shaoghal agus faic a h-uile càil a-rèir an ama. Chan eil sgeul air algairimean, sanasachd no clickbait.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Seo an aplacaid Android oifigeil airson Mastodon. The e àlainn s cho luath ris a ghaoth, air a dhealbhadh ach am biodh e cumhachdach ach furasta cleachdadh. Seo na nì thu san aplacaid againn:
EXPLORE
RÙRAICH
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Lorg sgrìobhadairean, luchd-naidheachd, luchd-ealain, luchd togail dhealbhan, luchd-saidheans is eile
Faic na tha a dol air an t-saoghal
READ
LEUGH
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Gabh naidheachdan na feadhainn a tha cudromach dhut a-rèir an ama s gun bhuairidhean
Lean tagaichean hais airson ceum a chumail ri cuspairean àraid ann am fìor-àm
CREATE
CRUTHAICH
■ Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
■ Postaich chun luchd-leantainn agad no chun t-saoghail air fad, le cunntasan-bheachd agus dealbhan s videothan le càileachd àrd
Gabh pàirt ann an còmhraidhean inntinneach le daoine eile
CURATE
CURAIDICH
■ Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
■ Cruthaich liostaichean de dhaoine ach nach caill thu post uapa-san
Criathraich faclan no abairtean a stiùireadh na chì s nach fhaic thu
AND MORE!
AGUS MÒRAN A BHARRACHD!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Ùrlar àlainn a fhreagras dhan sgeama dhathan phearsanaichte agad, soilleir no dorcha
Co-roinn is sganaich còdaichean QR airson pròifilean Mhastodon iomlaid le càch sa bhad
Clàraich a-steach s geàrr leum eadar iomadh cunntas
Faigh brath nuair a phostaich cuideigin sònraichte rud le putan a chluig
Gun spoilers! S urrainn dhut na postaichean agad a chur air cùlaibh rabhaidhean susbainte
A POWERFUL PUBLISHING PLATFORM
ÙRLAR FOILLSEACHAIDH CUMHACHDACH
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.

View File

@@ -0,0 +1,61 @@
Mastodon es le melior maniera de sequer lo que passa. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Iste es le app official de Mastodon pro Android. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In nostre app, tu pote:
EXPLORAR
■ Discover new writers, journalists, artists, photographers, scientists and more
■ See whats happening in the world
LEGER
■ Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
CREAR
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
CURATE
■ Create lists of people to never miss a post
■ Filter words or phrases to control what you do and dont want to see
AND MORE!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
■ Login and switch between multiple accounts
■ Get notified when a specific person posts with the bell button
■ No spoilers! You can put your posts behind content warnings
A POWERFUL PUBLISHING PLATFORM
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
A POWERFUL READING PLATFORM
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
PROTOCOLS, NOT PLATFORMS
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
NON-PROFIT IN NATURE
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon merupakan cara terbaik untuk tetap mengikuti apa yang terjadi. Ikuti siapapun dalam fediverse dan lihat semuanya secara kronologis. Tidak ada algoritma, iklan, atau clickbait.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Ini adalah aplikasi Android resmi untuk Mastodon. Cepat dan mengesankan, didesain tidak hanya sekadar kuat tetapi juga mudah digunakan. Di dalam aplikasi ini, anda dapat:
EXPLORE
MENJELAJAHI
■ Discover new writers, journalists, artists, photographers, scientists and more
■ See whats happening in the world
Temukan penulis, jurnalis, kreator, fotografer, ilmuwan, dan lainnya
Melihat apa yang sedang terjadi
READ
MEMBACA
■ Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
Tetap berhubungan dengan orang terdekat anda tanpa gangguan
Mengikuti tagar untuk tetap terupdate dengan topik-topik baru
CREATE
MEMBUAT
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
Tautkan konten kepada pengikut anda dengan poll, gambar dan video berkualitas tinggi
Berpartisipasi dalam percakapan yang menarik dengan orang lain
CURATE
MENYUSUN
■ Create lists of people to never miss a post
■ Filter words or phrases to control what you do and dont want to see
Buat daftar orang agar tidak ketinggalan
Saring kata atau frasa untuk mengontrol apa yang anda ingin lihat atau tidak
AND MORE!
DAN BANYAK LAGI!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
■ Get notified when a specific person posts with the bell button
■ No spoilers! You can put your posts behind content warnings
Tema indah yang menyesuaikan skema warna perangkat anda, terang atau gelap
Bagikan dan pindai QR code untuk bertukar profil Mastodon dengan cepat
Login dengan lebih dari satu akun
Tetap ternotifikasi dengan postingan orang tertentu menggunakan tombol dering
Tidak ada spoilers! Anda bisa mencantumkan postingan anda dibelakang tanda peringatan
A POWERFUL PUBLISHING PLATFORM
PLATFORM PUBLIKASI YANG KUAT
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Anda tidak lagi perlu mengikuti algoritma yang menentukan jika teman anda akan melihat apa yang anda tautkan. Jika mereka mengikuti anda, mereka akan melihat tautan tersebut.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Jika anda mempublikasikan di internet, tautan tersebut dapat diakses di internet. Anda bisa dengan aman membagikan situs di Mastodon dengan catatan bahwa siapa saja dapat melihatnya tanpa login.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Selain threads, polls, gambar berkualitas tinggi, video, audio, dan peringatan konten, Mastodon menawarkan beragam cara untuk mengekspresikan diri anda dengan cara yang anda inginkan.
A POWERFUL READING PLATFORM
SEBUAH PLATFORM MEMBACA YANG KUAT
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Kami tidak perlu menayangkan iklan, sehingga kami tidak perlu menahan anda di dalam aplikasi kami. Mastodon memiliki pilihan aplikasi pihak ketiga terbanyak sehingga anda mendapatkan pengalaman yang anda inginkan.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Berkat laman feed yang kronologis, anda dapat dengan mudah mengikuti update dan melakukan hal lain.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Tidak perlu khawatir jika anda salah mengklik dan mengubah rekomendasi. Kami tidak mengatur apa yang anda ingit lihat, kami serahkan kepada anda yang mengontrolnya.
PROTOCOLS, NOT PLATFORMS
PROTOKOL, BUKAN PLATFORM
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon tidak seperti platform media sosial pada umumnya, tetapi dibangun pada protokol yang terdesentralisasi. Anda dapat mendaftar pada server resmi kami, atau memilih pihak ketiga untuk melayani data anda.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Berkat protokol, apapun yang anda pilih, anda tetap dapat berkomunikasi dengan pengguna di server Mastodon yang berbeda. Dengan satu akun, anda dapat berkomunikasi dengan pengguna dari platform fediverse yang lain.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Tidak puas dengan pilihan anda? Anda dapat selalu berpindah ke server Mastodon yang berbeda selagi membawa pengikut anda. Untuk pengguna yang serius, anda bahkan bisa menyimpan data pada infrastruktur sendiri, karena Mastodon ini open-source.
NON-PROFIT IN NATURE
NON-PROFIT
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon terdaftar sebagai lembaga non-profit di US dan Jerman. Kami tidak terdorong untuk memperoleh keuntungan, tetapi apa yang terbaik.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
SEPERTI YANG DILIHAT DI: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, dan lainnya.

View File

@@ -1 +1 @@
Where conversations happen
Tempat di mana percakapan terjadi

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon er besta leiðin til að fylgjast með hvað sé í gangi. Fylgstu með hverjum sem er í fediverse-heiminum og skoðaðu það allt í tímaröð. Engin algrím, auglýsingar eða smellbeitur á ferðinni.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Þetta er opinbera Android-forritið fyrir Mastodon. Það er eldsnöggt og fjarska fallegt, hannað til að vera bæði öflugt og auðvelt í notkun. Í forritinu okkar geturðu:
EXPLORE
KANNAÐ
Discover new writers, journalists, artists, photographers, scientists and more
■ See whats happening in the world
Uppgötvaðu nýja rithöfunda, blaðamenn, listafólk, ljósmyndara, vísindafólk og fleira
■ Sjáðu hvað er að gerast í heiminum
READ
LESIÐ
Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
Vertu í sambandi við fólk sem þér er kært á streymi í tímaröð án truflana
■ Fylgst með myllumerkjum til að fá upplýsingar um tiltekin efni í rauntíma
CREATE
SKAPAÐ
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
Birt færslur til fylgjendanna þinna eða alls heimsins, með könnunum, hágæða myndum og myndskeiðum
Tekið þátt í áhugaverðum samræðum við annað fólk
CURATE
SKIPULAGT
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
Búið til lista yfir fólk sem þú vilt ekki missa af færslum frá
Síað orð og setningar til að stýra hvað þú sérð og hvað ekki
AND MORE!
OG FLEIRA!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Fallegt þema sem aðlagast persónusniðnu litastefi, ljóst eða dökkt
Deildu og skannaðu QR-kóða til að skiptast á Mastodon-notendasniðum við aðra
Skráðu þig inn og skiptu milli margra notendaaðganga
Með bjölluhnappnum geturðu fengið tilkynningar þegar tilteknir aðilar birta færslur
Ekkert sem afvegaleiðir! Þú getur sett færslurnar þínar á bakvið aðvörun vegna efnis
A POWERFUL PUBLISHING PLATFORM
ÖFLUGT KERFI TIL BIRTINGAR
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Þú þarft ekki lengur að prófa þig áfram með og friðþægja eitthvert ógagnsætt algrími sem ákvarðar hvort vinir þínir fái að sjá það sem þú birtir. Ef viðkomandi fylgist með þér, mun það sjást.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Ef þú birtir það á opna vefnum, er hægt að skoða það á opna vefnum. Þú ert örugg(ur) við að deila tenglum á Mastodon, vitandi það að hver sem er mun geta lesið þá án þess að skrá sig inn.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Með samræðum, hágæða myndefni, myndskeiðum, hljóðskrám og viðvörunum vegna efnis, býður Mastodon upp á margar leiðir til að tjá þig á þann hátt sem þér hentar.
A POWERFUL READING PLATFORM
ÖFLUGT KERFI TIL LESTRAR
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Við þurfum ekkert að sýna þér auglýsingar og höfum því enga ástæðu til að halda þér inni í okkar eigin forritum. Mastodon býður upp á mikið úrval forrita frá utanaðkomandi aðilum og samþættingu við önnur kerfi, þannig að þú getir valið það sem þér líkar best.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Þökk sé heimastreymi í tímaröð, þá er auðvelt að sjá þegar þú lýkur við að skoða allar nýjar færslur og getur snúið þér að einhverju öðru.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Þú þarft ekkert að hafa áhyggjur að einn smellur á rangan stað skemmi orðspor þitt að eilífu. Við eru ekkert að giska á hvað þú viljir sjá, við látum þér eftir að stýra því.
PROTOCOLS, NOT PLATFORMS
SAMSKIPTAMÁTAR, EKKI KERFI
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon er ekki hefðbundinn samfélagsmiðill, heldur byggist í kringum samskiptamáta sem ekki er með miðlæga stýringu. Þú getur skráð þig á opinbera netþjóninum okkar eða valið utanaðkomandi þjón til að hýsa gögnin þín og hafa umsjón með umhverfinu þínu.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Þökk sé sameiginlegum samskiptamáta, þá skiptir ekki máli hvaða netþjón þú velur; þú átt að eiga hindranalaus samskipti við fólk á öðrum Mastodon-þjónum. En það er meira til: Með einum notandaaðgangi geturðu átt í samskiptum við önnut tengd kerfi sem tilheyra fedi-heiminum.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Ekki ánægð/ur með valið þitt? Þú getur alltaf skipt yfir á annan Mastodon-þjón og tekið fylgjendurna þína með þér. Vanir tölvulæsir notendur geta meira að segja hýst sitt eigið kerfi, enda er Mastodon opinn hugbúnaður.
NON-PROFIT IN NATURE
ÁN HAGNAÐARMARKMIÐA INN AÐ BEINI
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon er skráð sem samtök án hagnaðarmarkmiða í BNA og Þýskalandi. Við höfum engan hvata til að ná peningum út úr kerfinu, heldur liggur áhuginn í því sem er best fyrir kerfið.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
EINS OG BIRST HEFUR Í: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com og víðar.

View File

@@ -1 +1 @@
Where conversations happen
Þar sem samræður eiga sér stað

View File

@@ -1,4 +1,4 @@
Mastodon è il modo migliore per tenere il passo con ciò che sta accadendo. Segui chiunque attraverso il fediverso e guarda tutto in ordine cronologico. No algorithms, ads, or clickbait in sight.
Mastodon è il modo migliore per tenere il passo con ciò che sta accadendo. Segui chiunque attraverso il fediverso e guarda tutto in ordine cronologico. Nessun algoritmo, pubblicità o clickbait in vista.
Questa è l'app Android ufficiale per Mastodon. È incredibilmente veloce e straordinariamente bella, progettata per essere non solo potente ma anche facile da usare. Nella nostra app, puoi:
@@ -50,7 +50,7 @@ PROTOCOLLI, NON PIATTAFORME
Mastodon non è come una piattaforma di social media tradizionale, ma è costruito su un protocollo decentralizzato. Puoi registrarti sul nostro server ufficiale o sceglierne uno di terze parti, per ospitare i tuoi dati e moderare la tua esperienza.
Grazie al protocollo comune, non importa cosa tu scelga, puoi comunicare senza problemi con le persone su altri server Mastodon. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Grazie al protocollo comune, non importa cosa tu scelga, puoi comunicare senza problemi con le persone su altri server Mastodon. Ma c'è di più: con un solo account puoi comunicare con persone di altre piattaforme del Fediverso.
Non sei felice della tua scelta? Puoi sempre passare a un altro server Mastodon portando con te i tuoi seguaci. Per gli utenti avanzati, puoi persino ospitare i tuoi dati sulla tua infrastruttura, poiché Mastodon è open source.

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon で世界に起きていることを探索しよう。 Fediverse の誰でもフォローして投稿を時系列で閲覧できます。 アルゴリズム、広告、クリックベイトはありません。
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
これは Mastodon の公式 Android アプリです。 パワフルで使いやすく、燃えるように高速で驚くほど美しいアプリです。 このアプリでできること:
EXPLORE
探索
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
作家、写真家、科学者、ジャーナリスト、アーティストなど多様な人々に出会えます
世界で起きていることを目撃しましょう
READ
閲覧
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
邪魔するもののない時系列フィードで、注目している人々に追い付きましょう
ハッシュタグをフォローして、話題をリアルタイムで把握できます
CREATE
投稿
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
投票、高画質の画像や動画をフォロワーや世界に投稿できます
人々との面白い会話にも参加できます
CURATE
整理
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
投稿を見逃したくない人々はリストにまとめられます
見たくない単語やフレーズをフィルターに指定すれば、表示しません
AND MORE!
他にも!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
あなたの色、ライト/ダークに合わせた美しいテーマ
QR コードの共有とスキャンで、Mastodonプロファイルを素早く交換
複数のアカウントにログイン、切り替え
ベルボタンで、特定の人の投稿を通知
ネタバレなし! コンテンツ閲覧警告で隠して投稿
A POWERFUL PUBLISHING PLATFORM
強力な表現プラットフォームとして
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
ここでは、友達の投稿をあなたから非表示にする不透明なアルゴリズムの心配はありません。 フォローすれば、表示されます。
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
オープンウェブに公開で投稿するだけで、投稿にオープンウェブからアクセスできます。 Mastodon へのリンクを共有すれば、ログインなしで誰でも読めて問題なく知識を共有できます。
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
スレッド、投票、高画質の画像、動画、音声、コンテンツの閲覧警告も含め、Mastodon はあなた自身を表現する最適な方法を提供しています。
A POWERFUL READING PLATFORM
強力な閲覧プラットフォームとして
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
広告を表示しなくてよいので、公式アプリ以外の選択も尊重します。Mastodon にはサードパーティのアプリや外部統合の豊富な選択肢があり、最適な体験を選択できます。
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
時系列ホームフィードなら、新しい投稿をすべて読み終わったか簡単にわかります。
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
ミスクリックのせいで、興味がないおすすめを永遠に表示される心配はありません。 あなたが見たいものを私たちが推測したりせず、あなた自身の制御に委ねます。
PROTOCOLS, NOT PLATFORMS
プラットフォームではなくプロトコル
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon は従来のソーシャルメディアプラットフォームとは違い、非中央集権プロトコル上に構築されています。 私たちの公式サーバーに登録するか、サードパーティーのサーバーにデータをホストしてモデレーションに従うことも選択できます。
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
あなたがどのサーバーを選択しても、共通プロトコルにより他の Mastodon サーバーの人々とシームレスに通信できます。 それだけでなく、他の Fediverse プラットフォームの人々とも交流できます。
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
選択に満足できなくても大丈夫です! いつでもフォロワーを引き継いだまま別のサーバーに引っ越せます。 Mastodon はオープンソースのため、上級ユーザーであれば自前でデータをホスティングできます。
NON-PROFIT IN NATURE
生まれながらに非営利
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon はアメリカとドイツで非営利団体として登録されています。 私たちはプラットフォームから金銭的な価値を生み出すことではない、プラットフォームにとってよりよい目的を追求しています。
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
TIMEForbesWiredThe GuardianCNNThe VergeTechCrunchFinancial TimesGizmodoPCMAG.com などで紹介されました。

View File

@@ -1 +1 @@
Where conversations happen
話題が生まれる場所

View File

@@ -1,4 +1,4 @@
„Mastodon“ tai geriausias būdas sekti, kas vyksta. Sek bet kurį asmenį visoje fediverse ir žiūrėk viską chronologine tvarka. Jokių algoritmų, reklamų ar tyčinių paspaudimų.
„Mastodon“ tai geriausias būdas sekti, kas vyksta. Sek bet kurį asmenį visoje fediversijoje ir žiūrėk viską chronologine tvarka. Jokių algoritmų, reklamų ar tyčinių paspaudimų.
Tai oficiali „Mastodon“, skirto „Android“ programėlė. Ji yra labai sparti ir nuostabiai graži, sukurta taip, kad būtų ne tik galinga, bet ir paprasta naudoti. Mūsų programėlėje galima:
@@ -42,7 +42,7 @@ GALINGA SKAITYMO PLATFORMA
Mums nereikia rodyti reklamų, todėl mums nereikia tave laikyti savo programėlėje. „Mastodon“ turi gausiausią trečiųjų šalių programėlių ir integracijų pasirinkimą, tad gali pasirinkti tau tinkamiausią patirtį.
Dėl chronologinio pagrindinio srauto lengva nustatyti, kada jau esi pasiekęs (-usi) visus naujinimus ir gali pereiti prie ko nors kito.
Dėl chronologinio pagrindinio srauto lengva nustatyti, kada jau pasiekei visas naujienas ir gali pereiti prie ko nors kito.
Nereikia nerimauti, kad neteisingas spustelėjimas sugadins tavo rekomendacijas visiems laikams. Mes nenuspėjame, ką nori matyti, o leidžiame tau tai valdyti.
@@ -50,9 +50,9 @@ PROTOKOLAI, O NE PLATFORMOS
„Mastodon“ nėra panaši į tradicinę socialinės medijos platformą, bet sukurta pagal decentralizuotą protokolą. Gali užsiregistruoti mūsų oficialiame serveryje arba pasirinkti trečiąją šalį, kuri patalpins tavo duomenis ir palengvins patirtį.
Dėl bendro protokolo, nesvarbu, ką pasirinktum, gali sklandžiai bendrauti su žmonėmis, esančiais kituose „Mastodon“ serveriuose. Bet yra ir daugiau: naudojant tik viena paskyra gali bendrauti su žmonėmis iš kitų fediversų platformų.
Dėl bendro protokolo, nesvarbu, ką pasirinktum, gali sklandžiai bendrauti su žmonėmis kituose „Mastodon“ serveriuose. Bet yra ir daugiau: su tik viena paskyra gali bendrauti su žmonėmis iš kitų fediversų platformų.
Nesi patenkintas (-a) savo pasirinkimu? Visada gali pereiti į kitą „Mastodon“ serverį ir kartu su savimi pasiimti sekėjus. Pažengę naudotojai gali net talpinti duomenis savo infrastruktūroje, nes „Mastodon“ yra atvirojo kodo.
Nepatenkinti savo pasirinkimu? Visada gali pereiti į kitą „Mastodon“ serverį ir kartu su savimi pasiimti sekėjus. Patyrę naudotojai gali net talpinti duomenis savo infrastruktūroje, nes „Mastodon“ yra atvirojo kodo.
NE PELNO SIEKIANTIS POBŪDIS

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon is de beste manier om op de hoogte te blijven van wat er gebeurt. Volg iedereen in de fediverse en zie alles in chronologische volgorde. Geen algoritmes, advertenties of clickbait te bekennen.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Dit is de officiële Android-app voor Mastodon. Het is razendsnel en verbluffend mooi. Ontworpen om niet alleen krachtig, maar ook gebruiksvriendelijk te zijn. In onze app kun je:
EXPLORE
VERKENNEN
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Nieuwe schrijvers, journalisten, kunstenaars, fotografen, wetenschappers en meer ontdekken
Zie wat er in de wereld gebeurt
READ
LEZEN
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Op de hoogte blijven van mensen om wie je geeft, op een chronologische tijdlijn en zonder onderbrekingen
Hashtags in realtime volgen om op de hoogte te blijven van specifieke onderwerpen
CREATE
CREËREN
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
Berichten naar alleen je volgers sturen of naar de hele wereld, met peilingen, afbeeldingen van hoge kwaliteit en video's
Aan interessante gesprekken met andere mensen deelnemen
CURATE
CUREREN
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
Lijsten maken met mensen die je volgt, om zo nooit een bericht te missen
Woorden of zinnen filteren om te bepalen wat je wel en niet wilt zien
AND MORE!
EN MEER!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Een prachtig thema dat zich aanpast aan je persoonlijke kleurenschema, licht of donker
■ QR-codes scannen om snel Mastodon-profielen met andere mensen te delen
Meerdere accounts gebruiken
Meldingen van specifiek personen ontvangen, wanneer deze berichten plaatsen
Geen spoilers! ■ Je kunt je berichten achter inhoudswaarschuwingen plaatsen
A POWERFUL PUBLISHING PLATFORM
EEN KRACHTIG PUBLICATIEPLATFORM
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Je hoeft niet langer te proberen een ondoorzichtig algoritme tevreden te stellen, dat beslist of je vrienden wel of niet mogen zien wat je hebt geplaatst. Als ze je volgen, zullen ze het zien.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Wanneer je het in de fediverse plaatst, is het ook toegankelijk in de fediverse. Je kunt veilig links naar Mastodon delen, in de wetenschap dat iedereen ze kan lezen zonder in te hoeven loggen.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Met gesprekken, peilingen, afbeeldingen van hoge kwaliteit, video's, audio en inhoudswaarschuwingen biedt Mastodon genoeg manieren om jezelf te kunnen uiten, op een manier die bij jou past.
A POWERFUL READING PLATFORM
EEN KRACHTIG LEESPLATFORM
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
We hoeven je geen advertenties te laten zien, dus we hoeven je ook niet voor onze app te behouden. Mastodon heeft de rijkste keuze aan door derden ontwikkelde apps en integraties, zodat je de gebruikerservaring kunt kiezen die het beste bij je past.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Dankzij de chronologische tijdlijn is het makkelijk om te zien wanneer je alle updates hebt bekeken en met iets anders kunt verder gaan.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Je hoeft niet bang te zijn dat wanneer je op iets verkeerds klikt, je aanbevelingen voor altijd zijn verpest. We gissen niet naar wat je wilt zien, we laten het aan jou over.
PROTOCOLS, NOT PLATFORMS
PROTOCOLLEN, GEEN PLATFORMS
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon is niet zoals een traditioneel social media platform, maar is gebouwd op een gedecentraliseerd protocol. Je kunt voor onze officiële server of voor eentje van een derde partij kiezen om je gegevens te hosten en gebruikers te modereren.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Dankzij het gemeenschappelijke protocol kun je, ongeacht wat je kiest, naadloos met mensen op andere Mastodon-servers communiceren. Maar er is meer: Met slechts één account kun je met mensen van andere fediverse-platforms communiceren.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Niet tevreden met je keuze? Je kunt altijd naar een andere Mastodon-server overstappen en je volgers meenemen. Gevorderde gebruikers kunnen hun gegevens zelfs op hun eigen infrastructuur hosten, aangezien Mastodon open-source is.
NON-PROFIT IN NATURE
NON-PROFIT VAN AARD
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon is een geregistreerde non-profit organisatie in de Verenigde Staten en Duitsland. We halen onze motivatie niet uit het commercieel uitbuiten van Mastodon, maar uit wat het beste is voor Mastodon.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
VERMELD IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com en meer.

View File

@@ -1 +1 @@
Where conversations happen
Waar gesprekken ontstaan

View File

@@ -0,0 +1,61 @@
O Mastodon é a melhor maneira de acompanhar o que está acontecendo. Siga qualquer pessoa durante o fediverso e veja tudo em ordem cronológica. Sem algoritmos, anúncios ou clickbait em vista.
Este é o aplicativo oficial do Mastodon para Android. Está a explodir rapidamente e incrivelmente bonito, projetado para ser não só poderoso mas também fácil de usar. Em nosso aplicativo, você pode:
EXPLORAR
Descobrir novos escritores, jornalistas, artistas, fotógrafos, cientistas e muito mais
Veja o que está acontecendo no mundo 🌎
LEIA
Mostra com as pessoas que você se importa em um feed cronológico e sem interrupções
Siga hashtags para acompanhar tópicos específicos em tempo real
CRIE
Publique para os seus seguidores ou para o mundo inteiro: pesquisas, imagens e vídeos de alta qualidade
Participe de conversas interessantes com outras pessoas
CURE
★ Crie uma lista de pessoas para nunca mais perder uma publicação
+ Filtre palavras ou frases para controlar o que você faz e o que não quer ver
E MAIS!
★ Um belo tema que se adapta ao seu esquema personalizado de cores, claro ou escuro
Compartilhe e digitalize os códigos QR para trocar rapidamente os perfis de Mastodon com outros
Entre e alterne entre várias contas
Seja notificado quando uma pessoa fizer uma publicação específica com o botão do sino 🔔
Nenhum spoiler! Você pode colocar as suas publicações atrás de avisos de conteúdo
UMA PLATAFORMA DE PUBLICAÇÃO PODEROSA ✨
Você não precisa mais tentar apaziguar um algoritmo opaco que decide se seus amigos vão ver o que você postou. Se seguirem você, verão isso.
Se você publicá-lo na web aberta, ele é acessível na web aberta. Você pode compartilhar com segurança links para o Mastodon sabendo que qualquer pessoa será capaz de lê-los sem fazer o login.
Entre threads, pesquisas, imagens de alta qualidade, vídeos, avisos de áudio e conteúdo, Mastodon oferece muitas maneiras de se expressar de uma forma que melhor lhe convém.
UMA PLATAFORMA DE LEITURA PODEROSA ✨
Não precisamos lhe mostrar anúncios, então não precisamos te manter em nosso aplicativo. O Mastodon tem a seleção mais rica de apps de terceiros e integrações para que você possa escolher a melhor experiência para você.
Graças ao feed cronológico, é fácil dizer quando se encontra em todas as atualizações e pode ir para outra coisa.
Não há necessidade de se preocupar com que um clique errado irá arruinar as suas recomendações para sempre. Não adivinhamos o que você quer ver, deixamos que você o controle.
PROTOCOLOS, NÃO PLATAFORMAS
O Mastodon não é como uma plataforma de mídia social tradicional, mas é construído em um protocolo descentralizado. Você pode se inscrever em nosso servidor oficial ou escolher um terceiro para disponibilizar seus dados e moderar sua experiência.
Graças ao protocolo comum, não importa o que escolher, você pode se comunicar perfeitamente com as pessoas de outros servidores Mastodon. E têm mais: com apenas uma conta, você pode se comunicar com pessoas de outras plataformas fediversas.
Não está satisfeito com sua escolha? Você sempre pode mudar para um servidor Mastodon diferente enquanto leva seus seguidores com você. Para usuários avançados, você pode até mesmo hospedar seus dados em sua própria infraestrutura, já que o Mastodon é de código aberto.
SEM FINS LUCRATIVOS
Mastodon é uma instituição sem fins lucrativos registada nos EUA e na Alemanha. Nós não somos motivados extraindo o valor monetário da plataforma, mas pelo que é melhor para a plataforma.
DESTAQUE EM: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com e muito mais.

View File

@@ -0,0 +1 @@
Onde as conversas acontecem

View File

@@ -1,4 +1,4 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
O Mastodon é a melhor maneira de manter com que está a acontecer. Segue qualquer um durante o fediverso e vê tudo em ordem cronológica. No algorithms, ads, or clickbait in sight.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon - лучший способ быть в курсе всего происходящего. Следуйте за любым человеком по всей федеральной вселенной и смотрите все в хронологическом порядке. Никаких алгоритмов, рекламы или кликбейта.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Это официальное приложение для Android от Mastodon. Он молниеносно быстрый и потрясающе красивый, разработанный, чтобы быть не только мощным, но и простым в использовании. В нашем приложении вы можете:
EXPLORE
ИССЛЕДУЙТЕ
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Откройте для себя новых писателей, журналистов, художников, фотографов, ученых и многое другое
Узнайте, что происходит в мире
READ
ЧИТАТЬ
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Следите за людьми, которые вам интересны, в хронологической ленте без прерываний
Следите за хэштегами, чтобы быть в курсе конкретных тем в режиме реального времени
CREATE
СОЗДАТЬ
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
Отправьте сообщение своим последователям или всему миру, используя опросы, высококачественные изображения и видео
Участвуйте в интересных беседах с другими людьми
CURATE
КУРАТОР
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
Создавайте списки людей, чтобы не пропустить ни одного сообщения
Фильтруйте слова и фразы, чтобы контролировать то, что вы хотите и не хотите видеть
AND MORE!
И БОЛЬШЕ!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Красивая тема, которая адаптируется к вашей индивидуальной цветовой схеме, светлой или темной
Обменивайтесь и сканируйте QR-коды, чтобы быстро обмениваться профилями Mastodon с другими людьми
Вход в систему и переключение между несколькими учетными записями
Получайте уведомления о сообщениях конкретного человека с помощью кнопки "звонок"
Никаких спойлеров! Вы можете поместить свои сообщения за предупреждениями о содержании
A POWERFUL PUBLISHING PLATFORM
МОЩНАЯ ИЗДАТЕЛЬСКАЯ ПЛАТФОРМА
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Вам больше не нужно пытаться угодить непрозрачному алгоритму, который решает, увидят ли ваши друзья то, что вы опубликовали. Если они будут следить за вами, то увидят это.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Если вы публикуете его в открытом интернете, он становится доступным в открытом интернете. Вы можете смело делиться ссылками на Mastodon, не сомневаясь, что любой пользователь сможет прочитать их, не заходя на сайт.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Благодаря темам, опросам, высококачественным изображениям, видео, аудио и предупреждениям о содержании, Mastodon предлагает множество способов выразить себя так, как вам удобно.
A POWERFUL READING PLATFORM
МОЩНАЯ ПЛАТФОРМА ДЛЯ ЧТЕНИЯ
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Нам не нужно показывать вам рекламу, поэтому нам не нужно удерживать вас в нашем приложении. У Mastodon самый богатый выбор сторонних приложений и интеграций, поэтому вы можете выбрать то, что подходит вам больше всего.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Благодаря хронологической главной ленте легко определить, когда вы проследили за всеми обновлениями и можете переходить к чему-то другому.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Не нужно беспокоиться, что один неверный щелчок навсегда испортит ваши рекомендации. Мы не угадываем, что вы хотите увидеть, мы позволяем вам управлять этим.
PROTOCOLS, NOT PLATFORMS
ПРОТОКОЛЫ, А НЕ ПЛАТФОРМЫ
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon не похож на традиционную платформу социальных сетей, он построен на децентрализованном протоколе. Вы можете зарегистрироваться на нашем официальном сервере или выбрать стороннюю компанию для размещения ваших данных и модерации вашего опыта.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Благодаря общему протоколу, независимо от того, что вы выберете, вы сможете легко общаться с людьми на других серверах Mastodon. Но это еще не все: С помощью одной учетной записи вы можете общаться с людьми с других платформ fediverse.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Не довольны своим выбором? Вы всегда можете перейти на другой сервер Mastodon, забрав с собой своих подписчиков. Опытные пользователи могут даже размещать свои данные на собственной инфраструктуре, поскольку Mastodon имеет открытый исходный код.
NON-PROFIT IN NATURE
НЕКОММЕРЧЕСКИЙ ХАРАКТЕР
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon является зарегистрированной некоммерческой организацией в США и Германии. Мы руководствуемся не стремлением извлечь из платформы денежную выгоду, а тем, что лучше для платформы.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
Опубликованы в: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com и других.

View File

@@ -1 +1 @@
Where conversations happen
Где происходят беседы

View File

@@ -0,0 +1,61 @@
Mastodon is the best way to keep up with whats happening. Sledujte kohokoľvek naprieč fediversom a prezerajte všetko chronologicky. No algorithms, ads, or clickbait in sight.
Toto je oficiálna Mastodon aplikácia pre Android. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
EXPLORE
■ Discover new writers, journalists, artists, photographers, scientists and more
■ See whats happening in the world
READ
■ Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
CREATE
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people
CURATE
■ Create lists of people to never miss a post
■ Filter words or phrases to control what you do and dont want to see
AND MORE!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
■ Login and switch between multiple accounts
■ Get notified when a specific person posts with the bell button
■ No spoilers! You can put your posts behind content warnings
A POWERFUL PUBLISHING PLATFORM
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
A POWERFUL READING PLATFORM
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
PROTOCOLS, NOT PLATFORMS
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
NON-PROFIT IN NATURE
Mastodon je nezisková organizácia registrovaná v Spojených Štátoch a Nemecku. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.

View File

@@ -0,0 +1 @@
Where conversations happen

View File

@@ -2,7 +2,7 @@ Mastodon is the best way to keep up with whats happening. Follow anyone acros
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
EXPLORE
UTFORSKA
■ Discover new writers, journalists, artists, photographers, scientists and more
■ See whats happening in the world
@@ -12,7 +12,7 @@ READ
■ Keep up with people you care about in a chronological feed with no interruptions
■ Follow hashtags to keep up with specific topics in real time
CREATE
SKAPA
■ Post to your followers or the whole world, with polls, high quality images and videos
■ Participate in interesting conversations with other people

View File

@@ -1,36 +1,36 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon เป็นวิธีที่ดีที่สุดที่จะติดตามสิ่งที่กำลังเกิดขึ้น ติดตามใครก็ตามทั่วทั้งจักรวาลสหพันธ์และดูจักรวาลสหพันธ์ทั้งหมดตามลำดับเวลา ไม่มีอัลกอริทึม, โฆษณา หรือคลิกเบตอยู่ในสายตา
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
นี่คือแอป Android อย่างเป็นทางการสำหรับ Mastodon แอปรวดเร็วมากและสวยงามอย่างน่าทึ่ง ได้รับการออกแบบให้ไม่ใช่แค่ทรงพลังแต่ยังใช้งานง่ายอีกด้วย ในแอปของเรา คุณสามารถ:
EXPLORE
สำรวจ
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
ค้นพบนักเขียน, นักข่าว, ศิลปิน, ช่างภาพ, นักวิทยาศาสตร์ และอื่น ๆ ใหม่ ๆ
ดูสิ่งที่กำลังเกิดขึ้นในโลก
READ
อ่าน
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
ติดตามผู้คนที่คุณห่วงใยในฟีดตามลำดับเวลาโดยไม่มีการขัดจังหวะ
ติดตามแฮชแท็กเพื่อติดตามหัวข้อที่เฉพาะเจาะจงตามเวลาจริง
CREATE
สร้าง
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
โพสต์ไปยังผู้ติดตามของคุณหรือทั้งโลก พร้อมการสำรวจความคิดเห็น, ภาพคุณภาพสูง และวิดีโอ
มีส่วนร่วมในการสนทนาที่น่าสนใจกับผู้คนอื่น ๆ
CURATE
เรียบเรียง
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
สร้างรายการผู้คนเพื่อไม่พลาดโพสต์ใด
กรองคำหรือวลีเพื่อควบคุมสิ่งที่คุณต้องการและไม่ต้องการเห็น
AND MORE!
และอื่น ๆ!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
ชุดรูปแบบที่สวยงามที่ปรับให้เข้ากับแบบแผนชุดสีเฉพาะบุคคลของคุณ สว่างหรือมืด
แชร์และสแกนรหัส QR เพื่อแลกเปลี่ยนโปรไฟล์ Mastodon กับผู้อื่นอย่างรวดเร็ว
เข้าสู่ระบบและสลับระหว่างหลายบัญชี
รับการแจ้งเตือนเมื่อบุคคลที่เฉพาะเจาะจงโพสต์ด้วยปุ่มกระดิ่ง
ไม่มีผู้สปอยล์! คุณสามารถนำโพสต์ของคุณไว้หลังคำเตือนเนื้อหา
A POWERFUL PUBLISHING PLATFORM
แพลตฟอร์มการเผยแพร่ที่ทรงพลัง
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
@@ -38,7 +38,7 @@ If you publish it to the open web, its accessible on the open web. You can sa
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
A POWERFUL READING PLATFORM
แพลตฟอร์มการอ่านที่ทรงพลัง
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
@@ -46,16 +46,16 @@ Thanks to the chronological home feed, its easy to tell when youve caught
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
PROTOCOLS, NOT PLATFORMS
โปรโตคอล ไม่ใช่แพลตฟอร์ม
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
ไม่พอใจกับตัวเลือกของคุณ? คุณสามารถสลับเป็นเซิร์ฟเวอร์ Mastodon อื่นได้เสมอพร้อมนำผู้ติดตามของคุณไปกับคุณ สำหรับผู้ใช้ขั้นสูง คุณยังสามารถโฮสต์ข้อมูลของคุณบนโครงสร้างพื้นฐานของคุณเองได้อีกด้วย เนื่องจาก Mastodon เป็นโอเพนซอร์ส
NON-PROFIT IN NATURE
ไม่แสวงหาผลกำไรโดยธรรมชาติ
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนในสหรัฐอเมริกาและเยอรมนี We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
ตามที่นำเสนอใน: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com และอื่น ๆ

View File

@@ -1 +1 @@
Where conversations happen
ที่ซึ่งการสนทนาเกิดขึ้น

View File

@@ -1,16 +1,16 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Olan biteni takip etmenin en iyi yolu. Herhangi birini federe ağında takip edin ve her şeyi kronolojik sırayla görün. Görünürde algoritma, reklam veya tıklama tuzağı yok.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Bu, aşağıdakiler için resmi uygulamasıdır. Sadece güçlü değil aynı zamanda kullanımı kolay olacak şekilde tasarlanmış, son derece hızlı ve şaşırtıcı derecede güzel. Uygulamamızda şunları yapabilirsiniz:
EXPLORE
KEŞFET
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Yeni yazarlar, gazeteciler, sanatçılar, fotoğraılar, bilim insanları ve daha fazlasını keşfedin
Dünyada neler olup bittiğini görün
READ
OKU
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Önem verdiğiniz kişileri kesintisiz bir kronolojik akışta takip edin
Belirli konuları gerçek zamanlı olarak takip etmek için etiketleri takip edin
CREATE
@@ -25,37 +25,37 @@ CURATE
AND MORE!
■ A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Profillerini başkalarıyla hızlıca paylaşmak için QR kodlarını paylaşın ve tarayın
Giriş yapın ve birden fazla hesap arasında geçiş yapın
Zil butonu ile belirli bir kişi paylaşım yaptığında bildirim alın
İpucu yok! Yayınlarınızı içerik uyarılarının arkasına koyabilirsiniz
A POWERFUL PUBLISHING PLATFORM
GÜÇLÜ BIR YAYINCILIK PLATFORMU
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Artık arkadaşlarınızın paylaştıklarını görüp görmeyeceğine karar veren şeffaf olmayan bir algoritmayı yatıştırmaya çalışmak zorunda değilsiniz. Sizi takip ederlerse görürler.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Eğer bunu açık olarak yayınlarsanız, herkese açık olarak erişilebilir olur. Herkesin giriş yapmadan okuyabileceğinden emin olarak bağlantılarını güvenle paylaşabilirsiniz.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Başlıklar, anketler, yüksek kaliteli görüntüler, videolar, sesler ve içerik uyarıları arasında, kendinizi size uygun bir şekilde ifade etmenin birçok yolunu sunar.
A POWERFUL READING PLATFORM
GÜÇLÜ BİR OKUMA PLATFORMU
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Size reklam göstermemize gerek yok, bu yüzden sizi uygulamamızda tutmamıza da gerek yok. Sosyal ağımız, size en uygun deneyimi seçebileceğiniz en zengin 3. parti uygulama ve entegrasyon seçeneklerine sahiptir.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Kronolojik ana sayfa akışı sayesinde, tüm güncellemeleri ne zaman yakaladığınızı ve başka bir şeye geçebileceğinizi anlamak kolaydır.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Yanlış bir tıklamanın önerilerinizi sonsuza dek mahvedeceğinden endişelenmenize gerek yok. Ne görmek istediğinizi tahmin etmiyoruz, sizin kontrol etmenize izin veriyoruz.
PROTOCOLS, NOT PLATFORMS
PLATFORMLAR DEĞİL, PROTOKOLLER
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Sosyal ağımız geleneksel bir sosyal medya platformu gibi değil, merkezi olmayan bir protokol üzerine inşa edilmiştir. Resmi sunucumuza kayıt olabilir veya verilerinizi barındırmak ve deneyiminizi denetlemek için 3. bir taraf seçebilirsiniz.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Ortak protokol sayesinde, neyi seçerseniz seçin, diğer sunucularındaki kişilerle sorunsuz bir şekilde iletişim kurabilirsiniz. Ama dahası da var: Sadece tek bir hesapla, diğer federe platformlarındaki kişilerle iletişim kurabilirsiniz.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Seçimden memnun değil misiniz? Takipçilerini yanınızda götürürken her zaman farklı bir sunucusuna geçebilirsiniz. İleri düzey kullanıcılar için, Açık kaynaklı olduğundan verilerinizi kendi altyapınızda bile barındırabilir.
NON-PROFIT IN NATURE
KÂR AMACI GÜTMEYEN NİTELİKTE
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastıdon,ABD ve Almanya'da kâr amacı gütmeyen kayıtlı bir kuruluştur. Platformdan parasal değer elde etmek için değil, platform için en iyi olanı yapmak için motive oluyoruz.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
YER ALDIĞI GİBİ: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, ve dahası.

View File

@@ -1 +1 @@
Where conversations happen
Konuşmaların gerçekleştiği yer

View File

@@ -1,61 +1,61 @@
Mastodon is the best way to keep up with whats happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
Mastodon найкращий спосіб бути в курсі подій. Слідкуйте за ким завгодно у всьому fediverse та дивіться все в хронологічному порядку. Немає алгоритмів, реклами чи наживки для натискань.
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Це офіційний застосунок для Android для Mastodon. Він блискавично швидкий і приголомшливо красивий, розроблений, щоб бути не тільки потужним, але й простим у використанні. У нашому застосунку ви можете:
EXPLORE
ДОСЛІДЖУВАТИ
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Відкрийте для себе нових письменників, журналістів, художників, фотографів, науковців та інших
Подивіться, що відбувається у світі
READ
ЧИТАТИ
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Будьте в курсі людей, які вам небайдужі, у хронологічній стрічці без переривання
Слідкуйте за хеш-тегами, щоб бути в курсі певних тем у реальному часі
CREATE
СТВОРЮВАТИ
Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
Публікуйте дописи для своїх підписників або для всього світу з опитуваннями, високоякісними зображеннями та відео
Беріть участь у цікавих бесідах з іншими людьми
CURATE
КЕРУВАТИ
Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
Створюйте списки людей, щоб ніколи не пропускати публікації
Фільтруйте слова чи фрази, щоб контролювати, що ви робите, а що не хочете бачити
AND MORE!
І БІЛЬШЕ!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Красива тема, яка адаптується до вашої персоналізованої колірної схеми, світлої чи темної
Діліться та скануйте QR-коди, щоб швидко обмінюватися профілями Mastodon з іншими
Увійдіть і перемикайтеся між кількома обліковими записами
Отримуйте сповіщення, коли певна особа публікує повідомлення за допомогою кнопки дзвінка
Без спойлерів! Ви можете розміщувати свої публікації з попередженням про вміст
A POWERFUL PUBLISHING PLATFORM
ПОТУЖНА ПЛАТФОРМА ПУБЛІКАЦІЙ
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.
Вам більше не потрібно намагатися заспокоїти непрозорий алгоритм, який вирішує, чи побачать ваші друзі те, що ви опублікували. Якщо вони слідкують за вами, вони це побачать.
If you publish it to the open web, its accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Якщо ви опублікуєте його у відкритому інтернеті, він стане доступним у відкритому інтернеті. Ви можете сміливо ділитися посиланнями на Mastodon, знаючи, що будь-хто зможе їх прочитати, не авторизуючись.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Окрім тем, опитувань, високоякісних зображень, відео, аудіо та попереджень щодо вмісту, Mastodon пропонує безліч способів виразити себе у спосіб, який вам підходить.
A POWERFUL READING PLATFORM
ПОТУЖНА ПЛАТФОРМА ДЛЯ ЧИТАННЯ
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Нам не потрібно показувати вам рекламу, тому нам не потрібно тримати вас у нашому додатку. Mastodon має найбагатший вибір сторонніх додатків та інтеграцій, тож ви можете вибрати той досвід, який вам найбільше підходить.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
Завдяки хронологічній домашній стрічці легко визначити, коли ви наздогнали всі оновлення та можете перейти до чогось іншого.
No need to worry that a misclick will ruin your recommendations forever. We dont guess what you want to see, we let you control it.
Не потрібно хвилюватися, що неправильне натискання назавжди зіпсує ваші рекомендації. Ми не вгадуємо, що ви хочете побачити, ми дозволяємо вам контролювати це.
PROTOCOLS, NOT PLATFORMS
ПРОТОКОЛИ, А НЕ ПЛАТФОРМИ
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
Mastodon не схожий на традиційну платформу соціальних мереж, він побудований на основі децентралізованого протоколу. Ви можете зареєструватися на нашому офіційному сервері або вибрати третю сторону для розміщення ваших даних і модерування вашого досвіду.
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But theres more: With just one account, you can communicate with people from other fediverse platforms.
Завдяки загальному протоколу, незалежно від того, що ви виберете, ви можете безперешкодно спілкуватися з людьми на інших серверах Mastodon. Але є ще більше: Лише з одним обліковим записом ви можете спілкуватися з людьми з інших платформ fediverse.
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
Не задоволені своїм вибором? Ви завжди можете перейти на інший сервер Mastodon, взявши з собою підписників. Для досвідчених користувачів ви навіть можете розмістити свої дані у власній інфраструктурі, оскільки Mastodon є відкритим кодом.
NON-PROFIT IN NATURE
НЕПРИБУТКОВИЙ ХАРАКТЕР
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
Mastodon є зареєстрованою неприбутковою організацією в США та Німеччині. Нас мотивує не отримання грошової цінності з платформи, а те, що найкраще для платформи.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
ПРЕДСТАВЛЕНО В: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com тощо.

View File

@@ -1 +1 @@
Where conversations happen
Де відбуваються розмови

View File

@@ -0,0 +1,61 @@
Mastodon 是了解最新动态的最佳途径。 横跨联邦宇宙关注其他人,并在一个时间顺序中查看。 没有算法、广告或诱导链接。
这是 Mastodon 官方的 Android 应用程序。 它风驰电掣般地快而且让你惊叹的美丽,拥有强大而易用的设计。 在我们的应用中,您可以:
探索新鲜事
■ 发现新的作家、记者、画家、摄影师和科学家以及更多
■ 看看世界上正在发生什么
阅读
■ 在时间顺序流中跟上你关心的人,没有打断
■ 关注标签以实时关注具体主题
创建
■ 用投票、高质量图像和视频向粉丝或整个世界发嘟
■ 与其他人一起参与有趣的对话
组织与整理
■ 创建用户列表,不错过任何帖子
■ 利用单词与短语过滤功能来控制你想看到什么
还有更多!
■ 一个美丽的主题,符合您的个性化主题色,无论明亮或黑暗
■ 分享并扫描二维码以便与其他人快速交换 Mastodon 个人资料
■ 登录并在多个账户间切换
■ 点铃铛按钮,获得特定的人发嘟时的通知
■ 禁止剧透! 你可以将你的嘟文键入内容警告中
强大的发表平台
你不再需要尝试迎合不透明的算法,来决定你的朋友是否能看到你发布的内容。 如果朋友们关注你,他们就会看到嘟文。
如果你在一个公开的网络上发布嘟文,那嘟文就可以在那里访问。 你可以安全地分享到 Mastodon 的链接,知道任何人都可以在不登录的情况下阅读它们。
在讨论串、投票、高质量图像、视频、音频和内容警告之中Mastodon 提供了大量适合你的表达方式。
强大的阅读平台
我们无需向您展示广告,所以我们不会挽留让您留在我们的应用中。 Mastodon 有最丰富的第三方应用和集成,您可以选择最适合您的体验。
感谢按时间顺序提供的首页流,你很容易就能知道自己什么时候看完了所有的更新,可以继续看别的东西了。
永远不用担心点一下喜欢就会污染你的推荐列表。 我们不猜你想看到什么,我们让你自己控制它。
成为一种协议,而非平台
Mastodon 不像传统的社交媒体平台,而是建立在一个去中心化的协议之上。 您可以在我们的官方服务器上注册,或者选择第三方托管您的数据并保持相似的体验。
感谢同样的协议,不管你选择哪个服务器,你都可以与其他 Mastodon 服务器上与人无缝跨服聊天。 并且:只要同一个帐户,您就可以与其他联邦宇宙的人通信。
对你的选择不满意吗? 您随时可以带着您的粉丝切换到另一个 Mastodon 服务器。 对于进阶用户,你甚至可以在您自己的基础设施上托管您的数据,因为 Mastodon 是开源的。
天生非盈利性
Mastodon 是在美国与德国注册的非营利机构。 我们的初衷不是从平台上提取金钱价值,而是为了打造最好的给平台。
目前入驻《时代》、《福布斯》、《连线》、The Guardian、美国有线电视新闻网、The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com还有更多。

View File

@@ -0,0 +1 @@
对话发生的场所

View File

@@ -13,8 +13,8 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 34
versionCode 112
versionName "2.6.1"
versionCode 128
versionName "2.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -90,7 +90,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.3.0'
implementation 'me.grishka.appkit:appkit:1.4.4'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -35,6 +35,10 @@
*;
}
-keepnames public class org.joinmastodon.android.api.session.**{
*;
}
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
@com.squareup.otto.Subscribe <methods>;

View File

@@ -1,10 +1,8 @@
package org.joinmastodon.android.test;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -14,7 +12,7 @@ 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.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.instance.GetInstanceV2;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -22,6 +20,7 @@ import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.junit.Assert;
@@ -32,12 +31,9 @@ import org.parceler.Parcels;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
@@ -47,19 +43,19 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.Screenshot;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import okio.BufferedSink;
import okio.Okio;
import okio.Sink;
import okio.Source;
import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@RunWith(AndroidJUnit4.class)
@LargeTest
@@ -148,10 +144,10 @@ public class StoreScreenshotsGenerator{
takeScreenshot("Thread");
Instance[] _instance={null};
new GetInstance()
new GetInstanceV2()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
public void onSuccess(InstanceV2 result){
_instance[0]=result;
try{
barrier.await();

View File

@@ -23,7 +23,11 @@
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
<data android:scheme="http" android:host="*"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https" android:host="*"/>
</intent>
</queries>

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android;
import android.content.Context;
import android.content.SharedPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
@@ -11,6 +12,11 @@ public class GlobalUserPreferences{
public static boolean useCustomTabs;
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
public static ThemePreference theme=ThemePreference.AUTO;
public static boolean useDynamicColors;
public static boolean showInteractionCounts;
public static boolean customEmojiInNames;
public static boolean showCWs;
public static boolean hideSensitiveMedia;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
@@ -29,6 +35,24 @@ public class GlobalUserPreferences{
confirmBoost=prefs.getBoolean("confirmBoost", false);
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
useDynamicColors=prefs.getBoolean("useDynamicColors", true);
showInteractionCounts=prefs.getBoolean("interactionCounts", true);
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
showCWs=prefs.getBoolean("showCWs", true);
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
if(!prefs.getBoolean("perAccountMigrationDone", false)){
AccountSession account=AccountSessionManager.getInstance().getLastActiveAccount();
if(account!=null){
SharedPreferences accPrefs=account.getRawLocalPreferences();
showInteractionCounts=accPrefs.getBoolean("interactionCounts", true);
customEmojiInNames=accPrefs.getBoolean("emojiInNames", true);
showCWs=accPrefs.getBoolean("showCWs", true);
hideSensitiveMedia=accPrefs.getBoolean("hideSensitive", true);
save();
}
// Also applies to new app installs
prefs.edit().putBoolean("perAccountMigrationDone", true).apply();
}
}
public static void save(){
@@ -40,6 +64,11 @@ public class GlobalUserPreferences{
.putBoolean("confirmUnfollow", confirmUnfollow)
.putBoolean("confirmBoost", confirmBoost)
.putBoolean("confirmDeletePost", confirmDeletePost)
.putBoolean("useDynamicColors", useDynamicColors)
.putBoolean("interactionCounts", showInteractionCounts)
.putBoolean("emojiInNames", customEmojiInNames)
.putBoolean("showCWs", showCWs)
.putBoolean("hideSensitive", hideSensitiveMedia)
.apply();
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android;
import android.Manifest;
import android.app.Application;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -16,6 +17,7 @@ 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.AssistContentProviderFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
@@ -229,4 +231,11 @@ public class MainActivity extends FragmentStackActivity{
return null;
return getFragmentManager().findFragmentById(fragmentContainers.get(fragmentContainers.size()-1).getId());
}
@Override
public void onProvideAssistContent(AssistContent outContent){
if(getTopmostFragment() instanceof AssistContentProviderFragment provider){
provider.onProvideAssistContent(outContent);
}
}
}

View File

@@ -6,7 +6,6 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
@@ -79,7 +78,7 @@ public class OAuthActivity extends Activity{
progress.dismiss();
}
})
.exec(instance.uri, token);
.exec(instance.getDomain(), token);
}
@Override
@@ -88,7 +87,7 @@ public class OAuthActivity extends Activity{
progress.dismiss();
}
})
.execNoAuth(instance.uri);
.execNoAuth(instance.getDomain());
}
private void handleError(ErrorResponse error){

View File

@@ -23,6 +23,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -36,6 +37,8 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.imageloader.ImageLoaderCallback;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@@ -103,12 +106,31 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
if(TextUtils.isEmpty(pn.icon)){
doNotify(context, pn, accountID, notification, null);
}else{
ImageCache.getInstance(context).get(new UrlImageLoaderRequest(pn.icon, V.dp(50), V.dp(50)), null, new ImageLoaderCallback(){
@Override
public void onImageLoaded(ImageLoaderRequest req, Drawable image){
doNotify(context, pn, accountID, notification, image);
}
@Override
public void onImageLoadingFailed(ImageLoaderRequest req, Throwable error){
doNotify(context, pn, accountID, notification, null);
}
}, true);
}
}
private void doNotify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification, Drawable avatar){
NotificationManager nm=context.getSystemService(NotificationManager.class);
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
Notification.Builder builder;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
boolean hasGroup=false;
int version=AccountSessionManager.get(accountID).getRawLocalPreferences().getInt("notificationChannelsVersion", 1);
List<NotificationChannelGroup> channelGroups=nm.getNotificationChannelGroups();
for(NotificationChannelGroup group:channelGroups){
if(group.getId().equals(accountID)){
@@ -116,7 +138,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
break;
}
}
if(!hasGroup){
if(!hasGroup || version!=2){
NotificationChannelGroup group=new NotificationChannelGroup(accountID, accountName);
nm.createNotificationChannelGroup(group);
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
@@ -129,6 +151,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
})
.collect(Collectors.toList());
nm.createNotificationChannels(channels);
AccountSessionManager.get(accountID).getRawLocalPreferences().edit().putInt("notificationChannelsVersion", 2).apply();
}
builder=new Notification.Builder(context, accountID+"_"+pn.notificationType);
}else{
@@ -136,7 +159,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setPriority(Notification.PRIORITY_DEFAULT)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
}
Drawable avatar=ImageCache.getInstance(context).get(new UrlImageLoaderRequest(pn.icon, V.dp(50), V.dp(50)));
Intent contentIntent=new Intent(context, MainActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
contentIntent.putExtra("fromNotification", true);
@@ -164,7 +186,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
builder.setSubText(accountName);
}
String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id);
if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){
if(notification!=null && (notification.type==NotificationType.MENTION)){
ArrayList<String> mentions=new ArrayList<>();
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!notification.status.account.id.equals(ownID))

View File

@@ -14,28 +14,35 @@ import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.notifications.GetNotificationsV1;
import org.joinmastodon.android.api.requests.notifications.GetNotificationsV2;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationGroup;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -43,7 +50,7 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=3;
private static final int DB_VERSION=5;
public static final WorkerThread databaseThread=new WorkerThread("databaseThread");
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
@@ -51,7 +58,7 @@ public class CacheController{
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
private boolean loadingNotifications;
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
private final ArrayList<Callback<PaginatedResponse<List<NotificationViewModel>>>> pendingNotificationsCallbacks=new ArrayList<>();
private List<FollowList> lists;
private static final int POST_FLAG_GAP_AFTER=1;
@@ -135,75 +142,187 @@ public class CacheController{
});
}
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
private List<NotificationViewModel> makeNotificationViewModels(List<NotificationGroup> notifications, Map<String, Account> accounts, Map<String, Status> statuses){
return notifications.stream()
.filter(ng->ng.type!=null)
.map(ng->{
NotificationViewModel nvm=new NotificationViewModel();
nvm.notification=ng;
nvm.accounts=ng.sampleAccountIds.stream().map(accounts::get).filter(Objects::nonNull).collect(Collectors.toList());
if(nvm.accounts.size()!=ng.sampleAccountIds.size())
return null;
if(ng.statusId!=null){
nvm.status=statuses.get(ng.statusId);
if(nvm.status==null)
return null;
}
return nvm;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<NotificationViewModel>>> callback){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
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, "`time` DESC", count+"")){
String suffix=onlyMentions ? "mentions" : "all";
String table="notifications_"+suffix;
String accountsTable="notifications_accounts_"+suffix;
String statusesTable="notifications_statuses_"+suffix;
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`max_id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
if(cursor.getCount()==count){
ArrayList<Notification> result=new ArrayList<>();
ArrayList<NotificationGroup> result=new ArrayList<>();
cursor.moveToFirst();
String newMaxID;
HashSet<String> needAccounts=new HashSet<>(), needStatuses=new HashSet<>();
do{
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
NotificationGroup ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), NotificationGroup.class);
ntf.postprocess();
newMaxID=ntf.id;
newMaxID=ntf.pageMinId;
needAccounts.addAll(ntf.sampleAccountIds);
if(ntf.statusId!=null)
needStatuses.add(ntf.statusId);
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)));
HashMap<String, Account> accounts=new HashMap<>();
HashMap<String, Status> statuses=new HashMap<>();
if(!needAccounts.isEmpty()){
try(Cursor cursor2=db.query(accountsTable, new String[]{"json"}, "`id` IN ("+String.join(", ", Collections.nCopies(needAccounts.size(), "?"))+")",
needAccounts.toArray(new String[0]), null, null, null)){
while(cursor2.moveToNext()){
Account acc=MastodonAPIController.gson.fromJson(cursor2.getString(0), Account.class);
acc.postprocess();
accounts.put(acc.id, acc);
}
}
}
if(!needStatuses.isEmpty()){
try(Cursor cursor2=db.query(statusesTable, new String[]{"json"}, "`id` IN ("+String.join(", ", Collections.nCopies(needStatuses.size(), "?"))+")",
needStatuses.toArray(new String[0]), null, null, null)){
while(cursor2.moveToNext()){
Status s=MastodonAPIController.gson.fromJson(cursor2.getString(0), Status.class);
s.postprocess();
statuses.put(s.id, s);
}
}
}
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(makeNotificationViewModels(result, accounts, statuses), _newMaxID)));
return;
}
}catch(IOException x){
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
}
}
if(!onlyMentions && loadingNotifications){
synchronized(pendingNotificationsCallbacks){
pendingNotificationsCallbacks.add(callback);
}
return;
}
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){
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);
if(AccountSessionManager.get(accountID).getInstanceInfo().getApiVersion()>=2){
new GetNotificationsV2(maxID, count, onlyMentions ? EnumSet.of(NotificationType.MENTION): EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes())
.setCallback(new Callback<>(){
@Override
public void onSuccess(GetNotificationsV2.GroupedNotificationsResults result){
Map<String, Account> accounts=result.accounts.stream().collect(Collectors.toMap(a->a.id, Function.identity(), (a1, a2)->a2));
Map<String, Status> statuses=result.statuses.stream().collect(Collectors.toMap(s->s.id, Function.identity(), (s1, s2)->s2));
List<NotificationViewModel> notifications=makeNotificationViewModels(result.notificationGroups, accounts, statuses);
databaseThread.postRunnable(()->putNotifications(result.notificationGroups, result.accounts, result.statuses, onlyMentions, maxID==null), 0);
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(notifications,
result.notificationGroups.isEmpty() ? null : result.notificationGroups.get(result.notificationGroups.size()-1).pageMinId);
callback.onSuccess(res);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
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);
@Override
public void onError(ErrorResponse error){
callback.onError(error);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
cb.onError(error);
}
pendingNotificationsCallbacks.clear();
}
pendingNotificationsCallbacks.clear();
}
}
}
})
.exec(accountID);
})
.exec(accountID);
}else{
new GetNotificationsV1(maxID, count, onlyMentions ? EnumSet.of(NotificationType.MENTION): EnumSet.allOf(NotificationType.class))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){
ArrayList<Notification> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
List<Status> statuses=filtered.stream().map(n->n.status).filter(Objects::nonNull).collect(Collectors.toList());
List<Account> accounts=filtered.stream().map(n->n.account).collect(Collectors.toList());
List<NotificationViewModel> converted=filtered.stream()
.map(n->{
NotificationGroup group=new NotificationGroup();
group.groupKey="converted-"+n.id;
group.notificationsCount=1;
group.type=n.type;
group.mostRecentNotificationId=group.pageMaxId=group.pageMinId=n.id;
group.latestPageNotificationAt=n.createdAt;
group.sampleAccountIds=List.of(n.account.id);
group.event=n.event;
group.moderationWarning=n.moderationWarning;
if(n.status!=null)
group.statusId=n.status.id;
NotificationViewModel nvm=new NotificationViewModel();
nvm.notification=group;
nvm.status=n.status;
nvm.accounts=List.of(n.account);
return nvm;
})
.collect(Collectors.toList());
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(converted, result.isEmpty() ? null : result.get(result.size()-1).id);
callback.onSuccess(res);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
}
databaseThread.postRunnable(()->putNotifications(converted.stream().map(nvm->nvm.notification).collect(Collectors.toList()), accounts, statuses, onlyMentions, maxID==null), 0);
}
@Override
public void onError(ErrorResponse error){
callback.onError(error);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
cb.onError(error);
}
pendingNotificationsCallbacks.clear();
}
}
}
})
.exec(accountID);
}
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
@@ -213,22 +332,40 @@ public class CacheController{
}, 0);
}
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean clear){
private void putNotifications(List<NotificationGroup> notifications, List<Account> accounts, List<Status> statuses, boolean onlyMentions, boolean clear){
runOnDbThread((db)->{
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
if(clear)
String suffix=onlyMentions ? "mentions" : "all";
String table="notifications_"+suffix;
String accountsTable="notifications_accounts_"+suffix;
String statusesTable="notifications_statuses_"+suffix;
if(clear){
db.delete(table, null, null);
db.delete(accountsTable, null, null);
db.delete(statusesTable, null, null);
}
ContentValues values=new ContentValues(4);
for(Notification n:notifications){
for(NotificationGroup n:notifications){
if(n.type==null){
continue;
}
values.put("id", n.id);
values.put("id", n.groupKey);
values.put("json", MastodonAPIController.gson.toJson(n));
values.put("type", n.type.ordinal());
values.put("time", n.createdAt.getEpochSecond());
values.put("time", n.latestPageNotificationAt.getEpochSecond());
values.put("max_id", n.pageMaxId);
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
values.clear();
for(Account acc:accounts){
values.put("id", acc.id);
values.put("json", MastodonAPIController.gson.toJson(acc));
db.insertWithOnConflict(accountsTable, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
for(Status s:statuses){
values.put("id", s.id);
values.put("json", MastodonAPIController.gson.toJson(s));
db.insertWithOnConflict(statusesTable, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
});
}
@@ -320,7 +457,7 @@ public class CacheController{
lists=result;
if(callback!=null)
callback.onSuccess(result);
writeListsToFile();
writeLists();
}
@Override
@@ -332,26 +469,22 @@ public class CacheController{
.exec(accountID);
}
private List<FollowList> loadListsFromFile(){
File file=getListsFile();
if(!file.exists())
return null;
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
}catch(Exception x){
Log.w(TAG, "failed to read lists from cache file", x);
return null;
private List<FollowList> loadLists(){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("misc", new String[]{"value"}, "`key`=?", new String[]{"lists"}, null, null, null)){
if(!cursor.moveToFirst())
return null;
return MastodonAPIController.gson.fromJson(cursor.getString(0), new TypeToken<List<FollowList>>(){}.getType());
}
}
private void writeListsToFile(){
databaseThread.postRunnable(()->{
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
MastodonAPIController.gson.toJson(lists, out);
}catch(IOException x){
Log.w(TAG, "failed to write lists to cache file", x);
}
}, 0);
private void writeLists(){
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("key", "lists");
values.put("value", MastodonAPIController.gson.toJson(lists));
db.insertWithOnConflict("misc", null, values, SQLiteDatabase.CONFLICT_REPLACE);
});
}
public void getLists(Callback<List<FollowList>> callback){
@@ -361,7 +494,7 @@ public class CacheController{
return;
}
databaseThread.postRunnable(()->{
List<FollowList> lists=loadListsFromFile();
List<FollowList> lists=loadLists();
if(lists!=null){
this.lists=lists;
if(callback!=null)
@@ -372,23 +505,19 @@ public class CacheController{
}, 0);
}
public File getListsFile(){
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
}
public void addList(FollowList list){
if(lists==null)
return;
lists.add(list);
lists.sort(Comparator.comparing(l->l.title));
writeListsToFile();
writeLists();
}
public void deleteList(String id){
if(lists==null)
return;
lists.removeIf(l->l.id.equals(id));
writeListsToFile();
writeLists();
}
public void updateList(FollowList list){
@@ -398,7 +527,7 @@ public class CacheController{
if(lists.get(i).id.equals(list.id)){
lists.set(i, list);
lists.sort(Comparator.comparing(l->l.title));
writeListsToFile();
writeLists();
break;
}
}
@@ -419,23 +548,10 @@ public class CacheController{
`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,
`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,
`time` INTEGER NOT NULL
)""");
createNotificationsTables(db, "all");
createNotificationsTables(db, "mentions");
createRecentSearchesTable(db);
createMiscTable(db);
}
@Override
@@ -446,6 +562,15 @@ public class CacheController{
if(oldVersion<3){
addTimeColumns(db);
}
if(oldVersion<4){
createMiscTable(db);
}
if(oldVersion<5){
db.execSQL("DROP TABLE `notifications_all`");
db.execSQL("DROP TABLE `notifications_mentions`");
createNotificationsTables(db, "all");
createNotificationsTables(db, "mentions");
}
}
private void createRecentSearchesTable(SQLiteDatabase db){
@@ -465,5 +590,36 @@ public class CacheController{
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");
}
private void createMiscTable(SQLiteDatabase db){
db.execSQL("""
CREATE TABLE `misc` (
`key` TEXT NOT NULL PRIMARY KEY,
`value` TEXT
)""");
}
private void createNotificationsTables(SQLiteDatabase db, String suffix){
db.execSQL("CREATE TABLE `notifications_"+suffix+"` ("+
"""
`id` VARCHAR(100) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL,
`flags` INTEGER NOT NULL DEFAULT 0,
`type` INTEGER NOT NULL,
`time` INTEGER NOT NULL,
`max_id` VARCHAR(25) NOT NULL
)""");
db.execSQL("CREATE INDEX `notifications_"+suffix+"_max_id` ON `notifications_"+suffix+"`(`max_id`)");
db.execSQL("CREATE TABLE `notifications_accounts_"+suffix+"` ("+
"""
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL
)""");
db.execSQL("CREATE TABLE `notifications_statuses_"+suffix+"` ("+
"""
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
`json` TEXT NOT NULL
)""");
}
}
}

View File

@@ -20,7 +20,7 @@ public class JsonObjectRequestBody extends RequestBody{
@Override
public MediaType contentType(){
return MediaType.get("application/json;charset=utf-8");
return MediaType.get("application/json");
}
@Override

View File

@@ -45,7 +45,6 @@ import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@@ -82,6 +81,7 @@ public class PushSubscriptionManager{
private static final String EXTRA_SENDER = "sender";
private static final String EXTRA_SCOPE = "scope";
private static final String KID_VALUE="|ID|1|"; // request ID?
private static final long TOKEN_REFRESH_INTERVAL=30*24*60*60*1000L;
private static String deviceToken;
private String accountID;
@@ -93,14 +93,19 @@ public class PushSubscriptionManager{
this.accountID=accountID;
}
public static void resetLocalPreferences(){
getPrefs().edit().clear().apply();
}
public static void tryRegisterFCM(){
deviceToken=getPrefs().getString("deviceToken", null);
int tokenVersion=getPrefs().getInt("version", 0);
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE){
long tokenLastRefreshed=getPrefs().getLong("lastRefresh", 0);
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE && System.currentTimeMillis()-tokenLastRefreshed<TOKEN_REFRESH_INTERVAL){
registerAllAccountsForPush(false);
return;
}
Log.i(TAG, "tryRegisterFCM: no token found or app was updated. Trying to get push token...");
Log.i(TAG, "tryRegisterFCM: no token found, token due for refresh, or app was updated. Trying to get push token...");
Intent intent = new Intent("com.google.iid.TOKEN_REQUEST");
intent.setPackage(GSF_PACKAGE);
intent.putExtra(EXTRA_APPLICATION_PENDING_INTENT,
@@ -146,7 +151,7 @@ public class PushSubscriptionManager{
session.pushPublicKey=Base64.encodeToString(publicKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
session.pushAuthKey=encodedAuthKey=Base64.encodeToString(authKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
session.pushAccountID=pushAccountID=Base64.encodeToString(randomAccountID, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
AccountSessionManager.getInstance().writeAccountsFile();
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
}catch(NoSuchAlgorithmException|InvalidAlgorithmParameterException e){
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
return;
@@ -165,7 +170,7 @@ public class PushSubscriptionManager{
if(session==null)
return;
session.pushSubscription=result;
AccountSessionManager.getInstance().writeAccountsFile();
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
Log.d(TAG, "Successfully registered "+accountID+" for push notifications");
});
}
@@ -191,7 +196,7 @@ public class PushSubscriptionManager{
result.policy=subscription.policy;
session.pushSubscription=result;
session.needUpdatePushSettings=false;
AccountSessionManager.getInstance().writeAccountsFile();
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
}
@Override
@@ -204,7 +209,7 @@ public class PushSubscriptionManager{
return;
session.needUpdatePushSettings=true;
session.pushSubscription=subscription;
AccountSessionManager.getInstance().writeAccountsFile();
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
}
}
})
@@ -380,7 +385,11 @@ public class PushSubscriptionManager{
deviceToken=intent.getStringExtra("registration_id");
if(deviceToken.startsWith(KID_VALUE))
deviceToken=deviceToken.substring(KID_VALUE.length()+1);
getPrefs().edit().putString("deviceToken", deviceToken).putInt("version", BuildConfig.VERSION_CODE).apply();
getPrefs().edit()
.putString("deviceToken", deviceToken)
.putInt("version", BuildConfig.VERSION_CODE)
.putLong("lastRefresh", System.currentTimeMillis())
.apply();
Log.i(TAG, "Successfully registered for FCM");
registerAllAccountsForPush(true);
}else{

View File

@@ -31,7 +31,7 @@ import okio.Source;
public class ResizedImageRequestBody extends CountingRequestBody{
private File tempFile;
private Uri uri;
private String contentType;
private MediaType contentType;
private int maxSize;
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
@@ -42,15 +42,16 @@ public class ResizedImageRequestBody extends CountingRequestBody{
opts.inJustDecodeBounds=true;
if("file".equals(uri.getScheme())){
BitmapFactory.decodeFile(uri.getPath(), opts);
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
contentType=UiUtils.getFileMediaType(new File(uri.getPath()));
}else{
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
}
contentType=MastodonApp.context.getContentResolver().getType(uri);
String mime=MastodonApp.context.getContentResolver().getType(uri);
contentType=TextUtils.isEmpty(mime) ? null : MediaType.get(mime);
}
if(TextUtils.isEmpty(contentType))
contentType="image/jpeg";
if(contentType==null)
contentType=MediaType.get("image/jpeg");
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
Bitmap bitmap;
if(Build.VERSION.SDK_INT>=28){
@@ -136,7 +137,7 @@ public class ResizedImageRequestBody extends CountingRequestBody{
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
}else{
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
contentType="image/jpeg";
contentType=MediaType.get("image/jpeg");
}
}
length=tempFile.length();
@@ -163,7 +164,7 @@ public class ResizedImageRequestBody extends CountingRequestBody{
@Override
public MediaType contentType(){
return MediaType.get(contentType);
return contentType;
}
@Override

View File

@@ -0,0 +1,21 @@
package org.joinmastodon.android.api;
import me.grishka.appkit.api.APIRequest;
/**
* Wraps a different API request to allow a chain of requests to be canceled
*/
public class WrapperRequest<T> extends APIRequest<T>{
public APIRequest<?> wrappedRequest;
@Override
public void cancel(){
if(wrappedRequest!=null)
wrappedRequest.cancel();
}
@Override
public APIRequest<T> exec(){
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.FamiliarFollowers;
import java.util.Collection;
import java.util.List;
public class GetAccountFamiliarFollowers extends MastodonAPIRequest<List<FamiliarFollowers>>{
public GetAccountFamiliarFollowers(Collection<String> ids){
super(HttpMethod.GET, "/accounts/familiar_followers", new TypeToken<>(){});
for(String id:ids){
addQueryParameter("id[]", id);
}
}
}

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
public class GetDomainBlockPreview extends MastodonAPIRequest<GetDomainBlockPreview.Response>{
public GetDomainBlockPreview(String domain){
super(HttpMethod.GET, "/domain_blocks/preview", Response.class);
addQueryParameter("domain", domain);
}
public static class Response{
public int followingCount;
public int followersCount;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.InstanceV2;
public class GetInstanceV2 extends MastodonAPIRequest<InstanceV2>{
public GetInstanceV2(){
super(HttpMethod.GET, "/instance", InstanceV2.class);
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -7,26 +7,27 @@ import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationType;
import java.util.EnumSet;
import java.util.List;
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
public class GetNotificationsV1 extends MastodonAPIRequest<List<Notification>>{
public GetNotificationsV1(String maxID, int limit, EnumSet<NotificationType> includeTypes){
this(maxID, limit, includeTypes, null);
}
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
public GetNotificationsV1(String maxID, int limit, EnumSet<NotificationType> includeTypes, String onlyAccountID){
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(includeTypes!=null){
for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){
for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
addQueryParameter("types[]", type);
}
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), Notification.Type.class)){
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
addQueryParameter("exclude_types[]", type);
}
}

View File

@@ -0,0 +1,69 @@
package org.joinmastodon.android.api.requests.notifications;
import android.text.TextUtils;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.BaseModel;
import org.joinmastodon.android.model.NotificationGroup;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.model.Status;
import java.util.EnumSet;
import java.util.List;
public class GetNotificationsV2 extends MastodonAPIRequest<GetNotificationsV2.GroupedNotificationsResults>{
public GetNotificationsV2(String maxID, int limit, EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
this(maxID, limit, includeTypes, groupedTypes, null);
}
public GetNotificationsV2(String maxID, int limit, EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes, String onlyAccountID){
super(HttpMethod.GET, "/notifications", GroupedNotificationsResults.class);
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(includeTypes!=null){
for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
addQueryParameter("types[]", type);
}
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
addQueryParameter("exclude_types[]", type);
}
}
if(groupedTypes!=null){
for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){
addQueryParameter("grouped_types[]", type);
}
}
if(!TextUtils.isEmpty(onlyAccountID))
addQueryParameter("account_id", onlyAccountID);
removeUnsupportedItems=true;
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
@AllFieldsAreRequired
public static class GroupedNotificationsResults extends BaseModel{
public List<Account> accounts;
public List<Status> statuses;
public List<NotificationGroup> notificationGroups;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
for(Account acc:accounts)
acc.postprocess();
for(Status s:statuses)
s.postprocess();
for(NotificationGroup ng:notificationGroups)
ng.postprocess();
}
}
}

View File

@@ -0,0 +1,35 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationType;
import java.util.EnumSet;
public class GetUnreadNotificationsCount extends MastodonAPIRequest<GetUnreadNotificationsCount.Response>{
public GetUnreadNotificationsCount(EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
super(HttpMethod.GET, "/notifications/unread_count", Response.class);
if(includeTypes!=null){
for(String type: ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
addQueryParameter("types[]", type);
}
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
addQueryParameter("exclude_types[]", type);
}
}
if(groupedTypes!=null){
for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){
addQueryParameter("grouped_types[]", type);
}
}
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
public static class Response{
public int count;
}
}

View File

@@ -8,7 +8,8 @@ import org.joinmastodon.android.model.Card;
import java.util.List;
public class GetTrendingLinks extends MastodonAPIRequest<List<Card>>{
public GetTrendingLinks(){
public GetTrendingLinks(int limit){
super(HttpMethod.GET, "/trends/links", new TypeToken<>(){});
addQueryParameter("limit", String.valueOf(limit));
}
}

View File

@@ -1,7 +1,11 @@
package org.joinmastodon.android.api.session;
import com.google.gson.annotations.SerializedName;
public class AccountActivationInfo{
@SerializedName(value="email", alternate="a")
public String email;
@SerializedName(value="last_email_confirmation_resend", alternate="b")
public long lastEmailConfirmationResend;
public AccountActivationInfo(String email, long lastEmailConfirmationResend){

View File

@@ -5,18 +5,10 @@ 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);
}
@@ -30,10 +22,6 @@ public class AccountLocalPreferences{
public void save(){
prefs.edit()
.putBoolean("interactionCounts", showInteractionCounts)
.putBoolean("emojiInNames", customEmojiInNames)
.putBoolean("showCWs", showCWs)
.putBoolean("hideSensitive", hideSensitiveMedia)
.putBoolean("serverSideFilters", serverSideFiltersSupported)
.apply();
}

View File

@@ -1,11 +1,17 @@
package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -13,6 +19,7 @@ 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.gson.JsonObjectBuilder;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
@@ -24,7 +31,7 @@ import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterResult;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
@@ -47,21 +54,40 @@ public class AccountSession{
private static final String TAG="AccountSession";
private static final int MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS=28;
public static final int FLAG_ACTIVATED=1;
public static final int FLAG_NEED_UPDATE_PUSH_SETTINGS=1 << 1;
@SerializedName(value="token", alternate="a")
public Token token;
@SerializedName(value="self", alternate="b")
public Account self;
@SerializedName(value="domain", alternate="c")
public String domain;
@SerializedName(value="app", alternate="d")
public Application app;
@SerializedName(value="info_last_updated", alternate="e")
public long infoLastUpdated;
@SerializedName(value="activated", alternate="f")
public boolean activated=true;
@SerializedName(value="push_private_key", alternate="g")
public String pushPrivateKey;
@SerializedName(value="push_public_key", alternate="h")
public String pushPublicKey;
@SerializedName(value="push_auth_key", alternate="i")
public String pushAuthKey;
@SerializedName(value="push_subscription", alternate="j")
public PushSubscription pushSubscription;
@SerializedName(value="need_update_push_settings", alternate="k")
public boolean needUpdatePushSettings;
@SerializedName(value="filters_last_updated", alternate="l")
public long filtersLastUpdated;
@SerializedName(value="word_filters", alternate="m")
public List<LegacyFilter> wordFilters=new ArrayList<>();
@SerializedName(value="push_account_i_d", alternate="n")
public String pushAccountID;
@SerializedName(value="activation_info", alternate="o")
public AccountActivationInfo activationInfo;
@SerializedName(value="preferences", alternate="p")
public Preferences preferences;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
@@ -70,7 +96,6 @@ public class AccountSession{
private transient SharedPreferences prefs;
private transient boolean preferencesNeedSaving;
private transient AccountLocalPreferences localPreferences;
private transient List<FollowList> lists;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
@@ -84,6 +109,64 @@ public class AccountSession{
AccountSession(){}
AccountSession(ContentValues values){
domain=values.getAsString("domain");
self=MastodonAPIController.gson.fromJson(values.getAsString("account_obj"), Account.class);
token=MastodonAPIController.gson.fromJson(values.getAsString("token"), Token.class);
app=MastodonAPIController.gson.fromJson(values.getAsString("application"), Application.class);
infoLastUpdated=values.getAsLong("info_last_updated");
long flags=values.getAsLong("flags");
activated=(flags & FLAG_ACTIVATED)==FLAG_ACTIVATED;
needUpdatePushSettings=(flags & FLAG_NEED_UPDATE_PUSH_SETTINGS)==FLAG_NEED_UPDATE_PUSH_SETTINGS;
JsonObject pushKeys=JsonParser.parseString(values.getAsString("push_keys")).getAsJsonObject();
if(!pushKeys.get("auth").isJsonNull() && !pushKeys.get("private").isJsonNull() && !pushKeys.get("public").isJsonNull()){
pushAuthKey=pushKeys.get("auth").getAsString();
pushPrivateKey=pushKeys.get("private").getAsString();
pushPublicKey=pushKeys.get("public").getAsString();
}
pushSubscription=MastodonAPIController.gson.fromJson(values.getAsString("push_subscription"), PushSubscription.class);
JsonObject legacyFilters=JsonParser.parseString(values.getAsString("legacy_filters")).getAsJsonObject();
wordFilters=MastodonAPIController.gson.fromJson(legacyFilters.getAsJsonArray("filters"), new TypeToken<List<LegacyFilter>>(){}.getType());
filtersLastUpdated=legacyFilters.get("updated").getAsLong();
pushAccountID=values.getAsString("push_id");
activationInfo=MastodonAPIController.gson.fromJson(values.getAsString("activation_info"), AccountActivationInfo.class);
preferences=MastodonAPIController.gson.fromJson(values.getAsString("preferences"), Preferences.class);
}
public void toContentValues(ContentValues values){
values.put("id", getID());
values.put("domain", domain.toLowerCase());
values.put("account_obj", MastodonAPIController.gson.toJson(self));
values.put("token", MastodonAPIController.gson.toJson(token));
values.put("application", MastodonAPIController.gson.toJson(app));
values.put("info_last_updated", infoLastUpdated);
values.put("flags", getFlagsForDatabase());
values.put("push_keys", new JsonObjectBuilder()
.add("auth", pushAuthKey)
.add("private", pushPrivateKey)
.add("public", pushPublicKey)
.build()
.toString());
values.put("push_subscription", MastodonAPIController.gson.toJson(pushSubscription));
values.put("legacy_filters", new JsonObjectBuilder()
.add("filters", MastodonAPIController.gson.toJsonTree(wordFilters))
.add("updated", filtersLastUpdated)
.build()
.toString());
values.put("push_id", pushAccountID);
values.put("activation_info", MastodonAPIController.gson.toJson(activationInfo));
values.put("preferences", MastodonAPIController.gson.toJson(preferences));
}
public long getFlagsForDatabase(){
long flags=0;
if(activated)
flags|=FLAG_ACTIVATED;
if(needUpdatePushSettings)
flags|=FLAG_NEED_UPDATE_PUSH_SETTINGS;
return flags;
}
public String getID(){
return domain+"_"+self.id;
}
@@ -124,7 +207,7 @@ public class AccountSession{
preferences=result;
if(callback!=null)
callback.accept(result);
AccountSessionManager.getInstance().writeAccountsFile();
AccountSessionManager.getInstance().updateAccountPreferences(getID(), result);
}
@Override
@@ -206,7 +289,7 @@ public class AccountSession{
public void onSuccess(Account result){
preferencesNeedSaving=false;
self=result;
AccountSessionManager.getInstance().writeAccountsFile();
AccountSessionManager.getInstance().updateAccountInfo(getID(), self);
}
@Override
@@ -286,4 +369,8 @@ public class AccountSession{
public int getDonationSeed(){
return Math.abs(getFullUsername().hashCode())%100;
}
public Instance getInstanceInfo(){
return AccountSessionManager.getInstance().getInstanceInfo(domain);
}
}

View File

@@ -10,6 +10,7 @@ import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
@@ -18,6 +19,12 @@ import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
@@ -26,11 +33,15 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.DatabaseRunnable;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.WrapperRequest;
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
import org.joinmastodon.android.api.requests.instance.GetInstanceV2;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account;
@@ -38,16 +49,17 @@ import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV1;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
@@ -61,6 +73,7 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -68,7 +81,7 @@ public class AccountSessionManager{
private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI="mastodon-android-auth://callback";
private static final int DB_VERSION=1;
private static final int DB_VERSION=3;
private static final AccountSessionManager instance=new AccountSessionManager();
@@ -84,6 +97,7 @@ public class AccountSessionManager{
private boolean loadedInstances;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
private final Object databaseLock=new Object();
public static AccountSessionManager getInstance(){
return instance;
@@ -91,53 +105,42 @@ public class AccountSessionManager{
private AccountSessionManager(){
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
if(!file.exists())
return;
HashSet<String> domains=new HashSet<>();
try(FileInputStream in=new FileInputStream(file)){
SessionsStorageWrapper w=MastodonAPIController.gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), SessionsStorageWrapper.class);
for(AccountSession session:w.accounts){
domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session);
runWithDatabase(db->{
HashSet<String> domains=new HashSet<>();
try(Cursor cursor=db.query("accounts", null, null, null, null, null, null)){
ContentValues values=new ContentValues();
while(cursor.moveToNext()){
DatabaseUtils.cursorRowToContentValues(cursor, values);
AccountSession session=new AccountSession(values);
domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session);
}
}
}catch(Exception x){
Log.e(TAG, "Error loading accounts", x);
}
readInstanceInfo(db, domains);
});
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
readInstanceInfo(domains);
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
instances.put(instance.getDomain(), instance);
runOnDbThread(db->insertInstanceIntoDatabase(db, instance.getDomain(), instance, List.of(), 0));
AccountSession session=new AccountSession(token, self, app, instance.getDomain(), activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
updateInstanceEmojis(instance, instance.uri);
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
runOnDbThread(db->{
ContentValues values=new ContentValues();
session.toContentValues(values);
db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE);
});
updateInstanceEmojis(instance, instance.getDomain());
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
maybeUpdateShortcuts();
}
public synchronized void writeAccountsFile(){
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
try{
try(FileOutputStream out=new FileOutputStream(file)){
SessionsStorageWrapper w=new SessionsStorageWrapper();
w.accounts=new ArrayList<>(sessions.values());
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(w, writer);
writer.flush();
}
}catch(IOException x){
Log.e(TAG, "Error writing accounts file", x);
}
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
}
@NonNull
public List<AccountSession> getLoggedInAccounts(){
return new ArrayList<>(sessions.values());
@@ -167,7 +170,7 @@ public class AccountSessionManager{
if(!sessions.containsKey(lastActiveAccountID)){
// TODO figure out why this happens. It should not be possible.
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
writeAccountsFile();
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
}
return getAccount(lastActiveAccountID);
}
@@ -186,7 +189,6 @@ public class AccountSessionManager{
public void removeAccount(String id){
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
session.getCacheController().getListsFile().delete();
MastodonApp.context.deleteDatabase(id+".db");
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
@@ -206,11 +208,10 @@ public class AccountSessionManager{
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
}
writeAccountsFile();
String domain=session.domain.toLowerCase();
if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){
getInstanceInfoFile(domain).delete();
}
runOnDbThread(db->{
db.delete("accounts", "`id`=?", new String[]{id});
db.delete("instances", "`domain` NOT IN (SELECT DISTINCT `domain` FROM `accounts`)", new String[]{});
});
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
nm.deleteNotificationChannelGroup(id);
@@ -232,7 +233,7 @@ public class AccountSessionManager{
authenticatingApp=result;
Uri uri=new Uri.Builder()
.scheme("https")
.authority(instance.uri)
.authority(instance.getDomain())
.path("/oauth/authorize")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", result.clientId)
@@ -253,7 +254,7 @@ public class AccountSessionManager{
}
})
.wrapProgress(activity, R.string.preparing_auth, false)
.execNoAuth(instance.uri);
.execNoAuth(instance.getDomain());
}
public boolean isSelf(String id, Account other){
@@ -302,7 +303,12 @@ public class AccountSessionManager{
public void onSuccess(Account result){
session.self=result;
session.infoLastUpdated=System.currentTimeMillis();
writeAccountsFile();
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("account_obj", MastodonAPIController.gson.toJson(result));
values.put("info_last_updated", session.infoLastUpdated);
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
});
}
@Override
@@ -320,7 +326,15 @@ public class AccountSessionManager{
public void onSuccess(List<LegacyFilter> result){
session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
writeAccountsFile();
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("legacy_filters", new JsonObjectBuilder()
.add("filters", MastodonAPIController.gson.toJsonTree(session.wordFilters))
.add("updated", session.filtersLastUpdated)
.build()
.toString());
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
});
}
@Override
@@ -332,11 +346,11 @@ public class AccountSessionManager{
}
public void updateInstanceInfo(String domain){
new GetInstance()
.setCallback(new Callback<>(){
loadInstanceInfo(domain, new Callback<>(){
@Override
public void onSuccess(Instance instance){
instances.put(domain, instance);
runOnDbThread(db->insertInstanceIntoDatabase(db, domain, instance, List.of(), 0));
updateInstanceEmojis(instance, domain);
}
@@ -344,8 +358,7 @@ public class AccountSessionManager{
public void onError(ErrorResponse error){
}
})
.execNoAuth(domain);
});
}
private void updateInstanceEmojis(Instance instance, String domain){
@@ -353,13 +366,10 @@ public class AccountSessionManager{
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Emoji> result){
InstanceInfoStorageWrapper emojis=new InstanceInfoStorageWrapper();
emojis.lastUpdated=System.currentTimeMillis();
emojis.emojis=result;
emojis.instance=instance;
customEmojis.put(domain, groupCustomEmojis(emojis));
instancesLastUpdated.put(domain, emojis.lastUpdated);
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(emojis, domain));
long lastUpdated=System.currentTimeMillis();
customEmojis.put(domain, groupCustomEmojis(result));
instancesLastUpdated.put(domain, lastUpdated);
runOnDbThread(db->insertInstanceIntoDatabase(db, domain, instance, result, lastUpdated));
E.post(new EmojiUpdatedEvent(domain));
}
@@ -371,30 +381,41 @@ public class AccountSessionManager{
.execNoAuth(domain);
}
private File getInstanceInfoFile(String domain){
return new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
}
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer);
writer.flush();
}catch(IOException x){
Log.w(TAG, "Error writing instance info file for "+domain, x);
}
}
private void readInstanceInfo(Set<String> domains){
for(String domain:domains){
try(FileInputStream in=new FileInputStream(getInstanceInfoFile(domain))){
InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8);
InstanceInfoStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, InstanceInfoStorageWrapper.class);
customEmojis.put(domain, groupCustomEmojis(emojis));
instances.put(domain, emojis.instance);
instancesLastUpdated.put(domain, emojis.lastUpdated);
}catch(Exception x){
Log.w(TAG, "Error reading instance info file for "+domain, x);
private void readInstanceInfo(SQLiteDatabase db, Set<String> domains){
for(String domain : domains){
final int maxEmojiLength=500000;
try(Cursor cursor=db.rawQuery("SELECT domain, instance_obj, substring(emojis,0,?) AS emojis, length(emojis) AS emoji_length, last_updated, version FROM instances WHERE `domain` = ?",
new String[]{String.valueOf(maxEmojiLength) , domain})) {
ContentValues values=new ContentValues();
while(cursor.moveToNext()){
DatabaseUtils.cursorRowToContentValues(cursor, values);
int version=values.getAsInteger("version");
Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), switch(version){
case 1 -> InstanceV1.class;
case 2 -> InstanceV2.class;
default -> throw new IllegalStateException("Unexpected value: "+version);
});
StringBuilder emojiSB=new StringBuilder();
emojiSB.append(values.getAsString("emojis"));
//get emoji in chunks of 1MB if it didn't fit in the first query
int emojiStringLength=values.getAsInteger("emoji_length");
if(emojiStringLength>maxEmojiLength){
final int pagesize=1000000;
for(int start=maxEmojiLength; start<emojiStringLength; start+=pagesize){
try(Cursor emojiCursor=db.rawQuery("SELECT substr(emojis,?, ?) FROM instances WHERE `domain` = ?", new String[]{String.valueOf(start), String.valueOf(pagesize), domain})){
emojiCursor.moveToNext();
emojiSB.append(emojiCursor.getString(0));
}
}
}
List<Emoji> emojis=MastodonAPIController.gson.fromJson(emojiSB.toString(), new TypeToken<List<Emoji>>(){}.getType());
instances.put(domain, instance);
customEmojis.put(domain, groupCustomEmojis(emojis));
instancesLastUpdated.put(domain, values.getAsLong("last_updated"));
}
}catch(Exception ex){
Log.d(TAG, "readInstanceInfo failed", ex);
return;
}
}
if(!loadedInstances){
@@ -403,8 +424,8 @@ public class AccountSessionManager{
}
}
private List<EmojiCategory> groupCustomEmojis(InstanceInfoStorageWrapper emojis){
return emojis.emojis.stream()
private List<EmojiCategory> groupCustomEmojis(List<Emoji> emojis){
return emojis.stream()
.filter(e->e.visibleInPicker)
.collect(Collectors.groupingBy(e->e.category==null ? "" : e.category))
.entrySet()
@@ -427,7 +448,49 @@ public class AccountSessionManager{
AccountSession session=getAccount(id);
session.self=account;
session.infoLastUpdated=System.currentTimeMillis();
writeAccountsFile();
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("account_obj", MastodonAPIController.gson.toJson(account));
values.put("info_last_updated", session.infoLastUpdated);
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
});
}
public void updateAccountPreferences(String id, Preferences prefs){
AccountSession session=getAccount(id);
session.preferences=prefs;
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("preferences", MastodonAPIController.gson.toJson(prefs));
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
});
}
public void writeAccountPushSettings(String id){
AccountSession session=getAccount(id);
runWithDatabase(db->{ // Called from a background thread anyway
ContentValues values=new ContentValues();
values.put("push_keys", new JsonObjectBuilder()
.add("auth", session.pushAuthKey)
.add("private", session.pushPrivateKey)
.add("public", session.pushPublicKey)
.build()
.toString());
values.put("push_subscription", MastodonAPIController.gson.toJson(session.pushSubscription));
values.put("flags", session.getFlagsForDatabase());
values.put("push_id", session.pushAccountID);
db.update("accounts", values, "`id`=?", new String[]{id});
});
}
public void writeAccountActivationInfo(String id){
AccountSession session=getAccount(id);
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("activation_info", MastodonAPIController.gson.toJson(session.activationInfo));
values.put("flags", session.getFlagsForDatabase());
db.update("accounts", values, "`id`=?", new String[]{id});
});
}
private void maybeUpdateShortcuts(){
@@ -487,8 +550,24 @@ public class AccountSessionManager{
}
private void runOnDbThread(DatabaseRunnable r){
cancelDelayedClose();
CacheController.databaseThread.postRunnable(()->{
synchronized(databaseLock){
cancelDelayedClose();
try{
SQLiteDatabase db=getOrOpenDatabase();
r.run(db);
}catch(SQLiteException|IOException x){
Log.w(TAG, x);
}finally{
closeDelayed();
}
}
}, 0);
}
private void runWithDatabase(DatabaseRunnable r){
synchronized(databaseLock){
cancelDelayedClose();
try{
SQLiteDatabase db=getOrOpenDatabase();
r.run(db);
@@ -497,7 +576,7 @@ public class AccountSessionManager{
}finally{
closeDelayed();
}
}, 0);
}
}
public void runIfDonationCampaignNotDismissed(String id, Runnable action){
@@ -523,14 +602,59 @@ public class AccountSessionManager{
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
public void clearInstanceInfo(){
SQLiteDatabase db=getOrOpenDatabase();
db.delete("instances", null, null);
db.close();
}
private static class InstanceInfoStorageWrapper{
public Instance instance;
public List<Emoji> emojis;
public long lastUpdated;
private static void insertInstanceIntoDatabase(SQLiteDatabase db, String domain, Instance instance, List<Emoji> emojis, long lastUpdated){
ContentValues values=new ContentValues();
values.put("domain", domain);
values.put("instance_obj", MastodonAPIController.gson.toJson(instance));
values.put("emojis", MastodonAPIController.gson.toJson(emojis));
values.put("last_updated", lastUpdated);
values.put("version", instance.getVersion());
db.insertWithOnConflict("instances", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public static APIRequest<Instance> loadInstanceInfo(String domain, Callback<Instance> callback){
final WrapperRequest<Instance> wrapper=new WrapperRequest<>();
wrapper.wrappedRequest=new GetInstanceV2()
.setCallback(new Callback<>(){
@Override
public void onSuccess(InstanceV2 result){
wrapper.wrappedRequest=null;
callback.onSuccess(result);
}
@Override
public void onError(ErrorResponse error){
if(error instanceof MastodonErrorResponse mr && mr.httpStatus==404){
// Mastodon pre-4.0 or a non-Mastodon server altogether. Let's try /api/v1/instance
wrapper.wrappedRequest=new GetInstanceV1()
.setCallback(new Callback<>(){
@Override
public void onSuccess(InstanceV1 result){
wrapper.wrappedRequest=null;
callback.onSuccess(result);
}
@Override
public void onError(ErrorResponse error){
wrapper.wrappedRequest=null;
callback.onError(error);
}
})
.execNoAuth(domain);
}else{
wrapper.wrappedRequest=null;
callback.onError(error);
}
}
})
.execNoAuth(domain);
return wrapper;
}
private static class DatabaseHelper extends SQLiteOpenHelper{
@@ -545,11 +669,86 @@ public class AccountSessionManager{
`id` text PRIMARY KEY,
`dismissed_at` bigint
)""");
createAccountsTable(db);
db.execSQL("""
CREATE TABLE `instances` (
`domain` text PRIMARY KEY,
`instance_obj` text,
`emojis` text,
`last_updated` bigint,
`version` integer NOT NULL DEFAULT 1
)""");
maybeMigrateAccounts(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
if(oldVersion<2){
createAccountsTable(db);
db.execSQL("""
CREATE TABLE `instances` (
`domain` text PRIMARY KEY,
`instance_obj` text,
`emojis` text,
`last_updated` bigint
)""");
maybeMigrateAccounts(db);
}
if(oldVersion<3){
db.execSQL("ALTER TABLE `instances` ADD `version` integer NOT NULL DEFAULT 1");
}
}
private void createAccountsTable(SQLiteDatabase db){
db.execSQL("""
CREATE TABLE `accounts` (
`id` text PRIMARY KEY,
`domain` text,
`account_obj` text,
`token` text,
`application` text,
`info_last_updated` bigint,
`flags` bigint,
`push_keys` text,
`push_subscription` text,
`legacy_filters` text DEFAULT NULL,
`push_id` text,
`activation_info` text,
`preferences` text
)""");
}
private void maybeMigrateAccounts(SQLiteDatabase db){
File accountsFile=new File(MastodonApp.context.getFilesDir(), "accounts.json");
if(accountsFile.exists()){
HashSet<String> domains=new HashSet<>();
try(FileInputStream in=new FileInputStream(accountsFile)){
JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
ContentValues values=new ContentValues();
JsonArray accounts=jobj.has("a") ? jobj.getAsJsonArray("a") : jobj.getAsJsonArray("accounts");
for(JsonElement jacc:accounts){
AccountSession session=MastodonAPIController.gson.fromJson(jacc, AccountSession.class);
domains.add(session.domain.toLowerCase());
session.toContentValues(values);
db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
}catch(Exception x){
Log.e(TAG, "Error migrating accounts", x);
return;
}
accountsFile.delete();
for(String domain:domains){
File file=new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
try(FileInputStream in=new FileInputStream(file)){
JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
insertInstanceIntoDatabase(db, domain, MastodonAPIController.gson.fromJson(jobj.get(jobj.has("instance") ? "instance" : "a"), Instance.class),
MastodonAPIController.gson.fromJson(jobj.get("emojis"), new TypeToken<>(){}.getType()), jobj.get("last_updated").getAsLong());
}catch(Exception x){
Log.w(TAG, "Error reading instance info file for "+domain, x);
}
file.delete();
}
}
}
}
}

View File

@@ -13,25 +13,22 @@ import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
import org.joinmastodon.android.ui.Snackbar;
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.EnumSet;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
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.SingleViewRecyclerAdapter;
@@ -56,16 +53,16 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Notification> result){
onDataLoaded(result, !result.isEmpty());
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
// currentRequest=new GetNotificationsV2(offset==0 ? null : maxID, count, EnumSet.allOf(NotificationType.class), account.id)
// .setCallback(new SimpleCallback<>(this){
// @Override
// public void onSuccess(List<NotificationViewModel> result){
// onDataLoaded(result, !result.isEmpty());
// maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
// endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
// }
// })
// .exec(accountID);
}
@Override
@@ -153,8 +150,8 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
protected List<StatusDisplayItem> buildDisplayItems(NotificationViewModel n){
if(n.notification.type==NotificationType.MENTION || n.notification.type==NotificationType.STATUS){
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
}
return super.buildDisplayItems(n);

View File

@@ -0,0 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
public interface AssistContentProviderFragment{
void onProvideAssistContent(AssistContent content);
}

View File

@@ -5,60 +5,73 @@ import android.view.LayoutInflater;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.NotificationWithButtonStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<NotificationViewModel>{
protected String maxID;
protected View endMark;
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
protected List<StatusDisplayItem> buildDisplayItems(NotificationViewModel n){
StatusDisplayItem titleItem;
if(n.notification.type==NotificationType.MENTION){
titleItem=null;
}else if(n.notification.type==NotificationType.STATUS){
if(n.status!=null)
titleItem=new ReblogOrReplyLineStatusDisplayItem(n.getID(), this, getString(R.string.user_just_posted, n.status.account.displayName), n.status.account.emojis, R.drawable.ic_notifications_wght700fill1_20px);
else
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.notification.type==NotificationType.SEVERED_RELATIONSHIPS || n.notification.type==NotificationType.MODERATION_WARNING)
titleItem=new NotificationWithButtonStatusDisplayItem(n.getID(), this, n, accountID);
else
titleItem=new NotificationHeaderStatusDisplayItem(n.getID(), this, n, accountID);
}
if(n.status!=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;
if(titleItem!=null && n.notification.type!=NotificationType.STATUS){
InlineStatusStatusDisplayItem inlineItem=new InlineStatusStatusDisplayItem(n.getID(), this, n.status);
inlineItem.removeTopPadding=true;
return List.of(titleItem, inlineItem);
}else{
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, 0);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
return List.of(titleItem);
}else{
return Collections.emptyList();
return List.of();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
protected void addAccountToKnown(NotificationViewModel s){
for(Account a:s.accounts){
if(!knownAccounts.containsKey(a.id))
knownAccounts.put(a.id, a);
}
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
NotificationViewModel n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
@@ -70,25 +83,25 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
args.putParcelable("profileAccount", Parcels.wrap(n.accounts.get(0)));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
private Notification getNotificationByID(String id){
for(Notification n : data){
if(n.id.equals(id))
protected NotificationViewModel getNotificationByID(String id){
for(NotificationViewModel n:data){
if(n.getID().equals(id))
return n;
}
return null;
}
protected void removeNotification(Notification n){
protected void removeNotification(NotificationViewModel n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0; i<displayItems.size(); i++){
if(n.id.equals(displayItems.get(i).parentID)){
if(n.getID().equals(displayItems.get(i).parentID)){
index=i;
break;
}
@@ -97,7 +110,7 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
return;
int lastIndex;
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
if(!displayItems.get(lastIndex).parentID.equals(n.getID()))
break;
}
displayItems.subList(index, lastIndex).clear();
@@ -111,10 +124,4 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
endMark.setVisibility(View.GONE);
return v;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
}

View File

@@ -29,8 +29,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
@@ -42,14 +41,16 @@ 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.sheets.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
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.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -185,7 +186,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
currentPhotoViewer=new PhotoViewer(getActivity(), this, status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder;
@Override
@@ -207,6 +208,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
list.setClipChildren(false);
gridHolder.setClipChildren(false);
transitioningHolder.view.setElevation(1f);
int cornerMask=((MediaGridLayout.LayoutParams)holder.view.getLayoutParams()).tile.getRoundCornersMask();
if((cornerMask & PhotoLayoutHelper.CORNER_TL)!=0)
outCornerRadius[0]=V.dp(8);
if((cornerMask & PhotoLayoutHelper.CORNER_TR)!=0)
outCornerRadius[1]=V.dp(8);
if((cornerMask & PhotoLayoutHelper.CORNER_BR)!=0)
outCornerRadius[2]=V.dp(8);
if((cornerMask & PhotoLayoutHelper.CORNER_BL)!=0)
outCornerRadius[3]=V.dp(8);
return true;
}
return false;
@@ -339,7 +349,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
tmpRect.offset(0, Math.round(child.getTranslationY()));
float y=tmpRect.bottom-V.dp(.5f);
float y=tmpRect.bottom;
int strokeWidth=V.dp(0.5f);
if(strokeWidth%2==1){
y-=0.5f;
}
paint.setAlpha(Math.round(255*child.getAlpha()));
c.drawLine(0, y, parent.getWidth(), y, paint);
}
@@ -351,7 +365,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public abstract void onItemClick(String id);
protected void updatePoll(String itemID, Status status, Poll poll){
status.poll=poll;
if(status.poll!=poll)
status.poll=poll;
int firstOptionIndex=-1, footerIndex=-1;
int i=0;
for(StatusDisplayItem item:displayItems){
@@ -382,28 +397,68 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
Poll poll=holder.getItem().poll;
Poll.Option option=holder.getItem().option;
if(poll.selectedOptions==null)
poll.selectedOptions=new ArrayList<>();
if(poll.multiple){
if(poll.selectedOptions==null)
poll.selectedOptions=new ArrayList<>();
if(poll.selectedOptions.contains(option)){
poll.selectedOptions.remove(option);
holder.itemView.setSelected(false);
}else{
poll.selectedOptions.add(option);
holder.itemView.setSelected(true);
}
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
if(footer.getItemID().equals(holder.getItemID())){
footer.rebind();
break;
}else{
if(poll.selectedOptions.contains(option))
return;
if(!poll.selectedOptions.isEmpty()){
Poll.Option previouslySelected=poll.selectedOptions.get(0);
poll.selectedOptions.clear();
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
if(vh instanceof PollOptionStatusDisplayItem.Holder otherOption){
if(otherOption.getItemID().equals(holder.getItemID()) && otherOption.getItem().option==previouslySelected){
otherOption.updateCheckedState();
break;
}
}
}
}
}else{
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
poll.selectedOptions.add(option);
}
holder.updateCheckedState();
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
if(footer.getItemID().equals(holder.getItemID())){
footer.rebind();
break;
}
}
}
}
public void onPollToggleResultsClick(PollFooterStatusDisplayItem.Holder holder){
Status status=holder.getItem().status.getContentStatus();
status.poll.showResults=!status.poll.showResults;
String itemID=holder.getItemID();
if(status.poll.selectedOptions!=null)
status.poll.selectedOptions.clear();
int firstOptionIndex=-1, footerIndex=-1;
int i=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(itemID)){
if(item instanceof PollOptionStatusDisplayItem optItem){
if(firstOptionIndex==-1)
firstOptionIndex=i;
optItem.showResults=status.poll.showResults;
}else if(item instanceof PollFooterStatusDisplayItem){
footerIndex=i;
break;
}
}
i++;
}
if(firstOptionIndex==-1 || footerIndex==-1)
throw new IllegalStateException("Can't find all poll items in displayItems");
adapter.notifyItemRangeChanged(firstOptionIndex, status.poll.options.size());
}
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
@@ -432,23 +487,33 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
toggleSpoiler(status, holder.getItemID());
}
SpoilerStatusDisplayItem spoilerItem=holder.getItem();
if(status.revealedSpoilers.contains(spoilerItem.spoilerType))
status.revealedSpoilers.remove(spoilerItem.spoilerType);
else
status.revealedSpoilers.add(spoilerItem.spoilerType);
protected void toggleSpoiler(Status status, String itemID){
status.spoilerRevealed=!status.spoilerRevealed;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null)
spoiler.rebind();
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
holder.rebind();
int index=displayItems.indexOf(spoilerItem);
if(status.spoilerRevealed){
if(status.revealedSpoilers.contains(spoilerItem.spoilerType)){
int itemCount=spoilerItem.contentItems.size();
displayItems.addAll(index+1, spoilerItem.contentItems);
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
if(spoilerItem.spoilerType==Status.SpoilerType.FILTER && spoilerItem.contentItems.get(0) instanceof SpoilerStatusDisplayItem nestedSpoiler
&& nestedSpoiler.spoilerType==Status.SpoilerType.CONTENT_WARNING && !GlobalUserPreferences.showCWs){
status.revealedSpoilers.add(Status.SpoilerType.CONTENT_WARNING);
displayItems.addAll(index+1+itemCount, nestedSpoiler.contentItems);
itemCount+=nestedSpoiler.contentItems.size();
}
adapter.notifyItemRangeInserted(index+1, itemCount);
}else{
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
int itemCount=spoilerItem.contentItems.size();
if(spoilerItem.contentItems.get(0) instanceof SpoilerStatusDisplayItem nestedSpoiler && status.revealedSpoilers.contains(nestedSpoiler.spoilerType)){
status.revealedSpoilers.remove(nestedSpoiler.spoilerType);
itemCount+=nestedSpoiler.contentItems.size();
}
displayItems.subList(index+1, index+1+itemCount).clear();
adapter.notifyItemRangeRemoved(index+1, itemCount);
}
list.invalidateItemDecorations();
}
@@ -690,6 +755,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
anim.start();
}
public void retryFailedImages(){
imgLoader.retryFailedRequests();
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){

View File

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

View File

@@ -290,7 +290,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements ComposeE
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text);
mediaBtn.setOnClickListener(v->openFilePicker());
mediaBtn.setOnClickListener(v->openFilePicker(false));
if(UiUtils.isPhotoPickerAvailable()){
mediaBtn.setOnLongClickListener(v->{
openFilePicker(true);
return true;
});
}
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -887,9 +893,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements ComposeE
*
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
*/
private void openFilePicker(){
private void openFilePicker(boolean forceGetContent){
Intent intent;
boolean usePhotoPicker=UiUtils.isPhotoPickerAvailable();
boolean usePhotoPicker=!forceGetContent && UiUtils.isPhotoPickerAvailable();
if(usePhotoPicker){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
if(mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount()>1)

View File

@@ -166,7 +166,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
photoViewer=new PhotoViewer(getActivity(), null, Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);

View File

@@ -84,7 +84,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
public void onSuccess(HeaderPaginationList<Account> result){
for(Account acc:result)
accountIDsInList.add(acc.id);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()));
}
})
.exec(accountID);

View File

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

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -21,6 +22,8 @@ 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.requests.notifications.GetNotificationsV1;
import org.joinmastodon.android.api.requests.notifications.GetUnreadNotificationsCount;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
@@ -28,8 +31,9 @@ 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.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -38,6 +42,7 @@ import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.IdRes;
@@ -53,7 +58,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment{
public class HomeFragment extends AppKitFragment implements AssistContentProviderFragment{
private FragmentRootLinearLayout content;
private HomeTimelineFragment homeTimelineFragment;
private NotificationsListFragment notificationsFragment;
@@ -288,37 +293,55 @@ public class HomeFragment extends AppKitFragment{
}
private void reloadNotificationsForUnreadCount(){
List<Notification>[] notifications=new List[]{null};
String[] marker={null};
Instance instance=AccountSessionManager.get(accountID).getInstanceInfo();
if(instance==null)
return;
if(instance.getApiVersion()>=2){
new GetUnreadNotificationsCount(EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes())
.setCallback(new Callback<>(){
@Override
public void onSuccess(GetUnreadNotificationsCount.Response result){
updateUnreadNotificationsBadge(result.count, false);
}
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
marker[0]=m;
if(notifications[0]!=null){
updateUnreadCount(notifications[0], marker[0]);
}
});
@Override
public void onError(ErrorResponse error){
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]);
}
}
})
.exec(accountID);
}else{
List<Notification>[] notifications=new List[]{null};
String[] marker={null};
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
marker[0]=m;
if(notifications[0]!=null){
updateUnreadCountV1(notifications[0], marker[0]);
}
});
@Override
public void onError(ErrorResponse error){}
});
new GetNotificationsV1(null, 40, EnumSet.allOf(NotificationType.class))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){
notifications[0]=result;
if(marker[0]!=null)
updateUnreadCountV1(notifications[0], marker[0]);
}
@Override
public void onError(ErrorResponse error){}
}).exec(accountID);
}
}
@SuppressLint("DefaultLocale")
private void updateUnreadCount(List<Notification> notifications, String marker){
private void updateUnreadCountV1(List<Notification> notifications, String marker){
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
notificationsBadge.setVisibility(View.GONE);
updateUnreadNotificationsBadge(0, false);
}else{
notificationsBadge.setVisibility(View.VISIBLE);
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
notificationsBadge.setText(String.format("%d+", notifications.size()));
updateUnreadNotificationsBadge(notifications.size(), true);
}else{
int count=0;
for(Notification n:notifications){
@@ -326,11 +349,20 @@ public class HomeFragment extends AppKitFragment{
break;
count++;
}
notificationsBadge.setText(String.format("%d", count));
updateUnreadNotificationsBadge(count, false);
}
}
}
private void updateUnreadNotificationsBadge(int count, boolean more){
if(count==0){
notificationsBadge.setVisibility(View.GONE);
}else{
notificationsBadge.setVisibility(View.VISIBLE);
notificationsBadge.setText(String.format(more ? "%d+" : "%d", count));
}
}
@Subscribe
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
@@ -348,4 +380,11 @@ public class HomeFragment extends AppKitFragment{
if(notificationsFragment.loaded)
notificationsFragment.rebuildAllDisplayItems();
}
@Override
public void onProvideAssistContent(AssistContent content){
if(fragmentForTab(currentTab) instanceof AssistContentProviderFragment provider){
provider.onProvideAssistContent(content);
}
}
}

View File

@@ -4,11 +4,14 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
@@ -30,6 +33,7 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import com.squareup.otto.Subscribe;
@@ -58,19 +62,25 @@ import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.NewPostsButtonContainer;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -89,7 +99,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private FixedAspectRatioImageView listsDropdownArrow;
private TextView listsDropdownText;
private Button newPostsBtn;
private View newPostsBtnWrap;
private NewPostsButtonContainer newPostsBtnWrap;
private boolean newPostsBtnShown;
private AnimatorSet currentNewPostsAnim;
private ToolbarDropdownMenuController dropdownController;
@@ -101,6 +111,11 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private DiscoverInfoBannerHelper localTimelineBannerHelper;
private View donationBanner;
private boolean donationBannerDismissing;
private NestedRecyclerScrollView scrollWrapper;
private String scrollBackItemID;
private int scrollBackItemOffset, scrollBackItemIndex;
private long scrollBackTime;
private String maxID;
private String lastSavedMarkerID;
@@ -108,6 +123,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private BottomSheet donationSheet;
public HomeTimelineFragment(){
setLayout(R.layout.fragment_loader_hiding_toolbar);
setListLayoutId(R.layout.fragment_timeline);
}
@@ -249,6 +265,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
@@ -267,15 +284,57 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
newPostsBtnWrap.setAlpha(0f);
newPostsBtnWrap.setTranslationY(V.dp(-56));
}
newPostsBtnWrap.setOnHideButtonListener(this::hideNewPostsButton);
updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
private HashSet<GapStatusDisplayItem> gaps=new HashSet<>();
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
hideNewPostsButton();
}
for(StatusDisplayItem item:displayItems){
if(item instanceof GapStatusDisplayItem gap){
gaps.add(gap);
}
}
if(gaps.isEmpty())
return;
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
if(list.getChildViewHolder(child) instanceof GapStatusDisplayItem.Holder holder){
GapStatusDisplayItem gap=holder.getItem();
if(!gap.visible){
gap.visible=true;
gap.enteredFromTop=child.getTop()<list.getHeight()/2;
holder.text.setText(gap.enteredFromTop ? R.string.load_missing_posts_above : R.string.load_missing_posts_below);
}
gaps.remove(gap);
}
}
for(GapStatusDisplayItem gap:gaps){
gap.visible=false;
}
gaps.clear();
}
});
View bottomOverlays=view.findViewById(R.id.bottom_overlays);
NestedRecyclerScrollView scroller=view.findViewById(R.id.scroller);
scroller.setScrollableChildSupplier(()->list);
scroller.setTakePriorityOverChildViews(true);
scroller.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY)->{
bottomOverlays.setTranslationY(scrollY-getToolbar().getHeight());
});
scroller.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
scroller.getViewTreeObserver().removeOnPreDrawListener(this);
bottomOverlays.setTranslationY(scroller.getScrollY()-getToolbar().getHeight());
return true;
}
});
scrollWrapper=scroller;
if(GithubSelfUpdater.needSelfUpdating()){
updateUpdateState(GithubSelfUpdater.getInstance().getState());
@@ -294,6 +353,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
state=updater.getState();
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
if("debug".equals(BuildConfig.BUILD_TYPE)){
menu.add(0, 1, 0, "Make a gap");
}
}
@Override
@@ -306,6 +369,17 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}else if(id==R.id.edit_list){
args.putParcelable("list", Parcels.wrap(currentList));
Nav.go(getActivity(), EditListFragment.class, args);
}else if(id==1){
if(data.size()<35){
Toast.makeText(getActivity(), "Too few posts. Load at least 35", Toast.LENGTH_SHORT).show();
return true;
}
Status gapStatus=data.get(1);
gapStatus.hasGapAfter=true;
onStatusUpdated(gapStatus);
for(Status s:new ArrayList<>(data.subList(2, 32))){
removeStatus(s);
}
}
return true;
}
@@ -383,6 +457,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
if(!(toAdd instanceof ArrayList<?>))
toAdd=new ArrayList<>(toAdd);
Set<String> existingPostIDs=data.stream().map(s->s.id).collect(Collectors.toSet());
toAdd.removeIf(s->existingPostIDs.contains(s.id));
if(needCache)
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
if(!toAdd.isEmpty()){
@@ -405,13 +483,23 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
public void onGapClick(GapStatusDisplayItem.Holder item){
if(dataLoading)
return;
item.getItem().loading=true;
GapStatusDisplayItem gap=item.getItem();
gap.loading=true;
V.setVisibilityAnimated(item.progress, View.VISIBLE);
V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem();
dataLoading=true;
boolean needCache=listMode==ListMode.FOLLOWING;
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){
boolean insertBelowGap=!gap.enteredFromTop;
String maxID, minID;
if(gap.enteredFromTop){
maxID=null;
int gapPos=displayItems.indexOf(gap);
minID=displayItems.get(gapPos+1).parentID;
}else{
maxID=item.getItemID();
minID=null;
}
loadAdditionalPosts(maxID, minID, 20, null, new Callback<>(){
@Override
public void onSuccess(List<Status> result){
@@ -429,9 +517,9 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(gapStatus!=null){
gapStatus.hasGapAfter=false;
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapStatus), false);
}
}else{
}else if(insertBelowGap){
Set<String> idsBelowGap=new HashSet<>();
boolean belowGap=false;
int gapPostIndex=0;
@@ -442,7 +530,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
belowGap=true;
s.hasGapAfter=false;
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(s), false);
}else{
gapPostIndex++;
}
@@ -460,8 +548,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
if(needCache)
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1); // Get a sub-list that contains the gap item
targetList.clear(); // remove the gap item
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
for(Status s:result){
if(idsBelowGap.contains(s.id))
@@ -478,6 +566,61 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
}else{
Set<String> idsAboveGap=new HashSet<>();
int gapPostIndex=0;
Status gapPost=null;
for(Status s:data){
if(s.id.equals(gap.parentID)){
gapPost=s;
break;
}else{
idsAboveGap.add(s.id);
gapPostIndex++;
}
}
if(gapPost==null)
return;
boolean needAdjustScroll=false;
int scrollTop=0;
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
if(list.getChildViewHolder(child) instanceof GapStatusDisplayItem.Holder gapHolder && gapHolder.getItem()==gap){
needAdjustScroll=true;
scrollTop=child.getBottom()+1;
break;
}
}
List<StatusDisplayItem> targetList=displayItems.subList(gapPos+1, gapPos+1);
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
for(int i=result.size()-1;i>=0;i--){
Status s=result.get(i);
if(idsAboveGap.contains(s.id))
break;
targetList.addAll(0, buildDisplayItems(s));
insertedPosts.add(0, s);
}
int addedItemCount=targetList.size();
boolean gapRemoved=false;
if(insertedPosts.size()<result.size()){ // There was an intersection, remove the gap
gapRemoved=true;
gapPost.hasGapAfter=false;
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapPost), false);
displayItems.remove(gapPos);
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
}else{
gap.loading=false;
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
}
if(!insertedPosts.isEmpty()){
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+(gapRemoved ? 0 : 1), addedItemCount);
if(needAdjustScroll){
((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(getMainAdapterOffset()+gapPos+(gapRemoved ? 0 : 1)+addedItemCount, scrollTop);
}
}
}
}
@@ -602,6 +745,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
@Override
public void onAnimationEnd(Animator animation){
newPostsBtnWrap.setVisibility(View.GONE);
newPostsBtn.setTranslationY(0);
currentNewPostsAnim=null;
}
});
@@ -612,7 +756,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private void onNewPostsBtnClick(View v){
if(newPostsBtnShown){
hideNewPostsButton();
scrollToTop();
smoothScrollRecyclerViewToTop(list);
}
}
@@ -719,6 +863,68 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
}
@Override
public void scrollToTop(){
if(list.getChildCount()==0)
return;
scrollWrapper.smoothScrollTo(0, 0);
View topChild=list.getLayoutManager().getChildAt(0);
if(list.getChildAdapterPosition(topChild)==0){
if(topChild.getTop()==list.getPaddingTop() && scrollBackItemID!=null && System.currentTimeMillis()-scrollBackTime<5*60_000){
int indexWithinPost=0;
for(int i=0;i<displayItems.size();i++){
StatusDisplayItem item=displayItems.get(i);
if(item.parentID.equals(scrollBackItemID)){
if(indexWithinPost==scrollBackItemIndex){
((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(i+getMainAdapterOffset(), scrollBackItemOffset);
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
list.getViewTreeObserver().removeOnPreDrawListener(this);
list.scrollBy(0, V.dp(-300));
list.smoothScrollBy(0, V.dp(300));
return true;
}
});
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S)
UiUtils.playVibrationEffectIfSupported(getActivity(), VibrationEffect.Composition.PRIMITIVE_THUD);
return;
}
indexWithinPost++;
}
}
}else{
smoothScrollRecyclerViewToTop(list);
return;
}
}else if(list.getChildViewHolder(topChild) instanceof StatusDisplayItem.Holder<?> itemHolder){
int postIndex;
String id=itemHolder.getItemID();
for(postIndex=0;postIndex<data.size();postIndex++){
if(data.get(postIndex).id.equals(id))
break;
}
if(postIndex>1){
scrollBackItemID=id;
scrollBackItemIndex=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(id)){
if(item==itemHolder.getItem())
break;
scrollBackItemIndex++;
}
}
scrollBackItemOffset=topChild.getTop();
scrollBackTime=System.currentTimeMillis();
}else{
scrollBackItemID=null;
}
}
smoothScrollRecyclerViewToTop(list);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S)
UiUtils.playVibrationEffectIfSupported(getActivity(), VibrationEffect.Composition.PRIMITIVE_QUICK_RISE);
}
private String getCurrentListTitle(){
return switch(listMode){
case FOLLOWING -> getString(R.string.timeline_following);
@@ -744,9 +950,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
TextView text=donationBanner.findViewById(R.id.banner_text);
SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage);
ssb.append(' ');
if(!campaign.bannerMessage.endsWith("\n"))
ssb.append(' ');
int start=ssb.length();
ssb.append(campaign.bannerButtonText);
ssb.append(campaign.bannerButtonText.trim());
ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0);
ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0);

View File

@@ -190,7 +190,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
@Subscribe
public void onAccountAddedToList(AccountAddedToListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
data.add(new AccountViewModel(ev.account, accountID));
data.add(new AccountViewModel(ev.account, accountID, getActivity()));
list.getAdapter().notifyItemInserted(data.size()-1);
}
}
@@ -281,7 +281,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_members)
.setTitle(selectedAccounts.size()>1 ? R.string.confirm_remove_list_members : R.string.confirm_remove_list_member)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts), null))
.setNegativeButton(R.string.cancel, null)
.show();
@@ -337,7 +337,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
onDone.run();
for(Account acc:accounts){
accountIDsInList.add(acc.id);
data.add(new AccountViewModel(acc, accountID));
data.add(new AccountViewModel(acc, accountID, getActivity()));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
}

View File

@@ -80,7 +80,7 @@ public class NotificationRequestsFragment extends MastodonRecyclerFragment<Notif
accountViewModels.clear();
maxID=result.getNextPageMaxID();
for(NotificationRequest req:result){
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false, getActivity()));
}
onDataLoaded(result, !TextUtils.isEmpty(maxID));
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);

View File

@@ -24,17 +24,16 @@ import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolic
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationsPolicy;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
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.viewcontrollers.GenericListItemsViewController;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
@@ -65,6 +64,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
private NotificationsPolicy lastPolicy;
private boolean refreshAfterLoading;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -97,13 +97,17 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
public void onSuccess(PaginatedResponse<List<NotificationViewModel>> result){
if(getActivity()==null)
return;
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
onDataLoaded(result.items, !result.items.isEmpty());
maxID=result.maxID;
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
reloadingFromCache=false;
if(refreshAfterLoading){
refreshAfterLoading=false;
refresh();
}
}
});
}
@@ -112,9 +116,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
protected void onShown(){
super.onShown();
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
if(!dataLoading && canRefreshWithoutUpsettingUser()){
reloadingFromCache=true;
refresh();
if(canRefreshWithoutUpsettingUser()){
if(dataLoading)
refreshAfterLoading=true;
else
refresh();
}
}
@@ -159,7 +165,10 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
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();
NotificationViewModel n=getNotificationByID(holder.getItemID());
if(n==null)
continue;
String itemID=n.notification.pageMaxId;
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
c.drawRect(tmpRect, paint);
@@ -181,12 +190,12 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
for(Notification ntf:data){
for(NotificationViewModel ntf:data){
if(ntf.status==null)
continue;
Status contentStatus=ntf.status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(ntf.id, ntf.status, ev.poll);
updatePoll(ntf.getID(), ntf.status, ev.poll);
}
}
}
@@ -195,10 +204,10 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
return;
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
List<NotificationViewModel> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(n->n.status!=null && n.status.account.id.equals(ev.postsByAccountID))
.collect(Collectors.toList());
for(Notification n:toRemove){
for(NotificationViewModel n:toRemove){
removeNotification(n);
}
}
@@ -235,7 +244,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.mark_all_read){
markAsRead();
markAsRead(true);
resetUnreadBackground();
}else if(id==R.id.filters){
showFiltersAlert();
@@ -251,11 +260,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
return mergeAdapter;
}
private void markAsRead(){
private void markAsRead(boolean force){
if(data.isEmpty())
return;
String id=data.get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
String id=data.get(0).notification.pageMaxId;
if(force || ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
@@ -277,13 +286,14 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
}
@Override
public void onAppendItems(List<Notification> items){
public void onAppendItems(List<NotificationViewModel> items){
super.onAppendItems(items);
if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker))
// TODO
if(data.isEmpty() || data.get(0).getID().equals(realUnreadMarker))
return;
for(Notification n:items){
if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0){
markAsRead();
for(NotificationViewModel n:items){
if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){
markAsRead(false);
break;
}
}
@@ -297,7 +307,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
String id=itemHolder.getItemID();
for(int j=0;j<data.size();j++){
if(data.get(j).id.equals(id))
if(data.get(j).getID().equals(id))
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
}
}

View File

@@ -36,7 +36,6 @@ import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
@@ -72,7 +71,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
list.setItemAnimator(new BetterItemAnimator());
list.setDrawSelectorOnTop(true);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
imgLoader=new ListImageLoaderWrapper(getActivity(), list, list, null);
list.setAdapter(adapter=new AboutAdapter());
list.setPadding(0, V.dp(16), 0, 0);
list.setClipToPadding(false);

View File

@@ -51,7 +51,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
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);
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, true);
};
if(s.firstInSection){

View File

@@ -6,9 +6,11 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Outline;
@@ -49,23 +51,28 @@ import android.widget.Toolbar;
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.GetAccountFamiliarFollowers;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.FamiliarFollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.FamiliarFollowers;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
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.Snackbar;
import org.joinmastodon.android.ui.photoviewer.AvatarCropper;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
@@ -89,6 +96,8 @@ import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -106,7 +115,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
public class ProfileFragment extends LoaderFragment implements ScrollableToTop, AssistContentProviderFragment{
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@@ -120,6 +129,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
private ProfileFeaturedFragment featuredFragment;
private AccountTimelineFragment timelineFragment;
private ProfileAboutFragment aboutFragment;
private SavedPostsTimelineFragment savedFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
private View followersBtn, followingBtn;
@@ -136,12 +146,16 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
private ImageButton qrCodeButton;
private ProgressBar innerProgress;
private View actions;
private View familiarFollowersRow;
private ImageView[] familiarFollowersAvatars;
private TextView familiarFollowersLabel;
private Account account;
private String accountID;
private Relationship relationship;
private boolean isOwnProfile;
private ArrayList<AccountField> fields=new ArrayList<>();
private List<Account> familiarFollowers=List.of();
private boolean isInEditMode, editDirty;
private Uri editNewAvatar, editNewCover;
@@ -222,6 +236,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
qrCodeButton=content.findViewById(R.id.qr_code);
innerProgress=content.findViewById(R.id.profile_progress);
actions=content.findViewById(R.id.profile_actions);
familiarFollowersRow=content.findViewById(R.id.familiar_followers);
familiarFollowersAvatars=new ImageView[]{
content.findViewById(R.id.familiar_followers_ava1),
content.findViewById(R.id.familiar_followers_ava2),
content.findViewById(R.id.familiar_followers_ava3),
};
familiarFollowersLabel=content.findViewById(R.id.familiar_followers_label);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@@ -234,13 +255,14 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
}
};
tabViews=new FrameLayout[3];
tabViews=new FrameLayout[4];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_featured;
case 1 -> R.id.profile_timeline;
case 2 -> R.id.profile_about;
case 3 -> R.id.profile_saved;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -248,7 +270,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
tabViews[i]=tabView;
}
pager.setOffscreenPageLimit(4);
pager.setOffscreenPageLimit(10);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -262,6 +284,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
case 0 -> R.string.profile_featured;
case 1 -> R.string.profile_timeline;
case 2 -> R.string.profile_about;
case 3 -> R.string.profile_saved_posts;
default -> throw new IllegalStateException();
}));
tabbar.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@@ -290,11 +313,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
fab.setOnClickListener(this::onFabClick);
familiarFollowersRow.setOnClickListener(this::onFamiliarFollowersClick);
if(savedInstanceState!=null){
featuredFragment=(ProfileFeaturedFragment) getChildFragmentManager().getFragment(savedInstanceState, "featured");
timelineFragment=(AccountTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "timeline");
aboutFragment=(ProfileAboutFragment) getChildFragmentManager().getFragment(savedInstanceState, "about");
savedFragment=(SavedPostsTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "saved");
}
if(loaded){
@@ -349,6 +374,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
qf.setArguments(args);
qf.show(getChildFragmentManager(), "qrDialog");
});
familiarFollowersRow.setVisibility(View.GONE);
return sizeWrapper;
}
@@ -376,6 +402,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
timelineFragment.onRefresh();
if(featuredFragment.loaded)
featuredFragment.onRefresh();
if(savedFragment!=null && savedFragment.loaded)
savedFragment.onRefresh();
}
V.setVisibilityAnimated(fab, View.VISIBLE);
}
@@ -411,16 +439,21 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
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;
}
});
if(savedFragment==null && isOwnProfile){
savedFragment=SavedPostsTimelineFragment.newInstance(accountID, account, false);
}
if(!refreshing){
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();
}
@@ -486,6 +519,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
getChildFragmentManager().putFragment(outState, "timeline", timelineFragment);
if(aboutFragment.isAdded())
getChildFragmentManager().putFragment(outState, "about", aboutFragment);
if(savedFragment!=null && savedFragment.isAdded())
getChildFragmentManager().putFragment(outState, "saved", savedFragment);
}
@Override
@@ -514,6 +549,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
if(timelineFragment!=null && timelineFragment.isAdded() && childInsets!=null){
timelineFragment.onApplyWindowInsets(childInsets);
featuredFragment.onApplyWindowInsets(childInsets);
if(savedFragment!=null)
savedFragment.onApplyWindowInsets(childInsets);
}
}
@@ -554,10 +591,10 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
}
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
ViewImageLoader.loadWithoutAnimation(avatar, avatar.getDrawable(), new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.loadWithoutAnimation(cover, cover.getDrawable(), new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
if(GlobalUserPreferences.customEmojiInNames)
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
@@ -580,7 +617,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
}else{
@@ -615,7 +652,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
fields.add(joined);
for(AccountField field:account.fields){
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
@@ -642,6 +679,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
if(onScrollListener!=null){
onScrollListener.setViews(getToolbar());
}
getToolbar().setTranslationZ(tabBarIsAtTop ? 0 : V.dp(3));
}
private CharSequence makeRedString(CharSequence s){
@@ -729,14 +767,6 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.bookmarks){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
}else if(id==R.id.favorites){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
}else if(id==R.id.save){
if(isInEditMode)
saveAndExitEditMode();
@@ -763,6 +793,9 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.copy_link){
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, account.url));
UiUtils.maybeShowTextCopiedToast(getActivity());
}
return true;
}
@@ -784,6 +817,25 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
}
})
.exec(accountID);
new GetAccountFamiliarFollowers(Set.of(account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<FamiliarFollowers> result){
for(FamiliarFollowers ff:result){
if(ff.id.equals(account.id)){
familiarFollowers=ff.accounts;
updateFamiliarFollowers();
break;
}
}
}
@Override
public void onError(ErrorResponse error){
}
})
.exec(accountID);
}
private void updateRelationship(){
@@ -794,6 +846,38 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
}
private void updateFamiliarFollowers(){
if(!familiarFollowers.isEmpty()){
familiarFollowersRow.setVisibility(View.VISIBLE);
List<AccountViewModel> followers=familiarFollowers.stream().limit(3).map(a->new AccountViewModel(a, accountID, false, getActivity())).collect(Collectors.toList());
String template=switch(familiarFollowers.size()){
case 1 -> getString(R.string.familiar_followers_one, "{first}");
case 2 -> getString(R.string.familiar_followers_two, "{first}", "{second}");
default -> getResources().getQuantityString(R.plurals.familiar_followers_many, familiarFollowers.size()-2, "{first}", "{second}", familiarFollowers.size()-2);
};
SpannableStringBuilder ssb=new SpannableStringBuilder(template);
if(familiarFollowers.size()>1){
int index=template.indexOf("{second}");
ssb.replace(index, index+8, followers.get(1).parsedName);
template=template.replace("{second}", "#".repeat(followers.get(1).parsedName.length()));
}
int index=template.indexOf("{first}");
ssb.replace(index, index+7, followers.get(0).parsedName);
familiarFollowersLabel.setText(ssb);
UiUtils.loadCustomEmojiInTextView(familiarFollowersLabel);
if(familiarFollowers.size()<3)
familiarFollowersAvatars[2].setVisibility(View.GONE);
if(familiarFollowers.size()<2)
familiarFollowersAvatars[1].setVisibility(View.GONE);
int i=0;
for(AccountViewModel avm:followers){
ViewImageLoader.loadWithoutAnimation(familiarFollowersAvatars[i], getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), avm.avaRequest);
i++;
}
}
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
if(scrollY>cover.getHeight()){
cover.setTranslationY(scrollY-(cover.getHeight()));
@@ -847,6 +931,9 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
editSaveMenuItem.setVisible(!buttonInView);
}
}
if((scrollY==0 && oldScrollY!=0) || (scrollY!=0 && oldScrollY==0)){
refreshLayout.setEnabled(scrollY==0);
}
}
private Fragment getFragmentForPage(int page){
@@ -854,6 +941,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
case 0 -> featuredFragment;
case 1 -> timelineFragment;
case 2 -> aboutFragment;
case 3 -> savedFragment;
default -> throw new IllegalStateException();
};
}
@@ -917,7 +1005,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
pager.setUserInputEnabled(false);
actionButton.setText(R.string.save_changes);
pager.setCurrentItem(2);
for(int i=0;i<3;i++){
for(int i=0;i<4;i++){
tabbar.getTabAt(i).view.setEnabled(false);
}
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
@@ -994,7 +1082,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
invalidateOptionsMenu();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
for(int i=0;i<4;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
@@ -1134,7 +1222,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
if(ava==null)
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
currentPhotoViewer=new PhotoViewer(getActivity(), null, createFakeAttachments(account.avatar, ava), 0,
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
@@ -1148,7 +1236,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
Drawable drawable=cover.getDrawable();
if(drawable==null || drawable instanceof ColorDrawable || account.headerStatic.endsWith("/missing.png"))
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
currentPhotoViewer=new PhotoViewer(getActivity(), null, createFakeAttachments(account.header, drawable), 0,
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}
@@ -1162,6 +1250,14 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
Nav.go(getActivity(), ComposeFragment.class, args);
}
private void onFamiliarFollowersClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
args.putInt("count", familiarFollowers.size());
Nav.go(getActivity(), FamiliarFollowerListFragment.class, args);
}
private void startImagePicker(int requestCode){
Intent intent=UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1);
startActivityForResult(intent, requestCode);
@@ -1171,9 +1267,16 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode==Activity.RESULT_OK){
if(requestCode==AVATAR_RESULT){
editNewAvatar=data.getData();
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
editDirty=true;
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
int radius=V.dp(25);
new AvatarCropper(getActivity(), data.getData(), new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->{}, null, null, null), (thumbnail, uri)->{
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
avatar.setImageDrawable(thumbnail);
editNewAvatar=uri;
editDirty=true;
}, ()->getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)).show();
}else if(requestCode==COVER_RESULT){
editNewCover=data.getData();
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
@@ -1206,6 +1309,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
return actionButton.getVisibility()==View.VISIBLE && actionButtonWrap.getTop()+actionButtonWrap.getHeight()>scrollView.getScrollY();
}
@Override
public void onProvideAssistContent(AssistContent content){
if(account!=null){
content.setWebUri(Uri.parse(account.url));
}
}
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
@@ -1241,7 +1351,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
@Override
public int getItemCount(){
return loaded ? 3 : 0;
return loaded ? (isOwnProfile ? 4 : 3) : 0;
}
@Override

View File

@@ -0,0 +1,169 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
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 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 SavedPostsTimelineFragment extends StatusListFragment{
private Account user;
private Mode mode;
private HorizontalScrollView filtersBar;
private FilterChipView favoritesChip, bookmarksChip;
public SavedPostsTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_no_refresh);
}
public static SavedPostsTimelineFragment newInstance(String accountID, Account profileAccount, boolean load){
SavedPostsTimelineFragment f=new SavedPostsTimelineFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
if(!load)
args.putBoolean("noAutoLoad", true);
args.putBoolean("__is_tab", true);
f.setArguments(args);
return f;
}
@Override
public void onAttach(Activity activity){
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
mode=Mode.FAVORITES;
super.onAttach(activity);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=(switch(mode){
case FAVORITES -> new GetFavoritedStatuses(offset>0 ? getMaxID() : null, count);
case BOOKMARKS -> new GetBookmarkedStatuses(offset>0 ? getMaxID() : null, count);
}).setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Status> result){
if(getActivity()==null)
return;
onDataLoaded(result, result.nextPageUri!=null);
}
}).exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackground(null); // prevents unnecessary overdraw
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
protected void onStatusCreated(Status status){
// no-op
}
@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), 0, V.dp(16), V.dp(8));
filtersLayout.setDividerDrawable(new EmptyDrawable(V.dp(8), 1));
filtersLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
favoritesChip=new FilterChipView(getActivity());
favoritesChip.setText(R.string.your_favorites);
favoritesChip.setTag(Mode.FAVORITES);
favoritesChip.setSelected(mode==Mode.FAVORITES);
favoritesChip.setOnClickListener(this::onFilterClick);
filtersLayout.addView(favoritesChip);
bookmarksChip=new FilterChipView(getActivity());
bookmarksChip.setText(R.string.bookmarks);
bookmarksChip.setTag(Mode.BOOKMARKS);
bookmarksChip.setSelected(mode==Mode.BOOKMARKS);
bookmarksChip.setOnClickListener(this::onFilterClick);
filtersLayout.addView(bookmarksChip);
View banner=getActivity().getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
TextView text=banner.findViewById(R.id.banner_text);
text.setText(R.string.profile_saved_posts_explanation);
ImageView icon=banner.findViewById(R.id.icon);
icon.setImageResource(R.drawable.ic_lock_24px);
// Prevents margins messing up things
FrameLayout bannerW=new FrameLayout(getActivity());
bannerW.addView(banner);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(bannerW));
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(filtersBar));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private FilterChipView getViewForMode(Mode mode){
return switch(mode){
case FAVORITES -> favoritesChip;
case BOOKMARKS -> bookmarksChip;
};
}
private void onFilterClick(View v){
Mode newMode=(Mode) v.getTag();
if(newMode==mode)
return;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
getViewForMode(mode).setSelected(false);
mode=newMode;
v.setSelected(true);
data.clear();
preloadedData.clear();
int size=displayItems.size();
displayItems.clear();
adapter.notifyItemRangeRemoved(0, size);
loaded=false;
dataLoading=true;
doLoadData();
}
private enum Mode{
FAVORITES,
BOOKMARKS
}
}

View File

@@ -19,11 +19,13 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
import org.joinmastodon.android.api.session.AccountSessionManager;
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.InstanceV1;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -172,15 +174,14 @@ public class SplashFragment extends AppKitFragment{
}
private void proceedWithServerDomain(String domain){
new GetInstance()
.setCallback(new Callback<>(){
AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
if(!result.areRegistrationsOpen() && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
@@ -203,8 +204,7 @@ public class SplashFragment extends AppKitFragment{
instanceLoadingProgress=null;
error.showToast(getActivity());
}
})
.execNoAuth(domain);
});
}
private void onLearnMoreClick(View v){
@@ -243,7 +243,7 @@ public class SplashFragment extends AppKitFragment{
@Override
public boolean wantsLightNavigationBar(){
return true;
return false;
}
@Override

View File

@@ -2,17 +2,17 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
@@ -54,7 +54,10 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
List<StatusDisplayItem> items=new ArrayList<>();
InlineStatusStatusDisplayItem inlineItem=new InlineStatusStatusDisplayItem(s.getID(), this, s);
inlineItem.fullWidth=true;
items.add(inlineItem);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -144,12 +147,6 @@ public class StatusEditHistoryFragment extends StatusListFragment{
return items;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
@Override
public boolean isItemEnabled(String id){
return false;

View File

@@ -29,7 +29,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true);
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true);
}
@Override

View File

@@ -1,6 +1,10 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -22,6 +26,7 @@ import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
@@ -31,7 +36,9 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
@@ -41,7 +48,7 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment{
public class ThreadFragment extends StatusListFragment implements AssistContentProviderFragment{
private Status mainStatus;
private ImageView endMark;
private FrameLayout replyContainer;
@@ -49,6 +56,7 @@ public class ThreadFragment extends StatusListFragment{
private ImageView replyButtonAva;
private TextView replyButtonText;
private int lastBottomInset;
private Paint replyLinePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
public void onCreate(Bundle savedInstanceState){
@@ -60,7 +68,7 @@ public class ThreadFragment extends StatusListFragment{
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
if(GlobalUserPreferences.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));
@@ -68,9 +76,10 @@ public class ThreadFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_NO_IN_REPLY_TO);
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
item.fullWidth=true;
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
@@ -157,6 +166,8 @@ public class ThreadFragment extends StatusListFragment{
showContent();
if(!loaded)
footerProgress.setVisibility(View.VISIBLE);
list.addItemDecoration(new ReplyLinesItemDecoration());
}
protected void onStatusCreated(Status status){
@@ -222,4 +233,88 @@ public class ThreadFragment extends StatusListFragment{
public int getSnackbarOffset(){
return replyContainer.getHeight()-lastBottomInset;
}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
if(holder instanceof StatusDisplayItem.Holder<?> statusHolder && siblingHolder instanceof StatusDisplayItem.Holder<?> siblingStatusHolder){
Status siblingStatus=getStatusByID(siblingStatusHolder.getItemID());
if(siblingStatus==null)
return;
if(statusHolder.getItemID().equals(siblingStatus.inReplyToId) && siblingStatus!=mainStatus && !statusHolder.getItemID().equals(mainStatus.id))
return;
}
super.drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, paint);
}
private Status findPreviousStatus(String id){
for(int i=0;i<data.size();i++){
if(data.get(i).id.equals(id))
return i>0 ? data.get(i-1) : null;
}
return null;
}
private Status findNextStatus(String id){
for(int i=0;i<data.size();i++){
if(data.get(i).id.equals(id))
return i<data.size()-1 ? data.get(i+1) : null;
}
return null;
}
@Override
public void onProvideAssistContent(AssistContent content){
content.setWebUri(Uri.parse(mainStatus.url));
}
private class ReplyLinesItemDecoration extends RecyclerView.ItemDecoration{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
String currentID=null;
boolean connectUp=false, connectToRoot=false, connectReply=false;
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(2));
paint.setStrokeCap(Paint.Cap.ROUND);
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
if(!(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder) || holder.getItemID().equals(mainStatus.id))
continue;
String itemID=holder.getItemID();
if(!Objects.equals(currentID, itemID)){
currentID=itemID;
Status current=getStatusByID(currentID);
Status previous=findPreviousStatus(currentID);
Status next=findNextStatus(currentID);
if(current==null)
continue;
connectUp=previous!=null && previous.id.equals(current.inReplyToId);
connectToRoot=mainStatus.id.equals(current.inReplyToId);
connectReply=next!=null && itemID.equals(next.inReplyToId);
}
if(!connectUp && !connectToRoot && !connectReply)
continue;
float lineX=V.dp(36);
paint.setAlpha(Math.round(255*child.getAlpha()));
c.save();
c.clipRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
if(holder instanceof HeaderStatusDisplayItem.Holder){
if(connectUp || connectToRoot){
c.drawLine(lineX, child.getY()-V.dp(2), lineX, child.getY()+V.dp(14), paint);
}
if(connectReply){
c.drawLine(lineX, child.getY()+V.dp(62), lineX, child.getY()+child.getHeight()+V.dp(2), paint);
}
}else if(connectReply){
c.drawLine(lineX, child.getY(), lineX, child.getY()+child.getHeight(), paint);
}
c.restore();
}
}
}
}

View File

@@ -61,7 +61,7 @@ public class AccountSearchFragment extends BaseAccountListFragment{
protected void onSuccess(List<Account> result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false);
}
protected String getSearchViewPlaceholder(){

View File

@@ -44,7 +44,7 @@ public class AddNewListMembersFragment extends AccountSearchFragment{
@Override
public void onSuccess(HeaderPaginationList<Account> result){
setEmptyText("");
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), result.nextPageUri!=null);
maxID=result.getNextPageMaxID();
}
})

View File

@@ -0,0 +1,48 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountFamiliarFollowers;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FamiliarFollowers;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.parceler.Parcels;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class FamiliarFollowerListFragment extends BaseAccountListFragment{
protected Account account;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct);
int count=getArguments().getInt("count");
setSubtitle(getResources().getQuantityString(R.plurals.x_followers_you_know, count, count));
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountFamiliarFollowers(Set.of(account.id))
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FamiliarFollowers> result){
onDataLoaded(result.get(0).accounts.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
public void onResume(){
super.onResume();
if(!loaded && !dataLoading)
loadData();
}
}

View File

@@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);

View File

@@ -31,7 +31,7 @@ public class DiscoverAccountsFragment extends BaseAccountListFragment implements
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity())).collect(Collectors.toList());
onDataLoaded(accounts, false);
bannerHelper.onBannerBecameVisible();
}

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