Compare commits

...

310 Commits

Author SHA1 Message Date
Grishka
e10faeefc4 Update languages 2022-12-06 19:18:51 +03:00
Grishka
65dbbb3d61 Fix #443 2022-12-06 18:44:17 +03:00
Grishka
fa69868ca1 Merge branch 'l10n_master' 2022-12-06 18:33:35 +03:00
Eugen Rochko
9c18de7b90 New translations strings.xml (Icelandic) 2022-12-06 15:26:12 +01:00
Eugen Rochko
61bd19f6ff New translations strings.xml (Icelandic) 2022-12-06 14:24:50 +01:00
Eugen Rochko
ba0689aef7 New translations strings.xml (German) 2022-12-05 21:20:13 +01:00
Eugen Rochko
ad54e6bb4b New translations full_description.txt (German) 2022-12-05 20:15:33 +01:00
Eugen Rochko
f15fcb43da New translations strings.xml (German) 2022-12-05 20:15:32 +01:00
Eugen Rochko
f2557b7815 New translations strings.xml (Thai) 2022-12-05 15:50:40 +01:00
Eugen Rochko
a2726f5b61 New translations strings.xml (Vietnamese) 2022-12-05 15:50:39 +01:00
Eugen Rochko
a30f5bdee8 New translations strings.xml (Russian) 2022-12-05 12:12:10 +01:00
Eugen Rochko
4cef005286 New translations strings.xml (German) 2022-12-04 21:26:22 +01:00
Eugen Rochko
58a05681fe New translations strings.xml (Turkish) 2022-12-04 20:14:06 +01:00
Eugen Rochko
2589faf499 New translations full_description.txt (Hungarian) 2022-12-04 12:08:30 +01:00
Eugen Rochko
a5bdf34289 New translations strings.xml (French) 2022-12-04 10:30:25 +01:00
Eugen Rochko
09fdd7f492 New translations strings.xml (Turkish) 2022-12-04 06:23:19 +01:00
Eugen Rochko
519d8b887d New translations strings.xml (Filipino) 2022-12-03 23:52:40 +01:00
Eugen Rochko
a2f2263bf7 New translations strings.xml (Swedish) 2022-12-03 23:52:39 +01:00
Eugen Rochko
5b73b10b34 New translations strings.xml (Russian) 2022-12-03 23:52:38 +01:00
Eugen Rochko
b7a4364a28 New translations strings.xml (Portuguese) 2022-12-03 23:52:37 +01:00
Eugen Rochko
3f075aff7b New translations strings.xml (Korean) 2022-12-03 23:52:37 +01:00
Eugen Rochko
f4c33a5970 New translations strings.xml (Japanese) 2022-12-03 23:52:36 +01:00
Eugen Rochko
809af0ec18 New translations strings.xml (Italian) 2022-12-03 23:52:35 +01:00
Eugen Rochko
4ee640e072 New translations strings.xml (Armenian) 2022-12-03 23:52:34 +01:00
Eugen Rochko
1cbf310555 New translations strings.xml (Hebrew) 2022-12-03 23:52:33 +01:00
Eugen Rochko
f1fdc8aa43 New translations strings.xml (Turkish) 2022-12-03 23:52:32 +01:00
Eugen Rochko
d696daece3 New translations strings.xml (Czech) 2022-12-03 23:52:29 +01:00
Eugen Rochko
967bb09282 New translations strings.xml (Catalan) 2022-12-03 23:52:28 +01:00
Eugen Rochko
136d910b3b New translations strings.xml (Spanish) 2022-12-03 23:52:27 +01:00
Eugen Rochko
51eb48a455 New translations strings.xml (French) 2022-12-03 23:52:26 +01:00
Eugen Rochko
6ee8afcf96 New translations strings.xml (Arabic) 2022-12-03 23:52:25 +01:00
Eugen Rochko
a59f2d4609 New translations strings.xml (German) 2022-12-03 23:52:24 +01:00
Eugen Rochko
b75d871837 New translations strings.xml (Chinese Traditional) 2022-12-03 23:52:23 +01:00
Eugen Rochko
c72f93b990 New translations strings.xml (Basque) 2022-12-03 23:52:22 +01:00
Eugen Rochko
586d337ead New translations strings.xml (Polish) 2022-12-03 23:52:21 +01:00
Eugen Rochko
d84e10a22e New translations strings.xml (Ukrainian) 2022-12-03 23:52:20 +01:00
Eugen Rochko
351ec89207 New translations strings.xml (Vietnamese) 2022-12-03 23:52:19 +01:00
Eugen Rochko
7db7bf0220 New translations strings.xml (Hungarian) 2022-12-03 23:52:18 +01:00
Eugen Rochko
a9764c4f46 New translations strings.xml (Icelandic) 2022-12-03 23:52:17 +01:00
Eugen Rochko
a430b6a280 New translations strings.xml (Slovenian) 2022-12-03 23:52:16 +01:00
Eugen Rochko
6a01124d13 New translations strings.xml (Romanian) 2022-12-03 23:52:14 +01:00
Eugen Rochko
2843e445e2 New translations strings.xml (Chinese Simplified) 2022-12-03 23:52:12 +01:00
Eugen Rochko
5c947d14b2 New translations strings.xml (Scottish Gaelic) 2022-12-03 23:52:11 +01:00
Eugen Rochko
590adba3e3 New translations strings.xml (Indonesian) 2022-12-03 23:52:10 +01:00
Eugen Rochko
efee249173 New translations strings.xml (Dutch) 2022-12-03 23:52:09 +01:00
Eugen Rochko
6d2ed27364 New translations strings.xml (Kabyle) 2022-12-03 23:52:08 +01:00
Eugen Rochko
55716d742f New translations strings.xml (Bosnian) 2022-12-03 23:52:06 +01:00
Eugen Rochko
e4555da735 New translations strings.xml (Croatian) 2022-12-03 23:52:05 +01:00
Eugen Rochko
8b4b99bec7 New translations strings.xml (Thai) 2022-12-03 23:52:04 +01:00
Eugen Rochko
5de4b19969 New translations strings.xml (Galician) 2022-12-03 23:52:03 +01:00
Eugen Rochko
a9460f401e New translations strings.xml (Portuguese, Brazilian) 2022-12-03 23:52:01 +01:00
Grishka
012cca550e New login screen 2022-12-04 01:34:03 +03:00
Eugen Rochko
0c743db412 New translations strings.xml (German) 2022-12-03 22:56:40 +01:00
Eugen Rochko
b819ee7d6d New translations strings.xml (German) 2022-12-03 21:24:27 +01:00
Eugen Rochko
e7e3a249b5 New translations strings.xml (German) 2022-12-03 20:28:32 +01:00
Eugen Rochko
980c580b55 New translations full_description.txt (Hungarian) 2022-12-03 18:23:40 +01:00
Eugen Rochko
e23c530e74 New translations short_description.txt (Hungarian) 2022-12-03 17:20:17 +01:00
Eugen Rochko
a64caccca2 New translations full_description.txt (Hungarian) 2022-12-03 17:20:16 +01:00
Eugen Rochko
726ec7159c New translations strings.xml (Kabyle) 2022-12-03 07:19:07 +01:00
Eugen Rochko
e74256ef6f New translations strings.xml (Polish) 2022-12-02 22:37:01 +01:00
Eugen Rochko
a18718ca81 New translations strings.xml (Chinese Traditional) 2022-12-02 17:34:15 +01:00
Eugen Rochko
5a9bc0e269 New translations full_description.txt (German) 2022-12-02 16:33:35 +01:00
Eugen Rochko
2d39c62ff0 New translations strings.xml (German) 2022-12-02 16:33:34 +01:00
Eugen Rochko
0da4f79413 New translations strings.xml (Hungarian) 2022-12-02 14:17:58 +01:00
Eugen Rochko
2bdef776a2 New translations strings.xml (Hungarian) 2022-12-02 13:08:55 +01:00
Eugen Rochko
a57ad67308 New translations strings.xml (Italian) 2022-12-01 22:01:18 +01:00
Eugen Rochko
e63d04cea9 New translations strings.xml (Basque) 2022-12-01 22:01:17 +01:00
Eugen Rochko
cf48cb6f75 New translations strings.xml (Hungarian) 2022-12-01 19:43:19 +01:00
Eugen Rochko
542e53cf6a New translations strings.xml (Basque) 2022-12-01 19:43:18 +01:00
Eugen Rochko
bab1d40038 New translations strings.xml (Filipino) 2022-12-01 18:41:53 +01:00
Eugen Rochko
18f605e5c5 New translations strings.xml (Catalan) 2022-11-30 22:54:44 +01:00
Eugen Rochko
cd8a80a6a1 New translations strings.xml (Slovenian) 2022-11-29 22:06:16 +01:00
Eugen Rochko
3ce8aa7894 New translations strings.xml (Hungarian) 2022-11-29 21:05:04 +01:00
Eugen Rochko
b356794da9 New translations strings.xml (Icelandic) 2022-11-29 18:17:42 +01:00
Eugen Rochko
afe8f6cf6a New translations strings.xml (Vietnamese) 2022-11-29 14:58:28 +01:00
Eugen Rochko
ed0df82fe9 New translations strings.xml (Thai) 2022-11-29 14:03:11 +01:00
Eugen Rochko
d3bc7a9790 New translations strings.xml (Japanese) 2022-11-29 05:13:57 +01:00
Eugen Rochko
633c0f870d New translations strings.xml (Galician) 2022-11-28 08:56:16 +01:00
Eugen Rochko
f9fe7819f9 New translations strings.xml (Hungarian) 2022-11-28 00:06:00 +01:00
Eugen Rochko
f3d13545e7 New translations strings.xml (Hungarian) 2022-11-27 23:06:27 +01:00
Eugen Rochko
f6b77777b5 New translations full_description.txt (Czech) 2022-11-27 20:34:15 +01:00
Eugen Rochko
340990fbd9 New translations strings.xml (Czech) 2022-11-27 20:34:14 +01:00
Eugen Rochko
a7687f8e35 New translations strings.xml (Czech) 2022-11-27 19:31:30 +01:00
Eugen Rochko
52aa4a5289 New translations strings.xml (Czech) 2022-11-27 17:43:12 +01:00
Eugen Rochko
268accea14 New translations strings.xml (Filipino) 2022-11-27 16:43:39 +01:00
Eugen Rochko
101cde4d84 New translations strings.xml (French) 2022-11-27 16:43:37 +01:00
Eugen Rochko
8863446f6a New translations strings.xml (Czech) 2022-11-27 15:03:46 +01:00
Eugen Rochko
28a0824f6b New translations strings.xml (Czech) 2022-11-27 13:59:12 +01:00
Grishka
4b16262a1a Sync last seen notification ID with server 2022-11-27 13:39:50 +03:00
Eugen Rochko
b1f9d0516d New translations strings.xml (Dutch) 2022-11-27 11:30:42 +01:00
Grishka
10e7cbf022 Merge branch 'add/verified-profile-fields' 2022-11-27 13:15:13 +03:00
Grishka
531b8ead04 Make verified fields more like on iOS 2022-11-27 13:14:31 +03:00
Grishka
4b2c94ab52 Implement bookmarks and add favorites list
Closes #22, at last
2022-11-27 12:43:07 +03:00
Eugen Rochko
5b21747d5d New translations strings.xml (Filipino) 2022-11-27 10:01:36 +01:00
Grishka
a98becf2f4 Update splash logo to purple 2022-11-27 11:40:55 +03:00
Eugen Rochko
9fda48cff0 New translations strings.xml (Filipino) 2022-11-27 09:04:16 +01:00
Grishka
54f9eace67 Fix #422 2022-11-27 10:02:47 +03:00
Eugen Rochko
0e6f3df212 New translations strings.xml (Galician) 2022-11-27 07:48:06 +01:00
Eugen Rochko
a8c3f1555e New translations title.txt (Filipino) 2022-11-26 22:17:53 +01:00
Eugen Rochko
cd797a637b New translations short_description.txt (Filipino) 2022-11-26 22:17:53 +01:00
Eugen Rochko
53b2eb59d3 New translations full_description.txt (Filipino) 2022-11-26 22:17:52 +01:00
Eugen Rochko
09e2224596 New translations strings.xml (Filipino) 2022-11-26 22:17:51 +01:00
Eugen Rochko
5999aad21b New translations title.txt (Hungarian) 2022-11-26 22:17:50 +01:00
Eugen Rochko
874ce07c3e New translations short_description.txt (Hungarian) 2022-11-26 22:17:49 +01:00
Eugen Rochko
1787d08718 New translations full_description.txt (Hungarian) 2022-11-26 22:17:48 +01:00
Eugen Rochko
9a12be88da New translations strings.xml (Hungarian) 2022-11-26 22:17:47 +01:00
Eugen Rochko
8f6bb74e61 New translations strings.xml (Thai) 2022-11-26 21:16:11 +01:00
Grishka
e4c9eb089a Hide posts when muting, blocking or unfollowing an account 2022-11-26 23:09:46 +03:00
Grishka
0e635aec23 Allow copying the username in profile 2022-11-26 20:33:02 +03:00
Grishka
dc90c09cea Shorten interaction counters 2022-11-26 20:24:27 +03:00
Grishka
06cb335a0a Add tooltips to some icon buttons
closes #423
2022-11-26 20:21:48 +03:00
Eugen Rochko
e67bd2972a New translations strings.xml (Catalan) 2022-11-26 18:17:13 +01:00
Grishka
5a681d3557 Fix #403 2022-11-26 20:16:43 +03:00
Grishka
4200486aeb fix 2022-11-26 20:14:30 +03:00
Grishka
62411a563f Fix poll expiration
fixes #238, fixes #417
2022-11-26 20:13:46 +03:00
Grishka
2cabe94ba0 Fix #398 2022-11-26 20:02:30 +03:00
Grishka
4a6baae97a Make URLs clickable in instance rules
closes #389
2022-11-26 19:29:05 +03:00
Grishka
bb12a66781 Fix #313 2022-11-26 18:40:17 +03:00
Eugen Rochko
de5929d8d2 New translations strings.xml (Icelandic) 2022-11-26 16:28:32 +01:00
Eugen Rochko
d7699ef079 New translations strings.xml (Galician) 2022-11-26 15:29:20 +01:00
Eugen Rochko
3ab04ebca8 New translations strings.xml (Icelandic) 2022-11-26 12:55:03 +01:00
Eugen Rochko
78d2aa96d7 New translations strings.xml (Belarusian) 2022-11-25 01:47:56 +01:00
Eugen Rochko
3e903f4a1d New translations short_description.txt (Icelandic) 2022-11-24 20:21:45 +01:00
Eugen Rochko
353b1873cd New translations full_description.txt (Icelandic) 2022-11-24 20:21:44 +01:00
Eugen Rochko
f4de7d18f3 New translations strings.xml (Icelandic) 2022-11-24 20:21:43 +01:00
Eugen Rochko
5dbac5fc6b New translations strings.xml (Icelandic) 2022-11-24 17:26:26 +01:00
Eugen Rochko
172d44997f New translations strings.xml (Portuguese, Brazilian) 2022-11-24 17:26:25 +01:00
Eugen Rochko
57b0b04c00 New translations strings.xml (Catalan) 2022-11-24 15:40:25 +01:00
Eugen Rochko
ca9ce43b07 New translations strings.xml (Indonesian) 2022-11-24 12:25:39 +01:00
Eugen Rochko
ef41122aca New translations full_description.txt (German) 2022-11-23 14:46:25 +01:00
Eugen Rochko
9e7676b62a New translations strings.xml (German) 2022-11-23 14:46:24 +01:00
Eugen Rochko
56492c07f5 New translations strings.xml (German) 2022-11-22 11:49:19 +01:00
Eugen Rochko
ce9fabd406 New translations title.txt (Icelandic) 2022-11-22 10:52:17 +01:00
Eugen Rochko
0dd6c43117 New translations short_description.txt (Icelandic) 2022-11-22 10:52:16 +01:00
Eugen Rochko
5159aab19c New translations full_description.txt (Icelandic) 2022-11-22 10:52:15 +01:00
Eugen Rochko
4e2cf247e9 New translations strings.xml (Icelandic) 2022-11-22 10:52:14 +01:00
Eugen Rochko
9df02d9857 New translations title.txt (Belarusian) 2022-11-22 10:52:13 +01:00
Eugen Rochko
bb4a5202d7 New translations short_description.txt (Belarusian) 2022-11-22 10:52:12 +01:00
Eugen Rochko
2320014eb3 New translations full_description.txt (Belarusian) 2022-11-22 10:52:10 +01:00
Eugen Rochko
3692c2b205 New translations strings.xml (Belarusian) 2022-11-22 10:52:09 +01:00
Grishka
9facdb938d Add a section about translations to readme 2022-11-22 11:48:02 +04:00
Eugen Rochko
7856858aea New translations strings.xml (Sinhala) 2022-11-22 08:37:11 +01:00
Eugen Rochko
d0328957f5 New translations strings.xml (Sinhala) 2022-11-22 07:24:00 +01:00
Gregory K
4d868cc5aa Merge pull request #400 from mastodon/fix-readme
Add note on contributions and trademarks to README
2022-11-22 06:59:44 +03:00
Eugen Rochko
ca9e515bd5 Merge pull request #232 from Poussinou/patch-1
Create FUNDING.yml
2022-11-22 04:34:03 +01:00
Eugen Rochko
524c0d607b Merge pull request #372 from sveinki/patch-1
Typo in full_description.txt
2022-11-22 04:33:07 +01:00
Eugen Rochko
df1d451e82 Add note on contributions and trademarks to README 2022-11-22 04:28:10 +01:00
Grishka
2c61551e5c Add a tool to generate locales_config.xml 2022-11-21 22:04:30 +04:00
Grishka
158af27309 Fix #363 2022-11-21 18:36:33 +04:00
Eugen Rochko
6e8542e33b New translations strings.xml (Korean) 2022-11-21 14:56:20 +01:00
Grishka
187693883c Fix #94
TODO support 4.0 filteing
2022-11-21 14:10:30 +04:00
Eugen Rochko
9017d00541 New translations strings.xml (Chinese Traditional) 2022-11-21 03:43:50 +01:00
Eugen Rochko
9182bd1a15 New translations strings.xml (Basque) 2022-11-21 02:38:55 +01:00
Eugen Rochko
5cdd726d21 New translations full_description.txt (German) 2022-11-20 23:59:20 +01:00
Eugen Rochko
d00fbe074b New translations strings.xml (German) 2022-11-20 23:59:19 +01:00
Eugen Rochko
365fac5efe New translations strings.xml (Basque) 2022-11-20 17:31:49 +01:00
Eugen Rochko
1d60031f4c New translations strings.xml (Vietnamese) 2022-11-20 15:03:02 +01:00
Eugen Rochko
2c7ed4be3e New translations strings.xml (Italian) 2022-11-20 13:57:40 +01:00
Eugen Rochko
3b9d4d3f9d New translations strings.xml (French) 2022-11-20 13:57:39 +01:00
Eugen Rochko
78824fa425 New translations strings.xml (Catalan) 2022-11-20 12:38:33 +01:00
Eugen Rochko
e271a4a330 New translations strings.xml (Kabyle) 2022-11-20 11:20:59 +01:00
Grishka
b898dc010e Show an error if a server has signups closed
closes #377
2022-11-20 13:36:23 +04:00
Grishka
de369633ec Fix #386 2022-11-20 12:54:56 +04:00
Gregory K
3f075eab13 Merge pull request #387 from sk22/fix-screenreader-middle-dot
Omit “middle dot” for screen reader
2022-11-20 07:54:06 +03:00
Eugen Rochko
3fb063bee4 New translations strings.xml (Catalan) 2022-11-20 02:30:29 +01:00
sk
e99917945a omit middle dot for screen reader 2022-11-20 01:34:02 +01:00
Eugen Rochko
c43dd5aa49 New translations strings.xml (Catalan) 2022-11-20 01:31:55 +01:00
Grishka
30151cafc2 Yes I can flip that to true 2022-11-19 23:13:50 +04:00
Eugen Rochko
0285158edc New translations strings.xml (Polish) 2022-11-19 15:59:12 +01:00
Eugen Rochko
9a6a3422fb New translations strings.xml (Japanese) 2022-11-18 17:40:42 +01:00
Eugen Rochko
523eb70ca6 New translations strings.xml (German) 2022-11-18 02:14:50 +01:00
Eugen Rochko
b57972ae0f New translations full_description.txt (German) 2022-11-17 23:29:50 +01:00
Eugen Rochko
37598df24e New translations strings.xml (German) 2022-11-17 23:29:49 +01:00
Eugen Rochko
f04df2d2c4 New translations strings.xml (Korean) 2022-11-17 13:16:17 +01:00
Sveinn í Felli
881762852e Typo in full_description.txt
Removing an extra "and" on line 3.
2022-11-17 09:49:59 +00:00
Jeff Bowen
5d056d5bea AboutViewHolder: Set the background of the field item to green when verified 2022-11-16 19:01:32 -05:00
Jeff Bowen
f500cc7ebf IsoInstantTypeAdapter: Enable parsing of 'offset' date times in profile response 2022-11-16 19:00:11 -05:00
Eugen Rochko
8d6eb0f810 New translations strings.xml (Scottish Gaelic) 2022-11-16 22:13:57 +01:00
Eugen Rochko
94de724d4e New translations strings.xml (Chinese Traditional) 2022-11-16 22:13:56 +01:00
Eugen Rochko
555e8838d4 New translations short_description.txt (Scottish Gaelic) 2022-11-16 19:41:58 +01:00
Eugen Rochko
58beb73595 New translations full_description.txt (Scottish Gaelic) 2022-11-16 19:41:57 +01:00
Eugen Rochko
865f66aa30 New translations strings.xml (Scottish Gaelic) 2022-11-16 19:41:56 +01:00
Eugen Rochko
82e95010a2 New translations strings.xml (Arabic) 2022-11-16 18:34:36 +01:00
Eugen Rochko
68a053cb76 New translations full_description.txt (Indonesian) 2022-11-16 10:21:47 +01:00
Eugen Rochko
2467017382 New translations strings.xml (Indonesian) 2022-11-16 10:21:46 +01:00
Eugen Rochko
d3a7faba51 New translations strings.xml (Indonesian) 2022-11-16 08:43:53 +01:00
Eugen Rochko
eb7574d282 New translations strings.xml (Indonesian) 2022-11-16 07:46:00 +01:00
Eugen Rochko
689328931a New translations short_description.txt (Ukrainian) 2022-11-15 17:19:19 +01:00
Eugen Rochko
d3a2ce0a57 New translations full_description.txt (Ukrainian) 2022-11-15 17:19:18 +01:00
Eugen Rochko
50a092a2c4 New translations strings.xml (Ukrainian) 2022-11-15 17:19:17 +01:00
Eugen Rochko
a852b66d94 New translations strings.xml (Ukrainian) 2022-11-15 15:28:02 +01:00
Eugen Rochko
109d967f2d New translations strings.xml (Ukrainian) 2022-11-15 14:12:09 +01:00
Grishka
a336f6be89 Support domain redirects via /.well-known/host-meta
Closes #312
2022-11-15 15:16:58 +04:00
Grishka
79d99a9484 Probably fix #297 2022-11-15 14:11:56 +04:00
Gregory K
e4e1dc102b Merge pull request #321 from julroy67/fix-onboarding-category-cjk-cutoff
Fix CJK text cut off for category names on Onboarding screen
2022-11-15 10:13:03 +03:00
Grishka
539ac26e0d Fix #300 2022-11-14 22:59:34 +04:00
Eugen Rochko
752435857d New translations strings.xml (Slovenian) 2022-11-14 16:16:57 +01:00
Eugen Rochko
03e68ba56c New translations strings.xml (Vietnamese) 2022-11-14 16:16:56 +01:00
Eugen Rochko
ef37d8afc4 New translations strings.xml (Czech) 2022-11-14 13:17:06 +01:00
Eugen Rochko
1ac390f6ee New translations full_description.txt (Dutch) 2022-11-14 12:02:24 +01:00
Eugen Rochko
586d04f311 New translations strings.xml (Dutch) 2022-11-14 12:02:23 +01:00
Eugen Rochko
6e81469b45 New translations strings.xml (German) 2022-11-14 08:59:56 +01:00
Eugen Rochko
a8431a498d New translations strings.xml (French) 2022-11-14 07:47:57 +01:00
Eugen Rochko
a2dca57eb5 New translations strings.xml (Thai) 2022-11-13 20:35:08 +01:00
Eugen Rochko
fdfb0a377d New translations strings.xml (Turkish) 2022-11-13 19:37:52 +01:00
Eugen Rochko
f5a9a11032 New translations strings.xml (Italian) 2022-11-13 19:37:51 +01:00
Eugen Rochko
32c6fc9a59 New translations strings.xml (Slovenian) 2022-11-13 17:32:45 +01:00
Eugen Rochko
b87b086dfa New translations strings.xml (Japanese) 2022-11-13 17:32:42 +01:00
Eugen Rochko
caa77a9c54 New translations strings.xml (Italian) 2022-11-13 17:32:41 +01:00
Eugen Rochko
ecf0c2b173 New translations strings.xml (French) 2022-11-13 17:32:35 +01:00
Eugen Rochko
8f7ef0d564 New translations strings.xml (German) 2022-11-13 17:32:33 +01:00
Eugen Rochko
d8e9bbd6b1 New translations strings.xml (Chinese Traditional) 2022-11-13 17:32:32 +01:00
Eugen Rochko
174029376c New translations strings.xml (Thai) 2022-11-13 17:32:20 +01:00
Eugen Rochko
8e06f72064 New translations strings.xml (Chinese Simplified) 2022-11-13 17:32:18 +01:00
Grishka
2c11b879ae Fix #283 2022-11-13 19:44:40 +04:00
Grishka
58735094f1 Fix #319 2022-11-13 19:42:25 +04:00
Grishka
3671803f49 Fix #336 2022-11-13 19:18:42 +04:00
Gregory K
56b42071ac Merge pull request #354 from cypressious/fix-activation-button-bar
Fix button bar layout in onboarding activation for smaller widths
2022-11-13 17:48:54 +03:00
Eugen Rochko
f75a7e793d New translations full_description.txt (Romanian) 2022-11-13 15:26:59 +01:00
Eugen Rochko
a395b82c85 New translations short_description.txt (Romanian) 2022-11-13 14:10:11 +01:00
Eugen Rochko
699f36bf1a New translations full_description.txt (Romanian) 2022-11-13 14:10:10 +01:00
Eugen Rochko
ef57b7425d New translations strings.xml (Romanian) 2022-11-13 14:10:09 +01:00
Eugen Rochko
c21b2b6a43 New translations strings.xml (Romanian) 2022-11-13 13:05:06 +01:00
Kirill Rakhman
ba930f72e8 fix button bar layout in onboarding activation for smaller widths 2022-11-13 12:58:33 +01:00
Eugen Rochko
c5d4318d57 New translations short_description.txt (Slovenian) 2022-11-13 09:24:42 +01:00
Eugen Rochko
7a4621ef13 New translations full_description.txt (Slovenian) 2022-11-13 09:24:41 +01:00
Eugen Rochko
fa53a5ed4f New translations strings.xml (Slovenian) 2022-11-13 09:24:40 +01:00
Eugen Rochko
f2ec2c5333 New translations title.txt (Slovenian) 2022-11-13 08:02:32 +01:00
Eugen Rochko
49e005eb84 New translations strings.xml (French) 2022-11-13 08:02:31 +01:00
Eugen Rochko
ad9b19646d New translations strings.xml (Italian) 2022-11-13 08:02:30 +01:00
Eugen Rochko
80336c7fae New translations strings.xml (Japanese) 2022-11-13 08:02:29 +01:00
Eugen Rochko
996c91420c New translations strings.xml (Chinese Simplified) 2022-11-13 08:02:28 +01:00
Eugen Rochko
48396344e3 New translations strings.xml (Thai) 2022-11-13 08:02:27 +01:00
Eugen Rochko
0a8c61226e New translations strings.xml (Bengali) 2022-11-13 08:02:25 +01:00
Eugen Rochko
e64d8ccf09 New translations strings.xml (German) 2022-11-13 08:02:23 +01:00
Eugen Rochko
9791121392 New translations full_description.txt (Romanian) 2022-11-13 08:02:21 +01:00
Eugen Rochko
06c5002aa5 New translations short_description.txt (Romanian) 2022-11-13 08:02:20 +01:00
Eugen Rochko
fcdbc2bc8d New translations title.txt (Romanian) 2022-11-13 08:02:20 +01:00
Eugen Rochko
749511630a New translations strings.xml (Irish) 2022-11-13 08:02:19 +01:00
Eugen Rochko
4befc0c045 New translations full_description.txt (Irish) 2022-11-13 08:02:18 +01:00
Eugen Rochko
8b2287fa84 New translations short_description.txt (Irish) 2022-11-13 08:02:17 +01:00
Eugen Rochko
3272b7553b New translations title.txt (Irish) 2022-11-13 08:02:16 +01:00
Eugen Rochko
00f7cff402 New translations strings.xml (Slovenian) 2022-11-13 08:02:15 +01:00
Eugen Rochko
87f45a74f0 New translations full_description.txt (Slovenian) 2022-11-13 08:02:14 +01:00
Eugen Rochko
0cd0fe1952 New translations short_description.txt (Slovenian) 2022-11-13 08:02:13 +01:00
Eugen Rochko
458effc27c New translations strings.xml (Romanian) 2022-11-13 08:02:13 +01:00
Eugen Rochko
4422b774b7 New translations strings.xml (Chinese Traditional) 2022-11-13 08:02:12 +01:00
Grishka
d9d8717eeb And more languages 2022-11-13 10:31:03 +04:00
Grishka
0056324100 Add new languages to locale config 2022-11-13 10:23:09 +04:00
Grishka
e49b4daafe Merge branch 'l10n_master' 2022-11-13 10:16:25 +04:00
Eugen Rochko
ad86f1b55e New translations title.txt (Scottish Gaelic) 2022-11-13 07:07:02 +01:00
Eugen Rochko
a8bc0f037f New translations title.txt (Hindi) 2022-11-13 07:07:01 +01:00
Eugen Rochko
9ae0c3f414 New translations title.txt (Bengali) 2022-11-13 07:07:00 +01:00
Eugen Rochko
44ef4d08e7 New translations short_description.txt (Bengali) 2022-11-13 07:06:53 +01:00
Eugen Rochko
bba5c51649 New translations full_description.txt (Bengali) 2022-11-13 07:06:52 +01:00
Eugen Rochko
b7129340f3 New translations strings.xml (Bengali) 2022-11-13 07:06:52 +01:00
Eugen Rochko
d695e1b21b New translations short_description.txt (Hebrew) 2022-11-13 07:06:51 +01:00
Eugen Rochko
148bd1cfb7 New translations full_description.txt (Hebrew) 2022-11-13 07:06:50 +01:00
Eugen Rochko
e619d0f1ac New translations full_description.txt (Swedish) 2022-11-13 07:06:49 +01:00
Eugen Rochko
475f486626 New translations short_description.txt (Hindi) 2022-11-13 07:06:48 +01:00
Eugen Rochko
6108043fae New translations full_description.txt (Hindi) 2022-11-13 07:06:47 +01:00
Eugen Rochko
76491090b0 New translations strings.xml (Hindi) 2022-11-13 07:06:46 +01:00
Eugen Rochko
5770a9c491 New translations short_description.txt (Scottish Gaelic) 2022-11-13 07:06:45 +01:00
Eugen Rochko
b8325d7387 New translations full_description.txt (Scottish Gaelic) 2022-11-13 07:06:43 +01:00
Eugen Rochko
f3a65dc169 New translations full_description.txt (Thai) 2022-11-13 07:06:32 +01:00
Eugen Rochko
d21495d5be New translations full_description.txt (Chinese Simplified) 2022-11-13 07:06:30 +01:00
Eugen Rochko
d95ff25573 New translations strings.xml (Scottish Gaelic) 2022-11-13 07:06:27 +01:00
Eugen Rochko
5dc170673f New translations title.txt (Sinhala) 2022-11-13 07:06:26 +01:00
Eugen Rochko
c148f7f23b New translations title.txt (Indonesian) 2022-11-13 07:06:24 +01:00
Eugen Rochko
1612e377d2 New translations title.txt (Dutch) 2022-11-13 07:06:23 +01:00
Eugen Rochko
f03074e91a New translations strings.xml (Turkish) 2022-11-13 07:06:22 +01:00
Eugen Rochko
345847652f New translations strings.xml (Swedish) 2022-11-13 07:06:21 +01:00
Eugen Rochko
c71bc55e95 New translations strings.xml (Korean) 2022-11-13 07:06:18 +01:00
Eugen Rochko
8521fe852f New translations strings.xml (Japanese) 2022-11-13 07:06:17 +01:00
Eugen Rochko
714444562f New translations strings.xml (Italian) 2022-11-13 07:06:16 +01:00
Eugen Rochko
212c4a43c5 New translations strings.xml (Hebrew) 2022-11-13 07:06:12 +01:00
Eugen Rochko
103dc55e1b New translations strings.xml (Finnish) 2022-11-13 07:06:11 +01:00
Eugen Rochko
b50161dc8f New translations strings.xml (Basque) 2022-11-13 07:06:10 +01:00
Eugen Rochko
9d390a06bf New translations strings.xml (Czech) 2022-11-13 07:06:08 +01:00
Eugen Rochko
635ca15722 New translations strings.xml (Catalan) 2022-11-13 07:06:07 +01:00
Eugen Rochko
e7c1b50cc1 New translations strings.xml (Spanish) 2022-11-13 07:06:06 +01:00
Eugen Rochko
21410566ef New translations strings.xml (French) 2022-11-13 07:06:05 +01:00
Eugen Rochko
d6a4fddbb9 New translations full_description.txt (Chinese Traditional) 2022-11-13 07:06:04 +01:00
Eugen Rochko
a911b1bd9a New translations strings.xml (Arabic) 2022-11-13 07:06:03 +01:00
Eugen Rochko
be4ee9436d New translations strings.xml (German) 2022-11-13 07:06:02 +01:00
Eugen Rochko
6447d470f9 New translations strings.xml (Chinese Traditional) 2022-11-13 07:06:01 +01:00
Eugen Rochko
82aba8d276 New translations strings.xml (Ukrainian) 2022-11-13 07:06:00 +01:00
Eugen Rochko
eeec1f8bed New translations strings.xml (Chinese Simplified) 2022-11-13 07:05:59 +01:00
Eugen Rochko
2043c74937 New translations strings.xml (Polish) 2022-11-13 07:05:57 +01:00
Eugen Rochko
844322a860 New translations short_description.txt (Sinhala) 2022-11-13 07:05:56 +01:00
Eugen Rochko
d458b9d825 New translations full_description.txt (Sinhala) 2022-11-13 07:05:55 +01:00
Eugen Rochko
cbd835b136 New translations strings.xml (Sinhala) 2022-11-13 07:05:54 +01:00
Eugen Rochko
8260d38d0f New translations short_description.txt (Indonesian) 2022-11-13 07:05:53 +01:00
Eugen Rochko
4fd8a8877e New translations full_description.txt (Indonesian) 2022-11-13 07:05:52 +01:00
Eugen Rochko
b3d7ffa799 New translations strings.xml (Indonesian) 2022-11-13 07:05:51 +01:00
Eugen Rochko
d6f20edbe5 New translations short_description.txt (Dutch) 2022-11-13 07:05:51 +01:00
Eugen Rochko
7eda308388 New translations full_description.txt (Dutch) 2022-11-13 07:05:50 +01:00
Eugen Rochko
1c256c2bba New translations strings.xml (Dutch) 2022-11-13 07:05:49 +01:00
Eugen Rochko
32f6a5664f New translations strings.xml (Vietnamese) 2022-11-13 07:05:48 +01:00
Eugen Rochko
13321ce5e2 New translations short_description.txt (Portuguese, Brazilian) 2022-11-13 07:05:47 +01:00
Eugen Rochko
d81e698d3b New translations full_description.txt (Polish) 2022-11-13 07:05:45 +01:00
Eugen Rochko
ee3123f45a New translations full_description.txt (Vietnamese) 2022-11-13 07:05:44 +01:00
Eugen Rochko
4bf1a5215c New translations strings.xml (Kabyle) 2022-11-13 07:05:43 +01:00
Eugen Rochko
d1b7c84473 New translations strings.xml (Thai) 2022-11-13 07:05:40 +01:00
Eugen Rochko
75c02f8d3c New translations full_description.txt (Portuguese, Brazilian) 2022-11-13 07:05:39 +01:00
Eugen Rochko
86f583ada4 New translations strings.xml (Portuguese, Brazilian) 2022-11-13 07:05:38 +01:00
Gregory K
8d60264ee3 Merge pull request #344 from sk22/fix-edit-polls
Apply poll duration when editing status
2022-11-13 08:55:31 +03:00
sk
3b28bd6ce9 get poll duration from edited status
fixes mastodon#343
2022-11-11 19:10:45 +01:00
Julien Humbert
20d838f576 Fix CJK text cut off for category names on Onboarding screen 2022-11-08 23:48:04 +09:00
Poussinou
68a9eba868 Create FUNDING.yml 2022-07-30 18:22:09 +02:00
185 changed files with 7267 additions and 1143 deletions

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

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

