Compare commits
414 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ad2d08e27 | ||
|
|
42658add38 | ||
|
|
b211789847 | ||
|
|
9c88183366 | ||
|
|
c76dba3a8c | ||
|
|
29bee87f2a | ||
|
|
c139f85b99 | ||
|
|
3247d4f2f5 | ||
|
|
77b2f98f17 | ||
|
|
82c6c8076a | ||
|
|
4177faa553 | ||
|
|
92ec125661 | ||
|
|
513a57663b | ||
|
|
20e7f716f1 | ||
|
|
71f92cb66c | ||
|
|
77b2abd0cb | ||
|
|
15385dd924 | ||
|
|
08847ec641 | ||
|
|
805fc5d8c7 | ||
|
|
3d7a95d336 | ||
|
|
c1869386ff | ||
|
|
7a728c52cf | ||
|
|
22f3aad538 | ||
|
|
42da6dcf48 | ||
|
|
c0f18b1f61 | ||
|
|
492d851d89 | ||
|
|
853f9dc8e4 | ||
|
|
0e2ae4d3c0 | ||
|
|
3ea1412faa | ||
|
|
e83f0749ee | ||
|
|
343d958677 | ||
|
|
2f32378978 | ||
|
|
cfc5683f75 | ||
|
|
823e2f6ac6 | ||
|
|
5f5fcdde46 | ||
|
|
cf74e252ce | ||
|
|
010ca587d2 | ||
|
|
2957ac813f | ||
|
|
5aa117e2e3 | ||
|
|
f1df4e72d2 | ||
|
|
afee257211 | ||
|
|
74986a10f6 | ||
|
|
77b6344032 | ||
|
|
cb0d7e73d4 | ||
|
|
1b714e5815 | ||
|
|
e5dd97f992 | ||
|
|
2a7838f2a7 | ||
|
|
25112c1fe3 | ||
|
|
e7850dfcfb | ||
|
|
bbdece33ea | ||
|
|
f861eefe78 | ||
|
|
07e4c6d0a9 | ||
|
|
fd1afc8c02 | ||
|
|
a008c025f5 | ||
|
|
c4768e7f87 | ||
|
|
812ea1023e | ||
|
|
e74e45f315 | ||
|
|
b590b23439 | ||
|
|
a8208b4c05 | ||
|
|
c742b695e3 | ||
|
|
9a8ff82f2d | ||
|
|
3b2f495a50 | ||
|
|
773d5a104b | ||
|
|
c4ac325c14 | ||
|
|
8964f22c0c | ||
|
|
6b99cc48a4 | ||
|
|
448949d5c2 | ||
|
|
e94f508680 | ||
|
|
a3472d0e1c | ||
|
|
3ebb058a62 | ||
|
|
32fd14b3ea | ||
|
|
49c1f14a20 | ||
|
|
7dea64676e | ||
|
|
a77142a9ee | ||
|
|
898ef9a560 | ||
|
|
47539ac47e | ||
|
|
5aac227ad6 | ||
|
|
38c8990f4e | ||
|
|
797a967cb1 | ||
|
|
0d7a5e55fe | ||
|
|
87de48c957 | ||
|
|
3489d8d5b3 | ||
|
|
aa6c3c56bb | ||
|
|
3fdf3dcdab | ||
|
|
2f716d63de | ||
|
|
bbe71fff51 | ||
|
|
74ae04e0d2 | ||
|
|
bb293ec319 | ||
|
|
bf6f8a7137 | ||
|
|
8080986e5b | ||
|
|
5ed24a227e | ||
|
|
2d89b4b539 | ||
|
|
378a675157 | ||
|
|
127662967a | ||
|
|
76b52dc06e | ||
|
|
55d3b127e7 | ||
|
|
0f9e0fe140 | ||
|
|
2b462f7289 | ||
|
|
977ca60655 | ||
|
|
309b8089b8 | ||
|
|
0563068ec3 | ||
|
|
e33282d686 | ||
|
|
0b83b9b5e0 | ||
|
|
8abb4d25f9 | ||
|
|
aadf404142 | ||
|
|
8a608a1e71 | ||
|
|
5571220b20 | ||
|
|
05c831a8b1 | ||
|
|
c237b36f8b | ||
|
|
2ed7fdecb4 | ||
|
|
1dce9f1928 | ||
|
|
4a6a707bbc | ||
|
|
989095a13d | ||
|
|
34556b0c45 | ||
|
|
7aa4926ac3 | ||
|
|
189cee1395 | ||
|
|
5a7b18983d | ||
|
|
72f01267ae | ||
|
|
e5df4a3886 | ||
|
|
3827047f05 | ||
|
|
0f262c41ec | ||
|
|
2d2d405c73 | ||
|
|
9b82c9bbe5 | ||
|
|
8573276a79 | ||
|
|
9f5f0d594c | ||
|
|
a7bfaf732f | ||
|
|
9b0af7d60d | ||
|
|
1dd937f047 | ||
|
|
e135b61dd0 | ||
|
|
8c3687ca4a | ||
|
|
3ceea6a83b | ||
|
|
cc22ccfb66 | ||
|
|
d1c4ca38ff | ||
|
|
52fb35e5e9 | ||
|
|
2b0faf0e23 | ||
|
|
bc74a72369 | ||
|
|
0c2e4e34fe | ||
|
|
67bd2e9629 | ||
|
|
d9dfd6e1bb | ||
|
|
1dc9adafc7 | ||
|
|
9e201f3c00 | ||
|
|
16e2632d9b | ||
|
|
9533b4f45d | ||
|
|
de83c1ea96 | ||
|
|
18b5c8ed50 | ||
|
|
14f7f5a1f4 | ||
|
|
4960589bef | ||
|
|
e2c7f8c7f0 | ||
|
|
1371260fdf | ||
|
|
01c816814a | ||
|
|
bbb61e7a87 | ||
|
|
30b24384d5 | ||
|
|
506df71b37 | ||
|
|
bb323ab08f | ||
|
|
6d8cb8cbb4 | ||
|
|
90dc391b99 | ||
|
|
8b974c2e96 | ||
|
|
baa0f2ba33 | ||
|
|
697fc713a0 | ||
|
|
1704f8079f | ||
|
|
7c1fd70569 | ||
|
|
dd19d1408b | ||
|
|
f36bd866f0 | ||
|
|
6311920b4c | ||
|
|
5cf20658bb | ||
|
|
c5c1015231 | ||
|
|
2fb45b5384 | ||
|
|
b6e1304df0 | ||
|
|
f0ed251438 | ||
|
|
0f2d218956 | ||
|
|
0756883af9 | ||
|
|
d76612f0b9 | ||
|
|
3c85421474 | ||
|
|
cba8050989 | ||
|
|
5060409597 | ||
|
|
e80d711738 | ||
|
|
903a5477a7 | ||
|
|
b7940df425 | ||
|
|
6b2685618d | ||
|
|
40c52c7df7 | ||
|
|
5b460fec39 | ||
|
|
f0085cb240 | ||
|
|
477dd0b219 | ||
|
|
9b585e5955 | ||
|
|
9e7e540068 | ||
|
|
c1e5daec3d | ||
|
|
ccc2c9d6d3 | ||
|
|
fbcad38d71 | ||
|
|
d487b3c114 | ||
|
|
62a5e36eec | ||
|
|
c6130e4d29 | ||
|
|
26f7a75628 | ||
|
|
4110eb064a | ||
|
|
3e8c4a8443 | ||
|
|
b8fc674fe8 | ||
|
|
cb123a178a | ||
|
|
80323f8236 | ||
|
|
edc03642cc | ||
|
|
114c77a665 | ||
|
|
0627644019 | ||
|
|
1874267b32 | ||
|
|
de763ab6f7 | ||
|
|
ace0072118 | ||
|
|
3820eee174 | ||
|
|
754cd807c0 | ||
|
|
e3f4951f95 | ||
|
|
b321c4e479 | ||
|
|
192c634755 | ||
|
|
ef27200764 | ||
|
|
aa4932077b | ||
|
|
6c87c4aa7c | ||
|
|
59e6f6033a | ||
|
|
82a0d046a7 | ||
|
|
c98f1d32e8 | ||
|
|
34698a5aa2 | ||
|
|
ee1a0cc666 | ||
|
|
eda1526830 | ||
|
|
f5961c8077 | ||
|
|
7f03bdae2b | ||
|
|
38ac8f14fb | ||
|
|
7d7bfad3c0 | ||
|
|
81ac72e4eb | ||
|
|
e915aab2fb | ||
|
|
a92c903ffb | ||
|
|
e174a7efd7 | ||
|
|
9f4575f349 | ||
|
|
9cbfb1a7f8 | ||
|
|
6db20a2cc1 | ||
|
|
fe067cba13 | ||
|
|
c0b5d34aae | ||
|
|
ee15655fc7 | ||
|
|
34a9cd0614 | ||
|
|
44b46bd83c | ||
|
|
8fbe1ef444 | ||
|
|
61e8615907 | ||
|
|
4eef3a3243 | ||
|
|
8585dc2cd0 | ||
|
|
3647fcaa0b | ||
|
|
184a211819 | ||
|
|
7d36b67652 | ||
|
|
7e0f786a43 | ||
|
|
0a8cec46c0 | ||
|
|
3cd558c44f | ||
|
|
4076067b3e | ||
|
|
b3593a0e30 | ||
|
|
1daede2c62 | ||
|
|
fefe529438 | ||
|
|
ba59aa6147 | ||
|
|
c42edb3b94 | ||
|
|
d647d9edd1 | ||
|
|
ed2d03b5de | ||
|
|
5e1b39bfd7 | ||
|
|
dc60f4bb51 | ||
|
|
6a5c564b99 | ||
|
|
8745e5d034 | ||
|
|
448a12f2ed | ||
|
|
e6cc15879e | ||
|
|
76fd8686b3 | ||
|
|
2dbc282769 | ||
|
|
4fca4ca332 | ||
|
|
a18ed46151 | ||
|
|
07d9ee00ff | ||
|
|
5789c3af29 | ||
|
|
d9a1ef059d | ||
|
|
7148d95830 | ||
|
|
9bfaa5db34 | ||
|
|
54a4d327ec | ||
|
|
90bf1113f9 | ||
|
|
7cc23856f7 | ||
|
|
09158f7036 | ||
|
|
687880bbd5 | ||
|
|
598f1fc3b3 | ||
|
|
7989fa2853 | ||
|
|
f81f14fb5d | ||
|
|
4acf7a13f2 | ||
|
|
c004db7ac9 | ||
|
|
e39ac3ac18 | ||
|
|
2011b08a2b | ||
|
|
5afd82585e | ||
|
|
5daaf15195 | ||
|
|
d8c4247a2c | ||
|
|
627870098f | ||
|
|
2a7a88466d | ||
|
|
4d02d659e7 | ||
|
|
86ab70757e | ||
|
|
0161f71d63 | ||
|
|
7b5664cc8f | ||
|
|
6992ab4f02 | ||
|
|
88d8f7afc8 | ||
|
|
1a344f777d | ||
|
|
ff158c28cf | ||
|
|
c39d2a8ec1 | ||
|
|
07d722dca3 | ||
|
|
5db7a9df18 | ||
|
|
ce475516ad | ||
|
|
fb0f15b844 | ||
|
|
3ca9d7b792 | ||
|
|
a8732fcd20 | ||
|
|
e7530993a8 | ||
|
|
fa319023f6 | ||
|
|
84a2585c63 | ||
|
|
a18fc769bb | ||
|
|
b3a4572f6d | ||
|
|
12d0ce8ff0 | ||
|
|
d21ca7c203 | ||
|
|
075780fe3f | ||
|
|
223b506284 | ||
|
|
a4dda389c7 | ||
|
|
6e2eaf10fa | ||
|
|
73230643fc | ||
|
|
e70cebe2b8 | ||
|
|
84e4a750bf | ||
|
|
df36166a9e | ||
|
|
bb7831483a | ||
|
|
87f96e6259 | ||
|
|
0db409eb97 | ||
|
|
b4b1f2cade | ||
|
|
884aa8377a | ||
|
|
7f021bc958 | ||
|
|
f4bab5a12e | ||
|
|
2bbe5da955 | ||
|
|
819478854d | ||
|
|
a58c17e844 | ||
|
|
bf0b91ca92 | ||
|
|
ed716794d6 | ||
|
|
a2aabe38ec | ||
|
|
2c9e92a254 | ||
|
|
390a28ad0e | ||
|
|
2b19135118 | ||
|
|
3bca29aee2 | ||
|
|
a9cd8954d1 | ||
|
|
d9bbd3b243 | ||
|
|
0670ac53dc | ||
|
|
f41972bda7 | ||
|
|
994360e52d | ||
|
|
a27ae00a5b | ||
|
|
84d6f162ae | ||
|
|
031a4e1d28 | ||
|
|
934a35b1d3 | ||
|
|
b4b60cee32 | ||
|
|
0a887528ac | ||
|
|
c2e5888d7a | ||
|
|
b51fcfea84 | ||
|
|
751f0a2726 | ||
|
|
9880a26636 | ||
|
|
0398dfd1c1 | ||
|
|
69cc090a2b | ||
|
|
9d4df68f02 | ||
|
|
cf621930a8 | ||
|
|
150f7c7137 | ||
|
|
b4a7828e6d | ||
|
|
8226e57597 | ||
|
|
97d52b625a | ||
|
|
33d0cfd89c | ||
|
|
585100f2a0 | ||
|
|
87485beddc | ||
|
|
da44b45af5 | ||
|
|
884bf2e9b4 | ||
|
|
6efa929e21 | ||
|
|
9f497724f5 | ||
|
|
21b55c18fb | ||
|
|
7d846d7862 | ||
|
|
67496023f7 | ||
|
|
30fcb19f9b | ||
|
|
506aa36017 | ||
|
|
0789753c28 | ||
|
|
bfeb230f3f | ||
|
|
f8e3b295ef | ||
|
|
0dbe21cb9c | ||
|
|
4fc36693be | ||
|
|
fa3366141e | ||
|
|
5583948e73 | ||
|
|
a71fccecec | ||
|
|
47a70dd648 | ||
|
|
3507043494 | ||
|
|
827185b394 | ||
|
|
1173e0d44a | ||
|
|
cca0428f35 | ||
|
|
bfe3c0316c | ||
|
|
2e3a035109 | ||
|
|
8ed73e0f03 | ||
|
|
3479618077 | ||
|
|
a429a90ce5 | ||
|
|
7c4d8577a6 | ||
|
|
d5bdb1afdb | ||
|
|
e4504e3d54 | ||
|
|
e679b17aa7 | ||
|
|
19b5586e09 | ||
|
|
3b3158bc68 | ||
|
|
15d21f4eb5 | ||
|
|
c76c27684f | ||
|
|
77c5dbf7f5 | ||
|
|
0e5fd46254 | ||
|
|
db69fd76f4 | ||
|
|
3723b275c6 | ||
|
|
07d7e2ff10 | ||
|
|
cac52e13f6 | ||
|
|
169a84ad93 | ||
|
|
b16da68f7b | ||
|
|
84b241e27e | ||
|
|
22041e43ed | ||
|
|
75a1ff5eb7 | ||
|
|
4b831d1ba0 | ||
|
|
b326acb018 | ||
|
|
160ed459e6 | ||
|
|
13c51ba464 | ||
|
|
99b78659ec | ||
|
|
9d5965725e | ||
|
|
e89fc80ca1 | ||
|
|
70e9fcbc72 | ||
|
|
f4412fdec1 | ||
|
|
1d25494274 | ||
|
|
74042dee06 | ||
|
|
0e630d6506 |
@@ -28,7 +28,7 @@ platform :android do
|
||||
build_type: "release",
|
||||
)
|
||||
upload_to_play_store(
|
||||
changes_not_sent_for_review: true,
|
||||
release_status: "draft",
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true
|
||||
)
|
||||
|
||||
@@ -2,60 +2,60 @@ Mastodon - лепшы спосаб быць у курсе ўсяго, што а
|
||||
|
||||
Гэта афіцыйная праграма для Android ад Mastodon. Ён вокамгненна хуткі і прыгожы, распрацаваны, каб быць не толькі функцыянальным, але і простым у выкарыстанні. У нашай праграме вы можаце:
|
||||
|
||||
АГЛЕДЗЕЦЬ
|
||||
ДАСЛЕДАВАЦЬ
|
||||
|
||||
■ Адкрыйце для сябе новых пісьменнікаў, журналістаў, мастакоў, фатографаў, навукоўцаў і многае іншае
|
||||
■ Даведайцеся, што адбываецца ў свеце
|
||||
|
||||
READ
|
||||
ЧЫТАЦЬ
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Сачыце за людзьмі, якія вам цікавыя, у храналагічнай стужцы без перапыненняў
|
||||
■ Сачыце за хэштэгамі, каб быць у курсе канкрэтных тэм у рэжыме рэальнага часу
|
||||
|
||||
CREATE
|
||||
СТВАРАЙЦЕ
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Адпраўце паведамленне сваім падпісчыкам ці ўсім свеце, выкарыстоўваючы апытанні, высакаякасныя відарысы і відэа
|
||||
■ Удзельнічайце ў цікавых гутарках з іншымі людзьмі
|
||||
|
||||
CURATE
|
||||
КУРЫРУЙЦЕ
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Стварайце спісы людзей, каб не прапусціць ні аднаго допісу
|
||||
■ Фільтруйце словы і фразы, каб кантраляваць тое, што вы хочаце і не хочаце бачыць
|
||||
|
||||
AND MORE!
|
||||
І БОЛЬШ!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Прыгожая тэма, якая адаптуецца да вашай індывідуальнай каляровай схемы, светлай ці цёмнай
|
||||
■ Абагульвайце і сканіруйце QR-коды, каб хутка абменьвацца профілямі Mastodon з іншымі
|
||||
■ Уваход і пераключэнне паміж некалькімі ўліковымі запісамі
|
||||
■ Атрымлівайце апавяшчэнне, калі хтосьці публікуе допіс з дапамогай кнопкі званочка
|
||||
■ Без спойлераў! Вы можаце размясціць свае допісы за папярэджаннямі аб змесце
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
МАГУТНАЯ ПУБЛІКАЦЫЙНАЯ ПЛАТФОРМА
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Вам больш не трэба спрабаваць супакоіць непразрысты алгарытм, які вырашае, ці ўбачаць вашы сябры тое, што вы апублікавалі. Калі яны пойдуць за вамі, яны гэта ўбачаць.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Калі вы публікуеце яго ў адкрытым Інтэрнэце, ён становіцца даступным у адкрытым Інтэрнэце. Вы можаце смела дзяліцца спасылкамі на Mastodon, ведаючы, што кожны зможа прачытаць іх без уваходу ў сістэму.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Акрамя стужак, апытанняў, высакаякасных відарысаў, відэа, аўдыя і папярэджанняў аб змесце, Mastodon прапануе мноства спосабаў выказаць сябе так, як вам зручна.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
МАГУТНАЯ ПЛАТФОРМА ДЛЯ ЧЫТАННЯ
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Нам не трэба паказваць вам рэкламу, таму нам не трэба трымаць вас у нашай праграме. Mastodon мае найбагацейшы выбар старонніх праграм і інтэграцый, так што вы можаце выбраць тое, што вам больш за ўсё падыходзіць.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Дзякуючы храналагічнай хатняй стужцы лёгка вызначыць, калі вы прагледзелі ўсе абнаўленні і можаце перайсці да чагосьці іншага.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Не трэба турбавацца аб тым, што няправільны клік сапсуе вашы рэкамендацыі назаўжды. Мы не адгадваем, што вы хочаце бачыць, мы дазваляем вам кіраваць гэтым.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
ПРАТАКОЛЫ, А НЕ ПЛАТФОРМЫ
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
У адрозненне ад традыцыйнай платформы сацыяльных сетак, Mastodon пабудаваны на дэцэнтралізаваным пратаколе. Вы можаце зарэгістравацца на нашым афіцыйным серверы або выбраць трэцюю асобу для размяшчэння вашых даных і мадэрацыі вашага вопыту.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
З агульным пратаколам, незалежна ад таго, што вы абралі, вы можаце бесперашкодна мець зносіны з людзьмі на іншых серверах Mastodon. Але ёсць і больш: толькі з адным уліковым запісам вы можаце размаўляць з людзьмі з іншых платформаў fediverse.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Не задаволены сваім выбарам? Вы заўсёды можаце пераключыцца на іншы сервер Mastodon, узяўшы з сабой падпісчыкаў. Для прасунутых карыстальнікаў вы нават можаце размясціць свае даныя ў сваёй інфраструктуры, паколькі Mastodon з'яўляецца адкрытым зыходным кодам.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
НЕКАМЕРЦЫЙНЫ ХАРАКТАРАТ
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon з'яўляецца зарэгістраванай некамерцыйнай арганізацыяй у ЗША і Германіі. Нас матывуе не здабыванне грашовай каштоўнасці з платформы, а тое, што лепш для платформы.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
РЭКАМЕНТАВАНЫЯ Ў: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com і інш.
|
||||
61
fastlane/metadata/android/cy/full_description.txt
Normal file
61
fastlane/metadata/android/cy/full_description.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
Mastodon yw'r ffordd orau o gadw i fyny â'r hyn sy'n digwydd. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
|
||||
DARGANFOD
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
|
||||
DARLLEN
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
|
||||
CREU
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
|
||||
CURADU
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
|
||||
A MWY!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
|
||||
PROTOCOLAU, NID PLATFFORMAU
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
|
||||
NID-ER-ELW
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
|
||||
FEL Y GWELWYD YN: Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, a mwy.
|
||||
1
fastlane/metadata/android/cy/short_description.txt
Normal file
1
fastlane/metadata/android/cy/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Lle mae sgyrsiau yn digwydd
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon er den bedste måde at holde sig ajour med, hvad der sker. Følg enhver på tværs af fediverset og se alt i kronologisk rækkefølge. Ingen algoritmer, annoncer eller clickbait i syne.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Dette er den officielle Android-app til Mastodon. Den er lynhurtigt og forbløffende smuk, designet til at være ikke blot kraftfuld, men også nem at bruge. I vores app kan man:
|
||||
|
||||
EXPLORE
|
||||
UDFORSKE
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
● Opdag nye forfattere, journalister, kunstnere, fotografer, forskere med mere
|
||||
● Se, hvad der sker i verden
|
||||
|
||||
READ
|
||||
LÆSE
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
● Bliv holdt ajour med folk, man holder af, i et kronologisk feed uden afbrydelser
|
||||
● Følg hashtags for at være i tråd med bestemte emner i realtid
|
||||
|
||||
CREATE
|
||||
OPRETTE
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
● Post til følgere eller hele verden, med meningsmålinger, højkvalitets billeder og videoer
|
||||
● Deltag i interessante samtaler med andre mennesker
|
||||
|
||||
CURATE
|
||||
KURATERE
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
● Opret lister over personer for aldrig at gå glip af et indlæg
|
||||
● Filtrér ord eller sætninger for at styre, hvad man ønsker, og ikke ønsker, at se
|
||||
|
||||
AND MORE!
|
||||
OG MERE!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
● Et smukt tema, der tilpasser sig den personlige farvesammensætning, lys eller mørk
|
||||
● Del og skan QR-koder for hurtigt at udveksle Mastodon-profiler med andre
|
||||
● Log ind og skift mellem flere konti
|
||||
● Få besked med klokkeknappen, når en bestemt person poster
|
||||
● Ingen spoilers! Man kan placere sine indlæg bag indholdsadvarsler
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
EN KRAFTFULD UDGIVELSESPLATFORM
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Man behøver ikke længere at forsøge at formilde en uigennemsigtig algoritme, der beslutter, om vennerne vil se, hvad man har postet. Følges man af dem, vil de se det.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Udgiver man det til det åbne net, er det tilgængeligt på det åbne net. Man kan sikkert dele links via Mastodon med den viden, at alle vil være i stand til at læse dem uden at logge ind.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Mellem tråde, afstemninger, højkvalitets billeder, videoer, lyd og indholdsadvarsler, tilbyder Mastodon masser af måder at udtrykke sig på en måde, man finder passende.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
EN KRAFTFULD LÆSEPLATFORM
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Vi behøver ikke at vise brugere annoncer, så vi behøver ikke at holde dem i vores app. Mastodon har det rigeste udvalg af 3. parts apps og integrationer, så man kan vælge mest passende oplevelse.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Takket være det kronologiske hjemme-feed er det nemt at vide, når man er i tråd med alle opdateringer og kan gå videre til noget andet.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Ingen grund til bekymringer over, at man får sine anbefalinger ødelagt for evigt med et fejlklik. Vi gætter ikke, hvad brugerne ønsker at se, vi lader dem styre det.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PROTOKOLLER, IKKE PLATFORME
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon er ikke som en traditionel social medieplatform, men baserer sig på en decentraliseret protokol. Man kan tilmelde sig på vores officielle server, eller vælge en 3. part til at være vært for sine data og moderere ens oplevelse.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Takket være den fælles protokol, kan man, uanset hvad man vælger, kommunikere problemfrit med folk på andre Mastodon-servere. Der er mere endnu: Med kun én konto kan man kommunikere med folk fra andre fediverse-platforme.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Ikke tilfreds med valget? Man kan altid skifte til en anden Mastodon-server, mens man tager sine tilhængere med sig. For avancerede brugere, man kan endda være vært for sine data på sin egen infrastruktur, da Mastodon er open-source.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
NON-PROFIT AF NATUR
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon er registreret som non-profit i USA og Tyskland. Vi er ikke motiveret af at udvinde pengeværdi fra platformen, men ved hvad der er bedst for den.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
SOM OMTALT I: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com og flere.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Hvor samtaler finder sted
|
||||
@@ -5,34 +5,34 @@ Das ist die offizielle Android-App für Mastodon. Blitzschnell und atemberaubend
|
||||
ENTDECKEN
|
||||
|
||||
■ Neue Autoren, Journalisten, Künstler, Fotografen, Wissenschaftler und viele andere entdecken
|
||||
■ See what’s happening in the world
|
||||
■ Sehen Sie, was in der Welt passiert
|
||||
|
||||
READ
|
||||
LESEN
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Bleiben Sie in einem chronologischen Feed ohne Unterbrechungen über die Menschen auf dem Laufenden, die Ihnen wichtig sind
|
||||
■ Folgen Sie Hashtags, um in Echtzeit über bestimmte Themen auf dem Laufenden zu bleiben
|
||||
|
||||
KREIEREN
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Posten Sie für Ihre Follower oder die ganze Welt mit Umfragen, hochwertigen Bildern und Videos
|
||||
■ Nehmen Sie an interessanten Gesprächen mit anderen Menschen teil
|
||||
|
||||
GESTALTEN
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Erstellen Sie Listen mit Personen, um nie einen Beitrag zu verpassen
|
||||
■ Filtern Sie Wörter oder Ausdrücke, um zu steuern, was Sie sehen möchten und was nicht
|
||||
|
||||
UND MEHR!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ Ein schönes Design, das sich an Ihr persönliches Farbschema anpasst, ob hell oder dunkel
|
||||
■ Teilen und scannen Sie QR-Codes, um Mastodon-Profile schnell mit anderen auszutauschen
|
||||
■ Anmelden und zwischen mehreren Konten wechseln
|
||||
■ Lassen Sie sich benachrichtigen, wenn eine bestimmte Person mit der Klingeltaste postet
|
||||
■ Keine Spoiler! Du kannst deine Beträge hinter Inhaltswarnungen stellen
|
||||
|
||||
EINE MÄCHTIGE PLATTFORM ZUM VERÖFFENTLICHEN
|
||||
|
||||
Du musst nicht länger versuchen, einen undurchsichtigen Algorithmus dir wohlgesinnt zu stimmen, der darüber entscheidet, ob deine Freunde sehen, was du postest. If they follow you, they’ll see it.
|
||||
Du musst nicht länger versuchen, einen undurchsichtigen Algorithmus dir wohlgesinnt zu stimmen, der darüber entscheidet, ob deine Freunde sehen, was du postest. Wenn Sie Ihnen folgen, werden Sie es sehen.
|
||||
|
||||
Wenn du es im offenen Web veröffentlichst, ist es auch zugänglich im offenen Web. Du kannst ganz unbekümmert Links auf Mastodon teilen in dem Wissen, dass jeder sie ohne Einloggen wird lesen können.
|
||||
|
||||
|
||||
4
fastlane/metadata/android/en-US/changelogs/116.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/116.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- We're rolling out grouped notifications for servers that support the feature
|
||||
- You can now click "show results" on polls without having to submit a vote
|
||||
- You will now see lines connecting reply chains in threads
|
||||
- Bug fixes and improvements, obviously!
|
||||
@@ -1 +1 @@
|
||||
110.txt
|
||||
116.txt
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon gertatzen ari denari buruz egunean egoteko modurik onena da. Jarraitu edonor fedibertsoan eta ikusi dena ordena kronologikoan. Ez dago algoritmorik, iragarkirik edo clickbait-ik bistan.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Hau Android gailuentzako Mastodon aplikazio ofiziala da. Ikaragarri azkarra eta ikaragarri ederra da, ahaltsua ez ezik, erabilerraza ere izateko diseinatua. Gure aplikazioan, hauek egin ditzakezu:
|
||||
|
||||
EXPLORE
|
||||
ESPLORATU
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Ezagutu idazle, kazetari, artista, argazkilari, zientzialari eta gehiago
|
||||
■ Ikusi zer ari den gertatzen munduan
|
||||
|
||||
READ
|
||||
IRAKURRI
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Axola zaizkizun pertsonei buruz egunean egotea, etenik gabeko horma kronologikoarekin
|
||||
■ Jarraitu hashtag-ak gai espezifikoekin denbora errealean informatuta egoteko
|
||||
|
||||
CREATE
|
||||
SORTU
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Bidali bidalketak zure jarraitzaileei edo mundu guztiari, inkestekin, kalitate handiko irudiekin eta bideoekin
|
||||
■ Beste pertsona batzuekin elkarrizketa interesgarrietan parte hartu
|
||||
|
||||
CURATE
|
||||
ONDU
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Pertsonen zerrendak sortu elkarrizketa bat inoiz ez galtzeko
|
||||
■ Hitzak edo esaldiak iragazi ikusi nahi duzuna eta ikusi nahi ez duzuna kontrolatzeko
|
||||
|
||||
AND MORE!
|
||||
ETA GEHIAGO!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Zure kolore eskema pertsonalizatura egokitzen den gai ederra, argia edo iluna
|
||||
■ Partekatu eta eskaneatu QR kodeak Mastodon profilak beste batzuekin azkar trukatzeko
|
||||
■ Saioa hasi eta aldatu hainbat konturen artean
|
||||
■ Jakinarazpenak jaso pertsona jakin batek argitaratzen duenean kanpaiaren botoiarekin
|
||||
■ Izorrakirik ez! Zure mezuak eduki abisuekin babes ditzakezu
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
ARGITARATZEKO PLATAFORMA INDARTSUA
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Jada ez duzu algoritmo opako bat baretzen saiatu behar, zure lagunek argitaratu zenuena ikusiko duten ala ez erabakitzen duena. Jarraitzen badizute, ikusiko dute.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Web irekian argitaratzen baduzu, web irekian eskuragarri dago. Mastodonerako loturak segurtasunez parteka ditzakezu, jakinda edozeinek irakurri ahal izango dituela saioa hasi gabe.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Harien, inkesten, irudien, bideoen, audioaren eta edukien ohartarazpenen artean, Mastodonek modu asko eskaintzen ditu komeni zaizun moduan adierazteko.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
IRAKURTZEKO PLATAFORMA INDARTSUA
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Ez dizugu iragarkirik erakutsi behar; beraz, ez dizugu eutsi behar gure aplikazioan. Mastodonek hirugarrenen aplikazio eta integrazioen aukeraketa aberatsena du, gehien komeni zaizun esperientzia aukera dezazun.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Orri nagusiko jario kronologikoari esker, erraza da jakitea noiz aurkitu dituzun eguneraketa guztiak eta beste gauza batera pasa zaitezke.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Ez dago kezkatu beharrik okerreko klik batek betiko zure gomendioak hondatuko dituela. Ez dugu asmatzen zer ikusi nahi duzun, zuk kontrola dezazun uzten dugu.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PROTOKOLOAK, PLATAFORMARIK EZ
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon ez da sare sozialen ohiko plataforma bat, protokolo deszentralizatu batean oinarritzen da. Gure zerbitzari ofizialean izena eman dezakezu edo hirugarren bat aukeratu zure datuak ostatatzeko eta zure esperientzia moderatzeko.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Protokolo komunari esker, ez du axola zer aukeratzen duzun, arazorik gabe komunika zaitezke jendearekin beste Mastodon zerbitzari batzuetan badaude ere. Are gehiago: kontu bakar batekin, fedibertsoko beste plataforma batzuetako jendearekin komunika zaitezke.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Ez al zaude pozik zure aukerarekin? Beti alda dezakezu Mastodon zerbitzari desberdin batera zure jarraitzaileak zurekin eramaten dituzun bitartean. Erabiltzaile aurreratuentzat, zure datuak zure azpiegituran bertan ostatatu ditzakezu, Mastodon software librea baita.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
IRABAZI ASMORIK GABE IZAERAZ
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon Estatu Batuetan eta Alemanian erregistratutako irabazi-asmorik gabeko erakundea da. Ez gaude motibatuta plataformaren diru-balioa ateratzeagatik, baizik eta plataformarentzat hobea denagatik.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
HEMEN AGERTZEN DEN BEZALA: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo PCMAG.com, eta gehiago.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Elkarrizketak gertatzen diren lekua
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
ماستودون بهترین راه برای پیگیری اتفاقات است. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
این کاره رسمی اندروید برای ماستودون است. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. در کاره ما، شما میتوانید:
|
||||
|
||||
EXPLORE
|
||||
گشت و گذار
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ ببینید در دنیا چه رخ میدهد
|
||||
|
||||
READ
|
||||
خواندن
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
|
||||
CREATE
|
||||
ایجاد کردن
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
|
||||
CURATE
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■سیاههای از افراد ایجاد کنید تا هرگز فرستهای را از دست ندهید
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
|
||||
AND MORE!
|
||||
و بیشتر!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ وارد شوید و بین چند حساب جابجا شوید
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
یک سکوی انتشار قدرتمند
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. اگر آنها شما را پی بگیرند، آن را خواهند دید.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
یک سکوی خواندن قدرتمند
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
ما نیازی به نمایش تبلیغات به شما نداریم، بنابراین لازم نیست شما را در کارهمان نگه داریم. ماستودون غنیترین مجموعه از برنامهها و ادغامهای شخص ثالث را دارد، بنابراین میتوانید تجربهای را انتخاب کنید که مناسب شما است.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
به لطف خوراک زمانی خانه، تشخیص اینکه چه زمانی از همه بهروزرسانیها مطلع شدهاید و میتوانید به چیز دیگری بروید، آسان است.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
لازم نیست نگران باشید که یک کلیک اشتباه توصیه های شما را برای همیشه خراب می کند. ما حدس نمیزنیم که چه چیزی میخواهید ببینید، به شما اجازه میدهیم آن را کنترل کنید.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
شیوهنامهها، بدون سکوها
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
به لطف پروتکل مشترک، مهم نیست که چه چیزی را انتخاب می کنید، میتوانید به طور یکپارچه با افراد در سایر کارسازهای ماستودون ارتباط برقرار کنید. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
از انتخاب خود خوشحال نیستید؟ You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
ماستودون یک سازمان غیرانتفاعی ثبت شده در ایالات متحده و آلمان است. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
کجا گفتگوها اتفاق میافتد
|
||||
@@ -1,44 +1,44 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon est le meilleur endroit pour se tenir au courant de ce qui se passe. Suivez n'importe qui à travers le fédivers et découvrez tout dans un ordre chronologique. Pas d'algorithmes, de publicités ou de pièges à clics dans le coin.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Ceci est l'application Android officielle de Mastodon. Elle est extrêmement rapide et d'une beauté époustouflante, conçue pour être non seulement puissante, mais aussi facile d'utilisation. Dans notre application, vous pouvez :
|
||||
|
||||
EXPLORE
|
||||
EXPLORER
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Découvrir de nouveaux et nouvelles écrivain·e·s, journalistes, artistes, photographes, scientifiques et plus encore
|
||||
■ Voir ce qui se passe dans le monde
|
||||
|
||||
READ
|
||||
LIRE
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Suivez les gens qui vous intéressent dans un flux chronologique sans interruption
|
||||
■ Suivez les hashtags pour suivre des sujets spécifiques en temps réel
|
||||
|
||||
CREATE
|
||||
CRÉER
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Participer à des conversations intéressantes avec d'autres personnes
|
||||
|
||||
CURATE
|
||||
ACTUALISER
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Créez des listes de personnes pour ne jamais manquer un post
|
||||
■ Filtrez les mots ou les phrases pour contrôler ce que vous voulez et ne voulez pas voir
|
||||
|
||||
AND MORE!
|
||||
ET PLUS !
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Un beau thème qui s’adapte à vos couleurs personnalisées, claires ou sombres
|
||||
■ Partagez et numérisez des codes QR pour échanger rapidement des profils Mastodon avec d'autres personnes
|
||||
■ Connexion et bascule entre plusieurs comptes
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Pas de spoilers ! Vous pouvez mettre vos messages derrière des avertissements de contenu
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
UNE PUISSANTE PLATEFORME DE PUBLICATION
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Vous n'avez plus à dépendre d'un algorithme opaque qui décide si vos amis vont voir ce que vous avez posté. S'ils vous suivent, ils le verront.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
UNE PUISSANTE PLATEFORME DE LECTURE
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
|
||||
@@ -46,9 +46,9 @@ Thanks to the chronological home feed, it’s easy to tell when you’ve caught
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PROTOCOLS, ET NON PLATEFORMES
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
« Mastodon » n'est pas un média social traditionnel, il est conçu sur la base d'un protocole décentralisé. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
@@ -56,6 +56,6 @@ Not happy with your choice? You can always switch to a different Mastodon server
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon est une organisation à but non lucratif immatriculée aux États-Unis et en Allemagne. Nous ne sommes pas motivés par des considérations monétaires, mais par ce qu'il y a de mieux pour la plateforme.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Là où les discussions se déroulent
|
||||
61
fastlane/metadata/android/fy/full_description.txt
Normal file
61
fastlane/metadata/android/fy/full_description.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
Mastodon is de beste manier om by te hâlden wat der bart. Folgje elkenien yn de fediverse en sjoch alles yn gronologyske folchoarder. Gjin algoritmen, advertinsjes of clickbait te bekennen.
|
||||
|
||||
Dit is de offisjele Android-app foar Mastodon. It is razendfluch en bjusterbaarlik moai. Untwurpen om net allinnich krêftich, mar ek brûkersfreonlik te wêzen. Yn ús app kinne jo:
|
||||
|
||||
UNTDEKKE
|
||||
|
||||
■ Nije skriuwers, sjoernalisten, keunstners, fotografen, wittenskippers en mear ûntdekke
|
||||
■ Sjoch wat der yn de wrâld bart
|
||||
|
||||
LÊZE
|
||||
|
||||
■ Op de hichte bliuwe fan minsken om wa’t jo jouwe, op in gronologyske tiidline en sûnder ûnderbrekkingen
|
||||
■ Hashtags yn realtime folgje om op de hichte te bliuwen fan spesifike ûnderwerpen
|
||||
|
||||
KREËARJE
|
||||
|
||||
■ Berjochten nei allinnich dyn folgers stjoere of nei de hiele wrâld, mei peilingen, ôfbyldingen fan hege kwaliteit en fideo’s
|
||||
■ Oan ynteressante petearen mei oare minsken dielnimme
|
||||
|
||||
ONTWERPEN
|
||||
|
||||
■ Listen meitsje mei minsken dy’t jo folgje, om sa nea in berjocht te missen
|
||||
■ Wurden of sinnen filterje om te bepalen wat jo wol en net sjen wolle
|
||||
|
||||
EN MEAR!
|
||||
|
||||
■ In prachtich tema dat har oanpast oan jo persoanlike kleureskema, ljocht of donker
|
||||
■ QR-koaden scanne om gau Mastodon-profilen mei oare minsken te dielen
|
||||
■ Meardere accounts brûke
|
||||
■ Meldingen fan spesifike persoanen ûntfange, wannear’t dizze berjochten pleatse
|
||||
■ Gjin spoilers! ■ Jo kinne jo berjochten efter ynhâldswarskôgingen pleatse
|
||||
|
||||
IN KRÊFTICH PUBLIKAASJEPLATFOARM
|
||||
|
||||
Jo hoege net langer te probearjen in ûntrochsichtich algoritme tefreden te stellen, dat beslist oft jo freonen wol of net sjen meie wat jo pleatst hawwe. As se jo folgje, sille se it sjen.
|
||||
|
||||
Wannear’t jo it yn de fediverse pleatse, is it ek tagonklik yn de fediverse. Jo kinne feilich keppelingen nei Mastodon diele, yn de wittenskip dat elkenien se lêze kin sûnder oan hoege te melden.
|
||||
|
||||
Mei petearen, peilingen, ôfbyldingen fan hege kwaliteit, fideo’s, audio en ynhâld s warskôgingen, biedt Mastodon genôch manieren om josels uterje te kinnen, op in manier dy’t by jo past.
|
||||
|
||||
IN KRÊFTICH LÊSPLATFOARM
|
||||
|
||||
Wy hoege jo gjin advertinsjes sjen te litten, dus wy hoege jo ek net foar ús app te behâlden. Mastodon hat de rykste kar oan troch tredden ûntwikkele apps en yntegraasjes, sadat jo de brûkersûnderfining kieze kinne dy’t it beste by jo past.
|
||||
|
||||
Mei tank oan de gronologyske tiidline is it maklik om te sjen wannear’t jo alle updates besjoen hawwe en mei wat oars troch gean kinne.
|
||||
|
||||
Jo hoege net bang te wêzen dat, wannear’t jo op wat ferkeards klikke, jo oanrekommandaasjes foar altyd ferpest binne. We riede net nei wat jo sjen wolle, wy litte it oan jo oer.
|
||||
|
||||
PROTOKOLLEN, GJIN PLATFOARMS
|
||||
|
||||
Mastodon is net lykas in tradisjoneel sosjaal-mediaplatfoarm, mar is boud op in desintralisearre protool. Jo kinne foar ús offisjele server of foar ien fan in tredde partij kieze om jo gegevens te hosten en brûkers te moderearjen.
|
||||
|
||||
Mei tank oan it mienskiplike protokol, kinne jo, likefolle wat jo kieze, naadleas mei minsken op oare Mastodon-servers kommunisearje. Mar der is mear: Mei mar ien account kinne jo mei minsken fan oare fediverse-platfoarms kommunisearje.
|
||||
|
||||
Net tefreden mei jo kar? Jo kinne altyd nei ien oare Mastodon-server oerstappe en jo folgers meinimme. Betûfte brûkers kinne harren gegevens sels op harren eigen ynfrastruktuer hoste, omdat Mastodon iepen-boarne is.
|
||||
|
||||
NON-PROFIT FAN AARD
|
||||
|
||||
Mastodon is in registrearre non-profitorganisaasje yn de Ferienige Steaten en Dútslân. Wy helje ús motivaasje net út it kommersjeel misbrûk meitsje fan Mastodon, mar út wat it beste is foar Mastodon.
|
||||
|
||||
FERMELD YN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com en mear.
|
||||
1
fastlane/metadata/android/fy/short_description.txt
Normal file
1
fastlane/metadata/android/fy/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Dêr’t petearen ûntstean
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Is Mastodon an dòigh as fheàrr airson sùil a chumail air na tha a’ dol. Lean duine sam bith air a’ cho-shaoghal agus faic a h-uile càil a-rèir an ama. Chan eil sgeul air algairimean, sanasachd no clickbait.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Seo an aplacaid Android oifigeil airson Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
|
||||
EXPLORE
|
||||
|
||||
|
||||
61
fastlane/metadata/android/ia/full_description.txt
Normal file
61
fastlane/metadata/android/ia/full_description.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
Mastodon es le melior maniera de sequer lo que passa. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
|
||||
Iste es le app official de Mastodon pro Android. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In nostre app, tu pote:
|
||||
|
||||
EXPLORAR
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
|
||||
LEGER
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
|
||||
CREAR
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
|
||||
CURATE
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
|
||||
AND MORE!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon merupakan cara terbaik untuk tetap mengikuti apa yang terjadi. Ikuti siapapun dalam fediverse dan lihat semuanya secara kronologis. Tidak ada algoritma, iklan, atau clickbait.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Ini adalah aplikasi Android resmi untuk Mastodon. Cepat dan mengesankan, didesain tidak hanya sekadar kuat tetapi juga mudah digunakan. Di dalam aplikasi ini, anda dapat:
|
||||
|
||||
EXPLORE
|
||||
MENJELAJAHI
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
Temukan penulis, jurnalis, kreator, fotografer, ilmuwan, dan lainnya
|
||||
Melihat apa yang sedang terjadi
|
||||
|
||||
READ
|
||||
MEMBACA
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
Tetap berhubungan dengan orang terdekat anda tanpa gangguan
|
||||
Mengikuti tagar untuk tetap terupdate dengan topik-topik baru
|
||||
|
||||
CREATE
|
||||
MEMBUAT
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
Tautkan konten kepada pengikut anda dengan poll, gambar dan video berkualitas tinggi
|
||||
Berpartisipasi dalam percakapan yang menarik dengan orang lain
|
||||
|
||||
CURATE
|
||||
MENYUSUN
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
Buat daftar orang agar tidak ketinggalan
|
||||
Saring kata atau frasa untuk mengontrol apa yang anda ingin lihat atau tidak
|
||||
|
||||
AND MORE!
|
||||
DAN BANYAK LAGI!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
Tema indah yang menyesuaikan skema warna perangkat anda, terang atau gelap
|
||||
Bagikan dan pindai QR code untuk bertukar profil Mastodon dengan cepat
|
||||
Login dengan lebih dari satu akun
|
||||
Tetap ternotifikasi dengan postingan orang tertentu menggunakan tombol dering
|
||||
Tidak ada spoilers! Anda bisa mencantumkan postingan anda dibelakang tanda peringatan
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
PLATFORM PUBLIKASI YANG KUAT
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Anda tidak lagi perlu mengikuti algoritma yang menentukan jika teman anda akan melihat apa yang anda tautkan. Jika mereka mengikuti anda, mereka akan melihat tautan tersebut.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Jika anda mempublikasikan di internet, tautan tersebut dapat diakses di internet. Anda bisa dengan aman membagikan situs di Mastodon dengan catatan bahwa siapa saja dapat melihatnya tanpa login.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Selain threads, polls, gambar berkualitas tinggi, video, audio, dan peringatan konten, Mastodon menawarkan beragam cara untuk mengekspresikan diri anda dengan cara yang anda inginkan.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
SEBUAH PLATFORM MEMBACA YANG KUAT
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Kami tidak perlu menayangkan iklan, sehingga kami tidak perlu menahan anda di dalam aplikasi kami. Mastodon memiliki pilihan aplikasi pihak ketiga terbanyak sehingga anda mendapatkan pengalaman yang anda inginkan.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Berkat laman feed yang kronologis, anda dapat dengan mudah mengikuti update dan melakukan hal lain.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Tidak perlu khawatir jika anda salah mengklik dan mengubah rekomendasi. Kami tidak mengatur apa yang anda ingit lihat, kami serahkan kepada anda yang mengontrolnya.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PROTOKOL, BUKAN PLATFORM
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon tidak seperti platform media sosial pada umumnya, tetapi dibangun pada protokol yang terdesentralisasi. Anda dapat mendaftar pada server resmi kami, atau memilih pihak ketiga untuk melayani data anda.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Berkat protokol, apapun yang anda pilih, anda tetap dapat berkomunikasi dengan pengguna di server Mastodon yang berbeda. Dengan satu akun, anda dapat berkomunikasi dengan pengguna dari platform fediverse yang lain.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Tidak puas dengan pilihan anda? Anda dapat selalu berpindah ke server Mastodon yang berbeda selagi membawa pengikut anda. Untuk pengguna yang serius, anda bahkan bisa menyimpan data pada infrastruktur sendiri, karena Mastodon ini open-source.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
NON-PROFIT
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon terdaftar sebagai lembaga non-profit di US dan Jerman. Kami tidak terdorong untuk memperoleh keuntungan, tetapi apa yang terbaik.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
SEPERTI YANG DILIHAT DI: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, dan lainnya.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Tempat di mana percakapan terjadi
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon er besta leiðin til að fylgjast með hvað sé í gangi. Fylgstu með hverjum sem er í fediverse-heiminum og skoðaðu það allt í tímaröð. Engin algrím, auglýsingar eða smellbeitur á ferðinni.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Þetta er opinbera Android-forritið fyrir Mastodon. Það er eldsnöggt og fjarska fallegt, hannað til að vera bæði öflugt og auðvelt í notkun. Í forritinu okkar geturðu:
|
||||
|
||||
EXPLORE
|
||||
KANNAÐ
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Uppgötvaðu nýja rithöfunda, blaðamenn, listafólk, ljósmyndara, vísindafólk og fleira
|
||||
■ Sjáðu hvað er að gerast í heiminum
|
||||
|
||||
READ
|
||||
LESIÐ
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Vertu í sambandi við fólk sem þér er kært á streymi í tímaröð án truflana
|
||||
■ Fylgst með myllumerkjum til að fá upplýsingar um tiltekin efni í rauntíma
|
||||
|
||||
CREATE
|
||||
SKAPAÐ
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Birt færslur til fylgjendanna þinna eða alls heimsins, með könnunum, hágæða myndum og myndskeiðum
|
||||
■ Tekið þátt í áhugaverðum samræðum við annað fólk
|
||||
|
||||
CURATE
|
||||
SKIPULAGT
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Búið til lista yfir fólk sem þú vilt ekki missa af færslum frá
|
||||
■ Síað orð og setningar til að stýra hvað þú sérð og hvað ekki
|
||||
|
||||
AND MORE!
|
||||
OG FLEIRA!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Fallegt þema sem aðlagast persónusniðnu litastefi, ljóst eða dökkt
|
||||
■ Deildu og skannaðu QR-kóða til að skiptast á Mastodon-notendasniðum við aðra
|
||||
■ Skráðu þig inn og skiptu milli margra notendaaðganga
|
||||
■ Með bjölluhnappnum geturðu fengið tilkynningar þegar tilteknir aðilar birta færslur
|
||||
■ Ekkert sem afvegaleiðir! Þú getur sett færslurnar þínar á bakvið aðvörun vegna efnis
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
ÖFLUGT KERFI TIL BIRTINGAR
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Þú þarft ekki lengur að prófa þig áfram með og friðþægja eitthvert ógagnsætt algrími sem ákvarðar hvort vinir þínir fái að sjá það sem þú birtir. Ef viðkomandi fylgist með þér, mun það sjást.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Ef þú birtir það á opna vefnum, er hægt að skoða það á opna vefnum. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
ÖFLUGT KERFI TIL LESTRAR
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
No need to worry that a misclick will ruin your recommendations forever. Við eru ekkert að giska á hvað þú viljir sjá, við látum þér eftir að stýra því.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
SAMSKIPTAMÁTAR, EKKI KERFI
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Ekki ánægð/ur með valið þitt? Þú getur alltaf skipt yfir á annan Mastodon-þjón og tekið fylgjendurna þína með þér. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
ÁN HAGNAÐARMARKMIÐA INN AÐ BEINI
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon er skráð sem samtök án hagnaðarmarkmiða í BNA og Þýskalandi. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Þar sem samræður eiga sér stað
|
||||
@@ -1,4 +1,4 @@
|
||||
Mastodon è il modo migliore per tenere il passo con ciò che sta accadendo. Segui chiunque attraverso il fediverso e guarda tutto in ordine cronologico. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon è il modo migliore per tenere il passo con ciò che sta accadendo. Segui chiunque attraverso il fediverso e guarda tutto in ordine cronologico. Nessun algoritmo, pubblicità o clickbait in vista.
|
||||
|
||||
Questa è l'app Android ufficiale per Mastodon. È incredibilmente veloce e straordinariamente bella, progettata per essere non solo potente ma anche facile da usare. Nella nostra app, puoi:
|
||||
|
||||
@@ -50,7 +50,7 @@ PROTOCOLLI, NON PIATTAFORME
|
||||
|
||||
Mastodon non è come una piattaforma di social media tradizionale, ma è costruito su un protocollo decentralizzato. Puoi registrarti sul nostro server ufficiale o sceglierne uno di terze parti, per ospitare i tuoi dati e moderare la tua esperienza.
|
||||
|
||||
Grazie al protocollo comune, non importa cosa tu scelga, puoi comunicare senza problemi con le persone su altri server Mastodon. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Grazie al protocollo comune, non importa cosa tu scelga, puoi comunicare senza problemi con le persone su altri server Mastodon. Ma c'è di più: con un solo account puoi comunicare con persone di altre piattaforme del Fediverso.
|
||||
|
||||
Non sei felice della tua scelta? Puoi sempre passare a un altro server Mastodon portando con te i tuoi seguaci. Per gli utenti avanzati, puoi persino ospitare i tuoi dati sulla tua infrastruttura, poiché Mastodon è open source.
|
||||
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon で世界に起きていることを探索しよう。 Fediverse の誰でもフォローして投稿を時系列で閲覧できます。 アルゴリズム、広告、クリックベイトはありません。
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
これは Mastodon の公式 Android アプリです。 パワフルで使いやすく、燃えるように高速で驚くほど美しいアプリです。 このアプリでできること:
|
||||
|
||||
EXPLORE
|
||||
探索
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ 作家、写真家、科学者、ジャーナリスト、アーティストなど多様な人々に出会えます
|
||||
■ 世界で起きていることを目撃しましょう
|
||||
|
||||
READ
|
||||
閲覧
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ 邪魔するもののない時系列フィードで、注目している人々に追い付きましょう
|
||||
■ ハッシュタグをフォローして、話題をリアルタイムで把握できます
|
||||
|
||||
CREATE
|
||||
投稿
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ 投票、高画質の画像や動画をフォロワーや世界に投稿できます
|
||||
■ 人々との面白い会話にも参加できます
|
||||
|
||||
CURATE
|
||||
整理
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ 投稿を見逃したくない人々はリストにまとめられます
|
||||
■ 見たくない単語やフレーズをフィルターに指定すれば、表示しません
|
||||
|
||||
AND MORE!
|
||||
他にも!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ あなたの色、ライト/ダークに合わせた美しいテーマ
|
||||
■ QR コードの共有とスキャンで、Mastodonプロファイルを素早く交換
|
||||
■ 複数のアカウントにログイン、切り替え
|
||||
■ ベルボタンで、特定の人の投稿を通知
|
||||
■ ネタバレなし! コンテンツ閲覧警告で隠して投稿
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
強力な表現プラットフォームとして
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
ここでは、友達の投稿をあなたから非表示にする不透明なアルゴリズムの心配はありません。 フォローすれば、表示されます。
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
オープンウェブに公開で投稿するだけで、投稿にオープンウェブからアクセスできます。 Mastodon へのリンクを共有すれば、ログインなしで誰でも読めて問題なく知識を共有できます。
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
スレッド、投票、高画質の画像、動画、音声、コンテンツの閲覧警告も含め、Mastodon はあなた自身を表現する最適な方法を提供しています。
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
強力な閲覧プラットフォームとして
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
広告を表示しなくてよいので、公式アプリ以外の選択も尊重します。Mastodon にはサードパーティのアプリや外部統合の豊富な選択肢があり、最適な体験を選択できます。
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
時系列ホームフィードなら、新しい投稿をすべて読み終わったか簡単にわかります。
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
ミスクリックのせいで、興味がないおすすめを永遠に表示される心配はありません。 あなたが見たいものを私たちが推測したりせず、あなた自身の制御に委ねます。
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
プラットフォームではなくプロトコル
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon は従来のソーシャルメディアプラットフォームとは違い、非中央集権プロトコル上に構築されています。 私たちの公式サーバーに登録するか、サードパーティーのサーバーにデータをホストしてモデレーションに従うことも選択できます。
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
あなたがどのサーバーを選択しても、共通プロトコルにより他の Mastodon サーバーの人々とシームレスに通信できます。 それだけでなく、他の Fediverse プラットフォームの人々とも交流できます。
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
選択に満足できなくても大丈夫です! いつでもフォロワーを引き継いだまま別のサーバーに引っ越せます。 Mastodon はオープンソースのため、上級ユーザーであれば自前でデータをホスティングできます。
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
生まれながらに非営利
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon はアメリカとドイツで非営利団体として登録されています。 私たちはプラットフォームから金銭的な価値を生み出すことではない、プラットフォームにとってよりよい目的を追求しています。
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
TIME、Forbes、Wired、The Guardian、CNN、The Verge、TechCrunch、Financial Times、Gizmodo、PCMAG.com などで紹介されました。
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
話題が生まれる場所
|
||||
@@ -1,4 +1,4 @@
|
||||
„Mastodon“ – tai geriausias būdas sekti, kas vyksta. Sek bet kurį asmenį visoje fediverse ir žiūrėk viską chronologine tvarka. Jokių algoritmų, reklamų ar tyčinių paspaudimų.
|
||||
„Mastodon“ – tai geriausias būdas sekti, kas vyksta. Sek bet kurį asmenį visoje fediversijoje ir žiūrėk viską chronologine tvarka. Jokių algoritmų, reklamų ar tyčinių paspaudimų.
|
||||
|
||||
Tai – oficiali „Mastodon“, skirto „Android“ programėlė. Ji yra labai sparti ir nuostabiai graži, sukurta taip, kad būtų ne tik galinga, bet ir paprasta naudoti. Mūsų programėlėje galima:
|
||||
|
||||
@@ -42,7 +42,7 @@ GALINGA SKAITYMO PLATFORMA
|
||||
|
||||
Mums nereikia rodyti reklamų, todėl mums nereikia tave laikyti savo programėlėje. „Mastodon“ turi gausiausią trečiųjų šalių programėlių ir integracijų pasirinkimą, tad gali pasirinkti tau tinkamiausią patirtį.
|
||||
|
||||
Dėl chronologinio pagrindinio srauto lengva nustatyti, kada jau esi pasiekęs (-usi) visus naujinimus ir gali pereiti prie ko nors kito.
|
||||
Dėl chronologinio pagrindinio srauto lengva nustatyti, kada jau pasiekei visas naujienas ir gali pereiti prie ko nors kito.
|
||||
|
||||
Nereikia nerimauti, kad neteisingas spustelėjimas sugadins tavo rekomendacijas visiems laikams. Mes nenuspėjame, ką nori matyti, o leidžiame tau tai valdyti.
|
||||
|
||||
@@ -50,9 +50,9 @@ PROTOKOLAI, O NE PLATFORMOS
|
||||
|
||||
„Mastodon“ nėra panaši į tradicinę socialinės medijos platformą, bet sukurta pagal decentralizuotą protokolą. Gali užsiregistruoti mūsų oficialiame serveryje arba pasirinkti trečiąją šalį, kuri patalpins tavo duomenis ir palengvins patirtį.
|
||||
|
||||
Dėl bendro protokolo, nesvarbu, ką pasirinktum, gali sklandžiai bendrauti su žmonėmis, esančiais kituose „Mastodon“ serveriuose. Bet yra ir daugiau: naudojant tik viena paskyra gali bendrauti su žmonėmis iš kitų fediversų platformų.
|
||||
Dėl bendro protokolo, nesvarbu, ką pasirinktum, gali sklandžiai bendrauti su žmonėmis kituose „Mastodon“ serveriuose. Bet yra ir daugiau: su tik viena paskyra gali bendrauti su žmonėmis iš kitų fediversų platformų.
|
||||
|
||||
Nesi patenkintas (-a) savo pasirinkimu? Visada gali pereiti į kitą „Mastodon“ serverį ir kartu su savimi pasiimti sekėjus. Pažengę naudotojai gali net talpinti duomenis savo infrastruktūroje, nes „Mastodon“ yra atvirojo kodo.
|
||||
Nepatenkinti savo pasirinkimu? Visada gali pereiti į kitą „Mastodon“ serverį ir kartu su savimi pasiimti sekėjus. Patyrę naudotojai gali net talpinti duomenis savo infrastruktūroje, nes „Mastodon“ yra atvirojo kodo.
|
||||
|
||||
NE PELNO SIEKIANTIS POBŪDIS
|
||||
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon is de beste manier om op de hoogte te blijven van wat er gebeurt. Volg iedereen in de fediverse en zie alles in chronologische volgorde. Geen algoritmes, advertenties of clickbait te bekennen.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Dit is de officiële Android-app voor Mastodon. Het is razendsnel en verbluffend mooi. Ontworpen om niet alleen krachtig, maar ook gebruiksvriendelijk te zijn. In onze app kun je:
|
||||
|
||||
EXPLORE
|
||||
VERKENNEN
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Nieuwe schrijvers, journalisten, kunstenaars, fotografen, wetenschappers en meer ontdekken
|
||||
■ Zie wat er in de wereld gebeurt
|
||||
|
||||
READ
|
||||
LEZEN
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Op de hoogte blijven van mensen om wie je geeft, op een chronologische tijdlijn en zonder onderbrekingen
|
||||
■ Hashtags in realtime volgen om op de hoogte te blijven van specifieke onderwerpen
|
||||
|
||||
CREATE
|
||||
CREËREN
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Berichten naar alleen je volgers sturen of naar de hele wereld, met peilingen, afbeeldingen van hoge kwaliteit en video's
|
||||
■ Aan interessante gesprekken met andere mensen deelnemen
|
||||
|
||||
CURATE
|
||||
CUREREN
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Lijsten maken met mensen die je volgt, om zo nooit een bericht te missen
|
||||
■ Woorden of zinnen filteren om te bepalen wat je wel en niet wilt zien
|
||||
|
||||
AND MORE!
|
||||
EN MEER!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Een prachtig thema dat zich aanpast aan je persoonlijke kleurenschema, licht of donker
|
||||
■ QR-codes scannen om snel Mastodon-profielen met andere mensen te delen
|
||||
■ Meerdere accounts gebruiken
|
||||
■ Meldingen van specifiek personen ontvangen, wanneer deze berichten plaatsen
|
||||
■ Geen spoilers! ■ Je kunt je berichten achter inhoudswaarschuwingen plaatsen
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
EEN KRACHTIG PUBLICATIEPLATFORM
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Je hoeft niet langer te proberen een ondoorzichtig algoritme tevreden te stellen, dat beslist of je vrienden wel of niet mogen zien wat je hebt geplaatst. Als ze je volgen, zullen ze het zien.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Wanneer je het in de fediverse plaatst, is het ook toegankelijk in de fediverse. Je kunt veilig links naar Mastodon delen, in de wetenschap dat iedereen ze kan lezen zonder in te hoeven loggen.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Met gesprekken, peilingen, afbeeldingen van hoge kwaliteit, video's, audio en inhoudswaarschuwingen biedt Mastodon genoeg manieren om jezelf te kunnen uiten, op een manier die bij jou past.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
EEN KRACHTIG LEESPLATFORM
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
We hoeven je geen advertenties te laten zien, dus we hoeven je ook niet voor onze app te behouden. Mastodon heeft de rijkste keuze aan door derden ontwikkelde apps en integraties, zodat je de gebruikerservaring kunt kiezen die het beste bij je past.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Dankzij de chronologische tijdlijn is het makkelijk om te zien wanneer je alle updates hebt bekeken en met iets anders kunt verder gaan.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Je hoeft niet bang te zijn dat wanneer je op iets verkeerds klikt, je aanbevelingen voor altijd zijn verpest. We gissen niet naar wat je wilt zien, we laten het aan jou over.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PROTOCOLLEN, GEEN PLATFORMS
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon is niet zoals een traditioneel social media platform, maar is gebouwd op een gedecentraliseerd protocol. Je kunt voor onze officiële server of voor eentje van een derde partij kiezen om je gegevens te hosten en gebruikers te modereren.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Dankzij het gemeenschappelijke protocol kun je, ongeacht wat je kiest, naadloos met mensen op andere Mastodon-servers communiceren. Maar er is meer: Met slechts één account kun je met mensen van andere fediverse-platforms communiceren.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Niet tevreden met je keuze? Je kunt altijd naar een andere Mastodon-server overstappen en je volgers meenemen. Gevorderde gebruikers kunnen hun gegevens zelfs op hun eigen infrastructuur hosten, aangezien Mastodon open-source is.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
NON-PROFIT VAN AARD
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon is een geregistreerde non-profit organisatie in de Verenigde Staten en Duitsland. We halen onze motivatie niet uit het commercieel uitbuiten van Mastodon, maar uit wat het beste is voor Mastodon.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
VERMELD IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com en meer.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Waar gesprekken ontstaan
|
||||
61
fastlane/metadata/android/pt-BR/full_description.txt
Normal file
61
fastlane/metadata/android/pt-BR/full_description.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
O Mastodon é a melhor maneira de acompanhar o que está acontecendo. Siga qualquer pessoa durante o fediverso e veja tudo em ordem cronológica. Sem algoritmos, anúncios ou clickbait em vista.
|
||||
|
||||
Este é o aplicativo oficial do Mastodon para Android. Está a explodir rapidamente e incrivelmente bonito, projetado para ser não só poderoso mas também fácil de usar. Em nosso aplicativo, você pode:
|
||||
|
||||
EXPLORAR
|
||||
|
||||
Descobrir novos escritores, jornalistas, artistas, fotógrafos, cientistas e muito mais
|
||||
Veja o que está acontecendo no mundo 🌎
|
||||
|
||||
LEIA
|
||||
|
||||
Mostra com as pessoas que você se importa em um feed cronológico e sem interrupções
|
||||
Siga ‘hashtags’ para acompanhar tópicos específicos em tempo real
|
||||
|
||||
CRIE
|
||||
|
||||
Publique para os seus seguidores ou para o mundo inteiro: pesquisas, imagens e vídeos de alta qualidade
|
||||
Participe de conversas interessantes com outras pessoas
|
||||
|
||||
CURE
|
||||
|
||||
★ Crie uma lista de pessoas para nunca mais perder uma publicação
|
||||
+ Filtre palavras ou frases para controlar o que você faz e o que não quer ver
|
||||
|
||||
E MAIS!
|
||||
|
||||
★ Um belo tema que se adapta ao seu esquema personalizado de cores, claro ou escuro
|
||||
Compartilhe e digitalize os códigos QR para trocar rapidamente os perfis de Mastodon com outros
|
||||
Entre e alterne entre várias contas
|
||||
Seja notificado quando uma pessoa fizer uma publicação específica com o botão do sino 🔔
|
||||
Nenhum spoiler! Você pode colocar as suas publicações atrás de avisos de conteúdo
|
||||
|
||||
UMA PLATAFORMA DE PUBLICAÇÃO PODEROSA ✨
|
||||
|
||||
Você não precisa mais tentar apaziguar um algoritmo opaco que decide se seus amigos vão ver o que você postou. Se seguirem você, verão isso.
|
||||
|
||||
Se você publicá-lo na web aberta, ele é acessível na web aberta. Você pode compartilhar com segurança links para o Mastodon sabendo que qualquer pessoa será capaz de lê-los sem fazer o login.
|
||||
|
||||
Entre threads, pesquisas, imagens de alta qualidade, vídeos, avisos de áudio e conteúdo, Mastodon oferece muitas maneiras de se expressar de uma forma que melhor lhe convém.
|
||||
|
||||
UMA PLATAFORMA DE LEITURA PODEROSA ✨
|
||||
|
||||
Não precisamos lhe mostrar anúncios, então não precisamos te manter em nosso aplicativo. O Mastodon tem a seleção mais rica de apps de terceiros e integrações para que você possa escolher a melhor experiência para você.
|
||||
|
||||
Graças ao feed cronológico, é fácil dizer quando se encontra em todas as atualizações e pode ir para outra coisa.
|
||||
|
||||
Não há necessidade de se preocupar com que um clique errado irá arruinar as suas recomendações para sempre. Não adivinhamos o que você quer ver, deixamos que você o controle.
|
||||
|
||||
PROTOCOLOS, NÃO PLATAFORMAS
|
||||
|
||||
O Mastodon não é como uma plataforma de mídia social tradicional, mas é construído em um protocolo descentralizado. Você pode se inscrever em nosso servidor oficial ou escolher um terceiro para disponibilizar seus dados e moderar sua experiência.
|
||||
|
||||
Graças ao protocolo comum, não importa o que escolher, você pode se comunicar perfeitamente com as pessoas de outros servidores Mastodon. E têm mais: com apenas uma conta, você pode se comunicar com pessoas de outras plataformas fediversas.
|
||||
|
||||
Não está satisfeito com sua escolha? Você sempre pode mudar para um servidor Mastodon diferente enquanto leva seus seguidores com você. Para usuários avançados, você pode até mesmo hospedar seus dados em sua própria infraestrutura, já que o Mastodon é de código aberto.
|
||||
|
||||
SEM FINS LUCRATIVOS
|
||||
|
||||
Mastodon é uma instituição sem fins lucrativos registada nos EUA e na Alemanha. Nós não somos motivados extraindo o valor monetário da plataforma, mas pelo que é melhor para a plataforma.
|
||||
|
||||
DESTAQUE EM: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com e muito mais.
|
||||
1
fastlane/metadata/android/pt-BR/short_description.txt
Normal file
1
fastlane/metadata/android/pt-BR/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Onde as conversas acontecem
|
||||
@@ -1,4 +1,4 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
O Mastodon é a melhor maneira de manter com que está a acontecer. Segue qualquer um durante o fediverso e vê tudo em ordem cronológica. No algorithms, ads, or clickbait in sight.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon - лучший способ быть в курсе всего происходящего. Следуйте за любым человеком по всей федеральной вселенной и смотрите все в хронологическом порядке. Никаких алгоритмов, рекламы или кликбейта.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Это официальное приложение для Android от Mastodon. Он молниеносно быстрый и потрясающе красивый, разработанный, чтобы быть не только мощным, но и простым в использовании. В нашем приложении вы можете:
|
||||
|
||||
EXPLORE
|
||||
ИССЛЕДУЙТЕ
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Откройте для себя новых писателей, журналистов, художников, фотографов, ученых и многое другое
|
||||
■ Узнайте, что происходит в мире
|
||||
|
||||
READ
|
||||
ЧИТАТЬ
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Следите за людьми, которые вам интересны, в хронологической ленте без прерываний
|
||||
■ Следите за хэштегами, чтобы быть в курсе конкретных тем в режиме реального времени
|
||||
|
||||
CREATE
|
||||
СОЗДАТЬ
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Отправьте сообщение своим последователям или всему миру, используя опросы, высококачественные изображения и видео
|
||||
■ Участвуйте в интересных беседах с другими людьми
|
||||
|
||||
CURATE
|
||||
КУРАТОР
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Создавайте списки людей, чтобы не пропустить ни одного сообщения
|
||||
■ Фильтруйте слова и фразы, чтобы контролировать то, что вы хотите и не хотите видеть
|
||||
|
||||
AND MORE!
|
||||
И БОЛЬШЕ!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Красивая тема, которая адаптируется к вашей индивидуальной цветовой схеме, светлой или темной
|
||||
■ Обменивайтесь и сканируйте QR-коды, чтобы быстро обмениваться профилями Mastodon с другими людьми
|
||||
■ Вход в систему и переключение между несколькими учетными записями
|
||||
■ Получайте уведомления о сообщениях конкретного человека с помощью кнопки "звонок"
|
||||
■ Никаких спойлеров! Вы можете поместить свои сообщения за предупреждениями о содержании
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
МОЩНАЯ ИЗДАТЕЛЬСКАЯ ПЛАТФОРМА
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Вам больше не нужно пытаться угодить непрозрачному алгоритму, который решает, увидят ли ваши друзья то, что вы опубликовали. Если они будут следить за вами, то увидят это.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Если вы публикуете его в открытом интернете, он становится доступным в открытом интернете. Вы можете смело делиться ссылками на Mastodon, не сомневаясь, что любой пользователь сможет прочитать их, не заходя на сайт.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Благодаря темам, опросам, высококачественным изображениям, видео, аудио и предупреждениям о содержании, Mastodon предлагает множество способов выразить себя так, как вам удобно.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
МОЩНАЯ ПЛАТФОРМА ДЛЯ ЧТЕНИЯ
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Нам не нужно показывать вам рекламу, поэтому нам не нужно удерживать вас в нашем приложении. У Mastodon самый богатый выбор сторонних приложений и интеграций, поэтому вы можете выбрать то, что подходит вам больше всего.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Благодаря хронологической главной ленте легко определить, когда вы проследили за всеми обновлениями и можете переходить к чему-то другому.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Не нужно беспокоиться, что один неверный щелчок навсегда испортит ваши рекомендации. Мы не угадываем, что вы хотите увидеть, мы позволяем вам управлять этим.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
ПРОТОКОЛЫ, А НЕ ПЛАТФОРМЫ
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon не похож на традиционную платформу социальных сетей, он построен на децентрализованном протоколе. Вы можете зарегистрироваться на нашем официальном сервере или выбрать стороннюю компанию для размещения ваших данных и модерации вашего опыта.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Благодаря общему протоколу, независимо от того, что вы выберете, вы сможете легко общаться с людьми на других серверах Mastodon. Но это еще не все: С помощью одной учетной записи вы можете общаться с людьми с других платформ fediverse.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Не довольны своим выбором? Вы всегда можете перейти на другой сервер Mastodon, забрав с собой своих подписчиков. Опытные пользователи могут даже размещать свои данные на собственной инфраструктуре, поскольку Mastodon имеет открытый исходный код.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
НЕКОММЕРЧЕСКИЙ ХАРАКТЕР
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon является зарегистрированной некоммерческой организацией в США и Германии. Мы руководствуемся не стремлением извлечь из платформы денежную выгоду, а тем, что лучше для платформы.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
Опубликованы в: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com и других.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Где происходят беседы
|
||||
61
fastlane/metadata/android/sk/full_description.txt
Normal file
61
fastlane/metadata/android/sk/full_description.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Sledujte kohokoľvek naprieč fediversom a prezerajte všetko chronologicky. No algorithms, ads, or clickbait in sight.
|
||||
|
||||
Toto je oficiálna Mastodon aplikácia pre Android. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
|
||||
EXPLORE
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
|
||||
READ
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
|
||||
CREATE
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
|
||||
CURATE
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
|
||||
AND MORE!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
|
||||
Mastodon je nezisková organizácia registrovaná v Spojených Štátoch a Nemecku. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
1
fastlane/metadata/android/sk/short_description.txt
Normal file
1
fastlane/metadata/android/sk/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Where conversations happen
|
||||
@@ -2,7 +2,7 @@ Mastodon is the best way to keep up with what’s happening. Follow anyone acros
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
|
||||
EXPLORE
|
||||
UTFORSKA
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
@@ -12,7 +12,7 @@ READ
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
|
||||
CREATE
|
||||
SKAPA
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon เป็นวิธีที่ดีที่สุดที่จะติดตามสิ่งที่กำลังเกิดขึ้น ติดตามใครก็ตามทั่วทั้งจักรวาลสหพันธ์และดูจักรวาลสหพันธ์ทั้งหมดตามลำดับเวลา ไม่มีอัลกอริทึม, โฆษณา หรือคลิกเบตอยู่ในสายตา
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
นี่คือแอป Android อย่างเป็นทางการสำหรับ Mastodon แอปรวดเร็วมากและสวยงามอย่างน่าทึ่ง ได้รับการออกแบบให้ไม่ใช่แค่ทรงพลังแต่ยังใช้งานง่ายอีกด้วย ในแอปของเรา คุณสามารถ:
|
||||
|
||||
EXPLORE
|
||||
สำรวจ
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ ค้นพบนักเขียน, นักข่าว, ศิลปิน, ช่างภาพ, นักวิทยาศาสตร์ และอื่น ๆ ใหม่ ๆ
|
||||
■ ดูสิ่งที่กำลังเกิดขึ้นในโลก
|
||||
|
||||
READ
|
||||
อ่าน
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ ติดตามผู้คนที่คุณห่วงใยในฟีดตามลำดับเวลาโดยไม่มีการขัดจังหวะ
|
||||
■ ติดตามแฮชแท็กเพื่อติดตามหัวข้อที่เฉพาะเจาะจงตามเวลาจริง
|
||||
|
||||
CREATE
|
||||
สร้าง
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ โพสต์ไปยังผู้ติดตามของคุณหรือทั้งโลก พร้อมการสำรวจความคิดเห็น, ภาพคุณภาพสูง และวิดีโอ
|
||||
■ มีส่วนร่วมในการสนทนาที่น่าสนใจกับผู้คนอื่น ๆ
|
||||
|
||||
CURATE
|
||||
เรียบเรียง
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ สร้างรายการผู้คนเพื่อไม่พลาดโพสต์ใด
|
||||
■ กรองคำหรือวลีเพื่อควบคุมสิ่งที่คุณต้องการและไม่ต้องการเห็น
|
||||
|
||||
AND MORE!
|
||||
และอื่น ๆ!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ ชุดรูปแบบที่สวยงามที่ปรับให้เข้ากับแบบแผนชุดสีเฉพาะบุคคลของคุณ สว่างหรือมืด
|
||||
■ แชร์และสแกนรหัส QR เพื่อแลกเปลี่ยนโปรไฟล์ Mastodon กับผู้อื่นอย่างรวดเร็ว
|
||||
■ เข้าสู่ระบบและสลับระหว่างหลายบัญชี
|
||||
■ รับการแจ้งเตือนเมื่อบุคคลที่เฉพาะเจาะจงโพสต์ด้วยปุ่มกระดิ่ง
|
||||
■ ไม่มีผู้สปอยล์! คุณสามารถนำโพสต์ของคุณไว้หลังคำเตือนเนื้อหา
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
แพลตฟอร์มการเผยแพร่ที่ทรงพลัง
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
|
||||
@@ -38,7 +38,7 @@ If you publish it to the open web, it’s accessible on the open web. You can sa
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
แพลตฟอร์มการอ่านที่ทรงพลัง
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
|
||||
@@ -46,16 +46,16 @@ Thanks to the chronological home feed, it’s easy to tell when you’ve caught
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
โปรโตคอล ไม่ใช่แพลตฟอร์ม
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
ไม่พอใจกับตัวเลือกของคุณ? คุณสามารถสลับเป็นเซิร์ฟเวอร์ Mastodon อื่นได้เสมอพร้อมนำผู้ติดตามของคุณไปกับคุณ สำหรับผู้ใช้ขั้นสูง คุณยังสามารถโฮสต์ข้อมูลของคุณบนโครงสร้างพื้นฐานของคุณเองได้อีกด้วย เนื่องจาก Mastodon เป็นโอเพนซอร์ส
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
ไม่แสวงหาผลกำไรโดยธรรมชาติ
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนในสหรัฐอเมริกาและเยอรมนี We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
ตามที่นำเสนอใน: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com และอื่น ๆ
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
ที่ซึ่งการสนทนาเกิดขึ้น
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Olan biteni takip etmenin en iyi yolu. Herhangi birini federe ağında takip edin ve her şeyi kronolojik sırayla görün. Görünürde algoritma, reklam veya tıklama tuzağı yok.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Bu, aşağıdakiler için resmi uygulamasıdır. Sadece güçlü değil aynı zamanda kullanımı kolay olacak şekilde tasarlanmış, son derece hızlı ve şaşırtıcı derecede güzel. Uygulamamızda şunları yapabilirsiniz:
|
||||
|
||||
EXPLORE
|
||||
KEŞFET
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Yeni yazarlar, gazeteciler, sanatçılar, fotoğrafçılar, bilim insanları ve daha fazlasını keşfedin
|
||||
■ Dünyada neler olup bittiğini görün
|
||||
|
||||
READ
|
||||
OKU
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Önem verdiğiniz kişileri kesintisiz bir kronolojik akışta takip edin
|
||||
■ Belirli konuları gerçek zamanlı olarak takip etmek için etiketleri takip edin
|
||||
|
||||
CREATE
|
||||
|
||||
@@ -25,37 +25,37 @@ CURATE
|
||||
AND MORE!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Profillerini başkalarıyla hızlıca paylaşmak için QR kodlarını paylaşın ve tarayın
|
||||
■ Giriş yapın ve birden fazla hesap arasında geçiş yapın
|
||||
■ Zil butonu ile belirli bir kişi paylaşım yaptığında bildirim alın
|
||||
■ İpucu yok! Yayınlarınızı içerik uyarılarının arkasına koyabilirsiniz
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
GÜÇLÜ BIR YAYINCILIK PLATFORMU
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Artık arkadaşlarınızın paylaştıklarını görüp görmeyeceğine karar veren şeffaf olmayan bir algoritmayı yatıştırmaya çalışmak zorunda değilsiniz. Sizi takip ederlerse görürler.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Eğer bunu açık olarak yayınlarsanız, herkese açık olarak erişilebilir olur. Herkesin giriş yapmadan okuyabileceğinden emin olarak bağlantılarını güvenle paylaşabilirsiniz.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Başlıklar, anketler, yüksek kaliteli görüntüler, videolar, sesler ve içerik uyarıları arasında, kendinizi size uygun bir şekilde ifade etmenin birçok yolunu sunar.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
GÜÇLÜ BİR OKUMA PLATFORMU
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Size reklam göstermemize gerek yok, bu yüzden sizi uygulamamızda tutmamıza da gerek yok. Sosyal ağımız, size en uygun deneyimi seçebileceğiniz en zengin 3. parti uygulama ve entegrasyon seçeneklerine sahiptir.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Kronolojik ana sayfa akışı sayesinde, tüm güncellemeleri ne zaman yakaladığınızı ve başka bir şeye geçebileceğinizi anlamak kolaydır.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Yanlış bir tıklamanın önerilerinizi sonsuza dek mahvedeceğinden endişelenmenize gerek yok. Ne görmek istediğinizi tahmin etmiyoruz, sizin kontrol etmenize izin veriyoruz.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PLATFORMLAR DEĞİL, PROTOKOLLER
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Sosyal ağımız geleneksel bir sosyal medya platformu gibi değil, merkezi olmayan bir protokol üzerine inşa edilmiştir. Resmi sunucumuza kayıt olabilir veya verilerinizi barındırmak ve deneyiminizi denetlemek için 3. bir taraf seçebilirsiniz.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Ortak protokol sayesinde, neyi seçerseniz seçin, diğer sunucularındaki kişilerle sorunsuz bir şekilde iletişim kurabilirsiniz. Ama dahası da var: Sadece tek bir hesapla, diğer federe platformlarındaki kişilerle iletişim kurabilirsiniz.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Seçimden memnun değil misiniz? Takipçilerini yanınızda götürürken her zaman farklı bir sunucusuna geçebilirsiniz. İleri düzey kullanıcılar için, Açık kaynaklı olduğundan verilerinizi kendi altyapınızda bile barındırabilir.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
KÂR AMACI GÜTMEYEN NİTELİKTE
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastıdon,ABD ve Almanya'da kâr amacı gütmeyen kayıtlı bir kuruluştur. Platformdan parasal değer elde etmek için değil, platform için en iyi olanı yapmak için motive oluyoruz.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
YER ALDIĞI GİBİ: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, ve dahası.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Konuşmaların gerçekleştiği yer
|
||||
@@ -1,61 +1,61 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Mastodon найкращий спосіб бути в курсі подій. Слідкуйте за ким завгодно у всьому fediverse та дивіться все в хронологічному порядку. Немає алгоритмів, реклами чи наживки для натискань.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Це офіційний застосунок для Android для Mastodon. Він блискавично швидкий і приголомшливо красивий, розроблений, щоб бути не тільки потужним, але й простим у використанні. У нашому застосунку ви можете:
|
||||
|
||||
EXPLORE
|
||||
ДОСЛІДЖУВАТИ
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Відкрийте для себе нових письменників, журналістів, художників, фотографів, науковців та інших
|
||||
■ Подивіться, що відбувається у світі
|
||||
|
||||
READ
|
||||
ЧИТАТИ
|
||||
|
||||
■ Keep up with people you care about in a chronological feed with no interruptions
|
||||
■ Follow hashtags to keep up with specific topics in real time
|
||||
■ Будьте в курсі людей, які вам небайдужі, у хронологічній стрічці без переривання
|
||||
■ Слідкуйте за хеш-тегами, щоб бути в курсі певних тем у реальному часі
|
||||
|
||||
CREATE
|
||||
СТВОРЮВАТИ
|
||||
|
||||
■ Post to your followers or the whole world, with polls, high quality images and videos
|
||||
■ Participate in interesting conversations with other people
|
||||
■ Публікуйте дописи для своїх підписників або для всього світу з опитуваннями, високоякісними зображеннями та відео
|
||||
■ Беріть участь у цікавих бесідах з іншими людьми
|
||||
|
||||
CURATE
|
||||
КЕРУВАТИ
|
||||
|
||||
■ Create lists of people to never miss a post
|
||||
■ Filter words or phrases to control what you do and don’t want to see
|
||||
■ Створюйте списки людей, щоб ніколи не пропускати публікації
|
||||
■ Фільтруйте слова чи фрази, щоб контролювати, що ви робите, а що не хочете бачити
|
||||
|
||||
AND MORE!
|
||||
І БІЛЬШЕ!
|
||||
|
||||
■ A beautiful theme that adapts to your personalized color scheme, light or dark
|
||||
■ Share and scan QR codes to quickly exchange Mastodon profiles with others
|
||||
■ Login and switch between multiple accounts
|
||||
■ Get notified when a specific person posts with the bell button
|
||||
■ No spoilers! You can put your posts behind content warnings
|
||||
■ Красива тема, яка адаптується до вашої персоналізованої колірної схеми, світлої чи темної
|
||||
■ Діліться та скануйте QR-коди, щоб швидко обмінюватися профілями Mastodon з іншими
|
||||
■ Увійдіть і перемикайтеся між кількома обліковими записами
|
||||
■ Отримуйте сповіщення, коли певна особа публікує повідомлення за допомогою кнопки дзвінка
|
||||
■ Без спойлерів! Ви можете розміщувати свої публікації з попередженням про вміст
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
ПОТУЖНА ПЛАТФОРМА ПУБЛІКАЦІЙ
|
||||
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, they’ll see it.
|
||||
Вам більше не потрібно намагатися заспокоїти непрозорий алгоритм, який вирішує, чи побачать ваші друзі те, що ви опублікували. Якщо вони слідкують за вами, вони це побачать.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Якщо ви опублікуєте його у відкритому інтернеті, він стане доступним у відкритому інтернеті. Ви можете сміливо ділитися посиланнями на Mastodon, знаючи, що будь-хто зможе їх прочитати, не авторизуючись.
|
||||
|
||||
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
|
||||
Окрім тем, опитувань, високоякісних зображень, відео, аудіо та попереджень щодо вмісту, Mastodon пропонує безліч способів виразити себе у спосіб, який вам підходить.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
ПОТУЖНА ПЛАТФОРМА ДЛЯ ЧИТАННЯ
|
||||
|
||||
We don’t need to show you ads, so we don’t need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
|
||||
Нам не потрібно показувати вам рекламу, тому нам не потрібно тримати вас у нашому додатку. Mastodon має найбагатший вибір сторонніх додатків та інтеграцій, тож ви можете вибрати той досвід, який вам найбільше підходить.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Завдяки хронологічній домашній стрічці легко визначити, коли ви наздогнали всі оновлення та можете перейти до чогось іншого.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Не потрібно хвилюватися, що неправильне натискання назавжди зіпсує ваші рекомендації. Ми не вгадуємо, що ви хочете побачити, ми дозволяємо вам контролювати це.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
ПРОТОКОЛИ, А НЕ ПЛАТФОРМИ
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon не схожий на традиційну платформу соціальних мереж, він побудований на основі децентралізованого протоколу. Ви можете зареєструватися на нашому офіційному сервері або вибрати третю сторону для розміщення ваших даних і модерування вашого досвіду.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Завдяки загальному протоколу, незалежно від того, що ви виберете, ви можете безперешкодно спілкуватися з людьми на інших серверах Mastodon. Але є ще більше: Лише з одним обліковим записом ви можете спілкуватися з людьми з інших платформ fediverse.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Не задоволені своїм вибором? Ви завжди можете перейти на інший сервер Mastodon, взявши з собою підписників. Для досвідчених користувачів ви навіть можете розмістити свої дані у власній інфраструктурі, оскільки Mastodon є відкритим кодом.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
НЕПРИБУТКОВИЙ ХАРАКТЕР
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastodon є зареєстрованою неприбутковою організацією в США та Німеччині. Нас мотивує не отримання грошової цінності з платформи, а те, що найкраще для платформи.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
ПРЕДСТАВЛЕНО В: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com тощо.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Де відбуваються розмови
|
||||
61
fastlane/metadata/android/zh-CN/full_description.txt
Normal file
61
fastlane/metadata/android/zh-CN/full_description.txt
Normal file
@@ -0,0 +1,61 @@
|
||||
Mastodon 是了解最新动态的最佳途径。 横跨联邦宇宙关注其他人,并在一个时间顺序中查看。 没有算法、广告或诱导链接。
|
||||
|
||||
这是 Mastodon 官方的 Android 应用程序。 它风驰电掣般地快而且让你惊叹的美丽,拥有强大而易用的设计。 在我们的应用中,您可以:
|
||||
|
||||
探索新鲜事
|
||||
|
||||
■ 发现新的作家、记者、画家、摄影师和科学家以及更多
|
||||
■ 看看世界上正在发生什么
|
||||
|
||||
阅读
|
||||
|
||||
■ 在时间顺序流中跟上你关心的人,没有打断
|
||||
■ 关注标签以实时关注具体主题
|
||||
|
||||
创建
|
||||
|
||||
■ 用投票、高质量图像和视频向粉丝或整个世界发嘟
|
||||
■ 与其他人一起参与有趣的对话
|
||||
|
||||
组织与整理
|
||||
|
||||
■ 创建用户列表,不错过任何帖子
|
||||
■ 利用单词与短语过滤功能来控制你想看到什么
|
||||
|
||||
还有更多!
|
||||
|
||||
■ 一个美丽的主题,符合您的个性化主题色,无论明亮或黑暗
|
||||
■ 分享并扫描二维码以便与其他人快速交换 Mastodon 个人资料
|
||||
■ 登录并在多个账户间切换
|
||||
■ 点铃铛按钮,获得特定的人发嘟时的通知
|
||||
■ 禁止剧透! 你可以将你的嘟文键入内容警告中
|
||||
|
||||
强大的发表平台
|
||||
|
||||
你不再需要尝试迎合不透明的算法,来决定你的朋友是否能看到你发布的内容。 如果朋友们关注你,他们就会看到嘟文。
|
||||
|
||||
如果你在一个公开的网络上发布嘟文,那嘟文就可以在那里访问。 你可以安全地分享到 Mastodon 的链接,知道任何人都可以在不登录的情况下阅读它们。
|
||||
|
||||
在讨论串、投票、高质量图像、视频、音频和内容警告之中,Mastodon 提供了大量适合你的表达方式。
|
||||
|
||||
强大的阅读平台
|
||||
|
||||
我们无需向您展示广告,所以我们不会挽留让您留在我们的应用中。 Mastodon 有最丰富的第三方应用和集成,您可以选择最适合您的体验。
|
||||
|
||||
感谢按时间顺序提供的首页流,你很容易就能知道自己什么时候看完了所有的更新,可以继续看别的东西了。
|
||||
|
||||
永远不用担心点一下喜欢就会污染你的推荐列表。 我们不猜你想看到什么,我们让你自己控制它。
|
||||
|
||||
成为一种协议,而非平台
|
||||
|
||||
Mastodon 不像传统的社交媒体平台,而是建立在一个去中心化的协议之上。 您可以在我们的官方服务器上注册,或者选择第三方托管您的数据并保持相似的体验。
|
||||
|
||||
感谢同样的协议,不管你选择哪个服务器,你都可以与其他 Mastodon 服务器上与人无缝跨服聊天。 并且:只要同一个帐户,您就可以与其他联邦宇宙的人通信。
|
||||
|
||||
对你的选择不满意吗? 您随时可以带着您的粉丝切换到另一个 Mastodon 服务器。 对于进阶用户,你甚至可以在您自己的基础设施上托管您的数据,因为 Mastodon 是开源的。
|
||||
|
||||
天生非盈利性
|
||||
|
||||
Mastodon 是在美国与德国注册的非营利机构。 我们的初衷不是从平台上提取金钱价值,而是为了打造最好的给平台。
|
||||
|
||||
目前入驻:《时代》、《福布斯》、《连线》、The Guardian、美国有线电视新闻网、The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com,还有更多。
|
||||
1
fastlane/metadata/android/zh-CN/short_description.txt
Normal file
1
fastlane/metadata/android/zh-CN/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
对话发生的场所
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode 112
|
||||
versionName "2.6.1"
|
||||
versionCode 123
|
||||
versionName "2.7.3"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.3.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.4.3'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
||||
4
mastodon/proguard-rules.pro
vendored
4
mastodon/proguard-rules.pro
vendored
@@ -35,6 +35,10 @@
|
||||
*;
|
||||
}
|
||||
|
||||
-keepnames public class org.joinmastodon.android.api.session.**{
|
||||
*;
|
||||
}
|
||||
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
@com.squareup.otto.Subscribe <methods>;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package org.joinmastodon.android.test;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
@@ -14,7 +12,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstanceV2;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -22,6 +20,7 @@ import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.InstanceV2;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.junit.Assert;
|
||||
@@ -32,12 +31,9 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.BrokenBarrierException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import androidx.test.core.app.ActivityScenario;
|
||||
import androidx.test.espresso.PerformException;
|
||||
import androidx.test.espresso.UiController;
|
||||
import androidx.test.espresso.ViewAction;
|
||||
@@ -47,19 +43,19 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.runner.screenshot.ScreenCapture;
|
||||
import androidx.test.runner.screenshot.Screenshot;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import okio.BufferedSink;
|
||||
import okio.Okio;
|
||||
import okio.Sink;
|
||||
import okio.Source;
|
||||
|
||||
import static androidx.test.espresso.Espresso.*;
|
||||
import static androidx.test.espresso.action.ViewActions.*;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.*;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.*;
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.typeText;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.Visibility;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
@@ -148,10 +144,10 @@ public class StoreScreenshotsGenerator{
|
||||
takeScreenshot("Thread");
|
||||
|
||||
Instance[] _instance={null};
|
||||
new GetInstance()
|
||||
new GetInstanceV2()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
public void onSuccess(InstanceV2 result){
|
||||
_instance[0]=result;
|
||||
try{
|
||||
barrier.await();
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="http" android:host="*"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https" android:host="*"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
@@ -79,7 +78,7 @@ public class OAuthActivity extends Activity{
|
||||
progress.dismiss();
|
||||
}
|
||||
})
|
||||
.exec(instance.uri, token);
|
||||
.exec(instance.getDomain(), token);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,7 +87,7 @@ public class OAuthActivity extends Activity{
|
||||
progress.dismiss();
|
||||
}
|
||||
})
|
||||
.execNoAuth(instance.uri);
|
||||
.execNoAuth(instance.getDomain());
|
||||
}
|
||||
|
||||
private void handleError(ErrorResponse error){
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -36,6 +37,8 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderCallback;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@@ -103,6 +106,24 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
}
|
||||
|
||||
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||
if(TextUtils.isEmpty(pn.icon)){
|
||||
doNotify(context, pn, accountID, notification, null);
|
||||
}else{
|
||||
ImageCache.getInstance(context).get(new UrlImageLoaderRequest(pn.icon, V.dp(50), V.dp(50)), null, new ImageLoaderCallback(){
|
||||
@Override
|
||||
public void onImageLoaded(ImageLoaderRequest req, Drawable image){
|
||||
doNotify(context, pn, accountID, notification, image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageLoadingFailed(ImageLoaderRequest req, Throwable error){
|
||||
doNotify(context, pn, accountID, notification, null);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void doNotify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification, Drawable avatar){
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
@@ -136,7 +157,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
.setPriority(Notification.PRIORITY_DEFAULT)
|
||||
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
|
||||
}
|
||||
Drawable avatar=ImageCache.getInstance(context).get(new UrlImageLoaderRequest(pn.icon, V.dp(50), V.dp(50)));
|
||||
Intent contentIntent=new Intent(context, MainActivity.class);
|
||||
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
contentIntent.putExtra("fromNotification", true);
|
||||
@@ -164,7 +184,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
builder.setSubText(accountName);
|
||||
}
|
||||
String notificationTag=accountID+"_"+(notification==null ? 0 : notification.id);
|
||||
if(notification!=null && (notification.type==org.joinmastodon.android.model.Notification.Type.MENTION)){
|
||||
if(notification!=null && (notification.type==NotificationType.MENTION)){
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!notification.status.account.id.equals(ownID))
|
||||
|
||||
@@ -14,28 +14,35 @@ import com.google.gson.reflect.TypeToken;
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationsV1;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationsV2;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.NotificationGroup;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -43,7 +50,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=3;
|
||||
private static final int DB_VERSION=5;
|
||||
public static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
@@ -51,7 +58,7 @@ public class CacheController{
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private final ArrayList<Callback<PaginatedResponse<List<NotificationViewModel>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private List<FollowList> lists;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
@@ -135,75 +142,187 @@ public class CacheController{
|
||||
});
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
|
||||
private List<NotificationViewModel> makeNotificationViewModels(List<NotificationGroup> notifications, Map<String, Account> accounts, Map<String, Status> statuses){
|
||||
return notifications.stream()
|
||||
.filter(ng->ng.type!=null)
|
||||
.map(ng->{
|
||||
NotificationViewModel nvm=new NotificationViewModel();
|
||||
nvm.notification=ng;
|
||||
nvm.accounts=ng.sampleAccountIds.stream().map(accounts::get).collect(Collectors.toList());
|
||||
if(nvm.accounts.size()!=ng.sampleAccountIds.size())
|
||||
return null;
|
||||
if(ng.statusId!=null){
|
||||
nvm.status=statuses.get(ng.statusId);
|
||||
if(nvm.status==null)
|
||||
return null;
|
||||
}
|
||||
return nvm;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<NotificationViewModel>>> callback){
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
if(!onlyMentions && loadingNotifications){
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
pendingNotificationsCallbacks.add(callback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
String suffix=onlyMentions ? "mentions" : "all";
|
||||
String table="notifications_"+suffix;
|
||||
String accountsTable="notifications_accounts_"+suffix;
|
||||
String statusesTable="notifications_statuses_"+suffix;
|
||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`max_id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Notification> result=new ArrayList<>();
|
||||
ArrayList<NotificationGroup> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
String newMaxID;
|
||||
HashSet<String> needAccounts=new HashSet<>(), needStatuses=new HashSet<>();
|
||||
do{
|
||||
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
|
||||
NotificationGroup ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), NotificationGroup.class);
|
||||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
newMaxID=ntf.pageMinId;
|
||||
needAccounts.addAll(ntf.sampleAccountIds);
|
||||
if(ntf.statusId!=null)
|
||||
needStatuses.add(ntf.statusId);
|
||||
result.add(ntf);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
|
||||
HashMap<String, Account> accounts=new HashMap<>();
|
||||
HashMap<String, Status> statuses=new HashMap<>();
|
||||
if(!needAccounts.isEmpty()){
|
||||
try(Cursor cursor2=db.query(accountsTable, new String[]{"json"}, "`id` IN ("+String.join(", ", Collections.nCopies(needAccounts.size(), "?"))+")",
|
||||
needAccounts.toArray(new String[0]), null, null, null)){
|
||||
while(cursor2.moveToNext()){
|
||||
Account acc=MastodonAPIController.gson.fromJson(cursor2.getString(0), Account.class);
|
||||
acc.postprocess();
|
||||
accounts.put(acc.id, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!needStatuses.isEmpty()){
|
||||
try(Cursor cursor2=db.query(statusesTable, new String[]{"json"}, "`id` IN ("+String.join(", ", Collections.nCopies(needStatuses.size(), "?"))+")",
|
||||
needStatuses.toArray(new String[0]), null, null, null)){
|
||||
while(cursor2.moveToNext()){
|
||||
Status s=MastodonAPIController.gson.fromJson(cursor2.getString(0), Status.class);
|
||||
s.postprocess();
|
||||
statuses.put(s.id, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(makeNotificationViewModels(result, accounts, statuses), _newMaxID)));
|
||||
return;
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||
}
|
||||
}
|
||||
|
||||
if(!onlyMentions && loadingNotifications){
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
pendingNotificationsCallbacks.add(callback);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!onlyMentions)
|
||||
loadingNotifications=true;
|
||||
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
callback.onSuccess(res);
|
||||
putNotifications(result, onlyMentions, maxID==null);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onSuccess(res);
|
||||
if(AccountSessionManager.get(accountID).getInstanceInfo().getApiVersion()>=2){
|
||||
new GetNotificationsV2(maxID, count, onlyMentions ? EnumSet.of(NotificationType.MENTION): EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(GetNotificationsV2.GroupedNotificationsResults result){
|
||||
Map<String, Account> accounts=result.accounts.stream().collect(Collectors.toMap(a->a.id, Function.identity(), (a1, a2)->a2));
|
||||
Map<String, Status> statuses=result.statuses.stream().collect(Collectors.toMap(s->s.id, Function.identity(), (s1, s2)->s2));
|
||||
List<NotificationViewModel> notifications=makeNotificationViewModels(result.notificationGroups, accounts, statuses);
|
||||
databaseThread.postRunnable(()->putNotifications(result.notificationGroups, result.accounts, result.statuses, onlyMentions, maxID==null), 0);
|
||||
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(notifications,
|
||||
result.notificationGroups.isEmpty() ? null : result.notificationGroups.get(result.notificationGroups.size()-1).pageMinId);
|
||||
callback.onSuccess(res);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onSuccess(res);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
callback.onError(error);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onError(error);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
callback.onError(error);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onError(error);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
})
|
||||
.exec(accountID);
|
||||
}else{
|
||||
new GetNotificationsV1(maxID, count, onlyMentions ? EnumSet.of(NotificationType.MENTION): EnumSet.allOf(NotificationType.class))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
ArrayList<Notification> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
|
||||
List<Status> statuses=filtered.stream().map(n->n.status).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
List<Account> accounts=filtered.stream().map(n->n.account).collect(Collectors.toList());
|
||||
List<NotificationViewModel> converted=filtered.stream()
|
||||
.map(n->{
|
||||
NotificationGroup group=new NotificationGroup();
|
||||
group.groupKey="converted-"+n.id;
|
||||
group.notificationsCount=1;
|
||||
group.type=n.type;
|
||||
group.mostRecentNotificationId=group.pageMaxId=group.pageMinId=n.id;
|
||||
group.latestPageNotificationAt=n.createdAt;
|
||||
group.sampleAccountIds=List.of(n.account.id);
|
||||
group.event=n.event;
|
||||
group.moderationWarning=n.moderationWarning;
|
||||
if(n.status!=null)
|
||||
group.statusId=n.status.id;
|
||||
NotificationViewModel nvm=new NotificationViewModel();
|
||||
nvm.notification=group;
|
||||
nvm.status=n.status;
|
||||
nvm.accounts=List.of(n.account);
|
||||
return nvm;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(converted, result.isEmpty() ? null : result.get(result.size()-1).id);
|
||||
callback.onSuccess(res);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onSuccess(res);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
databaseThread.postRunnable(()->putNotifications(converted.stream().map(nvm->nvm.notification).collect(Collectors.toList()), accounts, statuses, onlyMentions, maxID==null), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
callback.onError(error);
|
||||
if(!onlyMentions){
|
||||
loadingNotifications=false;
|
||||
synchronized(pendingNotificationsCallbacks){
|
||||
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
|
||||
cb.onError(error);
|
||||
}
|
||||
pendingNotificationsCallbacks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}catch(SQLiteException x){
|
||||
Log.w(TAG, x);
|
||||
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
|
||||
@@ -213,22 +332,40 @@ public class CacheController{
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean clear){
|
||||
private void putNotifications(List<NotificationGroup> notifications, List<Account> accounts, List<Status> statuses, boolean onlyMentions, boolean clear){
|
||||
runOnDbThread((db)->{
|
||||
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
if(clear)
|
||||
String suffix=onlyMentions ? "mentions" : "all";
|
||||
String table="notifications_"+suffix;
|
||||
String accountsTable="notifications_accounts_"+suffix;
|
||||
String statusesTable="notifications_statuses_"+suffix;
|
||||
if(clear){
|
||||
db.delete(table, null, null);
|
||||
db.delete(accountsTable, null, null);
|
||||
db.delete(statusesTable, null, null);
|
||||
}
|
||||
ContentValues values=new ContentValues(4);
|
||||
for(Notification n:notifications){
|
||||
for(NotificationGroup n:notifications){
|
||||
if(n.type==null){
|
||||
continue;
|
||||
}
|
||||
values.put("id", n.id);
|
||||
values.put("id", n.groupKey);
|
||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||
values.put("type", n.type.ordinal());
|
||||
values.put("time", n.createdAt.getEpochSecond());
|
||||
values.put("time", n.latestPageNotificationAt.getEpochSecond());
|
||||
values.put("max_id", n.pageMaxId);
|
||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
values.clear();
|
||||
for(Account acc:accounts){
|
||||
values.put("id", acc.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(acc));
|
||||
db.insertWithOnConflict(accountsTable, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
for(Status s:statuses){
|
||||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
db.insertWithOnConflict(statusesTable, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,7 +457,7 @@ public class CacheController{
|
||||
lists=result;
|
||||
if(callback!=null)
|
||||
callback.onSuccess(result);
|
||||
writeListsToFile();
|
||||
writeLists();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -332,26 +469,22 @@ public class CacheController{
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<FollowList> loadListsFromFile(){
|
||||
File file=getListsFile();
|
||||
if(!file.exists())
|
||||
return null;
|
||||
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "failed to read lists from cache file", x);
|
||||
return null;
|
||||
private List<FollowList> loadLists(){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("misc", new String[]{"value"}, "`key`=?", new String[]{"lists"}, null, null, null)){
|
||||
if(!cursor.moveToFirst())
|
||||
return null;
|
||||
return MastodonAPIController.gson.fromJson(cursor.getString(0), new TypeToken<List<FollowList>>(){}.getType());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeListsToFile(){
|
||||
databaseThread.postRunnable(()->{
|
||||
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||
MastodonAPIController.gson.toJson(lists, out);
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "failed to write lists to cache file", x);
|
||||
}
|
||||
}, 0);
|
||||
private void writeLists(){
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("key", "lists");
|
||||
values.put("value", MastodonAPIController.gson.toJson(lists));
|
||||
db.insertWithOnConflict("misc", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
});
|
||||
}
|
||||
|
||||
public void getLists(Callback<List<FollowList>> callback){
|
||||
@@ -361,7 +494,7 @@ public class CacheController{
|
||||
return;
|
||||
}
|
||||
databaseThread.postRunnable(()->{
|
||||
List<FollowList> lists=loadListsFromFile();
|
||||
List<FollowList> lists=loadLists();
|
||||
if(lists!=null){
|
||||
this.lists=lists;
|
||||
if(callback!=null)
|
||||
@@ -372,23 +505,19 @@ public class CacheController{
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public File getListsFile(){
|
||||
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||
}
|
||||
|
||||
public void addList(FollowList list){
|
||||
if(lists==null)
|
||||
return;
|
||||
lists.add(list);
|
||||
lists.sort(Comparator.comparing(l->l.title));
|
||||
writeListsToFile();
|
||||
writeLists();
|
||||
}
|
||||
|
||||
public void deleteList(String id){
|
||||
if(lists==null)
|
||||
return;
|
||||
lists.removeIf(l->l.id.equals(id));
|
||||
writeListsToFile();
|
||||
writeLists();
|
||||
}
|
||||
|
||||
public void updateList(FollowList list){
|
||||
@@ -398,7 +527,7 @@ public class CacheController{
|
||||
if(lists.get(i).id.equals(list.id)){
|
||||
lists.set(i, list);
|
||||
lists.sort(Comparator.comparing(l->l.title));
|
||||
writeListsToFile();
|
||||
writeLists();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -419,23 +548,10 @@ public class CacheController{
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_all` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_mentions` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
createNotificationsTables(db, "all");
|
||||
createNotificationsTables(db, "mentions");
|
||||
createRecentSearchesTable(db);
|
||||
createMiscTable(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -446,6 +562,15 @@ public class CacheController{
|
||||
if(oldVersion<3){
|
||||
addTimeColumns(db);
|
||||
}
|
||||
if(oldVersion<4){
|
||||
createMiscTable(db);
|
||||
}
|
||||
if(oldVersion<5){
|
||||
db.execSQL("DROP TABLE `notifications_all`");
|
||||
db.execSQL("DROP TABLE `notifications_mentions`");
|
||||
createNotificationsTables(db, "all");
|
||||
createNotificationsTables(db, "mentions");
|
||||
}
|
||||
}
|
||||
|
||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||
@@ -465,5 +590,36 @@ public class CacheController{
|
||||
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
|
||||
private void createMiscTable(SQLiteDatabase db){
|
||||
db.execSQL("""
|
||||
CREATE TABLE `misc` (
|
||||
`key` TEXT NOT NULL PRIMARY KEY,
|
||||
`value` TEXT
|
||||
)""");
|
||||
}
|
||||
|
||||
private void createNotificationsTables(SQLiteDatabase db, String suffix){
|
||||
db.execSQL("CREATE TABLE `notifications_"+suffix+"` ("+
|
||||
"""
|
||||
`id` VARCHAR(100) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL,
|
||||
`time` INTEGER NOT NULL,
|
||||
`max_id` VARCHAR(25) NOT NULL
|
||||
)""");
|
||||
db.execSQL("CREATE INDEX `notifications_"+suffix+"_max_id` ON `notifications_"+suffix+"`(`max_id`)");
|
||||
db.execSQL("CREATE TABLE `notifications_accounts_"+suffix+"` ("+
|
||||
"""
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL
|
||||
)""");
|
||||
db.execSQL("CREATE TABLE `notifications_statuses_"+suffix+"` ("+
|
||||
"""
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL
|
||||
)""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class JsonObjectRequestBody extends RequestBody{
|
||||
|
||||
@Override
|
||||
public MediaType contentType(){
|
||||
return MediaType.get("application/json;charset=utf-8");
|
||||
return MediaType.get("application/json");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,7 +45,6 @@ import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyAgreement;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
@@ -82,6 +81,7 @@ public class PushSubscriptionManager{
|
||||
private static final String EXTRA_SENDER = "sender";
|
||||
private static final String EXTRA_SCOPE = "scope";
|
||||
private static final String KID_VALUE="|ID|1|"; // request ID?
|
||||
private static final long TOKEN_REFRESH_INTERVAL=30*24*60*60*1000L;
|
||||
|
||||
private static String deviceToken;
|
||||
private String accountID;
|
||||
@@ -93,14 +93,19 @@ public class PushSubscriptionManager{
|
||||
this.accountID=accountID;
|
||||
}
|
||||
|
||||
public static void resetLocalPreferences(){
|
||||
getPrefs().edit().clear().apply();
|
||||
}
|
||||
|
||||
public static void tryRegisterFCM(){
|
||||
deviceToken=getPrefs().getString("deviceToken", null);
|
||||
int tokenVersion=getPrefs().getInt("version", 0);
|
||||
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE){
|
||||
long tokenLastRefreshed=getPrefs().getLong("lastRefresh", 0);
|
||||
if(!TextUtils.isEmpty(deviceToken) && tokenVersion==BuildConfig.VERSION_CODE && System.currentTimeMillis()-tokenLastRefreshed<TOKEN_REFRESH_INTERVAL){
|
||||
registerAllAccountsForPush(false);
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "tryRegisterFCM: no token found or app was updated. Trying to get push token...");
|
||||
Log.i(TAG, "tryRegisterFCM: no token found, token due for refresh, or app was updated. Trying to get push token...");
|
||||
Intent intent = new Intent("com.google.iid.TOKEN_REQUEST");
|
||||
intent.setPackage(GSF_PACKAGE);
|
||||
intent.putExtra(EXTRA_APPLICATION_PENDING_INTENT,
|
||||
@@ -146,7 +151,7 @@ public class PushSubscriptionManager{
|
||||
session.pushPublicKey=Base64.encodeToString(publicKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
session.pushAuthKey=encodedAuthKey=Base64.encodeToString(authKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
session.pushAccountID=pushAccountID=Base64.encodeToString(randomAccountID, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
|
||||
}catch(NoSuchAlgorithmException|InvalidAlgorithmParameterException e){
|
||||
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
|
||||
return;
|
||||
@@ -165,7 +170,7 @@ public class PushSubscriptionManager{
|
||||
if(session==null)
|
||||
return;
|
||||
session.pushSubscription=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
|
||||
Log.d(TAG, "Successfully registered "+accountID+" for push notifications");
|
||||
});
|
||||
}
|
||||
@@ -191,7 +196,7 @@ public class PushSubscriptionManager{
|
||||
result.policy=subscription.policy;
|
||||
session.pushSubscription=result;
|
||||
session.needUpdatePushSettings=false;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,7 +209,7 @@ public class PushSubscriptionManager{
|
||||
return;
|
||||
session.needUpdatePushSettings=true;
|
||||
session.pushSubscription=subscription;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().writeAccountPushSettings(accountID);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -380,7 +385,11 @@ public class PushSubscriptionManager{
|
||||
deviceToken=intent.getStringExtra("registration_id");
|
||||
if(deviceToken.startsWith(KID_VALUE))
|
||||
deviceToken=deviceToken.substring(KID_VALUE.length()+1);
|
||||
getPrefs().edit().putString("deviceToken", deviceToken).putInt("version", BuildConfig.VERSION_CODE).apply();
|
||||
getPrefs().edit()
|
||||
.putString("deviceToken", deviceToken)
|
||||
.putInt("version", BuildConfig.VERSION_CODE)
|
||||
.putLong("lastRefresh", System.currentTimeMillis())
|
||||
.apply();
|
||||
Log.i(TAG, "Successfully registered for FCM");
|
||||
registerAllAccountsForPush(true);
|
||||
}else{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
|
||||
/**
|
||||
* Wraps a different API request to allow a chain of requests to be canceled
|
||||
*/
|
||||
public class WrapperRequest<T> extends APIRequest<T>{
|
||||
public APIRequest<?> wrappedRequest;
|
||||
|
||||
@Override
|
||||
public void cancel(){
|
||||
if(wrappedRequest!=null)
|
||||
wrappedRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIRequest<T> exec(){
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
|
||||
public class GetInstance extends MastodonAPIRequest<Instance>{
|
||||
public GetInstance(){
|
||||
super(HttpMethod.GET, "/instance", Instance.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.InstanceV1;
|
||||
|
||||
public class GetInstanceV1 extends MastodonAPIRequest<InstanceV1>{
|
||||
public GetInstanceV1(){
|
||||
super(HttpMethod.GET, "/instance", InstanceV1.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.InstanceV2;
|
||||
|
||||
public class GetInstanceV2 extends MastodonAPIRequest<InstanceV2>{
|
||||
public GetInstanceV2(){
|
||||
super(HttpMethod.GET, "/instance", InstanceV2.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
||||
@@ -7,26 +7,27 @@ import com.google.gson.reflect.TypeToken;
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
|
||||
public class GetNotificationsV1 extends MastodonAPIRequest<List<Notification>>{
|
||||
public GetNotificationsV1(String maxID, int limit, EnumSet<NotificationType> includeTypes){
|
||||
this(maxID, limit, includeTypes, null);
|
||||
}
|
||||
|
||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
|
||||
public GetNotificationsV1(String maxID, int limit, EnumSet<NotificationType> includeTypes, String onlyAccountID){
|
||||
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(includeTypes!=null){
|
||||
for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){
|
||||
for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
|
||||
addQueryParameter("types[]", type);
|
||||
}
|
||||
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), Notification.Type.class)){
|
||||
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
|
||||
addQueryParameter("exclude_types[]", type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
import org.joinmastodon.android.model.NotificationGroup;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class GetNotificationsV2 extends MastodonAPIRequest<GetNotificationsV2.GroupedNotificationsResults>{
|
||||
public GetNotificationsV2(String maxID, int limit, EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
|
||||
this(maxID, limit, includeTypes, groupedTypes, null);
|
||||
}
|
||||
|
||||
public GetNotificationsV2(String maxID, int limit, EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes, String onlyAccountID){
|
||||
super(HttpMethod.GET, "/notifications", GroupedNotificationsResults.class);
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(includeTypes!=null){
|
||||
for(String type:ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
|
||||
addQueryParameter("types[]", type);
|
||||
}
|
||||
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
|
||||
addQueryParameter("exclude_types[]", type);
|
||||
}
|
||||
}
|
||||
if(groupedTypes!=null){
|
||||
for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){
|
||||
addQueryParameter("grouped_types[]", type);
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(onlyAccountID))
|
||||
addQueryParameter("account_id", onlyAccountID);
|
||||
removeUnsupportedItems=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public static class GroupedNotificationsResults extends BaseModel{
|
||||
public List<Account> accounts;
|
||||
public List<Status> statuses;
|
||||
public List<NotificationGroup> notificationGroups;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
for(Account acc:accounts)
|
||||
acc.postprocess();
|
||||
for(Status s:statuses)
|
||||
s.postprocess();
|
||||
for(NotificationGroup ng:notificationGroups)
|
||||
ng.postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.joinmastodon.android.api.requests.notifications;
|
||||
|
||||
import org.joinmastodon.android.api.ApiUtils;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class GetUnreadNotificationsCount extends MastodonAPIRequest<GetUnreadNotificationsCount.Response>{
|
||||
public GetUnreadNotificationsCount(EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
|
||||
super(HttpMethod.GET, "/notifications/unread_count", Response.class);
|
||||
if(includeTypes!=null){
|
||||
for(String type: ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
|
||||
addQueryParameter("types[]", type);
|
||||
}
|
||||
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
|
||||
addQueryParameter("exclude_types[]", type);
|
||||
}
|
||||
}
|
||||
if(groupedTypes!=null){
|
||||
for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){
|
||||
addQueryParameter("grouped_types[]", type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public int count;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import org.joinmastodon.android.model.Card;
|
||||
import java.util.List;
|
||||
|
||||
public class GetTrendingLinks extends MastodonAPIRequest<List<Card>>{
|
||||
public GetTrendingLinks(){
|
||||
public GetTrendingLinks(int limit){
|
||||
super(HttpMethod.GET, "/trends/links", new TypeToken<>(){});
|
||||
addQueryParameter("limit", String.valueOf(limit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class AccountActivationInfo{
|
||||
@SerializedName(value="email", alternate="a")
|
||||
public String email;
|
||||
@SerializedName(value="last_email_confirmation_resend", alternate="b")
|
||||
public long lastEmailConfirmationResend;
|
||||
|
||||
public AccountActivationInfo(String email, long lastEmailConfirmationResend){
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -13,6 +19,7 @@ import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
@@ -24,7 +31,7 @@ import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@@ -47,21 +54,40 @@ public class AccountSession{
|
||||
private static final String TAG="AccountSession";
|
||||
private static final int MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS=28;
|
||||
|
||||
public static final int FLAG_ACTIVATED=1;
|
||||
public static final int FLAG_NEED_UPDATE_PUSH_SETTINGS=1 << 1;
|
||||
|
||||
@SerializedName(value="token", alternate="a")
|
||||
public Token token;
|
||||
@SerializedName(value="self", alternate="b")
|
||||
public Account self;
|
||||
@SerializedName(value="domain", alternate="c")
|
||||
public String domain;
|
||||
@SerializedName(value="app", alternate="d")
|
||||
public Application app;
|
||||
@SerializedName(value="info_last_updated", alternate="e")
|
||||
public long infoLastUpdated;
|
||||
@SerializedName(value="activated", alternate="f")
|
||||
public boolean activated=true;
|
||||
@SerializedName(value="push_private_key", alternate="g")
|
||||
public String pushPrivateKey;
|
||||
@SerializedName(value="push_public_key", alternate="h")
|
||||
public String pushPublicKey;
|
||||
@SerializedName(value="push_auth_key", alternate="i")
|
||||
public String pushAuthKey;
|
||||
@SerializedName(value="push_subscription", alternate="j")
|
||||
public PushSubscription pushSubscription;
|
||||
@SerializedName(value="need_update_push_settings", alternate="k")
|
||||
public boolean needUpdatePushSettings;
|
||||
@SerializedName(value="filters_last_updated", alternate="l")
|
||||
public long filtersLastUpdated;
|
||||
@SerializedName(value="word_filters", alternate="m")
|
||||
public List<LegacyFilter> wordFilters=new ArrayList<>();
|
||||
@SerializedName(value="push_account_i_d", alternate="n")
|
||||
public String pushAccountID;
|
||||
@SerializedName(value="activation_info", alternate="o")
|
||||
public AccountActivationInfo activationInfo;
|
||||
@SerializedName(value="preferences", alternate="p")
|
||||
public Preferences preferences;
|
||||
private transient MastodonAPIController apiController;
|
||||
private transient StatusInteractionController statusInteractionController;
|
||||
@@ -70,7 +96,6 @@ public class AccountSession{
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
private transient List<FollowList> lists;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
@@ -84,6 +109,64 @@ public class AccountSession{
|
||||
|
||||
AccountSession(){}
|
||||
|
||||
AccountSession(ContentValues values){
|
||||
domain=values.getAsString("domain");
|
||||
self=MastodonAPIController.gson.fromJson(values.getAsString("account_obj"), Account.class);
|
||||
token=MastodonAPIController.gson.fromJson(values.getAsString("token"), Token.class);
|
||||
app=MastodonAPIController.gson.fromJson(values.getAsString("application"), Application.class);
|
||||
infoLastUpdated=values.getAsLong("info_last_updated");
|
||||
long flags=values.getAsLong("flags");
|
||||
activated=(flags & FLAG_ACTIVATED)==FLAG_ACTIVATED;
|
||||
needUpdatePushSettings=(flags & FLAG_NEED_UPDATE_PUSH_SETTINGS)==FLAG_NEED_UPDATE_PUSH_SETTINGS;
|
||||
JsonObject pushKeys=JsonParser.parseString(values.getAsString("push_keys")).getAsJsonObject();
|
||||
if(!pushKeys.get("auth").isJsonNull() && !pushKeys.get("private").isJsonNull() && !pushKeys.get("public").isJsonNull()){
|
||||
pushAuthKey=pushKeys.get("auth").getAsString();
|
||||
pushPrivateKey=pushKeys.get("private").getAsString();
|
||||
pushPublicKey=pushKeys.get("public").getAsString();
|
||||
}
|
||||
pushSubscription=MastodonAPIController.gson.fromJson(values.getAsString("push_subscription"), PushSubscription.class);
|
||||
JsonObject legacyFilters=JsonParser.parseString(values.getAsString("legacy_filters")).getAsJsonObject();
|
||||
wordFilters=MastodonAPIController.gson.fromJson(legacyFilters.getAsJsonArray("filters"), new TypeToken<List<LegacyFilter>>(){}.getType());
|
||||
filtersLastUpdated=legacyFilters.get("updated").getAsLong();
|
||||
pushAccountID=values.getAsString("push_id");
|
||||
activationInfo=MastodonAPIController.gson.fromJson(values.getAsString("activation_info"), AccountActivationInfo.class);
|
||||
preferences=MastodonAPIController.gson.fromJson(values.getAsString("preferences"), Preferences.class);
|
||||
}
|
||||
|
||||
public void toContentValues(ContentValues values){
|
||||
values.put("id", getID());
|
||||
values.put("domain", domain.toLowerCase());
|
||||
values.put("account_obj", MastodonAPIController.gson.toJson(self));
|
||||
values.put("token", MastodonAPIController.gson.toJson(token));
|
||||
values.put("application", MastodonAPIController.gson.toJson(app));
|
||||
values.put("info_last_updated", infoLastUpdated);
|
||||
values.put("flags", getFlagsForDatabase());
|
||||
values.put("push_keys", new JsonObjectBuilder()
|
||||
.add("auth", pushAuthKey)
|
||||
.add("private", pushPrivateKey)
|
||||
.add("public", pushPublicKey)
|
||||
.build()
|
||||
.toString());
|
||||
values.put("push_subscription", MastodonAPIController.gson.toJson(pushSubscription));
|
||||
values.put("legacy_filters", new JsonObjectBuilder()
|
||||
.add("filters", MastodonAPIController.gson.toJsonTree(wordFilters))
|
||||
.add("updated", filtersLastUpdated)
|
||||
.build()
|
||||
.toString());
|
||||
values.put("push_id", pushAccountID);
|
||||
values.put("activation_info", MastodonAPIController.gson.toJson(activationInfo));
|
||||
values.put("preferences", MastodonAPIController.gson.toJson(preferences));
|
||||
}
|
||||
|
||||
public long getFlagsForDatabase(){
|
||||
long flags=0;
|
||||
if(activated)
|
||||
flags|=FLAG_ACTIVATED;
|
||||
if(needUpdatePushSettings)
|
||||
flags|=FLAG_NEED_UPDATE_PUSH_SETTINGS;
|
||||
return flags;
|
||||
}
|
||||
|
||||
public String getID(){
|
||||
return domain+"_"+self.id;
|
||||
}
|
||||
@@ -124,7 +207,7 @@ public class AccountSession{
|
||||
preferences=result;
|
||||
if(callback!=null)
|
||||
callback.accept(result);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().updateAccountPreferences(getID(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,7 +289,7 @@ public class AccountSession{
|
||||
public void onSuccess(Account result){
|
||||
preferencesNeedSaving=false;
|
||||
self=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().updateAccountInfo(getID(), self);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -286,4 +369,8 @@ public class AccountSession{
|
||||
public int getDonationSeed(){
|
||||
return Math.abs(getFullUsername().hashCode())%100;
|
||||
}
|
||||
|
||||
public Instance getInstanceInfo(){
|
||||
return AccountSessionManager.getInstance().getInstanceInfo(domain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
@@ -18,6 +19,12 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
@@ -26,11 +33,15 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.DatabaseRunnable;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.WrapperRequest;
|
||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstanceV2;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.events.EmojiUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -38,16 +49,17 @@ import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.InstanceV1;
|
||||
import org.joinmastodon.android.model.InstanceV2;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -61,6 +73,7 @@ import java.util.stream.Collectors;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
@@ -68,7 +81,7 @@ public class AccountSessionManager{
|
||||
private static final String TAG="AccountSessionManager";
|
||||
public static final String SCOPE="read write follow push";
|
||||
public static final String REDIRECT_URI="mastodon-android-auth://callback";
|
||||
private static final int DB_VERSION=1;
|
||||
private static final int DB_VERSION=3;
|
||||
|
||||
private static final AccountSessionManager instance=new AccountSessionManager();
|
||||
|
||||
@@ -84,6 +97,7 @@ public class AccountSessionManager{
|
||||
private boolean loadedInstances;
|
||||
private DatabaseHelper db;
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private final Object databaseLock=new Object();
|
||||
|
||||
public static AccountSessionManager getInstance(){
|
||||
return instance;
|
||||
@@ -91,53 +105,41 @@ public class AccountSessionManager{
|
||||
|
||||
private AccountSessionManager(){
|
||||
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
if(!file.exists())
|
||||
return;
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
try(FileInputStream in=new FileInputStream(file)){
|
||||
SessionsStorageWrapper w=MastodonAPIController.gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), SessionsStorageWrapper.class);
|
||||
for(AccountSession session:w.accounts){
|
||||
domains.add(session.domain.toLowerCase());
|
||||
sessions.put(session.getID(), session);
|
||||
runWithDatabase(db->{
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
try(Cursor cursor=db.query("accounts", null, null, null, null, null, null)){
|
||||
ContentValues values=new ContentValues();
|
||||
while(cursor.moveToNext()){
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||
AccountSession session=new AccountSession(values);
|
||||
domains.add(session.domain.toLowerCase());
|
||||
sessions.put(session.getID(), session);
|
||||
}
|
||||
}
|
||||
}catch(Exception x){
|
||||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
readInstanceInfo(db, domains);
|
||||
});
|
||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||
readInstanceInfo(domains);
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
|
||||
instances.put(instance.uri, instance);
|
||||
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
|
||||
instances.put(instance.getDomain(), instance);
|
||||
AccountSession session=new AccountSession(token, self, app, instance.getDomain(), activationInfo==null, activationInfo);
|
||||
sessions.put(session.getID(), session);
|
||||
lastActiveAccountID=session.getID();
|
||||
writeAccountsFile();
|
||||
updateInstanceEmojis(instance, instance.uri);
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
session.toContentValues(values);
|
||||
db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
});
|
||||
updateInstanceEmojis(instance, instance.getDomain());
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
public synchronized void writeAccountsFile(){
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
try{
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
SessionsStorageWrapper w=new SessionsStorageWrapper();
|
||||
w.accounts=new ArrayList<>(sessions.values());
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(w, writer);
|
||||
writer.flush();
|
||||
}
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing accounts file", x);
|
||||
}
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<AccountSession> getLoggedInAccounts(){
|
||||
return new ArrayList<>(sessions.values());
|
||||
@@ -167,7 +169,7 @@ public class AccountSessionManager{
|
||||
if(!sessions.containsKey(lastActiveAccountID)){
|
||||
// TODO figure out why this happens. It should not be possible.
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
writeAccountsFile();
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
return getAccount(lastActiveAccountID);
|
||||
}
|
||||
@@ -186,7 +188,6 @@ public class AccountSessionManager{
|
||||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
session.getCacheController().getListsFile().delete();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
@@ -206,11 +207,10 @@ public class AccountSessionManager{
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){
|
||||
getInstanceInfoFile(domain).delete();
|
||||
}
|
||||
runOnDbThread(db->{
|
||||
db.delete("accounts", "`id`=?", new String[]{id});
|
||||
db.delete("instances", "`domain` NOT IN (SELECT DISTINCT `domain` FROM `accounts`)", new String[]{});
|
||||
});
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
|
||||
nm.deleteNotificationChannelGroup(id);
|
||||
@@ -232,7 +232,7 @@ public class AccountSessionManager{
|
||||
authenticatingApp=result;
|
||||
Uri uri=new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(instance.uri)
|
||||
.authority(instance.getDomain())
|
||||
.path("/oauth/authorize")
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("client_id", result.clientId)
|
||||
@@ -253,7 +253,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.preparing_auth, false)
|
||||
.execNoAuth(instance.uri);
|
||||
.execNoAuth(instance.getDomain());
|
||||
}
|
||||
|
||||
public boolean isSelf(String id, Account other){
|
||||
@@ -302,7 +302,12 @@ public class AccountSessionManager{
|
||||
public void onSuccess(Account result){
|
||||
session.self=result;
|
||||
session.infoLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("account_obj", MastodonAPIController.gson.toJson(result));
|
||||
values.put("info_last_updated", session.infoLastUpdated);
|
||||
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -320,7 +325,15 @@ public class AccountSessionManager{
|
||||
public void onSuccess(List<LegacyFilter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("legacy_filters", new JsonObjectBuilder()
|
||||
.add("filters", MastodonAPIController.gson.toJsonTree(session.wordFilters))
|
||||
.add("updated", session.filtersLastUpdated)
|
||||
.build()
|
||||
.toString());
|
||||
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -332,8 +345,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public void updateInstanceInfo(String domain){
|
||||
new GetInstance()
|
||||
.setCallback(new Callback<>(){
|
||||
loadInstanceInfo(domain, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance instance){
|
||||
instances.put(domain, instance);
|
||||
@@ -344,8 +356,7 @@ public class AccountSessionManager{
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateInstanceEmojis(Instance instance, String domain){
|
||||
@@ -353,13 +364,10 @@ public class AccountSessionManager{
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Emoji> result){
|
||||
InstanceInfoStorageWrapper emojis=new InstanceInfoStorageWrapper();
|
||||
emojis.lastUpdated=System.currentTimeMillis();
|
||||
emojis.emojis=result;
|
||||
emojis.instance=instance;
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(emojis, domain));
|
||||
long lastUpdated=System.currentTimeMillis();
|
||||
customEmojis.put(domain, groupCustomEmojis(result));
|
||||
instancesLastUpdated.put(domain, lastUpdated);
|
||||
runOnDbThread(db->insertInstanceIntoDatabase(db, domain, instance, result, lastUpdated));
|
||||
E.post(new EmojiUpdatedEvent(domain));
|
||||
}
|
||||
|
||||
@@ -371,30 +379,22 @@ public class AccountSessionManager{
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private File getInstanceInfoFile(String domain){
|
||||
return new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
|
||||
}
|
||||
|
||||
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
|
||||
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(emojis, writer);
|
||||
writer.flush();
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "Error writing instance info file for "+domain, x);
|
||||
}
|
||||
}
|
||||
|
||||
private void readInstanceInfo(Set<String> domains){
|
||||
for(String domain:domains){
|
||||
try(FileInputStream in=new FileInputStream(getInstanceInfoFile(domain))){
|
||||
InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8);
|
||||
InstanceInfoStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, InstanceInfoStorageWrapper.class);
|
||||
private void readInstanceInfo(SQLiteDatabase db, Set<String> domains){
|
||||
try(Cursor cursor=db.query("instances", null, "`domain` IN ("+String.join(", ", Collections.nCopies(domains.size(), "?"))+")", domains.toArray(new String[0]), null, null, null)){
|
||||
ContentValues values=new ContentValues();
|
||||
while(cursor.moveToNext()){
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||
String domain=values.getAsString("domain");
|
||||
int version=values.getAsInteger("version");
|
||||
Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), switch(version){
|
||||
case 1 -> InstanceV1.class;
|
||||
case 2 -> InstanceV2.class;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + version);
|
||||
});
|
||||
List<Emoji> emojis=MastodonAPIController.gson.fromJson(values.getAsString("emojis"), new TypeToken<List<Emoji>>(){}.getType());
|
||||
instances.put(domain, instance);
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
instances.put(domain, emojis.instance);
|
||||
instancesLastUpdated.put(domain, emojis.lastUpdated);
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "Error reading instance info file for "+domain, x);
|
||||
instancesLastUpdated.put(domain, values.getAsLong("last_updated"));
|
||||
}
|
||||
}
|
||||
if(!loadedInstances){
|
||||
@@ -403,8 +403,8 @@ public class AccountSessionManager{
|
||||
}
|
||||
}
|
||||
|
||||
private List<EmojiCategory> groupCustomEmojis(InstanceInfoStorageWrapper emojis){
|
||||
return emojis.emojis.stream()
|
||||
private List<EmojiCategory> groupCustomEmojis(List<Emoji> emojis){
|
||||
return emojis.stream()
|
||||
.filter(e->e.visibleInPicker)
|
||||
.collect(Collectors.groupingBy(e->e.category==null ? "" : e.category))
|
||||
.entrySet()
|
||||
@@ -427,7 +427,49 @@ public class AccountSessionManager{
|
||||
AccountSession session=getAccount(id);
|
||||
session.self=account;
|
||||
session.infoLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("account_obj", MastodonAPIController.gson.toJson(account));
|
||||
values.put("info_last_updated", session.infoLastUpdated);
|
||||
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
|
||||
});
|
||||
}
|
||||
|
||||
public void updateAccountPreferences(String id, Preferences prefs){
|
||||
AccountSession session=getAccount(id);
|
||||
session.preferences=prefs;
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("preferences", MastodonAPIController.gson.toJson(prefs));
|
||||
db.update("accounts", values, "`id`=?", new String[]{session.getID()});
|
||||
});
|
||||
}
|
||||
|
||||
public void writeAccountPushSettings(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
runWithDatabase(db->{ // Called from a background thread anyway
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("push_keys", new JsonObjectBuilder()
|
||||
.add("auth", session.pushAuthKey)
|
||||
.add("private", session.pushPrivateKey)
|
||||
.add("public", session.pushPublicKey)
|
||||
.build()
|
||||
.toString());
|
||||
values.put("push_subscription", MastodonAPIController.gson.toJson(session.pushSubscription));
|
||||
values.put("flags", session.getFlagsForDatabase());
|
||||
values.put("push_id", session.pushAccountID);
|
||||
db.update("accounts", values, "`id`=?", new String[]{id});
|
||||
});
|
||||
}
|
||||
|
||||
public void writeAccountActivationInfo(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
runOnDbThread(db->{
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("activation_info", MastodonAPIController.gson.toJson(session.activationInfo));
|
||||
values.put("flags", session.getFlagsForDatabase());
|
||||
db.update("accounts", values, "`id`=?", new String[]{id});
|
||||
});
|
||||
}
|
||||
|
||||
private void maybeUpdateShortcuts(){
|
||||
@@ -487,8 +529,24 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
private void runOnDbThread(DatabaseRunnable r){
|
||||
cancelDelayedClose();
|
||||
CacheController.databaseThread.postRunnable(()->{
|
||||
synchronized(databaseLock){
|
||||
cancelDelayedClose();
|
||||
try{
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
r.run(db);
|
||||
}catch(SQLiteException|IOException x){
|
||||
Log.w(TAG, x);
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void runWithDatabase(DatabaseRunnable r){
|
||||
synchronized(databaseLock){
|
||||
cancelDelayedClose();
|
||||
try{
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
r.run(db);
|
||||
@@ -497,7 +555,7 @@ public class AccountSessionManager{
|
||||
}finally{
|
||||
closeDelayed();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void runIfDonationCampaignNotDismissed(String id, Runnable action){
|
||||
@@ -523,14 +581,53 @@ public class AccountSessionManager{
|
||||
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
|
||||
}
|
||||
|
||||
private static class SessionsStorageWrapper{
|
||||
public List<AccountSession> accounts;
|
||||
private static void insertInstanceIntoDatabase(SQLiteDatabase db, String domain, Instance instance, List<Emoji> emojis, long lastUpdated){
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("domain", domain);
|
||||
values.put("instance_obj", MastodonAPIController.gson.toJson(instance));
|
||||
values.put("emojis", MastodonAPIController.gson.toJson(emojis));
|
||||
values.put("last_updated", lastUpdated);
|
||||
values.put("version", instance.getVersion());
|
||||
db.insertWithOnConflict("instances", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
private static class InstanceInfoStorageWrapper{
|
||||
public Instance instance;
|
||||
public List<Emoji> emojis;
|
||||
public long lastUpdated;
|
||||
public static APIRequest<Instance> loadInstanceInfo(String domain, Callback<Instance> callback){
|
||||
final WrapperRequest<Instance> wrapper=new WrapperRequest<>();
|
||||
wrapper.wrappedRequest=new GetInstanceV2()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(InstanceV2 result){
|
||||
wrapper.wrappedRequest=null;
|
||||
callback.onSuccess(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(error instanceof MastodonErrorResponse mr && mr.httpStatus==404){
|
||||
// Mastodon pre-4.0 or a non-Mastodon server altogether. Let's try /api/v1/instance
|
||||
wrapper.wrappedRequest=new GetInstanceV1()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(InstanceV1 result){
|
||||
wrapper.wrappedRequest=null;
|
||||
callback.onSuccess(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
wrapper.wrappedRequest=null;
|
||||
callback.onError(error);
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
}else{
|
||||
wrapper.wrappedRequest=null;
|
||||
callback.onError(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private static class DatabaseHelper extends SQLiteOpenHelper{
|
||||
@@ -545,11 +642,86 @@ public class AccountSessionManager{
|
||||
`id` text PRIMARY KEY,
|
||||
`dismissed_at` bigint
|
||||
)""");
|
||||
createAccountsTable(db);
|
||||
db.execSQL("""
|
||||
CREATE TABLE `instances` (
|
||||
`domain` text PRIMARY KEY,
|
||||
`instance_obj` text,
|
||||
`emojis` text,
|
||||
`last_updated` bigint,
|
||||
`version` integer NOT NULL DEFAULT 1
|
||||
)""");
|
||||
maybeMigrateAccounts(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||
if(oldVersion<2){
|
||||
createAccountsTable(db);
|
||||
db.execSQL("""
|
||||
CREATE TABLE `instances` (
|
||||
`domain` text PRIMARY KEY,
|
||||
`instance_obj` text,
|
||||
`emojis` text,
|
||||
`last_updated` bigint
|
||||
)""");
|
||||
maybeMigrateAccounts(db);
|
||||
}
|
||||
if(oldVersion<3){
|
||||
db.execSQL("ALTER TABLE `instances` ADD `version` integer NOT NULL DEFAULT 1");
|
||||
}
|
||||
}
|
||||
|
||||
private void createAccountsTable(SQLiteDatabase db){
|
||||
db.execSQL("""
|
||||
CREATE TABLE `accounts` (
|
||||
`id` text PRIMARY KEY,
|
||||
`domain` text,
|
||||
`account_obj` text,
|
||||
`token` text,
|
||||
`application` text,
|
||||
`info_last_updated` bigint,
|
||||
`flags` bigint,
|
||||
`push_keys` text,
|
||||
`push_subscription` text,
|
||||
`legacy_filters` text DEFAULT NULL,
|
||||
`push_id` text,
|
||||
`activation_info` text,
|
||||
`preferences` text
|
||||
)""");
|
||||
}
|
||||
|
||||
private void maybeMigrateAccounts(SQLiteDatabase db){
|
||||
File accountsFile=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
if(accountsFile.exists()){
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
try(FileInputStream in=new FileInputStream(accountsFile)){
|
||||
JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
ContentValues values=new ContentValues();
|
||||
JsonArray accounts=jobj.has("a") ? jobj.getAsJsonArray("a") : jobj.getAsJsonArray("accounts");
|
||||
for(JsonElement jacc:accounts){
|
||||
AccountSession session=MastodonAPIController.gson.fromJson(jacc, AccountSession.class);
|
||||
domains.add(session.domain.toLowerCase());
|
||||
session.toContentValues(values);
|
||||
db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
}catch(Exception x){
|
||||
Log.e(TAG, "Error migrating accounts", x);
|
||||
return;
|
||||
}
|
||||
accountsFile.delete();
|
||||
for(String domain:domains){
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
|
||||
try(FileInputStream in=new FileInputStream(file)){
|
||||
JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
insertInstanceIntoDatabase(db, domain, MastodonAPIController.gson.fromJson(jobj.get(jobj.has("instance") ? "instance" : "a"), Instance.class),
|
||||
MastodonAPIController.gson.fromJson(jobj.get("emojis"), new TypeToken<>(){}.getType()), jobj.get("last_updated").getAsLong());
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "Error reading instance info file for "+domain, x);
|
||||
}
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,25 +13,22 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
|
||||
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
@@ -56,16 +53,16 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(!refreshing && endMark!=null)
|
||||
endMark.setVisibility(View.GONE);
|
||||
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
// currentRequest=new GetNotificationsV2(offset==0 ? null : maxID, count, EnumSet.allOf(NotificationType.class), account.id)
|
||||
// .setCallback(new SimpleCallback<>(this){
|
||||
// @Override
|
||||
// public void onSuccess(List<NotificationViewModel> result){
|
||||
// onDataLoaded(result, !result.isEmpty());
|
||||
// maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||
// endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
// }
|
||||
// })
|
||||
// .exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,8 +150,8 @@ public class AccountNotificationsListFragment extends BaseNotificationsListFragm
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
protected List<StatusDisplayItem> buildDisplayItems(NotificationViewModel n){
|
||||
if(n.notification.type==NotificationType.MENTION || n.notification.type==NotificationType.STATUS){
|
||||
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
}
|
||||
return super.buildDisplayItems(n);
|
||||
|
||||
@@ -5,60 +5,73 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
|
||||
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.NotificationWithButtonStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<NotificationViewModel>{
|
||||
protected String maxID;
|
||||
protected View endMark;
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
NotificationHeaderStatusDisplayItem titleItem;
|
||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||
protected List<StatusDisplayItem> buildDisplayItems(NotificationViewModel n){
|
||||
StatusDisplayItem titleItem;
|
||||
if(n.notification.type==NotificationType.MENTION){
|
||||
titleItem=null;
|
||||
}else if(n.notification.type==NotificationType.STATUS){
|
||||
if(n.status!=null)
|
||||
titleItem=new ReblogOrReplyLineStatusDisplayItem(n.getID(), this, getString(R.string.user_just_posted, n.status.account.displayName), n.status.account.emojis, R.drawable.ic_notifications_wght700fill1_20px);
|
||||
else
|
||||
titleItem=null;
|
||||
}else{
|
||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||
if(n.status!=null){
|
||||
n.status.card=null;
|
||||
n.status.spoilerText=null;
|
||||
}
|
||||
if(n.notification.type==NotificationType.SEVERED_RELATIONSHIPS || n.notification.type==NotificationType.MODERATION_WARNING)
|
||||
titleItem=new NotificationWithButtonStatusDisplayItem(n.getID(), this, n, accountID);
|
||||
else
|
||||
titleItem=new NotificationHeaderStatusDisplayItem(n.getID(), this, n, accountID);
|
||||
}
|
||||
if(n.status!=null){
|
||||
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
if(titleItem!=null && n.notification.type!=NotificationType.STATUS){
|
||||
InlineStatusStatusDisplayItem inlineItem=new InlineStatusStatusDisplayItem(n.getID(), this, n.status);
|
||||
inlineItem.removeTopPadding=true;
|
||||
return List.of(titleItem, inlineItem);
|
||||
}else{
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, 0);
|
||||
if(titleItem!=null)
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}
|
||||
}else if(titleItem!=null){
|
||||
return Collections.singletonList(titleItem);
|
||||
return List.of(titleItem);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccountToKnown(Notification s){
|
||||
if(!knownAccounts.containsKey(s.account.id))
|
||||
knownAccounts.put(s.account.id, s.account);
|
||||
protected void addAccountToKnown(NotificationViewModel s){
|
||||
for(Account a:s.accounts){
|
||||
if(!knownAccounts.containsKey(a.id))
|
||||
knownAccounts.put(a.id, a);
|
||||
}
|
||||
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
||||
knownAccounts.put(s.status.account.id, s.status.account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Notification n=getNotificationByID(id);
|
||||
NotificationViewModel n=getNotificationByID(id);
|
||||
if(n.status!=null){
|
||||
Status status=n.status;
|
||||
Bundle args=new Bundle();
|
||||
@@ -70,25 +83,25 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||
}else{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||
args.putParcelable("profileAccount", Parcels.wrap(n.accounts.get(0)));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
for(Notification n : data){
|
||||
if(n.id.equals(id))
|
||||
protected NotificationViewModel getNotificationByID(String id){
|
||||
for(NotificationViewModel n:data){
|
||||
if(n.getID().equals(id))
|
||||
return n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void removeNotification(Notification n){
|
||||
protected void removeNotification(NotificationViewModel n){
|
||||
data.remove(n);
|
||||
preloadedData.remove(n);
|
||||
int index=-1;
|
||||
for(int i=0; i<displayItems.size(); i++){
|
||||
if(n.id.equals(displayItems.get(i).parentID)){
|
||||
if(n.getID().equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
@@ -97,7 +110,7 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
||||
if(!displayItems.get(lastIndex).parentID.equals(n.getID()))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
@@ -111,10 +124,4 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
|
||||
endMark.setVisibility(View.GONE);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
|
||||
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
|
||||
@@ -42,14 +41,16 @@ import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
|
||||
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.MediaGridLayout;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -207,6 +208,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
list.setClipChildren(false);
|
||||
gridHolder.setClipChildren(false);
|
||||
transitioningHolder.view.setElevation(1f);
|
||||
int cornerMask=((MediaGridLayout.LayoutParams)holder.view.getLayoutParams()).tile.getRoundCornersMask();
|
||||
if((cornerMask & PhotoLayoutHelper.CORNER_TL)!=0)
|
||||
outCornerRadius[0]=V.dp(8);
|
||||
if((cornerMask & PhotoLayoutHelper.CORNER_TR)!=0)
|
||||
outCornerRadius[1]=V.dp(8);
|
||||
if((cornerMask & PhotoLayoutHelper.CORNER_BR)!=0)
|
||||
outCornerRadius[2]=V.dp(8);
|
||||
if((cornerMask & PhotoLayoutHelper.CORNER_BL)!=0)
|
||||
outCornerRadius[3]=V.dp(8);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -339,7 +349,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
tmpRect.offset(0, Math.round(child.getTranslationY()));
|
||||
float y=tmpRect.bottom-V.dp(.5f);
|
||||
float y=tmpRect.bottom;
|
||||
int strokeWidth=V.dp(0.5f);
|
||||
if(strokeWidth%2==1){
|
||||
y-=0.5f;
|
||||
}
|
||||
paint.setAlpha(Math.round(255*child.getAlpha()));
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
@@ -351,7 +365,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
public abstract void onItemClick(String id);
|
||||
|
||||
protected void updatePoll(String itemID, Status status, Poll poll){
|
||||
status.poll=poll;
|
||||
if(status.poll!=poll)
|
||||
status.poll=poll;
|
||||
int firstOptionIndex=-1, footerIndex=-1;
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
@@ -382,28 +397,68 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
||||
Poll poll=holder.getItem().poll;
|
||||
Poll.Option option=holder.getItem().option;
|
||||
if(poll.selectedOptions==null)
|
||||
poll.selectedOptions=new ArrayList<>();
|
||||
if(poll.multiple){
|
||||
if(poll.selectedOptions==null)
|
||||
poll.selectedOptions=new ArrayList<>();
|
||||
if(poll.selectedOptions.contains(option)){
|
||||
poll.selectedOptions.remove(option);
|
||||
holder.itemView.setSelected(false);
|
||||
}else{
|
||||
poll.selectedOptions.add(option);
|
||||
holder.itemView.setSelected(true);
|
||||
}
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
||||
if(footer.getItemID().equals(holder.getItemID())){
|
||||
footer.rebind();
|
||||
break;
|
||||
}else{
|
||||
if(poll.selectedOptions.contains(option))
|
||||
return;
|
||||
if(!poll.selectedOptions.isEmpty()){
|
||||
Poll.Option previouslySelected=poll.selectedOptions.get(0);
|
||||
poll.selectedOptions.clear();
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(vh instanceof PollOptionStatusDisplayItem.Holder otherOption){
|
||||
if(otherOption.getItemID().equals(holder.getItemID()) && otherOption.getItem().option==previouslySelected){
|
||||
otherOption.updateCheckedState();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||
poll.selectedOptions.add(option);
|
||||
}
|
||||
holder.updateCheckedState();
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
||||
if(footer.getItemID().equals(holder.getItemID())){
|
||||
footer.rebind();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onPollToggleResultsClick(PollFooterStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status.getContentStatus();
|
||||
status.poll.showResults=!status.poll.showResults;
|
||||
String itemID=holder.getItemID();
|
||||
if(status.poll.selectedOptions!=null)
|
||||
status.poll.selectedOptions.clear();
|
||||
int firstOptionIndex=-1, footerIndex=-1;
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.parentID.equals(itemID)){
|
||||
if(item instanceof PollOptionStatusDisplayItem optItem){
|
||||
if(firstOptionIndex==-1)
|
||||
firstOptionIndex=i;
|
||||
optItem.showResults=status.poll.showResults;
|
||||
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||
footerIndex=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if(firstOptionIndex==-1 || footerIndex==-1)
|
||||
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||
adapter.notifyItemRangeChanged(firstOptionIndex, status.poll.options.size());
|
||||
}
|
||||
|
||||
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
|
||||
@@ -432,23 +487,33 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
}
|
||||
SpoilerStatusDisplayItem spoilerItem=holder.getItem();
|
||||
if(status.revealedSpoilers.contains(spoilerItem.spoilerType))
|
||||
status.revealedSpoilers.remove(spoilerItem.spoilerType);
|
||||
else
|
||||
status.revealedSpoilers.add(spoilerItem.spoilerType);
|
||||
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null)
|
||||
spoiler.rebind();
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
holder.rebind();
|
||||
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
if(status.revealedSpoilers.contains(spoilerItem.spoilerType)){
|
||||
int itemCount=spoilerItem.contentItems.size();
|
||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
||||
if(spoilerItem.spoilerType==Status.SpoilerType.FILTER && spoilerItem.contentItems.get(0) instanceof SpoilerStatusDisplayItem nestedSpoiler
|
||||
&& nestedSpoiler.spoilerType==Status.SpoilerType.CONTENT_WARNING && !AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
status.revealedSpoilers.add(Status.SpoilerType.CONTENT_WARNING);
|
||||
displayItems.addAll(index+1+itemCount, nestedSpoiler.contentItems);
|
||||
itemCount+=nestedSpoiler.contentItems.size();
|
||||
}
|
||||
adapter.notifyItemRangeInserted(index+1, itemCount);
|
||||
}else{
|
||||
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||
int itemCount=spoilerItem.contentItems.size();
|
||||
if(spoilerItem.contentItems.get(0) instanceof SpoilerStatusDisplayItem nestedSpoiler && status.revealedSpoilers.contains(nestedSpoiler.spoilerType)){
|
||||
status.revealedSpoilers.remove(nestedSpoiler.spoilerType);
|
||||
itemCount+=nestedSpoiler.contentItems.size();
|
||||
}
|
||||
displayItems.subList(index+1, index+1+itemCount).clear();
|
||||
adapter.notifyItemRangeRemoved(index+1, itemCount);
|
||||
}
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
@@ -690,6 +755,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
anim.start();
|
||||
}
|
||||
|
||||
public void retryFailedImages(){
|
||||
imgLoader.retryFailedRequests();
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
|
||||
@@ -84,7 +84,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
for(Account acc:result)
|
||||
accountIDsInList.add(acc.id);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -21,6 +21,8 @@ import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationsV1;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetUnreadNotificationsCount;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
@@ -28,8 +30,9 @@ import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.NotificationType;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -38,6 +41,7 @@ import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
@@ -288,37 +292,55 @@ public class HomeFragment extends AppKitFragment{
|
||||
}
|
||||
|
||||
private void reloadNotificationsForUnreadCount(){
|
||||
List<Notification>[] notifications=new List[]{null};
|
||||
String[] marker={null};
|
||||
Instance instance=AccountSessionManager.get(accountID).getInstanceInfo();
|
||||
if(instance==null)
|
||||
return;
|
||||
if(instance.getApiVersion()>=2){
|
||||
new GetUnreadNotificationsCount(EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(GetUnreadNotificationsCount.Response result){
|
||||
updateUnreadNotificationsBadge(result.count, false);
|
||||
}
|
||||
|
||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
marker[0]=m;
|
||||
if(notifications[0]!=null){
|
||||
updateUnreadCount(notifications[0], marker[0]);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, true, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
notifications[0]=result.items;
|
||||
if(marker[0]!=null)
|
||||
updateUnreadCount(notifications[0], marker[0]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}else{
|
||||
List<Notification>[] notifications=new List[]{null};
|
||||
String[] marker={null};
|
||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
marker[0]=m;
|
||||
if(notifications[0]!=null){
|
||||
updateUnreadCountV1(notifications[0], marker[0]);
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
new GetNotificationsV1(null, 40, EnumSet.allOf(NotificationType.class))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
notifications[0]=result;
|
||||
if(marker[0]!=null)
|
||||
updateUnreadCountV1(notifications[0], marker[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
}).exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void updateUnreadCount(List<Notification> notifications, String marker){
|
||||
private void updateUnreadCountV1(List<Notification> notifications, String marker){
|
||||
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
updateUnreadNotificationsBadge(0, false);
|
||||
}else{
|
||||
notificationsBadge.setVisibility(View.VISIBLE);
|
||||
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
|
||||
notificationsBadge.setText(String.format("%d+", notifications.size()));
|
||||
updateUnreadNotificationsBadge(notifications.size(), true);
|
||||
}else{
|
||||
int count=0;
|
||||
for(Notification n:notifications){
|
||||
@@ -326,11 +348,20 @@ public class HomeFragment extends AppKitFragment{
|
||||
break;
|
||||
count++;
|
||||
}
|
||||
notificationsBadge.setText(String.format("%d", count));
|
||||
updateUnreadNotificationsBadge(count, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUnreadNotificationsBadge(int count, boolean more){
|
||||
if(count==0){
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
}else{
|
||||
notificationsBadge.setVisibility(View.VISIBLE);
|
||||
notificationsBadge.setText(String.format(more ? "%d+" : "%d", count));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -34,7 +35,6 @@ import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
@@ -61,6 +61,7 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
import org.joinmastodon.android.ui.views.NewPostsButtonContainer;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -89,7 +90,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
private FixedAspectRatioImageView listsDropdownArrow;
|
||||
private TextView listsDropdownText;
|
||||
private Button newPostsBtn;
|
||||
private View newPostsBtnWrap;
|
||||
private NewPostsButtonContainer newPostsBtnWrap;
|
||||
private boolean newPostsBtnShown;
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
private ToolbarDropdownMenuController dropdownController;
|
||||
@@ -249,6 +250,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -267,6 +269,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
newPostsBtnWrap.setAlpha(0f);
|
||||
newPostsBtnWrap.setTranslationY(V.dp(-56));
|
||||
}
|
||||
newPostsBtnWrap.setOnHideButtonListener(this::hideNewPostsButton);
|
||||
updateToolbarLogo();
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
@@ -602,6 +605,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
newPostsBtnWrap.setVisibility(View.GONE);
|
||||
newPostsBtn.setTranslationY(0);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
@@ -744,9 +748,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
}
|
||||
TextView text=donationBanner.findViewById(R.id.banner_text);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage);
|
||||
ssb.append(' ');
|
||||
if(!campaign.bannerMessage.endsWith("\n"))
|
||||
ssb.append(' ');
|
||||
int start=ssb.length();
|
||||
ssb.append(campaign.bannerButtonText);
|
||||
ssb.append(campaign.bannerButtonText.trim());
|
||||
ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0);
|
||||
ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0);
|
||||
|
||||
@@ -190,7 +190,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
@Subscribe
|
||||
public void onAccountAddedToList(AccountAddedToListEvent ev){
|
||||
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||
data.add(new AccountViewModel(ev.account, accountID));
|
||||
data.add(new AccountViewModel(ev.account, accountID, getActivity()));
|
||||
list.getAdapter().notifyItemInserted(data.size()-1);
|
||||
}
|
||||
}
|
||||
@@ -337,7 +337,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
|
||||
onDone.run();
|
||||
for(Account acc:accounts){
|
||||
accountIDsInList.add(acc.id);
|
||||
data.add(new AccountViewModel(acc, accountID));
|
||||
data.add(new AccountViewModel(acc, accountID, getActivity()));
|
||||
}
|
||||
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class NotificationRequestsFragment extends MastodonRecyclerFragment<Notif
|
||||
accountViewModels.clear();
|
||||
maxID=result.getNextPageMaxID();
|
||||
for(NotificationRequest req:result){
|
||||
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
|
||||
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false, getActivity()));
|
||||
}
|
||||
onDataLoaded(result, !TextUtils.isEmpty(maxID));
|
||||
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);
|
||||
|
||||
@@ -24,17 +24,16 @@ import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolic
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.GenericListItemsViewController;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
@@ -65,6 +64,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
|
||||
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
|
||||
private NotificationsPolicy lastPolicy;
|
||||
private boolean refreshAfterLoading;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -97,13 +97,17 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
public void onSuccess(PaginatedResponse<List<NotificationViewModel>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
reloadingFromCache=false;
|
||||
if(refreshAfterLoading){
|
||||
refreshAfterLoading=false;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -112,9 +116,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
if(canRefreshWithoutUpsettingUser()){
|
||||
if(dataLoading)
|
||||
refreshAfterLoading=true;
|
||||
else
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +165,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
||||
String itemID=holder.getItemID();
|
||||
String itemID=getNotificationByID(holder.getItemID()).notification.pageMaxId;
|
||||
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
c.drawRect(tmpRect, paint);
|
||||
@@ -181,12 +187,12 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
public void onPollUpdated(PollUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(Notification ntf:data){
|
||||
for(NotificationViewModel ntf:data){
|
||||
if(ntf.status==null)
|
||||
continue;
|
||||
Status contentStatus=ntf.status.getContentStatus();
|
||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
||||
updatePoll(ntf.id, ntf.status, ev.poll);
|
||||
updatePoll(ntf.getID(), ntf.status, ev.poll);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,10 +201,10 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||
return;
|
||||
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
|
||||
List<NotificationViewModel> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||
.filter(n->n.status!=null && n.status.account.id.equals(ev.postsByAccountID))
|
||||
.collect(Collectors.toList());
|
||||
for(Notification n:toRemove){
|
||||
for(NotificationViewModel n:toRemove){
|
||||
removeNotification(n);
|
||||
}
|
||||
}
|
||||
@@ -235,7 +241,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.mark_all_read){
|
||||
markAsRead();
|
||||
markAsRead(true);
|
||||
resetUnreadBackground();
|
||||
}else if(id==R.id.filters){
|
||||
showFiltersAlert();
|
||||
@@ -251,11 +257,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void markAsRead(){
|
||||
private void markAsRead(boolean force){
|
||||
if(data.isEmpty())
|
||||
return;
|
||||
String id=data.get(0).id;
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
String id=data.get(0).notification.pageMaxId;
|
||||
if(force || ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
@@ -277,13 +283,14 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendItems(List<Notification> items){
|
||||
public void onAppendItems(List<NotificationViewModel> items){
|
||||
super.onAppendItems(items);
|
||||
if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker))
|
||||
// TODO
|
||||
if(data.isEmpty() || data.get(0).getID().equals(realUnreadMarker))
|
||||
return;
|
||||
for(Notification n:items){
|
||||
if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0){
|
||||
markAsRead();
|
||||
for(NotificationViewModel n:items){
|
||||
if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){
|
||||
markAsRead(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -297,7 +304,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
|
||||
String id=itemHolder.getItemID();
|
||||
for(int j=0;j<data.size();j++){
|
||||
if(data.get(j).id.equals(id))
|
||||
if(data.get(j).getID().equals(id))
|
||||
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
@@ -72,7 +71,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.setDrawSelectorOnTop(true);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
|
||||
imgLoader=new ListImageLoaderWrapper(getActivity(), list, list, null);
|
||||
list.setAdapter(adapter=new AboutAdapter());
|
||||
list.setPadding(0, V.dp(16), 0, 0);
|
||||
list.setClipToPadding(false);
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
|
||||
ArrayList<StatusDisplayItem> items=switch(s.type){
|
||||
case ACCOUNT -> new ArrayList<>(Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)));
|
||||
case HASHTAG -> new ArrayList<>(Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)));
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, true);
|
||||
};
|
||||
|
||||
if(s.firstInSection){
|
||||
|
||||
@@ -580,7 +580,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
domain=AccountSessionManager.get(accountID).domain;
|
||||
usernameDomain.setText(domain);
|
||||
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
@@ -615,7 +615,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
fields.add(joined);
|
||||
|
||||
for(AccountField field:account.fields){
|
||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
|
||||
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
|
||||
ssb=new SpannableStringBuilder(field.name);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
|
||||
@@ -19,11 +19,13 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.InstanceV1;
|
||||
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
|
||||
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
@@ -172,15 +174,14 @@ public class SplashFragment extends AppKitFragment{
|
||||
}
|
||||
|
||||
private void proceedWithServerDomain(String domain){
|
||||
new GetInstance()
|
||||
.setCallback(new Callback<>(){
|
||||
AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
|
||||
if(!result.areRegistrationsOpen() && TextUtils.isEmpty(inviteCode)){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.instance_signup_closed)
|
||||
@@ -203,8 +204,7 @@ public class SplashFragment extends AppKitFragment{
|
||||
instanceLoadingProgress=null;
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
});
|
||||
}
|
||||
|
||||
private void onLearnMoreClick(View v){
|
||||
|
||||
@@ -2,17 +2,17 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
@@ -54,7 +54,10 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
|
||||
List<StatusDisplayItem> items=new ArrayList<>();
|
||||
InlineStatusStatusDisplayItem inlineItem=new InlineStatusStatusDisplayItem(s.getID(), this, s);
|
||||
inlineItem.fullWidth=true;
|
||||
items.add(inlineItem);
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
@@ -144,12 +147,6 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
|
||||
@@ -29,7 +29,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
protected EventListener eventListener=new EventListener();
|
||||
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true);
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
@@ -22,6 +24,7 @@ import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
@@ -31,7 +34,9 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
@@ -49,6 +54,7 @@ public class ThreadFragment extends StatusListFragment{
|
||||
private ImageView replyButtonAva;
|
||||
private TextView replyButtonText;
|
||||
private int lastBottomInset;
|
||||
private Paint replyLinePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -68,9 +74,10 @@ public class ThreadFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=super.buildDisplayItems(s);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_NO_IN_REPLY_TO);
|
||||
if(s.id.equals(mainStatus.id)){
|
||||
for(StatusDisplayItem item:items){
|
||||
item.fullWidth=true;
|
||||
if(item instanceof TextStatusDisplayItem text)
|
||||
text.textSelectable=true;
|
||||
else if(item instanceof FooterStatusDisplayItem footer)
|
||||
@@ -157,6 +164,8 @@ public class ThreadFragment extends StatusListFragment{
|
||||
showContent();
|
||||
if(!loaded)
|
||||
footerProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
list.addItemDecoration(new ReplyLinesItemDecoration());
|
||||
}
|
||||
|
||||
protected void onStatusCreated(Status status){
|
||||
@@ -222,4 +231,83 @@ public class ThreadFragment extends StatusListFragment{
|
||||
public int getSnackbarOffset(){
|
||||
return replyContainer.getHeight()-lastBottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> statusHolder && siblingHolder instanceof StatusDisplayItem.Holder<?> siblingStatusHolder){
|
||||
Status siblingStatus=getStatusByID(siblingStatusHolder.getItemID());
|
||||
if(siblingStatus==null)
|
||||
return;
|
||||
if(statusHolder.getItemID().equals(siblingStatus.inReplyToId) && siblingStatus!=mainStatus && !statusHolder.getItemID().equals(mainStatus.id))
|
||||
return;
|
||||
}
|
||||
super.drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, paint);
|
||||
}
|
||||
|
||||
private Status findPreviousStatus(String id){
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).id.equals(id))
|
||||
return i>0 ? data.get(i-1) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Status findNextStatus(String id){
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).id.equals(id))
|
||||
return i<data.size()-1 ? data.get(i+1) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private class ReplyLinesItemDecoration extends RecyclerView.ItemDecoration{
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
String currentID=null;
|
||||
boolean connectUp=false, connectToRoot=false, connectReply=false;
|
||||
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(2));
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
if(!(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder) || holder.getItemID().equals(mainStatus.id))
|
||||
continue;
|
||||
String itemID=holder.getItemID();
|
||||
if(!Objects.equals(currentID, itemID)){
|
||||
currentID=itemID;
|
||||
Status current=getStatusByID(currentID);
|
||||
Status previous=findPreviousStatus(currentID);
|
||||
Status next=findNextStatus(currentID);
|
||||
if(current==null)
|
||||
continue;
|
||||
|
||||
connectUp=previous!=null && previous.id.equals(current.inReplyToId);
|
||||
connectToRoot=mainStatus.id.equals(current.inReplyToId);
|
||||
connectReply=next!=null && itemID.equals(next.inReplyToId);
|
||||
}
|
||||
|
||||
if(!connectUp && !connectToRoot && !connectReply)
|
||||
continue;
|
||||
|
||||
float lineX=V.dp(36);
|
||||
paint.setAlpha(Math.round(255*child.getAlpha()));
|
||||
c.save();
|
||||
c.clipRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
|
||||
if(holder instanceof HeaderStatusDisplayItem.Holder){
|
||||
if(connectUp || connectToRoot){
|
||||
c.drawLine(lineX, child.getY()-V.dp(2), lineX, child.getY()+V.dp(14), paint);
|
||||
}
|
||||
if(connectReply){
|
||||
c.drawLine(lineX, child.getY()+V.dp(62), lineX, child.getY()+child.getHeight()+V.dp(2), paint);
|
||||
}
|
||||
}else if(connectReply){
|
||||
c.drawLine(lineX, child.getY(), lineX, child.getY()+child.getHeight(), paint);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
|
||||
protected void onSuccess(List<Account> result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false);
|
||||
}
|
||||
|
||||
protected String getSearchViewPlaceholder(){
|
||||
|
||||
@@ -44,7 +44,7 @@ public class AddNewListMembersFragment extends AccountSearchFragment{
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
setEmptyText("");
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), result.nextPageUri!=null);
|
||||
maxID=result.getNextPageMaxID();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -31,7 +31,7 @@ public class DiscoverAccountsFragment extends BaseAccountListFragment implements
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
|
||||
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity())).collect(Collectors.toList());
|
||||
onDataLoaded(accounts, false);
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<DiscoverNewsFragm
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetTrendingLinks()
|
||||
currentRequest=new GetTrendingLinks(40)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Card> result){
|
||||
|
||||
@@ -30,8 +30,6 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
@@ -65,7 +63,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
return switch(s.type){
|
||||
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
|
||||
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
|
||||
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, true);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
return;
|
||||
|
||||
onDataLoaded(results.stream().map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true, getActivity());
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.setOnClick(i->openHashtag(sr));
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
|
||||
.flatMap(Function.identity())
|
||||
.map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false, getActivity());
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.setOnClick(i->openHashtag(sr));
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
session.activationInfo.lastEmailConfirmationResend=System.currentTimeMillis();
|
||||
}
|
||||
lastResendTime=session.activationInfo.lastEmailConfirmationResend;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
AccountSessionManager.getInstance().writeAccountActivationInfo(accountID);
|
||||
updateResendTimer();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -11,14 +8,11 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.jsoup.Jsoup;
|
||||
@@ -26,7 +20,6 @@ import org.jsoup.nodes.Document;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
@@ -36,14 +29,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
import okhttp3.Call;
|
||||
@@ -99,7 +88,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
|
||||
TextView text=headerView.findViewById(R.id.text);
|
||||
text.setText(getString(R.string.privacy_policy_subtitle, instance.uri));
|
||||
text.setText(getString(R.string.privacy_policy_subtitle, instance.getDomain()));
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
@@ -111,7 +100,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
Button backBtn=view.findViewById(R.id.btn_back);
|
||||
backBtn.setText(getString(R.string.server_policy_disagree, instance.uri));
|
||||
backBtn.setText(getString(R.string.server_policy_disagree, instance.getDomain()));
|
||||
backBtn.setOnClickListener(v->{
|
||||
setResult(false, null);
|
||||
Nav.finish(this);
|
||||
@@ -159,7 +148,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
|
||||
private void loadServerPrivacyPolicy(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://"+instance.uri+"/terms")
|
||||
.url("https://"+instance.getDomain()+"/terms")
|
||||
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
|
||||
.build();
|
||||
currentRequest=MastodonAPIController.getHttpClient().newCall(req);
|
||||
@@ -176,7 +165,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
if(!response.isSuccessful())
|
||||
return;
|
||||
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
|
||||
final Item item=new Item(doc.title(), null, instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
|
||||
final Item item=new Item(doc.title(), null, instance.getDomain(), req.url().toString(), "https://"+instance.getDomain()+"/favicon.ico");
|
||||
Activity activity=getActivity();
|
||||
if(activity!=null){
|
||||
activity.runOnUiThread(()->{
|
||||
|
||||
@@ -15,8 +15,11 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
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.instance.GetInstanceV1;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.InstanceV1;
|
||||
import org.joinmastodon.android.model.InstanceV2;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -43,6 +46,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
@@ -63,7 +67,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
protected HashMap<String, Instance> instancesCache=new HashMap<>();
|
||||
protected View buttonBar;
|
||||
protected List<CatalogInstance> filteredData=new ArrayList<>();
|
||||
protected GetInstance loadingInstanceRequest;
|
||||
protected APIRequest<Instance> loadingInstanceRequest;
|
||||
protected Call loadingInstanceRedirectRequest;
|
||||
protected ProgressDialog instanceProgressDialog;
|
||||
protected HashMap<String, String> redirects=new HashMap<>();
|
||||
@@ -181,7 +185,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
showInstanceInfoLoadError(domain, x);
|
||||
if(fakeInstance!=null){
|
||||
fakeInstance.description=getString(R.string.error);
|
||||
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||
if(!filteredData.isEmpty() && filteredData.get(0)==fakeInstance){
|
||||
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
|
||||
ivh.rebind();
|
||||
}
|
||||
@@ -190,13 +194,15 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
return;
|
||||
}
|
||||
loadingInstanceDomain=domain;
|
||||
loadingInstanceRequest=new GetInstance();
|
||||
loadingInstanceRequest.setCallback(new Callback<>(){
|
||||
loadingInstanceRequest=AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
loadingInstanceRequest=null;
|
||||
loadingInstanceDomain=null;
|
||||
result.uri=domain; // needed for instances that use domain redirection
|
||||
if(result instanceof InstanceV1 v1)
|
||||
v1.uri=domain; // needed for instances that use domain redirection
|
||||
else if(result instanceof InstanceV2 v2)
|
||||
v2.domain=domain;
|
||||
instancesCache.put(domain, result);
|
||||
if(instanceProgressDialog!=null || onError!=null)
|
||||
proceedWithAuthOrSignup(result);
|
||||
@@ -246,7 +252,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
}
|
||||
}
|
||||
}
|
||||
}).execNoAuth(domain);
|
||||
});
|
||||
}
|
||||
|
||||
private void cancelLoadingInstanceInfo(){
|
||||
|
||||
@@ -330,7 +330,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||
if(currentInviteLinkAlert!=null){
|
||||
currentInviteLinkAlert.dismiss();
|
||||
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.isValidInviteUrl(currentSearchQueryButWithCasePreserved)){
|
||||
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
|
||||
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.getDomain(), inviteCodeHost)){
|
||||
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
|
||||
new CheckInviteLink(inviteLink.getPath())
|
||||
.setCallback(new Callback<>(){
|
||||
@@ -368,9 +368,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||
}
|
||||
}
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
|
||||
if(instance.invitesEnabled){
|
||||
showInviteLinkAlert(instance.uri);
|
||||
if(!instance.areRegistrationsOpen() && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.getDomain(), inviteCodeHost))){
|
||||
if(instance.areInvitesEnabled()){
|
||||
showInviteLinkAlert(instance.getDomain());
|
||||
}else{
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
@@ -382,7 +382,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
|
||||
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.getDomain(), inviteCodeHost))
|
||||
args.putString("inviteCode", inviteCode);
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}
|
||||
@@ -667,7 +667,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||
radioButton.setChecked(chosenInstance==item);
|
||||
Instance realInstance=instancesCache.get(item.normalizedDomain);
|
||||
float alpha;
|
||||
if(realInstance!=null && !realInstance.registrations){
|
||||
if(realInstance!=null && !realInstance.areRegistrationsOpen()){
|
||||
alpha=0.38f;
|
||||
description.setText(R.string.not_accepting_new_members);
|
||||
enabled=false;
|
||||
|
||||
@@ -12,7 +12,6 @@ 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.adapters.InstanceRulesAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
@@ -58,7 +57,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
|
||||
TextView text=headerView.findViewById(R.id.text);
|
||||
text.setText(Html.fromHtml(getString(R.string.instance_rules_subtitle, "<b>"+Html.escapeHtml(instance.uri)+"</b>")));
|
||||
text.setText(Html.fromHtml(getString(R.string.instance_rules_subtitle, "<b>"+Html.escapeHtml(instance.getDomain())+"</b>")));
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
|
||||
@@ -70,7 +70,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity()).stripLinksFromBio()).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
@@ -115,7 +116,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
|
||||
reasonWrap=view.findViewById(R.id.reason_wrap);
|
||||
|
||||
domain.setText('@'+instance.uri);
|
||||
domain.setText('@'+instance.getDomain());
|
||||
|
||||
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
@@ -143,7 +144,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
|
||||
reason.addTextChangedListener(new ErrorClearingListener(reason));
|
||||
|
||||
if(!instance.approvalRequired){
|
||||
if(!instance.isApprovalRequired()){
|
||||
reason.setVisibility(View.GONE);
|
||||
reasonExplain.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -237,6 +238,11 @@ public class SignupFragment extends ToolbarFragment{
|
||||
fakeAccount.acct=fakeAccount.username=username;
|
||||
fakeAccount.id="tmp"+System.currentTimeMillis();
|
||||
fakeAccount.displayName=displayName.getText().toString();
|
||||
fakeAccount.createdAt=Instant.now();
|
||||
fakeAccount.username=username;
|
||||
fakeAccount.acct=username;
|
||||
fakeAccount.url="https://"+instance.getDomain()+"/@"+username;
|
||||
fakeAccount.avatar=fakeAccount.avatarStatic=fakeAccount.headerStatic=fakeAccount.header=fakeAccount.note="";
|
||||
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, new AccountActivationInfo(email, System.currentTimeMillis()));
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||
@@ -285,7 +291,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
progressDialog.dismiss();
|
||||
}
|
||||
})
|
||||
.exec(instance.uri, apiToken);
|
||||
.exec(instance.getDomain(), apiToken);
|
||||
}
|
||||
|
||||
private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){
|
||||
@@ -317,7 +323,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
case "email" -> switch(error.error){
|
||||
case "ERR_BLOCKED" -> {
|
||||
String emailAddr=email.getText().toString();
|
||||
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
|
||||
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.getDomain()), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
|
||||
yield makeLinkInErrorMessage(s, this::onGoBackLinkClick);
|
||||
}
|
||||
case "ERR_INVALID" -> getString(R.string.signup_email_invalid);
|
||||
@@ -364,7 +370,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
private void updateButtonState(){
|
||||
btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find()
|
||||
&& password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString())
|
||||
&& (!instance.approvalRequired || reason.length()>0));
|
||||
&& (!instance.isApprovalRequired() || reason.length()>0));
|
||||
}
|
||||
|
||||
private void createAppAndGetToken(){
|
||||
@@ -386,7 +392,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(instance.uri);
|
||||
.execNoAuth(instance.getDomain());
|
||||
}
|
||||
|
||||
private void getToken(){
|
||||
@@ -412,7 +418,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(instance.uri);
|
||||
.execNoAuth(instance.getDomain());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -426,7 +432,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
|
||||
private void onForgotPasswordLinkClick(LinkSpan span){
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new");
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+instance.getDomain()+"/auth/password/new");
|
||||
}
|
||||
|
||||
private void onPasswordFieldFocusChange(View v, boolean hasFocus){
|
||||
|
||||
@@ -198,7 +198,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -241,7 +241,7 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER);
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, StatusDisplayItem.FLAG_NO_FOOTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -29,13 +30,14 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
setTitle("Debug settings");
|
||||
ListItem<Void> selfUpdateItem, resetUpdateItem;
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>("Re-register for FCM", null, this::onUpdatePushRegistrationClick),
|
||||
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
|
||||
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
|
||||
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick),
|
||||
new ListItem<>("Clear dismissed donation campaigns", null, this::onClearDismissedCampaignsClick),
|
||||
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", null, CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem)
|
||||
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", "Restart app to apply", CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
@@ -52,6 +54,11 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
getPrefs().edit().putBoolean("donationsStaging", donationsStagingItem.checked).apply();
|
||||
}
|
||||
|
||||
private void onUpdatePushRegistrationClick(ListItem<?> item){
|
||||
PushSubscriptionManager.resetLocalPreferences();
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
}
|
||||
|
||||
private void onTestEmailConfirmClick(ListItem<?> item){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
|
||||
@@ -100,13 +100,13 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
scroller.setClipToPadding(false);
|
||||
scroller.addView(scrollingLayout);
|
||||
|
||||
if(!TextUtils.isEmpty(instance.thumbnail)){
|
||||
if(!TextUtils.isEmpty(instance.getThumbnailURL())){
|
||||
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
|
||||
banner.setAspectRatio(1.914893617f);
|
||||
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
|
||||
banner.setClipToOutline(true);
|
||||
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
|
||||
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.getThumbnailURL()));
|
||||
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
blp.bottomMargin=V.dp(24);
|
||||
scrollingLayout.addView(banner, blp);
|
||||
@@ -115,7 +115,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
}
|
||||
|
||||
boolean needDivider=false;
|
||||
if(instance.contactAccount!=null){
|
||||
if(instance.getContactAccount()!=null){
|
||||
needDivider=true;
|
||||
TextView heading=new TextView(getActivity());
|
||||
heading.setTextAppearance(R.style.m3_title_small);
|
||||
@@ -128,7 +128,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
hlp.leftMargin=hlp.rightMargin=V.dp(16);
|
||||
scrollingLayout.addView(heading, hlp);
|
||||
|
||||
AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID);
|
||||
AccountViewModel model=new AccountViewModel(instance.getContactAccount(), accountID, getActivity());
|
||||
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
|
||||
holder.bind(model);
|
||||
@@ -140,7 +140,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, i+1), null, model.emojiHelper.getImageRequest(i), false);
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(instance.email)){
|
||||
if(!TextUtils.isEmpty(instance.getContactEmail())){
|
||||
needDivider=true;
|
||||
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
|
||||
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, i->{});
|
||||
@@ -208,7 +208,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
public void onRefresh(){}
|
||||
|
||||
private void openAdminEmail(){
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.email, null));
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.getContactEmail(), null));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try{
|
||||
startActivity(intent);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
@Parcel
|
||||
public class AccountWarning extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public Action action=Action.NONE;
|
||||
public String text;
|
||||
|
||||
public enum Action{
|
||||
@SerializedName("none")
|
||||
NONE,
|
||||
@SerializedName("disable")
|
||||
DISABLE,
|
||||
@SerializedName("mark_statuses_as_sensitive")
|
||||
MARK_STATUSES_AS_SENSITIVE,
|
||||
@SerializedName("delete_statuses")
|
||||
DELETE_STATUSES,
|
||||
@SerializedName("sensitive")
|
||||
SENSITIVE,
|
||||
@SerializedName("silence")
|
||||
SILENCE,
|
||||
@SerializedName("suspend")
|
||||
SUSPEND
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user