View File

@@ -1,11 +1,17 @@
# Mastodon for Android
Mastodon for Android
======================
[![Crowdin](https://badges.crowdin.net/mastodon-for-android/localized.svg)](https://crowdin.com/project/mastodon-for-android)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
This is the repository for the official Android app for Mastodon.
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
## Contributing
Our goal is delivering a polished, professionally designed and user-friendly app. We proceed according to wireframes provided by a professional UX designer that works with Mastodon gGmbH. This means that any outside contributions that change the app visually must first be coordinated with the UX designer. *This can take time.* Furthermore, we work off of an internal roadmap and aim for feature-parity and consistency with our iOS app. The iOS app is designated as the "primary" between the two, therefore, if you want to request features, please do so in the [Mastodon for iOS](https://github.com/mastodon/mastodon-ios) repository, as you are requesting a feature to be both in iOS and Android (exceptions being system integrations specific to Android). On the other hand, any contributions that improve existing functionality, performance, or accessibility should not have any roadblocks to being merged.
If you would like to help translate the app into your language, please go to [Crowdin](https://crowdin.com/project/mastodon-for-android). If your language is not listed in the Crowdin project, please create an issue and we will add it. Please do not create pull requests that modify `strings.xml` files for languages other than English.
## Building
@@ -17,4 +23,6 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
## License
This project is released under the [GPL-3 License](./LICENSE).
This project is released under the [GPL-3 License](./LICENSE).
The Mastodon name and logo are trademarks of Mastodon gGmbH. If you intend to redistribute a modified version of this app, use a unique name and icon for your app that does not mistakenly imply any official connection with or endorsement by Mastodon gGmbH.

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -1,16 +1,16 @@
Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jednné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, kteří mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom vysílají na Mastodon!
Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jediné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, ve kterých mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom přispívají na Mastodon!
Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v bezreklamní a chronologické časové linii. Vyjádřete se pomocí vlastních emojí, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a reblogujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě.
Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v chronologické časové ose bez reklam. Vyjádřete se pomocí vlastních emoji, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a boostujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě.
Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se svými sledujícími, jen s lidmi, které zmiňujete, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a hlášení nástrojů pomáhá předcházet zneužití.
Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se vašimi sledujícími, jen s lidmi, které zmíníte, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a nahlašovací nástroje pomáhá předcháze zneužití.
Další funkce:
Více funkcí:
• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo zcela černém režimu
• Ankety: Požádejte sledující o jejich názor a spojte se s jejich hlasováním
Průzkum: Trendové hashtagy a účty jsou pryč na jedno klepnutí
Upozornění: Dostávejte upozorněna nové sledování, odpovědi a reblogy
• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo pravém černém režimu
• Ankety: Požádejte sledující o jejich názor a sečtěte jejich hlasy
Objevit: Populární hashtagy a účty jsou pryč na jedno klepnutí
Oznámení: Dostávejte oznámeo nových sledujících, odpovědích a boostech
• Sdílení: Odesílání přímo do Mastodonu z libovolného seznamu sdílení v jakékoliv aplikaci
• Roztomilost: Naším maskotem je roztomilý slon, kterého čas od času uvidíte
Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a my máme v plánu to udržet.
Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a máme v plánu to udržet.

View File

@@ -1,16 +1,16 @@
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Website ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal was dich interessiert, auf Mastodon kannst du interessierte Leute treffen, die darüber schreiben!
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Webseite ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal, was du magst, auf Mastodon kannst du begeisterte Menschen treffen, die darüber schreiben!
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten, und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bildern, GIFs, Videos und Audio in 500-Zeichen-Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen, um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit eigenen Emojis, Bildern, GIFs, Videos und Klängen in 500-Zeichen-Beiträgen aus. Antworte auf Themen und teile Beiträge von anderen, um tolle Dinge zu verbreiten. Finde neue Konten zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder bedenklichen Inhalten ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
Weitere Funktionen:
• Dunkler Modus: Beiträge im hellen, dunklen oder schwarzen Modus lesen
• Umfragen: Frage deine Follower nach ihrer Meinung und zähle die Stimmen
• Umfragen: frage deine Follower nach ihrer Meinung und zähle die Stimmen
• Entdecken: trendende Hashtags und Profile sind nur einen Fingertipp entfernt
• Benachrichtigungen: Erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
• Teilen: Veröffentliche auf Mastodon aus jeder beliebigen anderen App
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant, und du wirst ihn von Zeit zu Zeit auftauchen sehen
• Benachrichtigungen: erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
• Teilen: veröffentliche auf Mastodon aus jeder beliebigen anderen App
• Niedlichkeit: unser Maskottchen ist ein entzückender Elefant und du wirst ihn von Zeit zu Zeit auftauchen sehen
Mastodon ist eine eingetragene gemeinnützige Organisation, und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen, das auch so beizubehalten.
Mastodon ist eine eingetragene gemeinnützige Organisation und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetarisierung und kein Risikokapital und so soll es auch bleiben.

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
S e an lìonra sòisealta sgaoilte as motha air an eadar-lìon a th ann am Mastodon. Seach aon làrach-lìn a-mhàin, s e lìonra de mhilleanan de dhaoine ann an coimhearsnachdan neo-eisimeileach a th ann agus s urrainn dhan a h-uile duine bruidhinn ri chèile fhathast gun duilgheadas. Ge b e dè na rudan a tha ùidh agad annta, coinnichidh tu ri daoine a sgrìobhas mun dèidhinn air Mastodon!
Faigh ballrachd ann an coimhearsnachd s cruthaich pròifil dhut. Lorg is lean daoine inntinneach agus leugh na postaichean aca air loidhne-ama cheart gun sanasachd. Cuir thu fhèin an cèill le Emojis gnàthaichte, dealbhan, GIFs, videothan is fuaimean ann am postaichean le 500 caractar. Freagair ri snàithleanan is brosnaich postaichean le neach sam bith airson deagh rudan a cho-roinneadh. Lorg cunntasan ùra ri leantainn is tagaichean hais a treandadh airson an lìonra agad a leudachadh.
Chaidh Mastodon a thogail leis an aire air prìobhaideachd is sàbhailteachd. Tha e an urra riut fhèin an co-roinn thu post leis an luchd-leantainn agad, leis na daoine air an doir thu iomradh a-mhàin no leis an t-saoghal mhòr. Leigidh rabhaidhean susbainte leat postaichean sa bheil susbaint fhrionasach fhalach is cha leig daoine leas coimhead air ach nuair a bhios iad deònach. Tha riaghailtean is maoir fa leth aig gach coimhearsnachd airson a buill a chumail sàbhailte agus cuidichidh innealan bacaidh is gearain le dìon o dhroch-dhìol.
Gleusan eile:
• Modh dorcha: Leugh postaichean le modh soilleir, dorcha no dubh dorcha
• Cunntasan-bheachd: Faighnich dhen luchd-leantainn dè am beachd is faigh cunntas nam bhòt
• Rùraich: Ruig tagaichean hais is cunntasan a treandadh le aon ghnogag
• Brathan: Faigh brathan mu luchd-leantainn, freagairtean is brosnachaidhean ùra
• Co-roinn: Postaich gu Mastodon gu dìreach o shiota co-roinnidh ann an aplacaid sam bith
• Stampachd: S e ailbhean ealanta a tha san t-suaichnean againn is nochdaidh e o àm gu àm
S e bhuidheann neo-phrothaideach clàraichte a th ann am Mastodon a gheibh taic dhìreach o na tabhartasan agad. Chan eil sanasachd, airgeadachadh no calpa iomairte sam bith ann agus tha fainear dhuinn ga chumail mar sin.

View File

@@ -0,0 +1 @@
Lìonra sòisealta sgaoilte

View File

@@ -0,0 +1 @@
Mastodon

View File

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

View File

@@ -1 +1 @@
Decentralized social network
רשת חברתית מבוזרת

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
A Mastodon a legnagyobb decentralizált közösségi hálózat az interneten. Egyetlen weboldal helyett, ez több millió felhasználóból álló, független közösségek hálózata, amelyek egymással kapcsolatba tudnak lépni, zökkenőmentesen. Nem számít, mi a hobbid, a Mastodonon találkozhatsz róla posztoló lelkes emberekkel!
Csatlakozz egy közösséghez és készítsd el a profilodat. Keress és kövess lenyűgöző embereket, és olvasd egy reklámmentes, kronologikus idővonalon a bejegyzéseiket. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú posztokban. Reply to threads and reblog posts from anyone to share great stuff. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat.
A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a posztjaidat csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:
• Dark Mode: Read posts in light, dark, or true black mode
• Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -0,0 +1 @@
Decentralizált szociális hálózat

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
Mastodon adalah jejaring sosial terdesentralisasi terbesar di internet. Daripada sebuah satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat berinteraksi antar sesama, tanpa masalah. Tanpa memedulikan apa yang Anda minat, Anda dapat bertemu orang-orang yang mengirimkan apa yang mereka minat di Mastodon!
Bergabung sebuah komunitas dan buat profil Anda. Temukan dan ikuti orang-orang menarik dan lihat kiriman mereka dalam linimasa kronologis tanpa iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.
Fitur lainnya:
• Mode Gelap: Baca kiriman dalam mode terang, gelap, atau gelap asli
• Pemungutan suara: Tanya pengikut tentang opini mereka dan hitung pilihannya
• Jelajahi: Tagar dan akun tren dengan satu ketuk
• Pemberitahuan: Dapatkan pemberitahuan tentang pengikut, balasan, dan pembagian baru
• Pembagian: Kirim langsung ke Mastodon dari lembar pembagian apa pun dalam aplikasi apa pun
• Kelucuan: Maskot kami adalah seekor gajah yang lucu, dan Anda akan melihat dia muncul dari waktu ke waktu
Mastodon adalah nirlaba yang terdaftar dan pengembangan didukung secara langsung dari donasi Anda. Tanpa periklanan, tanpa monetisasi, dan tanpa kapitalisme ventura, dan kami berencana untuk tetap seperti itu.

View File

@@ -0,0 +1 @@
Jaringan sosial terdesentralisasi

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,30 @@
Mastodon er stærsta ómiðstýrða samfélagsnetið á internetinu. Í staðinn fyrir að vera á inu vefsvæði, er þetta net með milljónum notenda í
sjálfstæðum samfélögum, sem geta óhindrað átt í samskiptum við hvern annan. Sama hvað þú ert að pæla, alltaf geturðu hitt áhugasamt fólk í gegnum
færslur á Mastodon!
Taktu þátt í samfélagi og útbúðu notandasnið fyrir þig. Finndu og fylgstu með áhugaverðu fólki og lestu færslurnar þeirra á
auglýsingalausri, raðaðri tímalínu. Tjáðu þig með sérsniðnum emoji-táknum, myndum, GIF-hreyfimyndum, myndskeiðum
og hljóðskrám í 500-stafa færslum. Svaraðu spjallþráðum og endurbirtu færslur frá hverjum sem er til að deila
frábæru efni. Finndu nýja notendur til að fylgjast með og skoðaðu vinsæl myllumerki til að
útvíkka netið þitt.
Mastodon er byggt með áherslu á gagnaleynd og öryggi. Ákveddu hvort færslunum þínum sé deilt með þeim sem fylgjast með þér, aðeins
fólkinu sem þú minnist á, eða allri veröldinni. Viðvaranir vegna efnis gera þér kleift að fela færslur sem innihalda
viðkvæmt eða eldfimt efni þangað til þú ert í stuði til að eiga við slíkt. Hvert samfélag er með sínar eigin reglur og umsjónarmenn til að passa upp á
öryggi meðlimanna, auk áreiðanlegra verkfæra til að útiloka aðila og
meðhöndla kærur, sem hjálpar til við að koma í veg fyrir misnotkun.
Fleiri eiginleikar:
• Dökkur hamur: Lestu færslur í ljósum, dökkum eða sönnum kolsvörtum ham
• Kannanir: Spyrðu fylgjendur um skoðanir þeirra og teldu atkvæðin
• Uppgötva: Vinsæl myllumerki og notendaaðgangar eru við hendina
• Tilkynningar: Fáðu tilkynningar um nýja fylgjendur, svör og endurbirtingar
• Deiling: Birtu beint á Mastodon frá hvaða deilingarblaði sem er í hvaða
forriti sem er
• Krúttlegheit: Gæludýrið okkar er vinalegur loðfíll sem þú gætir rekist á
öðru hverju
Mastodon er skráð óhagnaðardrifin sjálfseignarstofnun og er þróun þess
drifin áfram með styrkjum frá þér. Það eru engar auglýsingar, engin gjaldtaka og engir áhættufjárfestar - við
höfum hugsað okkur að halda því þannig.

View File

@@ -0,0 +1 @@
Dreifstýrt samfélagsnet

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke gemeenschappen die allemaal naadloos met elkaar kunnen communiceren. Waar je ook mee bezig bent, je kunt gepassioneerde mensen ontmoeten die erover berichten op Mastodon!
Word lid van een gemeenschap en maak je profiel aan. Vind en volg fascinerende mensen en lees hun berichten in een advertentievrije, chronologische tijdlijn. Druk jezelf uit met aangepaste emoji, afbeeldingen, GIFs, videos en audio in berichten van 500 karakters. Antwoord op berichten en boost iedereens berichten om geweldige dingen te delen. Vind nieuwe accounts om te volgen en hashtags om je netwerk uit te breiden.
Mastodon is gebouwd met een focus op privacy en veiligheid. Bepaal zelf of je berichten met je volgers, alleen de mensen die je noemt, of de hele wereld worden gedeeld. Inhoudswaarschuwingen laten je berichten verbergen die gevoelig of aanmatigend materiaal bevatten, totdat je er klaar voor bent om ermee ze te bekijken. Elke gemeenschap heeft haar eigen richtlijnen en moderators om haar leden veilig te houden, en robuuste blokkerings- en rapportagetools helpen misbruik te voorkomen.
Meer mogelijkheden:
• Donkere Modus: Berichten lezen in licht, donker of echt zwart
• Polls: Vraag volgers om hun mening en tel de stemmen
• Ontdekken: Trending hashtags en accounts zijn een tik weg
• Meldingen: Krijg een melding over nieuwe volgers, reacties en boosts
• Delen: Deel vanuit elke app direct op Mastodon
• Schattigheid: Onze mascotte is een schattige olifant, en je zult ze van tijd tot tijd zien verschijnen
Mastodon is een geregistreerde non-profit en de ontwikkeling wordt direct ondersteund door jouw donaties. Er is geen reclame, geen geldelijk gewin en geen durfkapitaal en we zijn van plan het zo te houden.

View File

@@ -0,0 +1 @@
Gedecentraliseerd sociaal netwerk

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -1,8 +1,8 @@
Mastodon to największa zdecentralizowana sieć społecznościowa w Internecie. Zamiast jednej strony internetowej, jest to sieć milionów użytkowników w niezależnych społecznościach, które mogą ze sobą wchodzić w interakcje. Niezależnie od swoich zainteresowań, momżesz poznać interesujących ludzi piszących o nich na Mastodonie!
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Odnajduj nowe konta do śledzenia i zyskujące popularność hashtagi, by poszerzać swoją sieć.
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Odnajduj nowe konta do obserwowania i zyskujące popularność hashtagi, by poszerzać swoją sieć.
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decyduj, czy Twoje wpisy są udostępniane osobom śledzącym Cię, tylko wzmiankowanym, czy całemu światu. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Każda społeczność ma własne wytyczne i moderatorów, aby zapewniać swoim członkom bezpieczeństwo, a także solidne narzędzia blokowania i raportowania pomagające zapobiegać nadużyciom.
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decyduj, czy Twoje wpisy są udostępniane osobom obserwującym Cię, tylko wzmiankowanym, czy całemu światu. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Każda społeczność ma własne wytyczne i moderatorów, aby zapewniać swoim członkom bezpieczeństwo, a także solidne narzędzia blokowania i raportowania pomagające zapobiegać nadużyciom.
Więcej funkcji:
@@ -13,4 +13,4 @@ Więcej funkcji:
• Udostępnianie: Publikuj bezpośrednio na Mastodonie z menu udostępniania w dowolnej aplikacji
• Słodycz: Nasza maskotka to uroczy słoń i zobaczysz go pojawiającego się od czasu do czasu
Mastodon to zarejestrowana organizacja non-profit i rozwój jest wspierany bezpośrednio przez darowizny. Nie ma reklam, monetyzacji, ani kapitału inwestycyjnego i planujemy, by tak pozostało.
Mastodon to zarejestrowana organizacja non-profit, a rozwój jest wspierany bezpośrednio przez darowizny. Nie ma reklam, monetyzacji, ani kapitału inwestycyjnego i planujemy, by tak pozostało.

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Mastodon é a maior rede social descentralizada na internet. Ao invés de ser um website, é uma rede de milhões de usuários em comunidades independentes que podem comunicar-se uma à outra, de forma transparente. Independente do que você gostar, você pode encontrar pessoas dedicadas a isso no Mastodon!
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Junte-se a uma comunidade e crie o seu perfil. Encontre e siga pessoas fascinantes e leia seus posts em uma linha cronológica sem publicidade. Se expresse com emojis personalizados, imagens, GIFs, vídeos e áudio em publicações de 500 caracteres. Responda a tópicos e reblogue publicações de qualquer um para compartilhar coisas ótimas. Encontre contas novas para seguir e hashtags em alta para expandir sua rede.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
O Mastodon foi construído com foco em privacidade e segurança. Decida se seus posts serão compartilhados com seus seguidores, apenas com as pessoas que você menciona, ou com o mundo inteiro. Avisos de conteúdo permitem que você oculte mensagens que contenham conteúdo sensível até você estar pronto para interagir com elas. Cada comunidade tem suas próprias diretrizes e moderadores para manter seus membros seguros, e ferramentas robustas de bloqueio e denúncias ajudam a prevenir abusos.
More features:
Mais detalhes:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Modo escuro: Leia as postagens no modo claro, escuro ou preto de verdade
Enquetes: Peça as opiniões de seus seguidores e registre os resultados
• Explorar: Hashtags e contas em destaque estão a um toque de distância
• Notificações: Seja notificado sobre novos seguidores, respostas e reblogs
Compartilhar: Poste diretamente ao Mastodon pela página de compartilhamento de qualquer aplicativo
Fofura: Nossa mascote é um elefante fofinho, e você vai vê-lo por aí ocasionalmente
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Mastodon é uma instituição sem fins lucrativos e o desenvolvimento é suportado diretamente pelas suas doações. Não há publicidade, não monetização, nem capital de risco, e planejamos manter-se assim.

View File

@@ -1 +1 @@
Decentralized social network
Rede social descentralizada

View File

@@ -0,0 +1,16 @@
Mastodon este cea mai mare rețea socială descentralizată de pe internet. În loc de un singur site, este o rețea de milioane de utilizatori din comunități independente care pot interacționa cu ceilalți, fără nici o întrerupere. Indiferent în ce te afli, poți întâlni oameni pasionați care postează despre asta pe Mastodon!
Alătură-te unei comunități și creează-ți profilul. Găsește și urmărește oameni fascinanți și citește postările lor într-un calendar cronologic fără reclame. Exprimă-te cu emoji-uri personalizate, imagini, GIF-uri, videoclipuri și audio în postări de 500 de caractere. Răspunde la subiectele de discuție și impulsionează postările de la oricine pentru a împărtăși lucruri minunate. Găsește conturi noi de urmărit și haștag-uri populare pentru a-ți extinde rețeaua.
Mastodon a fost construit cu accent pe confidențialitate și siguranță. Decide dacă postările tale sunt partajate cu urmăritorii tăi, doar cu cei pe care îi menționezi sau cu întreaga lume. Avertismentele de conținut vă permit să ascundeți postările care conțin materiale sensibile sau declanșatoare până când sunteți gata să le implicați. Fiecare comunitate are propriile sale orientări și proprii moderatori pentru a-și menține membrii în siguranță, iar instrumentele solide de blocare și raportare contribuie la prevenirea abuzurilor.
Mai multe caracteristici:
• Mod întunecat: Citește postările în modul luminos, întunecat sau negru total
• Sondaje: Cereți celor care vă urmăresc opinia lor și numărați voturile
• Explorează: Hașhtag-urile populare și conturile sunt la o apăsare distanță
• Notificări: Primiți notificări despre noi urmăritori, răspunsuri și impulsionări
• Distribuire: Postează direct pe Mastodon din orice foaie de partajare în orice aplicație
• Drăgălășenie: Mascota noastră este un elefant adorabil, și îi veți vedea apărând din când în când
Mastodon este o organizație non-profit înregistrată, iar dezvoltarea este sprijinită direct de donațiile tale. Nu există publicitate, monetizare și capital de risc, și intenționăm să păstrăm lucrurile astfel.

View File

@@ -0,0 +1 @@
Rețea socială descentralizată

View File

@@ -0,0 +1 @@
Mastodon

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
Mastodon je največje decentralizirano družbeno omrežje na internetu. Namesto enega samega spletišča ga tvorijo milijoni uporabnikov v neodvisnih skupnostih, ki lahko med seboj komunicirajo brez težav. Ne glede na to, kaj vas zanima, lahko srečate predane ljudi, ki o tem objavljajo na Mastodonu!
Pridružite se skupnosti in ustvarite svoj profil. Poiščite in sledite zanimivim osebam ter berite njihove objave na časovnici brez oglasov v kronološkem zaporedju. Izrazite se s čustvenčki po meri, slikami, GIF-i, videoposnetki in zvočnimi posnetki v objavah z največ 500 znaki. Odgovarjajte na niti in poobjavite objave drugih, da delite dobro z drugimi. Poiščite nove račune za sledenje ter ključnike v trendu, da razširite svoje omrežje.
Mastodon je izdelan s poudarkom na zasebnosti in varnosti. Odločite se, ali se vaše objave delijo z vašimi sledilci, zgolj z omenjenimi ali s celim svetom. Opozorila o vsebini omogočajo skrivanje objav, ki vsebujejo občutljive ali netilne zadeve, vse dokler niste pripravljeni, da se z njimi spopadete. Vsak skupnost ima svoja lastna pravila in moderatorje, ki varujejo svoje člane, ter robustna orodja za blokiranje in poročanje, ki pomagajo preprečiti žalitve in kršitve človeškega dostojanstva ter pravic.
Dodatne funkcionalnosti:
• Temni način: objave berite v svetlem, temnem ali povsem črnem načinu;
• Ankete: vprašajte sledilce o njihovem mnenju in preštejte njihove glasove;
• Razišči: ključniki in računi v trendu so le en tap stran;
• Obvestila: bodite obveščeni o novih sledenjih, odgovorih in poobjavah;
• Skupna raba: objavljajte neposredno v Mastodon s poljubne preglednice v skupni rabi;
• Srčkano: naša maskota je ljubek slon in videli boste, kako se sem ter tja pojavi.
Mastodon je registrirana neprofitna organizacija, razvoj pa podpirajo neposredno vaše donacije. Je brez oglaševanja, monetizacije in brez rizičnega kapitala; nameravamo ga takšnega tudi obdržati.

View File

@@ -0,0 +1 @@
Decentralizirano družbeno omrežje

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -1,6 +1,6 @@
Mastodon är det största decentraliserade sociala nätverket på internet. I stället för en enda webbplats är det ett nätverk av miljontals användare på oberoende servrar som alla kan interagera med varandra, sömlöst. Oavsett vad du är intresserad av kan du träffa passionerade personer som diskuterar ämnet på Mastodon!
Gå med på en server och skapa din profil. Hitta och följ fascinerande människor och läsa deras inlägg i en annonsfri, kronologisk tidslinje. Uttryck dig med anpassade emoji, bilder, GIF:ar, videor och ljud i 500-teckensinlägg. Svara på trådar och ompostningar från vem som helst för att dela bra saker. Hitta nya konton att följa och trendande hashtaggar för att utöka ditt nätverk.
Gå med på en server och skapa din profil. Hitta och följ fascinerande människor och läsa deras inlägg i en annonsfri, kronologisk tidslinje. Uttryck dig med anpassade emoji, bilder, GIF:ar, videor och ljud i 500-teckensinlägg. Svara på trådar och boostar från vem som helst för att dela bra saker. Hitta nya konton att följa och trendande hashtaggar för att utöka ditt nätverk.
Mastodon är byggt med fokus på integritet och trygghet. Bestäm om dina inlägg delas med dina följare, bara personer du omnämner, eller hela världen. Innehållsvarningar låter dig dölja inlägg som innehåller känsligt eller triggande material tills du är redo att interagera med dem. Varje server har sina egna riktlinjer och moderatorer för att hålla sina medlemmar trygga, och robusta blockerings- och rapporteringsverktyg för att förhindra missbruk.
@@ -9,7 +9,7 @@ Fler funktioner:
• Mörkt läge: Läs inlägg i ljust, mörkt eller helsvart läge
• Omröstningar: Fråga följare om deras åsikt och sammanställ deras röster
• Utforska: Trendande hashtaggar och konton är ett tryck bort
• Notiser: Bli meddelad om nya följare, svar och ompostningar
• Notiser: Bli meddelad om nya följare, svar och boostar
• Delning: Posta direkt till Mastodon från delningsbladet i alla appar
• Gullighet: Vår maskot är en bedårande elefant, och du kommer att se dem dyka upp då och då

View File

@@ -1,4 +1,4 @@
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon! ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในไทม์ไลน์ที่ไร้โฆษณาและเรียงตามลำดับเวลาล้วน ๆ แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
@@ -13,4 +13,4 @@ Mastodon สร้างขึ้นโดยเน้นความเป็
• การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
• ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป

View File

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

View File

@@ -1 +1 @@
Decentralized social network
Децентралізована соціальна мережа

View File

@@ -1,6 +1,6 @@
Mastodon là mạng xã hội liên hợp lớn nhất trên internet. Thay vì một trang web duy nhất, nó là một mạng lưới hàng triệu người dùng trong các máy chủ độc lập, tất cả đều có thể tương tác với nhau một cách liền mạch. Bất kể bạn thích gì, bạn đều có thể gặp gỡ những người đăng tút về nó trên Mastodon!
Mastodon là mạng xã hội liên hợp lớn nhất trên internet. Thay vì một trang web duy nhất, nó là một mạng lưới hàng triệu người trong các máy chủ độc lập, tất cả đều có thể tương tác với nhau một cách liền mạch. Bất kể bạn thích gì, bạn đều có thể gặp gỡ những người đăng tút về nó trên Mastodon!
Tham gia một máy chủ và tạo trang hồ sơ của bạn. Tìm, theo dõi những người thú vị và đọc tút của họ theo trình tự thời gian, không có quảng cáo. Thể hiện bản thân bằng emoji, hình ảnh, GIF, video và âm thanh trong tút tối đa 500 ký tự. Trả lời tút và đăng lại tút từ bất kỳ ai để chia sẻ những điều tuyệt vời. Tìm những người dùng mới để theo dõi và các hashtag nổi bật để mở rộng mạng lưới của bạn.
Tham gia một máy chủ và tạo trang hồ sơ của bạn. Tìm, theo dõi những người thú vị và đọc tút của họ theo trình tự thời gian, không có quảng cáo. Thể hiện bản thân bằng emoji, hình ảnh, GIF, video và âm thanh trong tút tối đa 500 ký tự. Trả lời tút và đăng lại tút từ bất kỳ ai để chia sẻ những điều tuyệt vời. Tìm những người mới để theo dõi và các hashtag nổi bật để mở rộng mạng lưới của bạn.
Mastodon được xây dựng tập trung vào sự riêng tư và an toàn. Quyết định xem tút của bạn được chia sẻ với những người theo dõi, chỉ những người bạn nhắc đến hay cả thế giới. Nội dung ẩn cho phép bạn ẩn các tút chứa nội dung nhạy cảm hoặc chơi chữ cho đến khi bạn sẵn sàng tương tác với chúng. Mỗi máy chủ có các nguyên tắc riêng và kiểm duyệt viên riêng để giữ an toàn cho các thành viên, song song với các công cụ chặn và báo cáo mạnh mẽ giúp ngăn chặn hành vi bậy.

View File

@@ -1,4 +1,4 @@
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。
Mastodon 是互联网上最大的去中心化社交网络。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。 它不是一个网站,而是由独立社区节点及其数以百万计的用户组成的网络,所有这些用户都能够无缝地相互交流。 无论你进入哪一个节点,你都可以与所有在 Mastodon 的人之间进行交流。
加入一个社区节点并创建你的账户。 寻找并关注感兴趣的同好,无广告地浏览他们的时间线。 用自定义表情、图像、GIF、视频和音频在 500 个字符的帖子里写下你的想法。 回复及转发其他人的帖子来共同分享美好的事物。 寻找新的账户以及热门的话题来拓展你的网络。
@@ -13,4 +13,4 @@ Mastodon 以隐私和安全为首要目标。 自由选择你的帖子是分享
• 分享:从其他应用中的分享菜单中直接发布到 Mastodon
• 吉祥物:你会不时地看到我们可爱的长毛象
Mastodon 是一个注册的非营利开发项目,直接由您的捐赠支持。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。
Mastodon 是一个注册的非营利开发项目,直接由您的捐赠支持。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。 没有广告,没有商业化,也没有风险资本,我们计划保持这种方式。

View File

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

View File

@@ -9,7 +9,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 43
versionCode 45
versionName "1.1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
@@ -72,7 +72,7 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.6'
implementation 'me.grishka.appkit:appkit:1.2.7'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -73,7 +73,7 @@ public class CacheController{
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id;
for(Filter filter:filters){
if(filter.matches(status.getContentStatus().content))
if(filter.matches(status))
continue outer;
}
result.add(status);
@@ -145,7 +145,7 @@ public class CacheController{
newMaxID=ntf.id;
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status.getContentStatus().content))
if(filter.matches(ntf.status))
continue outer;
}
}
@@ -166,7 +166,7 @@ public class CacheController{
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){
for(Filter filter:filters){
if(filter.matches(ntf.status.getContentStatus().content)){
if(filter.matches(ntf.status)){
return false;
}
}

View File

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

View File

@@ -191,15 +191,18 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
void onError(ErrorResponse err){
invokeErrorCallback(err);
if(!canceled)
invokeErrorCallback(err);
}
void onError(String msg, int httpStatus, Throwable exception){
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
if(!canceled)
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
}
void onSuccess(T resp){
invokeSuccessCallback(resp);
if(!canceled)
invokeSuccessCallback(resp);
}
@Override

View File

@@ -365,6 +365,8 @@ public class PushSubscriptionManager{
}
private static void registerAllAccountsForPush(boolean forceReRegister){
if(!arePushNotificationsAvailable())
return;
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
if(session.pushSubscription==null || forceReRegister)
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
@@ -86,4 +87,9 @@ public class AccountTimelineFragment extends StatusListFragment{
}
prependItems(Collections.singletonList(ev.status), true);
}
@Override
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
}

View File

@@ -439,7 +439,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.wrapProgress(getActivity(), R.string.loading, true)
.exec(accountID);
}

View File

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

View File

@@ -50,7 +50,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.twitter.twittertext.Regex;
import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E;
@@ -100,6 +99,7 @@ import org.parceler.Parcels;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -129,21 +129,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
private static final String VALID_URL_PATTERN_STRING =
"(" + // $1 total match
"(" + Regex.URL_VALID_PRECEDING_CHARS + ")" + // $2 Preceding character
"(" + // $3 URL
"(https?://)" + // $4 Protocol (optional)
"(" + Regex.URL_VALID_DOMAIN + ")" + // $5 Domain(s)
"(?::(" + Regex.URL_VALID_PORT_NUMBER + "))?" + // $6 Port number (optional)
"(/" +
Regex.URL_VALID_PATH + "*+" +
")?" + // $7 URL Path and anchor
"(\\?" + Regex.URL_VALID_URL_QUERY_CHARS + "*" + // $8 Query String
Regex.URL_VALID_URL_QUERY_ENDING_CHARS + ")?" +
")" +
")";
private static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
@SuppressLint("NewApi") // this class actually exists on 6.0
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
@@ -331,6 +316,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftPollOption opt=createDraftPollOption();
opt.edit.setText(eopt.title);
}
pollDuration=(int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond();
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), editingStatus.poll.expiresAt);
updatePollOptionHints();
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
}else{
@@ -607,7 +594,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
MENTION_PATTERN.matcher(
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
HtmlParser.URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
).replaceAll("$1@$3")
).replaceAll("x");
charCount=0;
@@ -700,7 +687,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result));
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));

View File

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

View File

@@ -142,7 +142,6 @@ public class HomeTimelineFragment extends StatusListFragment{
}
}
@Subscribe
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
}
@@ -258,7 +257,7 @@ public class HomeTimelineFragment extends StatusListFragment{
if(idsBelowGap.contains(s.id))
break;
for(Filter filter:filters){
if(filter.matches(s.getContentStatus().content)){
if(filter.matches(s)){
continue outer;
}
}
@@ -423,4 +422,9 @@ public class HomeTimelineFragment extends StatusListFragment{
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateState(ev.state);
}
@Override
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true;
}
}

View File

@@ -8,8 +8,10 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
@@ -26,6 +28,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
@@ -111,6 +114,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
.collect(Collectors.toSet());
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset==0 && !result.items.isEmpty()){
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
}
}
});
}
@@ -180,4 +187,36 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
return;
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
.collect(Collectors.toList());
for(Notification n:toRemove){
removeNotification(n);
}
}
private void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
}

View File

@@ -183,7 +183,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
private ShapeDrawable background=new ShapeDrawable();
protected ShapeDrawable background=new ShapeDrawable();
public BaseViewHolder(int layout){
super(getActivity(), layout, list);
@@ -220,6 +220,20 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
super.onBind(item);
title.setText(item.parsedName);
value.setText(item.parsedValue);
if(item.verifiedAt!=null){
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
check.setTint(textColor);
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
}else{
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
value.setCompoundDrawables(null, null, null, null);
}
}
@Override

View File

@@ -6,6 +6,8 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
@@ -34,6 +36,7 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -272,6 +275,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
}
return true;
});
return sizeWrapper;
}
@@ -519,15 +534,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(R.menu.profile, menu);
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
if(isOwnProfile){
for(int i=0;i<menu.size();i++){
MenuItem item=menu.getItem(i);
item.setVisible(item.getItemId()==R.id.share);
}
if(isOwnProfile)
return;
}
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
@@ -580,6 +591,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.bookmarks){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
}else if(id==R.id.favorites){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
}
return true;
}

View File

@@ -35,6 +35,7 @@ import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -166,7 +167,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
@Override
public void onDestroy(){
super.onDestroy();
if(needUpdateNotificationSettings){
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
}
}

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -10,7 +9,8 @@ import android.view.WindowInsets;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
@@ -66,8 +66,9 @@ public class SplashFragment extends AppKitFragment{
private void onButtonClick(View v){
Bundle extras=new Bundle();
extras.putBoolean("signup", v.getId()==R.id.btn_get_started);
Nav.go(getActivity(), InstanceCatalogFragment.class, extras);
boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
}
private void updateArtSize(int w, int h){

View File

@@ -6,6 +6,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
@@ -18,6 +19,8 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
@@ -134,6 +137,40 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return null;
}
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return false;
}
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
List<Status> toRemove=Stream.concat(data.stream(), preloadedData.stream())
.filter(s->s.account.id.equals(ev.postsByAccountID) || (s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID)))
.collect(Collectors.toList());
for(Status s:toRemove){
removeStatus(s);
}
}
protected void removeStatus(Status status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(status.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
public class EventListener{
@Subscribe
@@ -165,28 +202,13 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
Status status=getStatusByID(ev.id);
if(status==null)
return;
data.remove(status);
preloadedData.remove(status);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(ev.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
removeStatus(status);
}
@Subscribe
public void onStatusCreated(StatusCreatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
StatusListFragment.this.onStatusCreated(ev);
}
@@ -206,5 +228,14 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
}
}
}
@Subscribe
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
if(!ev.accountID.equals(accountID))
return;
if(ev.isUnfollow && !shouldRemoveAccountPostsWhenUnfollowing())
return;
StatusListFragment.this.onRemoveAccountPostsEvent(ev);
}
}
}

View File

@@ -97,7 +97,7 @@ public class ThreadFragment extends StatusListFragment{
return statuses;
return statuses.stream().filter(status->{
for(Filter filter:filters){
if(filter.matches(status.getContentStatus().content))
if(filter.matches(status))
return false;
}
return true;

View File

@@ -35,6 +35,8 @@ import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
@@ -128,7 +130,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}else{
progressVisibilityListener.onProgressVisibilityChanged(true);
currentRequest=new GetSearchResults(currentQuery, null, true)
.setCallback(new SimpleCallback<>(this){
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
@@ -148,6 +150,17 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
})
.exec(accountID);
}
@@ -159,12 +172,11 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
super.updateList();
return;
}
imgLoader.deactivate();
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.activate();
imgLoader.forceUpdateImages();
prevDisplayItems=null;
}

View File

@@ -1,42 +1,35 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.IOException;
import java.net.IDN;
import java.util.ArrayList;
import java.util.Collections;
@@ -44,47 +37,48 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
private InstancesAdapter adapter;
private MergeRecyclerAdapter mergeAdapter;
private View headerView;
private CatalogInstance chosenInstance;
private List<CatalogInstance> filteredData=new ArrayList<>();
private Button nextButton;
private MastodonAPIRequest<?> getCategoriesRequest;
private EditText searchEdit;
private TabLayout categoriesList;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private String currentSearchQuery;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
private String loadingInstanceDomain;
private GetInstance loadingInstanceRequest;
private HashMap<String, Instance> instancesCache=new HashMap<>();
private ProgressDialog instanceProgressDialog;
private View buttonBar;
private boolean isSignup;
abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
protected RecyclerView.Adapter adapter;
protected MergeRecyclerAdapter mergeAdapter;
protected CatalogInstance chosenInstance;
protected Button nextButton;
protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery;
protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
protected List<CatalogInstance> filteredData=new ArrayList<>();
protected GetInstance loadingInstanceRequest;
protected Call loadingInstanceRedirectRequest;
protected ProgressDialog instanceProgressDialog;
protected HashMap<String, String> redirects=new HashMap<>();
protected HashMap<String, String> redirectsInverse=new HashMap<>();
protected boolean isSignup;
protected CatalogInstance fakeInstance=new CatalogInstance();
private static final double DUNBAR=Math.log(800);
public InstanceCatalogFragment(){
super(R.layout.fragment_onboarding_common, 10);
public InstanceCatalogFragment(int layout, int perPage){
super(layout, perPage);
}
@Override
@@ -93,258 +87,9 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
isSignup=getArguments().getBoolean("signup");
}
@Override
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
loadData();
}
protected abstract void proceedWithAuthOrSignup(Instance instance);
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetCatalogInstances(null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
if(getActivity()==null)
return;
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
for(List<CatalogInstance> group:byLang.values()){
Collections.sort(group, (a, b)->{
double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers));
double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers));
return Double.compare(aa, bb);
});
}
// get the list of user-configured system languages
List<String> userLangs;
if(Build.VERSION.SDK_INT<24){
userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage());
}else{
LocaleList ll=getResources().getConfiguration().getLocales();
userLangs=new ArrayList<>(ll.size());
for(int i=0;i<ll.size();i++){
userLangs.add(ll.get(i).getLanguage());
}
}
// add instances in preferred languages to the top of the list, in the order of preference
ArrayList<CatalogInstance> sortedList=new ArrayList<>();
for(String lang:userLangs){
List<CatalogInstance> langInstances=byLang.remove(lang);
if(langInstances!=null){
sortedList.addAll(langInstances);
}
}
// sort the remaining language groups by aggregate lastWeekUsers
class InstanceGroup{
public int activeUsers;
public List<CatalogInstance> instances;
}
byLang.values().stream().map(il->{
InstanceGroup group=new InstanceGroup();
group.instances=il;
for(CatalogInstance instance:il){
group.activeUsers+=instance.lastWeekUsers;
}
return group;
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
onDataLoaded(sortedList, false);
updateFilteredList();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
onDataLoaded(Collections.emptyList(), false);
}
})
.execNoAuth("");
getCategoriesRequest=new GetCatalogCategories(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogCategory> result){
getCategoriesRequest=null;
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
updateCategories();
}
@Override
public void onError(ErrorResponse error){
getCategoriesRequest=null;
error.showToast(getActivity());
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
updateCategories();
}
})
.execNoAuth("");
}
private void updateCategories(){
categoriesList.removeAllTabs();
for(CatalogCategory cat:categories){
int titleRes=getTitleForCategory(cat.category);
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
emoji.setImageResource(getEmojiForCategory(cat.category));
categoriesList.addTab(tab);
}
}
@Override
public void onDestroy(){
super.onDestroy();
if(getCategoriesRequest!=null)
getCategoriesRequest.cancel();
}
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
categoriesList=headerView.findViewById(R.id.categories_list);
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
CatalogCategory category=categories.get(tab.getPosition());
currentCategory=category.category;
updateFilteredList();
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
}
});
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
}
});
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setEnabled(chosenInstance!=null);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
buttonBar=view.findViewById(R.id.button_bar);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
private void onNextClick(View v){
String domain=chosenInstance.domain;
Instance instance=instancesCache.get(domain);
if(instance!=null){
proceedWithAuthOrSignup(instance);
}else{
showProgressDialog();
if(!domain.equals(loadingInstanceDomain)){
loadInstanceInfo(domain);
}
}
}
private void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(isSignup){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}else{
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
}
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
// case "academia" -> "📚";
// case "activism" -> "✊";
// case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
private int getEmojiForCategory(String category){
return switch(category){
case "all" -> R.drawable.ic_category_all;
case "academia" -> R.drawable.ic_category_academia;
case "activism" -> R.drawable.ic_category_activism;
case "food" -> R.drawable.ic_category_food;
case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
case "general" -> R.drawable.ic_category_general;
case "journalism" -> R.drawable.ic_category_journalism;
case "lgbt" -> R.drawable.ic_category_lgbt;
case "regional" -> R.drawable.ic_category_regional;
case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
case "tech" -> R.drawable.ic_category_tech;
default -> R.drawable.ic_category_unknown;
};
}
private int getTitleForCategory(String category){
return switch(category){
case "all" -> R.string.category_all;
case "academia" -> R.string.category_academia;
case "activism" -> R.string.category_activism;
case "food" -> R.string.category_food;
case "furry" -> R.string.category_furry;
case "games" -> R.string.category_games;
case "general" -> R.string.category_general;
case "journalism" -> R.string.category_journalism;
case "lgbt" -> R.string.category_lgbt;
case "regional" -> R.string.category_regional;
case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
case "tech" -> R.string.category_tech;
default -> 0;
};
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
@@ -353,73 +98,80 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
if(instance==null){
showProgressDialog();
loadInstanceInfo(currentSearchQuery);
loadInstanceInfo(currentSearchQuery, false);
}else{
proceedWithAuthOrSignup(instance);
}
return true;
}
private void onSearchChangedDebounced(){
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
updateFilteredList();
loadInstanceInfo(currentSearchQuery);
loadInstanceInfo(currentSearchQuery, false);
}
private void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
for(CatalogInstance instance:data){
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
filteredData.add(instance);
}
protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
for(List<CatalogInstance> group:byLang.values()){
Collections.sort(group, (a, b)->{
double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers));
double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers));
return Double.compare(aa, bb);
});
}
// get the list of user-configured system languages
List<String> userLangs;
if(Build.VERSION.SDK_INT<24){
userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage());
}else{
LocaleList ll=getResources().getConfiguration().getLocales();
userLangs=new ArrayList<>(ll.size());
for(int i=0;i<ll.size();i++){
userLangs.add(ll.get(i).getLanguage());
}
}
DiffUtil.calculateDiff(new DiffUtil.Callback(){
@Override
public int getOldListSize(){
return prevData.size();
// add instances in preferred languages to the top of the list, in the order of preference
ArrayList<CatalogInstance> sortedList=new ArrayList<>();
for(String lang:userLangs){
List<CatalogInstance> langInstances=byLang.remove(lang);
if(langInstances!=null){
sortedList.addAll(langInstances);
}
@Override
public int getNewListSize(){
return filteredData.size();
}
// sort the remaining language groups by aggregate lastWeekUsers
class InstanceGroup{
public int activeUsers;
public List<CatalogInstance> instances;
}
byLang.values().stream().map(il->{
InstanceGroup group=new InstanceGroup();
group.instances=il;
for(CatalogInstance instance:il){
group.activeUsers+=instance.lastWeekUsers;
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
}).dispatchUpdatesTo(adapter);
return group;
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
return sortedList;
}
private void showProgressDialog(){
protected abstract void updateFilteredList();
protected void showProgressDialog(){
instanceProgressDialog=new ProgressDialog(getActivity());
instanceProgressDialog.setMessage(getString(R.string.loading_instance));
instanceProgressDialog.setOnCancelListener(dialog->{
if(loadingInstanceRequest!=null){
loadingInstanceRequest.cancel();
loadingInstanceRequest=null;
loadingInstanceDomain=null;
}
});
instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
instanceProgressDialog.show();
}
private String normalizeInstanceDomain(String _domain){
protected String normalizeInstanceDomain(String _domain){
if(TextUtils.isEmpty(_domain))
return null;
if(_domain.contains(":")){
try{
_domain=Uri.parse(_domain).getAuthority();
}catch(Exception ignore){}
}catch(Exception ignore){
}
if(TextUtils.isEmpty(_domain))
return null;
}
@@ -429,15 +181,17 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
}catch(IllegalArgumentException x){
return null;
}
if(redirects.containsKey(domain))
return redirects.get(domain);
return domain;
}
private void loadInstanceInfo(String _domain){
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain);
if(cachedInstance!=null){
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain))
for(CatalogInstance ci : filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance)
return;
}
CatalogInstance ci=cachedInstance.toCatalogInstance();
@@ -446,56 +200,155 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
return;
}
if(loadingInstanceDomain!=null){
if(loadingInstanceDomain.equals(domain))
if(loadingInstanceDomain.equals(domain)){
return;
else
loadingInstanceRequest.cancel();
}else{
cancelLoadingInstanceInfo();
}
}
loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance();
loadingInstanceRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
loadingInstanceRequest=null;
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(domain.equals(currentSearchQuery)){
boolean found=false;
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain)){
found=true;
break;
}
}
if(!found){
CatalogInstance ci=result.toCatalogInstance();
filteredData.add(0, ci);
adapter.notifyItemInserted(0);
}
@Override
public void onSuccess(Instance result){
loadingInstanceRequest=null;
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
for(CatalogInstance ci : filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance){
found=true;
break;
}
}
if(!found){
CatalogInstance ci=result.toCatalogInstance();
if(filteredData.size()==1 && filteredData.get(0)==fakeInstance){
filteredData.set(0, ci);
adapter.notifyItemChanged(0);
}else{
filteredData.add(0, ci);
adapter.notifyItemInserted(0);
}
}
}
}
@Override
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
loadingInstanceDomain=null;
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(getString(R.string.not_a_mastodon_instance, domain)+"\n\n"+((MastodonErrorResponse)error).error)
.setPositiveButton(R.string.ok, null)
.show();
@Override
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
ivh.rebind();
}
}
}).execNoAuth(domain);
}
}
}).execNoAuth(domain);
}
private void cancelLoadingInstanceInfo(){
if(loadingInstanceRequest!=null){
loadingInstanceRequest.cancel();
loadingInstanceRequest=null;
}
if(loadingInstanceRedirectRequest!=null){
loadingInstanceRedirectRequest.cancel();
loadingInstanceRedirectRequest=null;
}
loadingInstanceDomain=null;
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
}
}
private void showInstanceInfoLoadError(String domain, Object error){
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
String additionalInfo;
if(error instanceof MastodonErrorResponse me){
additionalInfo="\n\n"+me.error;
}else if(error instanceof Throwable t){
additionalInfo="\n\n"+t.getLocalizedMessage();
}else{
additionalInfo="";
}
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(getString(R.string.not_a_mastodon_instance, domain)+additionalInfo)
.setPositiveButton(R.string.ok, null)
.show();
}
}
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder()
.url(url)
.build();
loadingInstanceRedirectRequest=MastodonAPIController.getHttpClient().newCall(req);
loadingInstanceRedirectRequest.enqueue(new okhttp3.Callback(){
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e){
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
return;
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
return;
}
InputSource source=new InputSource(response.body().charStream());
Document doc=DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
NodeList list=doc.getElementsByTagName("Link");
for(int i=0; i<list.getLength(); i++){
if(list.item(i) instanceof Element el){
String template=el.getAttribute("template");
if("lrdd".equals(el.getAttribute("rel")) && !TextUtils.isEmpty(template) && template.contains("{uri}")){
Uri uri=Uri.parse(template.replace("{uri}", "qwe"));
String redirectDomain=normalizeInstanceDomain(uri.getHost());
redirects.put(domain, redirectDomain);
redirectsInverse.put(redirectDomain, domain);
a.runOnUiThread(()->loadInstanceInfo(redirectDomain, true));
return;
}
}
}
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
}catch(Exception x){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
}
}
});
}
@Override
@@ -509,78 +362,26 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
}
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setEnabled(chosenInstance!=null);
buttonBar=view.findViewById(R.id.button_bar);
setRefreshEnabled(false);
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_catalog, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
protected void onNextClick(View v){
String domain=chosenInstance.domain;
Instance instance=instancesCache.get(domain);
if(instance!=null){
proceedWithAuthOrSignup(instance);
}else{
showProgressDialog();
if(!domain.equals(loadingInstanceDomain)){
loadInstanceInfo(domain, false);
}
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
radioButton.setChecked(chosenInstance==item);
}
@Override
public void onClick(){
if(chosenInstance==item)
return;
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
}
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain);
}
}
}

View File

@@ -0,0 +1,374 @@
package org.joinmastodon.android.fragments.onboarding;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
private View headerView;
private MastodonAPIRequest<?> getCategoriesRequest;
private TabLayout categoriesList;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@Override
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetCatalogInstances(null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
if(getActivity()==null)
return;
onDataLoaded(sortInstances(result), false);
updateFilteredList();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
onDataLoaded(Collections.emptyList(), false);
}
})
.execNoAuth("");
getCategoriesRequest=new GetCatalogCategories(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogCategory> result){
getCategoriesRequest=null;
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
updateCategories();
}
@Override
public void onError(ErrorResponse error){
getCategoriesRequest=null;
error.showToast(getActivity());
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
updateCategories();
}
})
.execNoAuth("");
}
private void updateCategories(){
categoriesList.removeAllTabs();
for(CatalogCategory cat:categories){
int titleRes=getTitleForCategory(cat.category);
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
emoji.setImageResource(getEmojiForCategory(cat.category));
categoriesList.addTab(tab);
}
}
@Override
public void onDestroy(){
super.onDestroy();
if(getCategoriesRequest!=null)
getCategoriesRequest.cancel();
}
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
categoriesList=headerView.findViewById(R.id.categories_list);
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
CatalogCategory category=categories.get(tab.getPosition());
currentCategory=category.category;
updateFilteredList();
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
}
});
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
}
});
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(isSignup){
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}else{
}
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
// case "academia" -> "📚";
// case "activism" -> "✊";
// case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
private int getEmojiForCategory(String category){
return switch(category){
case "all" -> R.drawable.ic_category_all;
case "academia" -> R.drawable.ic_category_academia;
case "activism" -> R.drawable.ic_category_activism;
case "food" -> R.drawable.ic_category_food;
case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
case "general" -> R.drawable.ic_category_general;
case "journalism" -> R.drawable.ic_category_journalism;
case "lgbt" -> R.drawable.ic_category_lgbt;
case "regional" -> R.drawable.ic_category_regional;
case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
case "tech" -> R.drawable.ic_category_tech;
default -> R.drawable.ic_category_unknown;
};
}
private int getTitleForCategory(String category){
return switch(category){
case "all" -> R.string.category_all;
case "academia" -> R.string.category_academia;
case "activism" -> R.string.category_activism;
case "food" -> R.string.category_food;
case "furry" -> R.string.category_furry;
case "games" -> R.string.category_games;
case "general" -> R.string.category_general;
case "journalism" -> R.string.category_journalism;
case "lgbt" -> R.string.category_lgbt;
case "regional" -> R.string.category_regional;
case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
case "tech" -> R.string.category_tech;
default -> 0;
};
}
@Override
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
for(CatalogInstance instance:data){
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
filteredData.add(instance);
}
}
}
DiffUtil.calculateDiff(new DiffUtil.Callback(){
@Override
public int getOldListSize(){
return prevData.size();
}
@Override
public int getNewListSize(){
return filteredData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
}).dispatchUpdatesTo(adapter);
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceCatalogSignupFragment.InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceCatalogSignupFragment.InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceCatalogSignupFragment.InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_catalog, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
}
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
radioButton.setChecked(chosenInstance==item);
}
@Override
public void onClick(){
if(chosenInstance==item)
return;
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceCatalogSignupFragment.InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
}
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
}
}

View File

@@ -0,0 +1,259 @@
package org.joinmastodon.android.fragments.onboarding;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.ImageButton;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
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.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
private View headerView;
private boolean loadedAutocomplete;
private ImageButton clearBtn;
public InstanceChooserLoginFragment(){
super(R.layout.fragment_login, 10);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
setTitle(R.string.login_title);
if(!loadedAutocomplete){
loadAutocompleteServers();
}
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
}
@Override
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
if(currentSearchQuery.length()>0){
boolean foundExactMatch=false;
for(CatalogInstance inst:data){
if(inst.normalizedDomain.contains(currentSearchQuery)){
filteredData.add(inst);
if(inst.normalizedDomain.equals(currentSearchQuery))
foundExactMatch=true;
}
}
if(!foundExactMatch)
filteredData.add(0, fakeInstance);
}
UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals);
for(int i=0;i<list.getChildCount();i++){
list.getChildAt(i).invalidateOutline();
}
}
@Override
protected void doLoadData(int offset, int count){
}
private void loadAutocompleteServers(){
loadedAutocomplete=true;
new GetCatalogInstances(null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
data.clear();
data.addAll(sortInstances(result));
}
@Override
public void onError(ErrorResponse error){
}
})
.execNoAuth("");
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
Toolbar toolbar=getToolbar();
toolbar.setElevation(0);
toolbar.setBackground(null);
}
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_login, list, false);
clearBtn=headerView.findViewById(R.id.search_clear);
searchEdit=headerView.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
if(s.length()>0){
fakeInstance.domain=fakeInstance.normalizedDomain=s.toString();
fakeInstance.description=getString(R.string.loading_instance);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){
ivh.rebind();
}
}
if(filteredData.isEmpty()){
filteredData.add(fakeInstance);
adapter.notifyItemInserted(0);
}
clearBtn.setVisibility(View.VISIBLE);
}else{
clearBtn.setVisibility(View.GONE);
}
}
@Override
public void afterTextChanged(Editable s){
}
});
clearBtn.setOnClickListener(v->searchEdit.setText(""));
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildViewHolder(view) instanceof InstanceViewHolder){
outRect.left=outRect.right=V.dp(16);
}
}
});
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_login, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
radioButton=findViewById(R.id.radiobtn);
radioButton.setMinWidth(0);
radioButton.setMinHeight(0);
itemView.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, getAbsoluteAdapterPosition()==1 ? 0 : V.dp(-4), view.getWidth(), view.getHeight()+(getAbsoluteAdapterPosition()==filteredData.size() ? 0 : V.dp(4)), V.dp(4));
}
});
itemView.setClipToOutline(true);
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
radioButton.setChecked(chosenInstance==item);
}
@Override
public void onClick(){
if(chosenInstance==item)
return;
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
break;
}
}
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
}
}

View File

@@ -14,6 +14,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -131,7 +132,10 @@ public class InstanceRulesFragment extends AppKitFragment{
@Override
public void onBind(Instance.Rule item){
title.setText(item.text);
if(item.parsedText==null){
item.parsedText=HtmlParser.parseLinks(item.text);
}
title.setText(item.parsedText);
}
}
}

View File

@@ -102,7 +102,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), false)
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){

View File

@@ -11,8 +11,10 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
@@ -130,6 +132,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
@Override
public void onSuccess(Relationship result){
Nav.finish(ReportDoneFragment.this);
E.post(new RemoveAccountPostsEvent(accountID, reportAccount.id, true));
}
@Override

View File

@@ -50,6 +50,10 @@ public class Filter extends BaseModel{
return pattern.matcher(text).find();
}
public boolean matches(Status status){
return matches(status.getContentStatus().getStrippedText());
}
@Override
public String toString(){
return "Filter{"+

View File

@@ -134,6 +134,8 @@ public class Instance extends BaseModel{
public static class Rule{
public String id;
public String text;
public transient CharSequence parsedText;
}
@Parcel

View File

@@ -0,0 +1,21 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import java.time.Instant;
@AllFieldsAreRequired
public class Marker extends BaseModel{
public String lastReadId;
public long version;
public Instant updatedAt;
@Override
public String toString(){
return "Marker{"+
"lastReadId='"+lastReadId+'\''+
", version="+version+
", updatedAt="+updatedAt+
'}';
}
}

View File

@@ -13,7 +13,7 @@ public class Poll extends BaseModel{
@RequiredField
public String id;
public Instant expiresAt;
public boolean expired;
private boolean expired;
public boolean multiple;
public int votersCount;
public boolean voted;
@@ -48,6 +48,10 @@ public class Poll extends BaseModel{
'}';
}
public boolean isExpired(){
return expired || (expiresAt!=null && expiresAt.isBefore(Instant.now()));
}
@Parcel
public static class Option{
public String title;

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcel;
import java.time.Instant;
@@ -56,6 +57,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public transient boolean spoilerRevealed;
public transient boolean hasGapAfter;
private transient String strippedText;
@Override
public void postprocess() throws ObjectValidationException{
@@ -127,9 +129,16 @@ public class Status extends BaseModel implements DisplayItemsParent{
repliesCount=ev.replies;
favourited=ev.favorited;
reblogged=ev.reblogged;
bookmarked=ev.bookmarked;
}
public Status getContentStatus(){
return reblog!=null ? reblog : this;
}
public String getStrippedText(){
if(strippedText==null)
strippedText=HtmlParser.strip(content);
return strippedText;
}
}

View File

@@ -93,7 +93,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private void bindButton(TextView btn, long count){
if(count>0 && !item.hideCounts){
btn.setText(DecimalFormat.getIntegerInstance().format(count));
btn.setText(UiUtils.abbreviateNumber(count));
btn.setCompoundDrawablePadding(V.dp(8));
}else{
btn.setText("");

View File

@@ -5,6 +5,7 @@ import android.app.ProgressDialog;
import android.graphics.Outline;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -192,6 +193,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
});
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
}else if(id==R.id.bookmark){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
}
return true;
});
@@ -209,6 +212,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
if(item.hasVisibilityToggle){
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
visibility.setTooltipText(visibility.getContentDescription());
}
}
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
if(TextUtils.isEmpty(item.extraText)){
@@ -286,6 +292,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
MenuItem block=menu.findItem(R.id.block);
MenuItem report=menu.findItem(R.id.report);
MenuItem follow=menu.findItem(R.id.follow);
MenuItem bookmark=menu.findItem(R.id.bookmark);
if(item.status!=null){
bookmark.setVisible(true);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
}else{
bookmark.setVisible(false);
}
if(isOwnPost){
mute.setVisible(false);
block.setVisible(false);

View File

@@ -85,6 +85,7 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
@Override
public void clearImage(int index){
crossfadeDrawable.setCrossfadeAlpha(1f);
crossfadeDrawable.setImageDrawable(null);
didClear=true;
}

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