Compare commits

...

270 Commits

Author SHA1 Message Date
Grishka
8dffbff97c Domain badges & info sheet & my fanciest animation yet 2024-02-13 07:31:42 +03:00
Grishka
efb8cd565b Update locales 2024-02-12 21:40:28 +03:00
Grishka
1f5bdb975b Merge branch 'l10n_master' 2024-02-12 21:38:00 +03:00
Grishka
22dfc33974 Update strings 2024-02-12 21:37:44 +03:00
Grishka
6915d19fb4 fix 2024-02-09 03:38:00 +03:00
Grishka
ad2ef39ace AND-122 Mute, block, and domain block confirmation screens 2024-02-09 03:27:05 +03:00
Eugen Rochko
3cff655e6f New translations strings.xml (Persian) 2024-02-08 14:34:47 +01:00
Eugen Rochko
ed86a5a3e8 New translations strings.xml (Russian) 2024-02-07 19:48:30 +01:00
Eugen Rochko
f329435f51 New translations strings.xml (Catalan) 2024-02-06 18:19:58 +01:00
Eugen Rochko
6a6a80bcd7 New translations strings.xml (Belarusian) 2024-02-03 17:27:48 +01:00
Eugen Rochko
62e4983f02 New translations strings.xml (Hindi) 2024-02-03 09:56:54 +01:00
Eugen Rochko
6dfd991e87 New translations strings.xml (Hindi) 2024-02-03 08:52:38 +01:00
Eugen Rochko
e205462bf4 New translations short_description.txt (Hindi) 2024-02-03 07:47:35 +01:00
Eugen Rochko
03f341f6f8 New translations full_description.txt (Hindi) 2024-02-03 07:47:34 +01:00
Eugen Rochko
b9b08c5ea7 New translations strings.xml (Hindi) 2024-02-03 07:47:33 +01:00
Eugen Rochko
2b5498ff5d New translations full_description.txt (Hindi) 2024-02-03 06:46:31 +01:00
Eugen Rochko
84b058873d New translations strings.xml (Hindi) 2024-02-03 06:46:30 +01:00
Eugen Rochko
fcf5c0822e New translations strings.xml (Hindi) 2024-02-03 05:07:26 +01:00
Eugen Rochko
53c3da6a3d New translations strings.xml (Hindi) 2024-02-02 18:19:31 +01:00
Eugen Rochko
68371c9a0f New translations strings.xml (Hindi) 2024-02-02 16:58:56 +01:00
Grishka
e7295aac07 Fix #770 2024-02-02 15:34:06 +01:00
Eugen Rochko
ae7f65954a New translations strings.xml (Hindi) 2024-02-02 15:30:16 +01:00
Grishka
350a73c3eb Fix ripple color on Android 14
closes #767
2024-02-02 15:23:52 +01:00
Eugen Rochko
66d8ba9b5d New translations strings.xml (Hungarian) 2024-01-26 21:49:50 +01:00
Eugen Rochko
f944b12f45 New translations strings.xml (Hungarian) 2024-01-26 20:20:12 +01:00
Eugen Rochko
61928a1cf0 New translations strings.xml (Greek) 2024-01-26 16:15:35 +01:00
Eugen Rochko
f06196802e New translations strings.xml (Greek) 2024-01-26 11:34:11 +01:00
Grishka
e162833ad7 Update fastlane screenshots (closes #633) 2024-01-26 13:28:43 +03:00
Eugen Rochko
936ffdc793 New translations strings.xml (Swedish) 2024-01-25 21:43:29 +01:00
Eugen Rochko
0bbf6abc0c New translations strings.xml (Russian) 2024-01-25 11:03:38 +01:00
Gregory K
5552dc2ac6 Merge pull request #769 from jixiaoyong/master
fix: NullPointerException crash while change post language twice
2024-01-25 11:14:52 +03:00
JI,XIAOYONG
a65d6fbeb3 fix: NullPointerException crash while change post language twice
Closes https://github.com/mastodon/mastodon-android/issues/766
2024-01-25 15:54:15 +08:00
Eugen Rochko
43612ffbc1 New translations strings.xml (Polish) 2024-01-24 20:55:45 +01:00
Eugen Rochko
971881bbd3 New translations strings.xml (Slovenian) 2024-01-21 13:00:18 +01:00
Eugen Rochko
390cc6b65d New translations strings.xml (Slovenian) 2024-01-21 12:01:14 +01:00
Eugen Rochko
ee31288769 New translations strings.xml (Thai) 2024-01-20 22:09:59 +01:00
Eugen Rochko
401986af29 New translations strings.xml (Galician) 2024-01-19 15:25:19 +01:00
Eugen Rochko
e41e89c5cd New translations strings.xml (Hungarian) 2024-01-19 15:25:18 +01:00
Eugen Rochko
53de0cfc63 New translations strings.xml (Polish) 2024-01-19 14:07:59 +01:00
Eugen Rochko
e68481395f New translations strings.xml (Polish) 2024-01-19 13:07:50 +01:00
Eugen Rochko
9a361e0688 New translations strings.xml (Czech) 2024-01-16 15:39:51 +01:00
Eugen Rochko
b8cce74824 New translations strings.xml (Portuguese) 2024-01-16 14:39:36 +01:00
Eugen Rochko
f1ad6fc511 New translations strings.xml (Hungarian) 2024-01-16 14:39:35 +01:00
Eugen Rochko
2aa4cc1a88 New translations strings.xml (Czech) 2024-01-16 14:39:34 +01:00
Eugen Rochko
fb17ba4777 New translations strings.xml (Portuguese, Brazilian) 2024-01-16 12:35:13 +01:00
Eugen Rochko
6e3c464c97 New translations strings.xml (Portuguese) 2024-01-16 12:35:12 +01:00
Eugen Rochko
640e5163a8 New translations strings.xml (Portuguese, Brazilian) 2024-01-16 10:41:19 +01:00
Eugen Rochko
fdd3f2f398 New translations strings.xml (Italian) 2024-01-16 00:28:25 +01:00
Eugen Rochko
dfc55a13b8 New translations strings.xml (Italian) 2024-01-15 23:26:28 +01:00
Eugen Rochko
5a83b79ac2 New translations strings.xml (Polish) 2024-01-15 01:45:23 +01:00
Eugen Rochko
7d954ab3c2 New translations strings.xml (Polish) 2024-01-15 00:43:45 +01:00
Eugen Rochko
ec0b830f4f New translations strings.xml (Polish) 2024-01-14 23:43:15 +01:00
Eugen Rochko
26256b67d3 New translations strings.xml (Polish) 2024-01-14 20:48:10 +01:00
Eugen Rochko
2f9d60b9c0 New translations strings.xml (Polish) 2024-01-14 19:51:03 +01:00
Eugen Rochko
499a325bc8 New translations strings.xml (Polish) 2024-01-14 18:07:35 +01:00
Eugen Rochko
97ca2634a0 New translations strings.xml (Lithuanian) 2024-01-12 16:34:20 +01:00
Eugen Rochko
6630f0f8da New translations strings.xml (Lithuanian) 2024-01-12 14:51:16 +01:00
Eugen Rochko
129b253176 New translations strings.xml (Armenian) 2024-01-11 20:43:52 +01:00
Eugen Rochko
c2382d065e New translations strings.xml (Armenian) 2024-01-11 19:46:15 +01:00
Eugen Rochko
085264755a New translations strings.xml (Chinese Traditional) 2024-01-08 14:54:56 +01:00
Eugen Rochko
baac955e52 New translations strings.xml (Chinese Traditional) 2024-01-08 13:30:18 +01:00
Eugen Rochko
4a0501209a New translations strings.xml (Turkish) 2024-01-07 21:08:49 +01:00
Eugen Rochko
e471b36d39 New translations strings.xml (Japanese) 2024-01-07 09:53:25 +01:00
Eugen Rochko
5c2961cf7c New translations strings.xml (Japanese) 2024-01-07 08:56:52 +01:00
Eugen Rochko
6e980f17c6 New translations strings.xml (Vietnamese) 2024-01-07 06:43:58 +01:00
Eugen Rochko
2860ce8755 New translations strings.xml (Vietnamese) 2024-01-07 05:25:19 +01:00
Eugen Rochko
ca25a868a0 New translations strings.xml (Thai) 2024-01-04 21:01:23 +01:00
Eugen Rochko
74f3bd5905 New translations strings.xml (Thai) 2024-01-04 19:45:26 +01:00
Eugen Rochko
e0a53b4296 New translations strings.xml (Slovenian) 2024-01-04 14:57:53 +01:00
Eugen Rochko
c20f043f38 New translations strings.xml (Icelandic) 2024-01-04 12:50:19 +01:00
Eugen Rochko
daf3005178 New translations strings.xml (Dutch) 2024-01-04 11:32:03 +01:00
Eugen Rochko
d17e24faae New translations strings.xml (Scottish Gaelic) 2024-01-03 22:18:18 +01:00
Eugen Rochko
0cd17accf9 New translations strings.xml (Thai) 2024-01-03 22:18:13 +01:00
Eugen Rochko
65f7b97e60 New translations strings.xml (Persian) 2024-01-03 22:18:11 +01:00
Eugen Rochko
c7324285f3 New translations strings.xml (Indonesian) 2024-01-03 22:18:10 +01:00
Eugen Rochko
6bc795ebea New translations strings.xml (Portuguese, Brazilian) 2024-01-03 22:18:09 +01:00
Eugen Rochko
f2616cdd58 New translations strings.xml (Galician) 2024-01-03 22:18:08 +01:00
Eugen Rochko
d50f65ffd8 New translations strings.xml (Vietnamese) 2024-01-03 22:18:07 +01:00
Eugen Rochko
b39b2d0544 New translations strings.xml (Chinese Traditional) 2024-01-03 22:18:06 +01:00
Eugen Rochko
cdaaa91bcc New translations strings.xml (Ukrainian) 2024-01-03 22:18:05 +01:00
Eugen Rochko
109dca0b8a New translations strings.xml (Turkish) 2024-01-03 22:18:03 +01:00
Eugen Rochko
ee87da564b New translations strings.xml (Swedish) 2024-01-03 22:18:02 +01:00
Eugen Rochko
b143559a0f New translations strings.xml (Russian) 2024-01-03 22:18:01 +01:00
Eugen Rochko
9b89727c80 New translations strings.xml (Polish) 2024-01-03 22:17:59 +01:00
Eugen Rochko
68a252c85c New translations strings.xml (Norwegian) 2024-01-03 22:17:58 +01:00
Eugen Rochko
d99cb91e89 New translations strings.xml (Lithuanian) 2024-01-03 22:17:57 +01:00
Eugen Rochko
38879cd2fe New translations strings.xml (Korean) 2024-01-03 22:17:56 +01:00
Eugen Rochko
af4d98a48b New translations strings.xml (Japanese) 2024-01-03 22:17:54 +01:00
Eugen Rochko
39bb93d650 New translations strings.xml (Italian) 2024-01-03 22:17:53 +01:00
Eugen Rochko
0a3568f424 New translations strings.xml (Hungarian) 2024-01-03 22:17:52 +01:00
Eugen Rochko
e0b45720f0 New translations strings.xml (Finnish) 2024-01-03 22:17:50 +01:00
Eugen Rochko
f5b7024bb5 New translations strings.xml (Basque) 2024-01-03 22:17:49 +01:00
Eugen Rochko
f1bfa1f598 New translations strings.xml (Greek) 2024-01-03 22:17:48 +01:00
Eugen Rochko
653304f9a4 New translations strings.xml (German) 2024-01-03 22:17:47 +01:00
Eugen Rochko
3d416a038a New translations strings.xml (Danish) 2024-01-03 22:17:46 +01:00
Eugen Rochko
e0eeb87182 New translations strings.xml (Czech) 2024-01-03 22:17:45 +01:00
Eugen Rochko
2570a86da9 New translations strings.xml (Belarusian) 2024-01-03 22:17:43 +01:00
Eugen Rochko
7b110f16b3 New translations strings.xml (Arabic) 2024-01-03 22:17:42 +01:00
Eugen Rochko
d170e87325 New translations strings.xml (Spanish) 2024-01-03 22:17:41 +01:00
Eugen Rochko
4a60f0c576 New translations strings.xml (Icelandic) 2024-01-03 22:17:39 +01:00
Eugen Rochko
4b5e9d604c New translations strings.xml (French) 2024-01-03 22:17:38 +01:00
Eugen Rochko
f9562d5087 New translations strings.xml (Chinese Simplified) 2024-01-03 22:17:37 +01:00
Eugen Rochko
786091c0a4 New translations strings.xml (Armenian) 2024-01-03 22:17:36 +01:00
Eugen Rochko
436b8240ef New translations strings.xml (Slovenian) 2024-01-03 22:17:35 +01:00
Eugen Rochko
e7253dcf97 New translations strings.xml (Dutch) 2024-01-03 22:17:34 +01:00
Grishka
48f9aabaf7 Support for invite links (AND-90) 2024-01-03 23:51:35 +03:00
Eugen Rochko
14d353ae27 New translations strings.xml (Icelandic) 2024-01-03 14:57:33 +01:00
Eugen Rochko
9a82846b84 New translations strings.xml (Icelandic) 2024-01-02 15:54:39 +01:00
Eugen Rochko
a4c9bbadc4 New translations strings.xml (Icelandic) 2024-01-02 14:01:52 +01:00
Eugen Rochko
fa70c55084 New translations strings.xml (French) 2023-12-30 16:27:55 +01:00
Eugen Rochko
8d0a89fb06 New translations strings.xml (Chinese Simplified) 2023-12-29 18:47:34 +01:00
Eugen Rochko
3caf6cb94c New translations strings.xml (Chinese Simplified) 2023-12-29 17:46:24 +01:00
Eugen Rochko
f4854061ea New translations strings.xml (Armenian) 2023-12-27 21:54:36 +01:00
Eugen Rochko
bf7607674e New translations strings.xml (Armenian) 2023-12-27 20:06:13 +01:00
Eugen Rochko
137a8ca27b New translations strings.xml (Slovenian) 2023-12-21 22:08:11 +01:00
Eugen Rochko
b9ed4e0ee2 New translations strings.xml (Dutch) 2023-12-21 15:36:39 +01:00
Eugen Rochko
bc04672d32 New translations strings.xml (Dutch) 2023-12-21 12:26:19 +01:00
Eugen Rochko
70c668ecf1 New translations strings.xml (Polish) 2023-12-17 22:13:43 +01:00
Eugen Rochko
64bbe2c438 New translations strings.xml (Polish) 2023-12-17 21:16:34 +01:00
Eugen Rochko
32209e766e New translations strings.xml (Greek) 2023-12-17 11:29:11 +01:00
Eugen Rochko
99349cff0a New translations strings.xml (Persian) 2023-12-17 09:35:27 +01:00
Eugen Rochko
74ca1961e0 New translations strings.xml (Spanish) 2023-12-17 03:38:08 +01:00
Eugen Rochko
ef6ba7fe0c New translations strings.xml (Spanish) 2023-12-17 02:07:50 +01:00
Eugen Rochko
9ace2b71cc New translations strings.xml (Spanish) 2023-12-16 22:08:55 +01:00
Eugen Rochko
0c54654b8b New translations strings.xml (Basque) 2023-12-16 20:25:55 +01:00
Eugen Rochko
bf686309fb New translations strings.xml (Basque) 2023-12-16 18:48:35 +01:00
Eugen Rochko
ce4f46537b New translations strings.xml (Spanish) 2023-12-16 17:44:05 +01:00
Eugen Rochko
4c43207f17 New translations strings.xml (Spanish) 2023-12-16 16:46:39 +01:00
Eugen Rochko
afe5bcd1f3 New translations strings.xml (Portuguese, Brazilian) 2023-12-15 17:01:27 +01:00
Eugen Rochko
3bda81bd43 New translations strings.xml (Portuguese, Brazilian) 2023-12-15 16:02:20 +01:00
Eugen Rochko
7339b2325f New translations strings.xml (Armenian) 2023-12-15 12:30:11 +01:00
Eugen Rochko
ee84a9ee7e New translations strings.xml (Armenian) 2023-12-15 11:09:33 +01:00
Eugen Rochko
fef594150a New translations strings.xml (Turkish) 2023-12-13 17:09:47 +01:00
Eugen Rochko
10371f69cb New translations strings.xml (Turkish) 2023-12-13 15:52:37 +01:00
Eugen Rochko
75cf3d76fb New translations strings.xml (Dutch) 2023-12-12 16:01:19 +01:00
Eugen Rochko
51a7d00c47 New translations strings.xml (Lithuanian) 2023-12-11 15:13:33 +01:00
Eugen Rochko
9ac8261cc4 New translations strings.xml (Lithuanian) 2023-12-11 10:21:28 +01:00
Eugen Rochko
1f4ad80b7d New translations strings.xml (Lithuanian) 2023-12-11 07:10:38 +01:00
Eugen Rochko
4b090f0d68 New translations strings.xml (Lithuanian) 2023-12-10 20:30:16 +01:00
Eugen Rochko
4002bcde26 New translations strings.xml (Lithuanian) 2023-12-10 10:29:09 +01:00
Eugen Rochko
ded3777b40 New translations strings.xml (Lithuanian) 2023-12-10 09:10:25 +01:00
Eugen Rochko
7236066003 New translations strings.xml (Vietnamese) 2023-12-10 03:30:15 +01:00
Eugen Rochko
033f07ea09 New translations strings.xml (Lithuanian) 2023-12-09 22:16:10 +01:00
Eugen Rochko
283c0cba4b New translations strings.xml (Lithuanian) 2023-12-09 21:05:47 +01:00
Eugen Rochko
e3a1fc2fbb New translations strings.xml (Lithuanian) 2023-12-09 19:50:11 +01:00
Eugen Rochko
95de9e2917 New translations strings.xml (Lithuanian) 2023-12-09 16:56:00 +01:00
Eugen Rochko
a82ebeed11 New translations strings.xml (Basque) 2023-12-09 15:50:00 +01:00
Eugen Rochko
3a3aa0be1c New translations strings.xml (Lithuanian) 2023-12-09 07:31:00 +01:00
Eugen Rochko
e72491c2d1 New translations strings.xml (Lithuanian) 2023-12-08 19:29:39 +01:00
Eugen Rochko
36dede1f93 New translations strings.xml (Lithuanian) 2023-12-08 16:59:20 +01:00
Eugen Rochko
ed15daf9e9 New translations strings.xml (Lithuanian) 2023-12-08 15:26:26 +01:00
Eugen Rochko
c6052c841d New translations strings.xml (Lithuanian) 2023-12-08 14:07:22 +01:00
Eugen Rochko
ce39c7ca8f New translations strings.xml (Lithuanian) 2023-12-08 10:16:11 +01:00
Eugen Rochko
b7723dcb98 New translations strings.xml (Lithuanian) 2023-12-08 08:50:52 +01:00
Eugen Rochko
ad0774f8a5 New translations strings.xml (Armenian) 2023-12-07 22:59:36 +01:00
Eugen Rochko
9172feb72b New translations strings.xml (Italian) 2023-12-07 21:21:39 +01:00
Eugen Rochko
a297bd3281 New translations strings.xml (Lithuanian) 2023-12-07 16:52:37 +01:00
Eugen Rochko
e713a9cfc3 New translations strings.xml (Lithuanian) 2023-12-07 15:46:22 +01:00
Eugen Rochko
195395a22d New translations strings.xml (Lithuanian) 2023-12-07 14:14:31 +01:00
Eugen Rochko
7b6a62b047 New translations strings.xml (Lithuanian) 2023-12-07 13:14:37 +01:00
Eugen Rochko
ada1c9ff6d New translations strings.xml (Lithuanian) 2023-12-07 12:05:29 +01:00
Eugen Rochko
5a0a14ed56 New translations strings.xml (Lithuanian) 2023-12-07 09:11:41 +01:00
Eugen Rochko
cad3879646 New translations strings.xml (Galician) 2023-12-07 07:52:19 +01:00
Eugen Rochko
5d961991d4 New translations strings.xml (Lithuanian) 2023-12-06 22:10:19 +01:00
Eugen Rochko
e27536743f New translations strings.xml (Czech) 2023-12-06 22:10:18 +01:00
Eugen Rochko
9f8d4a0f34 New translations strings.xml (Lithuanian) 2023-12-06 20:56:46 +01:00
Eugen Rochko
67b6a89fd9 New translations strings.xml (Swedish) 2023-12-06 20:56:45 +01:00
Eugen Rochko
dabc4058ba New translations strings.xml (Lithuanian) 2023-12-06 18:59:35 +01:00
Eugen Rochko
6c468602c6 New translations full_description.txt (Lithuanian) 2023-12-06 17:20:30 +01:00
Eugen Rochko
9c5d29a860 New translations strings.xml (Lithuanian) 2023-12-06 17:20:29 +01:00
Eugen Rochko
da5e2a6b50 New translations short_description.txt (Lithuanian) 2023-12-06 16:23:00 +01:00
Eugen Rochko
a194569fd4 New translations full_description.txt (Lithuanian) 2023-12-06 16:22:58 +01:00
Eugen Rochko
78a4ace9b2 New translations strings.xml (Lithuanian) 2023-12-06 14:26:32 +01:00
Eugen Rochko
9a664088cd New translations strings.xml (Lithuanian) 2023-12-06 13:02:34 +01:00
Eugen Rochko
1d2e6f880b New translations strings.xml (Lithuanian) 2023-12-06 11:26:57 +01:00
Eugen Rochko
2cd2918d53 New translations strings.xml (Japanese) 2023-12-06 11:26:56 +01:00
Eugen Rochko
9b49db6677 New translations strings.xml (French) 2023-12-06 08:58:00 +01:00
Eugen Rochko
9f6c61e5c0 New translations title.txt (Lithuanian) 2023-12-05 21:30:20 +01:00
Eugen Rochko
b6a2bb7881 New translations short_description.txt (Lithuanian) 2023-12-05 21:30:19 +01:00
Eugen Rochko
62262010b9 New translations full_description.txt (Lithuanian) 2023-12-05 21:30:18 +01:00
Eugen Rochko
72fe9a04a6 New translations strings.xml (Lithuanian) 2023-12-05 21:30:17 +01:00
Eugen Rochko
d8cf55ae21 New translations strings.xml (Greek) 2023-12-05 17:58:41 +01:00
Eugen Rochko
dfb393b934 New translations strings.xml (Chinese Traditional) 2023-12-05 07:46:21 +01:00
Eugen Rochko
cd27716f6a New translations strings.xml (Russian) 2023-12-05 07:46:20 +01:00
Eugen Rochko
469553b34e New translations strings.xml (Thai) 2023-12-05 06:46:35 +01:00
Grishka
5d7c37262e Info sheet in media viewer (AND-109) 2023-12-04 21:33:25 +03:00
Eugen Rochko
3f3867473f New translations strings.xml (Czech) 2023-12-04 11:21:21 +01:00
Grishka
b08cd1eb4b Crash fixes 2023-12-04 06:22:21 +03:00
Grishka
1f9ff8d341 Increase timeout to 60 seconds 2023-12-03 21:38:00 +03:00
Eugen Rochko
528b362f64 New translations strings.xml (Thai) 2023-12-03 09:24:19 +01:00
Eugen Rochko
1db10c5047 New translations strings.xml (Thai) 2023-12-03 08:16:34 +01:00
Eugen Rochko
f295f5f4e7 New translations strings.xml (Japanese) 2023-12-02 17:44:51 +01:00
Eugen Rochko
08924bd9b0 New translations strings.xml (Russian) 2023-12-02 15:31:03 +01:00
Eugen Rochko
5d432435a1 New translations strings.xml (Russian) 2023-12-02 14:26:43 +01:00
Eugen Rochko
8bd76aa833 New translations strings.xml (Italian) 2023-12-02 00:37:04 +01:00
Eugen Rochko
2147cb87ac New translations strings.xml (Slovenian) 2023-12-01 18:50:27 +01:00
Eugen Rochko
00ed0f5402 New translations strings.xml (Galician) 2023-12-01 16:15:43 +01:00
Eugen Rochko
870f79f6cd New translations strings.xml (Galician) 2023-12-01 14:51:25 +01:00
Eugen Rochko
da879213fc New translations strings.xml (Greek) 2023-11-30 23:34:22 +01:00
Eugen Rochko
db66974bd6 New translations strings.xml (Chinese Traditional) 2023-11-30 02:07:51 +01:00
Eugen Rochko
e3d5ae1d65 New translations strings.xml (Thai) 2023-11-29 20:48:42 +01:00
Eugen Rochko
b06bc5b3b7 New translations strings.xml (Russian) 2023-11-29 11:46:16 +01:00
Eugen Rochko
a4c988012d New translations strings.xml (Georgian) 2023-11-29 09:16:14 +01:00
Grishka
a200701e4c Link card improvements (AND-115)
closes #651
2023-11-29 07:51:11 +03:00
Eugen Rochko
e8f604792c New translations strings.xml (Chinese Traditional) 2023-11-29 03:02:35 +01:00
Eugen Rochko
c8b0666ef9 New translations strings.xml (Scottish Gaelic) 2023-11-29 00:35:44 +01:00
Eugen Rochko
13aa72b150 New translations strings.xml (Thai) 2023-11-29 00:35:39 +01:00
Eugen Rochko
6694074b18 New translations strings.xml (Persian) 2023-11-29 00:35:37 +01:00
Eugen Rochko
63aa32c636 New translations strings.xml (Indonesian) 2023-11-29 00:35:36 +01:00
Eugen Rochko
5fbab870c3 New translations strings.xml (Portuguese, Brazilian) 2023-11-29 00:35:35 +01:00
Eugen Rochko
4a34e248e0 New translations strings.xml (Icelandic) 2023-11-29 00:35:34 +01:00
Eugen Rochko
2c45165e53 New translations strings.xml (Galician) 2023-11-29 00:35:33 +01:00
Eugen Rochko
3f029ac45b New translations strings.xml (Chinese Traditional) 2023-11-29 00:35:32 +01:00
Eugen Rochko
a4cf76d5ba New translations strings.xml (Chinese Simplified) 2023-11-29 00:35:31 +01:00
Eugen Rochko
3044000cf8 New translations strings.xml (Ukrainian) 2023-11-29 00:35:30 +01:00
Eugen Rochko
ab1ef5cfd8 New translations strings.xml (Turkish) 2023-11-29 00:35:29 +01:00
Eugen Rochko
16b91a283a New translations strings.xml (Swedish) 2023-11-29 00:35:28 +01:00
Eugen Rochko
e9fbdc21fa New translations strings.xml (Slovenian) 2023-11-29 00:35:27 +01:00
Eugen Rochko
b429e662aa New translations strings.xml (Russian) 2023-11-29 00:35:26 +01:00
Eugen Rochko
834ad1736e New translations strings.xml (Polish) 2023-11-29 00:35:24 +01:00
Eugen Rochko
91021699d2 New translations strings.xml (Norwegian) 2023-11-29 00:35:23 +01:00
Eugen Rochko
0f86aa12ab New translations strings.xml (Japanese) 2023-11-29 00:35:21 +01:00
Eugen Rochko
fb7bf6f308 New translations strings.xml (Italian) 2023-11-29 00:35:20 +01:00
Eugen Rochko
5aa67aaa78 New translations strings.xml (Hungarian) 2023-11-29 00:35:19 +01:00
Eugen Rochko
2e892e7305 New translations strings.xml (Finnish) 2023-11-29 00:35:16 +01:00
Eugen Rochko
6486a1689f New translations strings.xml (Basque) 2023-11-29 00:35:15 +01:00
Eugen Rochko
5966535111 New translations strings.xml (German) 2023-11-29 00:35:14 +01:00
Eugen Rochko
a2cf4bda99 New translations strings.xml (Danish) 2023-11-29 00:35:13 +01:00
Eugen Rochko
7a93c8615d New translations strings.xml (Arabic) 2023-11-29 00:35:12 +01:00
Eugen Rochko
cf29f11cea New translations strings.xml (Spanish) 2023-11-29 00:35:10 +01:00
Eugen Rochko
23188a26d7 New translations strings.xml (Belarusian) 2023-11-29 00:35:08 +01:00
Eugen Rochko
0480dc0140 New translations strings.xml (French) 2023-11-29 00:35:07 +01:00
Eugen Rochko
cb14b29c78 New translations strings.xml (Armenian) 2023-11-29 00:35:06 +01:00
Eugen Rochko
bf68272de3 New translations strings.xml (Greek) 2023-11-29 00:35:05 +01:00
Eugen Rochko
730f5f8cc9 New translations strings.xml (Dutch) 2023-11-29 00:35:03 +01:00
Eugen Rochko
4b6d328e3d New translations strings.xml (Czech) 2023-11-29 00:35:02 +01:00
Eugen Rochko
cfde38be2d New translations strings.xml (Vietnamese) 2023-11-29 00:35:01 +01:00
Grishka
a2ea8e76fb Improve follow recommendations screen (AND-101) 2023-11-29 02:09:59 +03:00
Grishka
e797d8a1c2 Revert "Update icon"
This reverts commit b58c157c87.
2023-11-29 01:26:51 +03:00
Grishka
b58c157c87 Update icon 2023-11-29 01:22:27 +03:00
Eugen Rochko
58f746a285 New translations strings.xml (Georgian) 2023-11-28 10:52:56 +01:00
Eugen Rochko
a6bba42a49 New translations title.txt (Georgian) 2023-11-28 07:16:50 +01:00
Eugen Rochko
519d6868b2 New translations short_description.txt (Georgian) 2023-11-28 07:16:49 +01:00
Eugen Rochko
5322120097 New translations full_description.txt (Georgian) 2023-11-28 07:16:48 +01:00
Eugen Rochko
2c88c86480 New translations strings.xml (Georgian) 2023-11-28 07:16:47 +01:00
Eugen Rochko
55f32fd45b New translations strings.xml (Dutch) 2023-11-28 01:12:21 +01:00
Eugen Rochko
f39f0b03d1 New translations strings.xml (Dutch) 2023-11-27 23:54:03 +01:00
Eugen Rochko
ff2f1a4955 New translations strings.xml (Dutch) 2023-11-27 21:48:52 +01:00
Grishka
b283e216a7 Increase default HTTP timeouts to 30 seconds
fixes #751
2023-11-27 20:30:33 +03:00
Eugen Rochko
4328d568b3 New translations strings.xml (Belarusian) 2023-11-27 15:23:31 +01:00
Eugen Rochko
8edc47703f New translations strings.xml (Dutch) 2023-11-27 14:08:47 +01:00
Eugen Rochko
92ce906163 New translations strings.xml (Dutch) 2023-11-27 05:46:48 +01:00
Eugen Rochko
6e141e360e New translations strings.xml (French) 2023-11-25 22:27:46 +01:00
Grishka
d1aba87e13 Fix #747 2023-11-25 03:38:57 +03:00
Grishka
723853079e Fix "go to account" in search 2023-11-25 03:35:21 +03:00
Eugen Rochko
cd0742c093 New translations strings.xml (Armenian) 2023-11-24 23:26:11 +01:00
Eugen Rochko
52d5de5aec New translations strings.xml (Dutch) 2023-11-24 23:26:10 +01:00
Eugen Rochko
4f8a5ae5db New translations strings.xml (Dutch) 2023-11-24 21:58:52 +01:00
Eugen Rochko
616f2463c7 New translations strings.xml (Dutch) 2023-11-24 20:51:33 +01:00
Eugen Rochko
d3b711a966 New translations strings.xml (Greek) 2023-11-24 19:55:43 +01:00
Eugen Rochko
827fe34709 New translations strings.xml (Dutch) 2023-11-24 19:55:41 +01:00
Eugen Rochko
4b6c0242d5 New translations strings.xml (Dutch) 2023-11-24 18:54:54 +01:00
Eugen Rochko
c3cbc16084 New translations strings.xml (Czech) 2023-11-23 18:37:42 +01:00
Eugen Rochko
36493bfc88 New translations strings.xml (Vietnamese) 2023-11-23 18:37:41 +01:00
Eugen Rochko
66ce93a3ff New translations strings.xml (Vietnamese) 2023-11-23 16:34:34 +01:00
Grishka
957bc76dbb Merge branch 'l10n_master' 2023-11-23 00:27:58 +03:00
Grishka
1f5a28fb33 Add top margin to pre-reply shits 2023-11-23 00:27:24 +03:00
Grishka
045c58ce66 fix color 2023-11-22 18:26:56 +03:00
Grishka
e2dde7239f Render custom emojis in non-mutual pre-reply sheet 2023-11-22 18:06:14 +03:00
Eugen Rochko
512ad93eea New translations strings.xml (Turkish) 2023-11-22 00:00:30 +01:00
150 changed files with 5671 additions and 774 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

@@ -1 +1 @@
Decentralized social network
डिसेंट्रलाइज़्ड सोशल नेटवर्क

View File

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

View File

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

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
Mastodon didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
Daugiau funkcijų:
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
• Naršyti: tendencingos saitažodžiai ir paskyros vos nuo vieno prisilietimo
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.

View File

@@ -0,0 +1 @@
Decentralizuotas socialinis tinklas

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -9,10 +9,10 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 79
versionName "2.2.3"
versionCode 84
versionName "2.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -76,7 +76,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.2.15'
implementation 'me.grishka.appkit:appkit:1.2.16'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -102,11 +102,11 @@ public class MainActivity extends FragmentStackActivity{
session=AccountSessionManager.get(accountID);
if(session==null || !session.activated)
return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
}
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true, null, 0, 0)
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
new GetSearchResults(q, type, true, null, 0, 0)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){

View File

@@ -45,7 +45,11 @@ public class MastodonAPIController{
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
.create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
private static OkHttpClient httpClient=new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
private AccountSession session;

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
public CheckInviteLink(String path){
super(HttpMethod.GET, path, Response.class);
addHeader("Accept", "application/json");
}
@Override
protected String getPathPrefix(){
return "";
}
public static class Response extends BaseModel{
@RequiredField
public String inviteCode;
}
}

View File

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

View File

@@ -29,10 +29,9 @@ 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.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.OldPostPreReplySheet;
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
@@ -182,7 +181,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder;
@Override
@@ -661,7 +660,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
new NonMutualPreReplySheet(getActivity(), notAgain->{
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
proceed.run();
}, status.account).show();
}, status.account, accountID).show();
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
new OldPostPreReplySheet(getActivity(), notAgain->{

View File

@@ -1077,6 +1077,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Editable e=mainEditText.getText();
int start=e.getSpanStart(currentAutocompleteSpan);
int end=e.getSpanEnd(currentAutocompleteSpan);
if(start==-1 || end==-1)
return;
e.replace(start, end, text+" ");
finishAutocomplete();
InputConnection conn=mainEditText.getCurrentInputConnection();

View File

@@ -7,10 +7,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -131,20 +128,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text)
.setMessage(msg)
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
@@ -181,7 +167,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);

View File

@@ -30,7 +30,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;

View File

@@ -178,6 +178,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(refreshing)
list.scrollToPosition(0);
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty());
}
})
@@ -191,6 +192,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(refreshing)
list.scrollToPosition(0);
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
onDataLoaded(result, !result.isEmpty());
}
})

View File

@@ -40,7 +40,6 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -63,10 +62,12 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.ImageSpanThatDoesNotBreakShitForNoGoodReason;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView;
@@ -108,7 +109,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
@@ -186,6 +187,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name);
username=content.findViewById(R.id.username);
usernameDomain=content.findViewById(R.id.username_domain);
bio=content.findViewById(R.id.bio);
followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label);
@@ -321,6 +323,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
return sizeWrapper;
}
@@ -500,22 +504,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb=new SpannableStringBuilder(account.username);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
lock.setTint(username.getCurrentTextColor());
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
ssb.append(getString(R.string.manually_approves_followers), new ImageSpanThatDoesNotBreakShitForNoGoodReason(lock, ImageSpan.ALIGN_BOTTOM), 0);
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
username.setText(account.username);
}
String domain=account.getDomain();
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
@@ -631,10 +634,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account, relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
updateRelationship();
});
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
.setCallback(new Callback<>(){
@@ -977,7 +980,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
@@ -989,7 +992,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(drawable==null || drawable instanceof ColorDrawable)
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}

View File

@@ -1,7 +1,12 @@
package org.joinmastodon.android.fragments;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp;
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.fragments.onboarding.InstanceCatalogSignupFragment;
@@ -20,6 +27,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
@@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer, loadedDefaultServer;
private Uri currentInviteLink;
private ProgressDialog instanceLoadingProgress;
private String inviteCode;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
Bundle extras=new Bundle();
boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup);
extras.putString("defaultServer", chosenDefaultServer);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
}
private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer)
return;
instanceLoadingProgress=new ProgressDialog(getActivity());
instanceLoadingProgress.setCancelable(false);
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
instanceLoadingProgress.show();
if(currentInviteLink!=null){
new CheckInviteLink(currentInviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCode=result.inviteCode;
proceedWithServerDomain(currentInviteLink.getHost());
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.execNoAuth(currentInviteLink.getHost());
return;
}
proceedWithServerDomain(chosenDefaultServer);
}
private void proceedWithServerDomain(String domain){
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
if(!result.registrations){
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
@@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
if(inviteCode!=null)
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
@@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(chosenDefaultServer);
.execNoAuth(domain);
}
private void onLearnMoreClick(View v){
@@ -198,9 +258,18 @@ public class SplashFragment extends AppKitFragment{
}
private void loadAndChooseDefaultServer(){
loadingDefaultServer=true;
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
currentInviteLink=Uri.parse(clipText.toString());
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
}
}else{
loadingDefaultServer=true;
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){
@Override
@@ -243,7 +312,7 @@ public class SplashFragment extends AppKitFragment{
chosenDefaultServer=domain;
loadingDefaultServer=false;
loadedDefaultServer=true;
if(defaultServerButton!=null && getActivity()!=null){
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));

View File

@@ -38,6 +38,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
protected int itemLayoutRes=R.layout.item_account_list;
public BaseAccountListFragment(){
super(40);
@@ -151,7 +152,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
onConfigureViewHolder(holder);
return holder;
}

View File

@@ -408,7 +408,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain;
}
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true, GetSearchResults.Type.ACCOUNTS);
}
private void onGoToStatusSearchClick(ListItem<?> item_){

View File

@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;

View File

@@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
}

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
@@ -37,6 +36,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -48,7 +48,6 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery;
protected String currentSearchQueryButWithCasePreserved;
protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
loadInstanceInfo(currentSearchQuery, false);
}
@@ -149,6 +151,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
loadInstanceInfo(_domain, isFromRedirect, null);
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain))
return;
String domain=normalizeInstanceDomain(_domain);
@@ -173,7 +179,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){
showInstanceInfoLoadError(domain, x);
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -193,10 +202,11 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
@@ -223,11 +233,14 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(onError!=null)
onError.accept(error);
else
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -276,7 +289,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){
String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder()
.url(url)
@@ -290,7 +303,12 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
Activity a=getActivity();
if(a==null)
return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(e);
else
showInstanceInfoLoadError(domain, e);
});
}
@Override
@@ -302,7 +320,13 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return;
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
a.runOnUiThread(()->{
String err=response.code()+" "+response.message();
if(onError!=null)
onError.accept(err);
else
showInstanceInfoLoadError(domain, err);
});
return;
}
InputSource source=new InputSource(response.body().charStream());
@@ -321,9 +345,19 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
}
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(origError);
else
showInstanceInfoLoadError(domain, origError);
});
}catch(Exception x){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
});
}
}
});

View File

@@ -1,8 +1,13 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
@@ -12,6 +17,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@@ -19,9 +26,12 @@ import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance;
@@ -29,6 +39,8 @@ import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -40,7 +52,9 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@@ -317,7 +334,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus();
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick);
nextButton.setEnabled(chosenInstance!=null);
}
@@ -351,34 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
@Override
protected void proceedWithAuthOrSignup(Instance instance){
if(currentInviteLinkAlert!=null){
currentInviteLinkAlert.dismiss();
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCodeHost=inviteLink.getHost();
inviteCode=result.inviteCode;
proceedWithAuthOrSignup(instance);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(inviteLink.getHost());
return;
}
}
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
if(instance.invitesEnabled){
showInviteLinkAlert(instance.uri);
}else{
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
}
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
private void onPickRandomInstanceClick(View v){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
private void onUseInviteClick(View v){
showInviteLinkAlert(null);
}
private void showInviteLinkAlert(String domain){
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setView(R.layout.alert_invite_link)
.setPositiveButton(R.string.next, null)
.setNegativeButton(R.string.cancel, null)
.create();
Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE);
EditText edit=alert.findViewById(R.id.edit);
TextView supportingText=alert.findViewById(R.id.supporting_text);
TextView label=alert.findViewById(R.id.label);
TextView subtitle=alert.findViewById(R.id.subtitle);
ImageButton clear=alert.findViewById(R.id.clear);
clear.setVisibility(View.GONE);
if(TextUtils.isEmpty(domain)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setText(getString(R.string.need_invite_to_join_server, domain));
}
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
Consumer<String> errorSetter=err->{
supportingText.setText(err);
int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
supportingText.setTextColor(errorColor);
label.setTextColor(errorColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error);
};
next.setOnClickListener(_v->{
Uri inviteLink=Uri.parse(edit.getText().toString());
if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){
errorSetter.accept(getString(R.string.this_invite_is_invalid));
return;
}
UiUtils.showProgressForAlertButton(next, true);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
if(getActivity()==null || !alert.isShowing())
return;
String host=inviteLink.getHost();
inviteCode=result.inviteCode;
inviteCodeHost=host;
Instance instance=instancesCache.get(normalizeInstanceDomain(host));
if(instance==null){
loadInstanceInfo(host, false, err->{
String errorStr;
if(err instanceof String str){
errorStr=str;
}else if(err instanceof Throwable x){
errorStr=x.getMessage();
}else if(err instanceof MastodonErrorResponse mer){
errorStr=mer.error;
}else{
errorStr=getString(R.string.error);
}
errorSetter.accept(errorStr);
UiUtils.showProgressForAlertButton(next, false);
});
}else{
proceedWithAuthOrSignup(instance);
}
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null || !alert.isShowing())
return;
UiUtils.showProgressForAlertButton(next, false);
if(error instanceof MastodonErrorResponse mer){
errorSetter.accept(switch(mer.httpStatus){
case 404 -> getString(R.string.this_invite_is_invalid);
case 401 -> getString(R.string.this_invite_has_expired);
default -> mer.error;
});
}
}
})
.execNoAuth(inviteLink.getHost());
});
next.setEnabled(false);
edit.addTextChangedListener(new SimpleTextWatcher(e->{
boolean wasEmpty=!next.isEnabled();
next.setEnabled(e.length()>0);
if(supportingText.length()>0){
supportingText.setText("");
int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant);
supportingText.setTextColor(regularColor);
label.setTextColor(regularColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field);
}
if(wasEmpty!=(e.length()==0)){
int padEnd;
if(e.length()==0){
clear.setVisibility(View.GONE);
padEnd=V.dp(16);
}else{
clear.setVisibility(View.VISIBLE);
padEnd=V.dp(48);
}
edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom());
}
}));
clear.setOnClickListener(_v->edit.setText(""));
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
edit.setText(clipText);
supportingText.setText(R.string.invite_link_pasted);
}
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
onNextClick(v);
currentInviteLinkAlert=alert;
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
alert.show();
}
@Override
@@ -387,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
filteredData.clear();
if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){
String actualQuery;
if(currentSearchQuery.startsWith("https:")){
actualQuery=Uri.parse(currentSearchQuery).getHost();
}else{
actualQuery=currentSearchQuery;
}
for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){
if(instance.domain.contains(actualQuery)){
filteredData.add(instance);
}
}

View File

@@ -91,6 +91,9 @@ public class InstanceRulesFragment extends ToolbarFragment{
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
}

View File

@@ -4,6 +4,7 @@ import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
@@ -12,35 +13,38 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
private String accountID;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40);
itemLayoutRes=R.layout.item_account_list_onboarding;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.popular_on_mastodon);
setTitle(R.string.onboarding_recommendations_title);
accountID=getArguments().getString("account");
loadData();
}
@@ -49,7 +53,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
@@ -58,9 +61,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
getToolbar().setContentInsetsRelative(V.dp(56), 0);
}
@Override
@@ -69,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)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
}
})
.exec(accountID);
@@ -80,6 +81,19 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
TextView introText=new TextView(getActivity());
introText.setTextAppearance(R.style.m3_body_large);
introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
introText.setText(R.string.onboarding_recommendations_intro);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty())
return;
@@ -155,5 +169,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
}
}

View File

@@ -219,7 +219,9 @@ public class SignupFragment extends ToolbarFragment{
if(!serverSupportedTimezones.contains(timezone))
timezone=null;
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone)
String inviteCode=getArguments().getString("inviteCode");
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
@@ -17,7 +16,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;

View File

@@ -11,6 +11,7 @@ import org.joinmastodon.android.ui.utils.BlurHashDecoder;
import org.joinmastodon.android.ui.utils.BlurHashDrawable;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
@Parcel
@@ -34,11 +35,14 @@ public class Card extends BaseModel{
public String embedUrl;
public String blurhash;
public List<History> history;
public Instant publishedAt;
public transient Drawable blurhashPlaceholder;
@Override
public void postprocess() throws ObjectValidationException{
if(type==null)
type=Type.LINK;
super.postprocess();
if(blurhash!=null){
Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16);
@@ -64,6 +68,7 @@ public class Card extends BaseModel{
", embedUrl='"+embedUrl+'\''+
", blurhash='"+blurhash+'\''+
", history="+history+
", publishedAt="+publishedAt+
'}';
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model.viewmodel;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -7,6 +8,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import java.util.Collections;
@@ -43,4 +45,13 @@ public class AccountViewModel{
}
this.verifiedLink=verifiedLink;
}
public AccountViewModel stripLinksFromBio(){
if(parsedBio instanceof Spannable spannable){
for(LinkSpan span:spannable.getSpans(0, spannable.length(), LinkSpan.class)){
spannable.removeSpan(span);
}
}
return this;
}
}

View File

@@ -90,7 +90,7 @@ public class Snackbar{
if(current!=null)
current.dismiss();
current=this;
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.LAST_APPLICATION_WINDOW, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
lp.width=ViewGroup.LayoutParams.MATCH_PARENT;
lp.height=ViewGroup.LayoutParams.WRAP_CONTENT;
lp.gravity=Gravity.BOTTOM;

View File

@@ -1,6 +1,8 @@
package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
@@ -13,6 +15,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -35,7 +38,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
@Override
public Type getType(){
return Type.CARD;
return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height) ? Type.CARD_LARGE : Type.CARD_COMPACT;
}
@Override
@@ -49,36 +52,65 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView title, description, domain;
private final TextView title, description, domain, timestamp;
private final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
private final View inner;
private final boolean isLarge;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_link_card, parent);
public Holder(Context context, ViewGroup parent, boolean isLarge){
super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent);
this.isLarge=isLarge;
title=findViewById(R.id.title);
description=findViewById(R.id.description);
domain=findViewById(R.id.domain);
timestamp=findViewById(R.id.timestamp);
photo=findViewById(R.id.photo);
findViewById(R.id.inner).setOnClickListener(this::onClick);
inner=findViewById(R.id.inner);
inner.setOnClickListener(this::onClick);
inner.setOutlineProvider(OutlineProviders.roundedRect(12));
inner.setClipToOutline(true);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(LinkCardStatusDisplayItem item){
Card card=item.status.card;
title.setText(card.title);
description.setText(card.description);
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
domain.setText(Uri.parse(card.url).getHost());
if(description!=null){
description.setText(card.description);
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
}
String cardDomain=Uri.parse(card.url).getHost();
if(isLarge && !TextUtils.isEmpty(card.authorName)){
domain.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)+" · "+cardDomain);
}else{
domain.setText(cardDomain);
}
if(card.publishedAt!=null){
timestamp.setVisibility(View.VISIBLE);
timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt));
}else{
timestamp.setVisibility(View.GONE);
}
photo.setImageDrawable(null);
if(item.imgRequest!=null){
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
photo.setBackground(null);
photo.setImageTintList(null);
crossfadeDrawable.setSize(card.width, card.height);
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
didClear=false;
}else{
photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant));
photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
photo.setScaleType(ImageView.ScaleType.CENTER);
photo.setImageResource(R.drawable.ic_feed_48px);
}
}

View File

@@ -26,6 +26,7 @@ import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
import org.joinmastodon.android.ui.photoviewer.AltTextSheet;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
@@ -103,11 +104,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
private final MaxWidthFrameLayout overlays;
private final FrameLayout altTextWrapper;
private final TextView altTextButton;
private final View altTextScroller;
private final ImageButton altTextClose;
private final TextView altText;
private final View sensitiveOverlay;
private final LayerDrawable sensitiveOverlayBG;
@@ -115,9 +111,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final TextView hideSensitiveButton;
private final TextView sensitiveText;
private int altTextIndex=-1;
private Animator altTextAnimator;
public Holder(Activity activity, ViewGroup parent){
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
wrapper=(FrameLayout)itemView;
@@ -129,14 +122,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
overlays.setMaxWidth(V.dp(MediaGridLayout.MAX_WIDTH));
wrapper.addView(overlays, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL));
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, overlays);
altTextWrapper=findViewById(R.id.alt_text_wrapper);
altTextButton=findViewById(R.id.alt_button);
altTextScroller=findViewById(R.id.alt_text_scroller);
altTextClose=findViewById(R.id.alt_text_close);
altText=findViewById(R.id.alt_text);
altTextClose.setOnClickListener(this::onAltTextCloseClick);
hideSensitiveButton=(TextView) activity.getLayoutInflater().inflate(R.layout.alt_text_badge, overlays, false);
hideSensitiveButton.setText(R.string.hide);
FrameLayout.LayoutParams lp;
@@ -160,9 +145,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
public void onBind(MediaGridStatusDisplayItem item){
wrapper.setPadding(0, 0, 0, item.inset ? 0 : V.dp(8));
if(altTextAnimator!=null)
altTextAnimator.cancel();
layout.setTiledLayout(item.tiledLayout);
for(MediaAttachmentViewController c:controllers){
item.viewPool.reuse(c.type, c);
@@ -212,8 +194,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
c.bind(att, item.status);
i++;
}
altTextWrapper.setVisibility(View.GONE);
altTextIndex=-1;
if(!item.sensitiveRevealed){
sensitiveOverlay.setVisibility(View.VISIBLE);
@@ -246,115 +226,9 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
private void onAltTextClick(View v){
if(altTextAnimator!=null)
altTextAnimator.cancel();
v.setVisibility(View.INVISIBLE);
int index=(Integer)v.getTag();
altTextIndex=index;
Attachment att=item.attachments.get(index);
altText.setText(att.description);
altTextWrapper.setVisibility(View.VISIBLE);
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
int[] loc={0, 0};
v.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
overlays.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL, altTextWrapper.getLeft()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT, altTextWrapper.getTop()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth(), altTextWrapper.getRight()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight(), altTextWrapper.getBottom()));
for(Animator a:anims)
a.setDuration(300);
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null && c.altButton!=v){
anims.add(ObjectAnimator.ofFloat(c.altButton, View.ALPHA, 1, 0).setDuration(150));
}
}
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
altTextAnimator=null;
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null){
c.altButton.setVisibility(View.INVISIBLE);
}
}
}
});
altTextAnimator=set;
set.start();
return true;
}
});
}
private void onAltTextCloseClick(View v){
if(altTextAnimator!=null)
altTextAnimator.cancel();
View btn=controllers.get(altTextIndex).altButton;
int i=0;
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null && c.altButton!=btn && !TextUtils.isEmpty(item.attachments.get(i).description))
c.altButton.setVisibility(View.VISIBLE);
i++;
}
int[] loc={0, 0};
btn.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
overlays.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()));
for(Animator a:anims)
a.setDuration(300);
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null && c.altButton!=btn){
anims.add(ObjectAnimator.ofFloat(c.altButton, View.ALPHA, 1).setDuration(150));
}
}
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
altTextAnimator=null;
altTextWrapper.setVisibility(View.GONE);
btn.setVisibility(View.VISIBLE);
btn.setAlpha(1);
}
});
altTextAnimator=set;
set.start();
new AltTextSheet(v.getContext(), att).show();
}
public MediaAttachmentViewController getViewController(int index){

View File

@@ -68,7 +68,8 @@ public abstract class StatusDisplayItem{
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true);
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false);
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
@@ -208,7 +209,8 @@ public abstract class StatusDisplayItem{
AUDIO,
POLL_OPTION,
POLL_FOOTER,
CARD,
CARD_LARGE,
CARD_COMPACT,
FOOTER,
ACCOUNT,
HASHTAG,

View File

@@ -0,0 +1,37 @@
package org.joinmastodon.android.ui.photoviewer;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.views.BottomSheet;
public class AltTextSheet extends BottomSheet{
public AltTextSheet(@NonNull Context context, Attachment attachment){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_alt_text, null);
setContentView(content);
TextView altText=findViewById(R.id.alt_text);
altText.setText(attachment.description);
findViewById(R.id.alt_text_help).setOnClickListener(v->showAltTextHelp());
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
}
private void showAltTextHelp(){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.what_is_alt_text)
.setMessage(UiUtils.fixBulletListInString(getContext(), R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
}

View File

@@ -1,6 +1,10 @@
package org.joinmastodon.android.ui.photoviewer;
import android.Manifest;
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.app.DownloadManager;
@@ -26,6 +30,8 @@ import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.Log;
import android.util.Property;
import android.view.ContextThemeWrapper;
import android.view.DisplayCutout;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -48,8 +54,12 @@ import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileOutputStream;
@@ -85,6 +95,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
private int currentIndex;
private WindowManager wm;
private Listener listener;
private Status status;
private String accountID;
private FrameLayout windowView;
private FragmentRootLinearLayout uiOverlay;
@@ -104,17 +116,32 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(uiVisible)
toggleUI();
};
private Animator currentSheetRelatedToolbarAnimation;
private boolean videoPositionNeedsUpdating;
private Runnable videoPositionUpdater=this::updateVideoPosition;
private int videoDuration, videoInitialPosition, videoLastTimeUpdatePosition;
private long videoInitialPositionTime;
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
private static final Property<FragmentRootLinearLayout, Integer> STATUS_BAR_COLOR_PROPERTY=new Property<>(Integer.class, "Fdsafdsa"){
@Override
public Integer get(FragmentRootLinearLayout object){
return object.getStatusBarColor();
}
@Override
public void set(FragmentRootLinearLayout object, Integer value){
object.setStatusBarColor(value);
}
};
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Status status, String accountID, Listener listener){
this.activity=activity;
this.attachments=attachments.stream().filter(a->a.type==Attachment.Type.IMAGE || a.type==Attachment.Type.GIFV || a.type==Attachment.Type.VIDEO).collect(Collectors.toList());
currentIndex=index;
this.listener=listener;
this.status=status;
this.accountID=accountID;
wm=activity.getWindowManager();
@@ -175,9 +202,15 @@ public class PhotoViewer implements ZoomPanView.Listener{
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_download_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
if(status!=null)
toolbar.getMenu().add(R.string.info).setIcon(R.drawable.ic_info_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
else
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_download_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.setOnMenuItemClickListener(item->{
saveCurrentFile();
if(status!=null)
showInfoSheet();
else
saveCurrentFile();
return true;
});
uiOverlay.setAlpha(0f);
@@ -610,6 +643,93 @@ public class PhotoViewer implements ZoomPanView.Listener{
}
}
private void showInfoSheet(){
pauseVideo();
PhotoViewerInfoSheet sheet=new PhotoViewerInfoSheet(new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark), attachments.get(currentIndex), toolbar.getHeight(), new PhotoViewerInfoSheet.Listener(){
private boolean ignoreBeforeDismiss;
@Override
public void onBeforeDismiss(int duration){
if(ignoreBeforeDismiss)
return;
if(currentSheetRelatedToolbarAnimation!=null)
currentSheetRelatedToolbarAnimation.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(pager, View.TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(toolbarWrap, View.ALPHA, 1f),
ObjectAnimator.ofArgb(uiOverlay, STATUS_BAR_COLOR_PROPERTY, 0x80000000)
);
set.setDuration(duration);
set.setInterpolator(CubicBezierInterpolator.EASE_OUT);
currentSheetRelatedToolbarAnimation=set;
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentSheetRelatedToolbarAnimation=null;
}
});
set.start();
}
@Override
public void onDismissEntireViewer(){
ignoreBeforeDismiss=true;
onStartSwipeToDismissTransition(0);
}
@Override
public void onButtonClick(int id){
if(id==R.id.btn_boost){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setReblogged(status, !status.reblogged);
}
}else if(id==R.id.btn_favorite){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setFavorited(status, !status.favourited);
}
}else if(id==R.id.btn_share){
if(status!=null){
UiUtils.openSystemShareSheet(activity, status.url);
}
}else if(id==R.id.btn_bookmark){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked);
}
}else if(id==R.id.btn_download){
saveCurrentFile();
}
}
});
sheet.setStatus(status);
sheet.show();
if(currentSheetRelatedToolbarAnimation!=null)
currentSheetRelatedToolbarAnimation.cancel();
sheet.getWindow().getDecorView().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
sheet.getWindow().getDecorView().getViewTreeObserver().removeOnPreDrawListener(this);
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(pager, View.TRANSLATION_Y, -pager.getHeight()*0.2f),
ObjectAnimator.ofFloat(toolbarWrap, View.ALPHA, 0f),
ObjectAnimator.ofArgb(uiOverlay, STATUS_BAR_COLOR_PROPERTY, 0)
);
set.setDuration(300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
currentSheetRelatedToolbarAnimation=set;
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentSheetRelatedToolbarAnimation=null;
}
});
set.start();
return true;
}
});
}
public interface Listener{
void setPhotoViewVisibility(int index, boolean visible);

View File

@@ -0,0 +1,180 @@
package org.joinmastodon.android.ui.photoviewer;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class PhotoViewerInfoSheet extends BottomSheet{
private final Attachment attachment;
private final View buttonsContainer;
private final TextView altText;
private final ImageButton backButton, infoButton;
private final Button boostBtn, favoriteBtn, bookmarkBtn;
private final Listener listener;
private String statusID;
public PhotoViewerInfoSheet(@NonNull Context context, Attachment attachment, int toolbarHeight, Listener listener){
super(context);
this.attachment=attachment;
this.listener=listener;
dimAmount=0;
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_photo_viewer_info, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
buttonsContainer=findViewById(R.id.buttons_container);
altText=findViewById(R.id.alt_text);
if(TextUtils.isEmpty(attachment.description)){
findViewById(R.id.alt_text).setVisibility(View.GONE);
findViewById(R.id.alt_text_title).setVisibility(View.GONE);
findViewById(R.id.divider).setVisibility(View.GONE);
}else{
altText.setText(attachment.description);
findViewById(R.id.alt_text_help).setOnClickListener(v->showAltTextHelp());
}
backButton=new ImageButton(context);
backButton.setImageResource(R.drawable.ic_arrow_back);
backButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
backButton.setBackgroundResource(R.drawable.bg_button_m3_tonal_icon);
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
backButton.setElevation(V.dp(2));
backButton.setAlpha(0f);
backButton.setOnClickListener(v->{
listener.onDismissEntireViewer();
dismiss();
});
infoButton=new ImageButton(context);
infoButton.setImageResource(R.drawable.ic_info_fill1_24px);
infoButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnPrimary)));
infoButton.setBackgroundResource(R.drawable.bg_button_m3_filled_icon);
infoButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
infoButton.setElevation(V.dp(2));
infoButton.setAlpha(0f);
infoButton.setSelected(true);
infoButton.setOnClickListener(v->dismiss());
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));
lp.topMargin=toolbarHeight/2-V.dp(24);
lp.leftMargin=lp.rightMargin=V.dp(4);
lp.gravity=Gravity.START | Gravity.TOP;
container.addView(backButton, lp);
lp=new FrameLayout.LayoutParams(lp);
lp.leftMargin=lp.rightMargin=0;
lp.gravity=Gravity.END | Gravity.TOP;
container.addView(infoButton, lp);
boostBtn=findViewById(R.id.btn_boost);
favoriteBtn=findViewById(R.id.btn_favorite);
bookmarkBtn=findViewById(R.id.btn_bookmark);
View.OnClickListener clickListener=v->listener.onButtonClick(v.getId());
boostBtn.setOnClickListener(clickListener);
favoriteBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_share).setOnClickListener(clickListener);
bookmarkBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_download).setOnClickListener(clickListener);
}
private void showAltTextHelp(){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.what_is_alt_text)
.setMessage(UiUtils.fixBulletListInString(getContext(), R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
@Override
public void dismiss(){
if(dismissed)
return;
int height=content.getHeight();
int duration=Math.max(60, (int) (180 * (height - content.getTranslationY()) / (float) height));
listener.onBeforeDismiss(duration);
backButton.animate().alpha(0).setDuration(duration).setInterpolator(CubicBezierInterpolator.EASE_OUT).start();
infoButton.animate().alpha(0).setDuration(duration).setInterpolator(CubicBezierInterpolator.EASE_OUT).start();
super.dismiss();
E.unregister(this);
}
@Override
public void show(){
super.show();
E.register(this);
content.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
content.getViewTreeObserver().removeOnPreDrawListener(this);
backButton.animate().alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
infoButton.animate().alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
return true;
}
});
}
public void setStatus(Status status){
statusID=status.id;
boostBtn.setCompoundDrawablesWithIntrinsicBounds(0, switch(status.visibility){
case DIRECT -> R.drawable.ic_boost_disabled_24px;
case PUBLIC, UNLISTED -> R.drawable.ic_boost;
case PRIVATE -> R.drawable.ic_boost_private;
}, 0, 0);
boostBtn.setEnabled(status.visibility!=StatusPrivacy.DIRECT);
setButtonStates(status.reblogged, status.favourited, status.bookmarked);
}
@Subscribe
public void onCountersUpdated(StatusCountersUpdatedEvent ev){
if(ev.id.equals(statusID)){
setButtonStates(ev.reblogged, ev.favorited, ev.bookmarked);
}
}
private void setButtonStates(boolean reblogged, boolean favorited, boolean bookmarked){
boostBtn.setText(reblogged ? R.string.button_reblogged : R.string.button_reblog);
boostBtn.setSelected(reblogged);
favoriteBtn.setText(favorited ? R.string.button_favorited : R.string.button_favorite);
favoriteBtn.setSelected(favorited);
bookmarkBtn.setText(bookmarked ? R.string.bookmarked : R.string.add_bookmark);
bookmarkBtn.setSelected(bookmarked);
}
public interface Listener{
void onBeforeDismiss(int duration);
void onDismissEntireViewer();
void onButtonClick(int id);
}
}

View File

@@ -0,0 +1,90 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.InsetDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public abstract class AccountRestrictionConfirmationSheet extends BottomSheet{
private LinearLayout contentWrap;
protected Button cancelBtn;
protected ProgressBarButton confirmBtn, secondaryBtn;
protected TextView titleView, subtitleView;
protected ImageView icon;
protected boolean loading;
public AccountRestrictionConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_restrict_account, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
contentWrap=findViewById(R.id.content_wrap);
titleView=findViewById(R.id.title);
subtitleView=findViewById(R.id.text);
cancelBtn=findViewById(R.id.btn_cancel);
confirmBtn=findViewById(R.id.btn_confirm);
secondaryBtn=findViewById(R.id.btn_secondary);
icon=findViewById(R.id.icon);
contentWrap.setDividerDrawable(new EmptyDrawable(1, V.dp(8)));
contentWrap.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
confirmBtn.setOnClickListener(v->{
if(loading)
return;
loading=true;
confirmBtn.setProgressBarVisible(true);
confirmCallback.onConfirmed(this::dismiss, ()->{
confirmBtn.setProgressBarVisible(false);
loading=false;
});
});
cancelBtn.setOnClickListener(v->{
if(!loading)
dismiss();
});
}
protected void addRow(@DrawableRes int icon, CharSequence text){
TextView tv=new TextView(getContext());
tv.setTextAppearance(R.style.m3_body_large);
tv.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
tv.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)));
tv.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
tv.setText(text);
InsetDrawable drawable=new InsetDrawable(getContext().getResources().getDrawable(icon, getContext().getTheme()), V.dp(8));
drawable.setBounds(0, 0, V.dp(40), V.dp(40));
tv.setCompoundDrawablesRelative(drawable, null, null, null);
tv.setCompoundDrawablePadding(V.dp(16));
contentWrap.addView(tv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
protected void addRow(@DrawableRes int icon, @StringRes int text){
addRow(icon, getContext().getString(text));
}
public interface ConfirmCallback{
void onConfirmed(Runnable onSuccess, Runnable onError);
}
}

View File

@@ -1,14 +1,12 @@
package org.joinmastodon.android.ui;
package org.joinmastodon.android.ui.sheets;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -25,6 +23,9 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.ui.ClickableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
@@ -37,7 +38,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import androidx.annotation.NonNull;
public class BlockAccountConfirmationSheet extends AccountRestrictionConfirmationSheet{
public BlockAccountConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context, user, confirmCallback);
titleView.setText(R.string.block_user_confirm_title);
confirmBtn.setText(R.string.do_block);
secondaryBtn.setVisibility(View.GONE);
icon.setImageResource(R.drawable.ic_block_24px);
subtitleView.setText(user.getDisplayUsername());
addRow(R.drawable.ic_campaign_24px, R.string.user_can_see_blocked);
addRow(R.drawable.ic_visibility_off_24px, R.string.user_cant_see_each_other_posts);
addRow(R.drawable.ic_alternate_email_24px, R.string.you_wont_see_user_mentions);
addRow(R.drawable.ic_reply_24px, R.string.user_cant_mention_or_follow_you);
}
}

View File

@@ -0,0 +1,36 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import androidx.annotation.NonNull;
public class BlockDomainConfirmationSheet extends AccountRestrictionConfirmationSheet{
public BlockDomainConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback, ConfirmCallback blockUserConfirmCallback){
super(context, user, confirmCallback);
titleView.setText(R.string.block_domain_confirm_title);
confirmBtn.setText(R.string.do_block_server);
secondaryBtn.setText(context.getString(R.string.block_user_x_instead, user.getDisplayUsername()));
icon.setImageResource(R.drawable.ic_domain_disabled_24px);
subtitleView.setText(user.getDomain());
addRow(R.drawable.ic_campaign_24px, R.string.users_cant_see_blocked);
addRow(R.drawable.ic_visibility_off_24px, R.string.you_wont_see_server_posts);
addRow(R.drawable.ic_person_remove_24px, R.string.server_followers_will_be_removed);
addRow(R.drawable.ic_reply_24px, R.string.server_cant_mention_or_follow_you);
addRow(R.drawable.ic_history_24px, R.string.server_can_interact_with_older);
secondaryBtn.setOnClickListener(v->{
if(loading)
return;
loading=true;
secondaryBtn.setProgressBarVisible(true);
blockUserConfirmCallback.onConfirmed(this::dismiss, ()->{
secondaryBtn.setProgressBarVisible(false);
loading=false;
});
});
}
}

View File

@@ -0,0 +1,101 @@
package org.joinmastodon.android.ui.sheets;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.RippleAnimationTextView;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeVisitor;
import androidx.annotation.NonNull;
import me.grishka.appkit.views.BottomSheet;
public class DecentralizationExplainerSheet extends BottomSheet{
private final String handleStr;
public DecentralizationExplainerSheet(@NonNull Context context, String accountID, Account account){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_decentralization_info, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
TextView handleTitle=findViewById(R.id.handle_title);
RippleAnimationTextView handle=findViewById(R.id.handle);
TextView usernameExplanation=findViewById(R.id.username_text);
TextView serverExplanation=findViewById(R.id.server_text);
TextView handleExplanation=findViewById(R.id.handle_explanation);
findViewById(R.id.btn_cancel).setOnClickListener(v->dismiss());
String domain=account.getDomain();
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
handleStr="@"+account.username+"@"+domain;
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
handleTitle.setText(isSelf ? R.string.handle_title_own : R.string.handle_title);
handle.setText(handleStr);
usernameExplanation.setText(isSelf ? R.string.handle_username_explanation_own : R.string.handle_username_explanation);
serverExplanation.setText(isSelf ? R.string.handle_server_explanation_own : R.string.handle_server_explanation);
String explanation=context.getString(isSelf ? R.string.handle_explanation_own : R.string.handle_explanation);
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(explanation).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", DecentralizationExplainerSheet.this::showActivityPubAlert, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
handleExplanation.setText(ssb);
findViewById(R.id.handle_wrap).setOnClickListener(v->{
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, handleStr));
if(UiUtils.needShowClipboardToast()){
new Snackbar.Builder(context)
.setText(R.string.handle_copied)
.show();
}
});
String _domain=domain;
findViewById(R.id.username_row).setOnClickListener(v->handle.animate(1, account.username.length()+1));
findViewById(R.id.server_row).setOnClickListener(v->handle.animate(handleStr.length()-_domain.length(), handleStr.length()));
}
private void showActivityPubAlert(LinkSpan s){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.what_is_activitypub_title)
.setMessage(R.string.what_is_activitypub)
.setPositiveButton(R.string.ok, null)
.show();
}
}

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import androidx.annotation.NonNull;
public class MuteAccountConfirmationSheet extends AccountRestrictionConfirmationSheet{
public MuteAccountConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context, user, confirmCallback);
titleView.setText(R.string.mute_user_confirm_title);
confirmBtn.setText(R.string.do_mute);
secondaryBtn.setVisibility(View.GONE);
icon.setImageResource(R.drawable.ic_volume_off_24px);
subtitleView.setText(user.getDisplayUsername());
addRow(R.drawable.ic_campaign_24px, R.string.user_wont_know_muted);
addRow(R.drawable.ic_visibility_off_24px, R.string.user_can_still_see_your_posts);
addRow(R.drawable.ic_alternate_email_24px, R.string.you_wont_see_user_mentions);
addRow(R.drawable.ic_reply_24px, R.string.user_can_mention_and_follow_you);
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.ui;
package org.joinmastodon.android.ui.sheets;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -12,7 +12,9 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -25,7 +27,7 @@ public class NonMutualPreReplySheet extends PreReplySheet{
private boolean fullBioShown=false;
@SuppressLint("DefaultLocale")
public NonMutualPreReplySheet(@NonNull Context context, ResultListener resultListener, Account account){
public NonMutualPreReplySheet(@NonNull Context context, ResultListener resultListener, Account account, String accountID){
super(context, resultListener);
icon.setImageResource(R.drawable.ic_waving_hand_24px);
title.setText(R.string.non_mutual_sheet_title);
@@ -55,11 +57,16 @@ public class NonMutualPreReplySheet extends PreReplySheet{
name.setEllipsize(TextUtils.TruncateAt.END);
name.setTextAppearance(R.style.m3_title_medium);
name.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
name.setText(account.displayName);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames){
name.setText(HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
UiUtils.loadCustomEmojiInTextView(name);
}else{
name.setText(account.displayName);
}
name.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
nameAndFields.addView(name, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
if(!TextUtils.isEmpty(account.note)){
String strippedBio=HtmlParser.stripAndRemoveInvisibleSpans(account.note);
CharSequence strippedBio=HtmlParser.parseCustomEmoji(HtmlParser.stripAndRemoveInvisibleSpans(account.note), account.emojis);
TextView bioShort=new TextView(context);
bioShort.setTextAppearance(R.style.m3_body_medium);
bioShort.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
@@ -85,6 +92,8 @@ public class NonMutualPreReplySheet extends PreReplySheet{
bioShort.setVisibility(View.VISIBLE);
}
});
UiUtils.loadCustomEmojiInTextView(bioShort);
UiUtils.loadCustomEmojiInTextView(bioFull);
}else{
TextView username=new TextView(context);
username.setTextAppearance(R.style.m3_body_medium);

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.ui;
package org.joinmastodon.android.ui.sheets;
import android.content.Context;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.ui;
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;

View File

@@ -52,6 +52,7 @@ public class HtmlParser{
")" +
")";
public static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
public static final Pattern INVITE_LINK_PATTERN=Pattern.compile("^https://"+Regex.URL_VALID_DOMAIN+"/invite/[a-z\\d]+$", Pattern.CASE_INSENSITIVE);
private static Pattern EMOJI_CODE_PATTERN=Pattern.compile(":([\\w]+):");
private HtmlParser(){}

View File

@@ -0,0 +1,67 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.style.ImageSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class ImageSpanThatDoesNotBreakShitForNoGoodReason extends ImageSpan{
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Bitmap b){
super(b);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Bitmap b, int verticalAlignment){
super(b, verticalAlignment);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Bitmap bitmap){
super(context, bitmap);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Bitmap bitmap, int verticalAlignment){
super(context, bitmap, verticalAlignment);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable){
super(drawable);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, int verticalAlignment){
super(drawable, verticalAlignment);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, @NonNull String source){
super(drawable, source);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, @NonNull String source, int verticalAlignment){
super(drawable, source, verticalAlignment);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Uri uri){
super(context, uri);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Uri uri, int verticalAlignment){
super(context, uri, verticalAlignment);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, int resourceId){
super(context, resourceId);
}
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, int resourceId, int verticalAlignment){
super(context, resourceId, verticalAlignment);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
// Purposefully not touching the font metrics
return getDrawable().getBounds().right;
}
}

View File

@@ -87,7 +87,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
StatusDisplayItem.Type type=sdi.getItem().getType();
if(type==StatusDisplayItem.Type.CARD || type==StatusDisplayItem.Type.MEDIA_GRID)
if(type==StatusDisplayItem.Type.CARD_LARGE || type==StatusDisplayItem.Type.MEDIA_GRID)
outRect.left=outRect.right=V.dp(16);
else
outRect.left=outRect.right=V.dp(8);

View File

@@ -11,9 +11,11 @@ import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -26,11 +28,14 @@ import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BulletSpan;
import android.transition.ChangeBounds;
import android.transition.ChangeScroll;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -38,7 +43,9 @@ import android.view.ViewGroup;
import android.view.WindowInsets;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
@@ -68,6 +75,10 @@ import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.sheets.BlockAccountConfirmationSheet;
import org.joinmastodon.android.ui.sheets.BlockDomainConfirmationSheet;
import org.joinmastodon.android.ui.sheets.MuteAccountConfirmationSheet;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.parceler.Parcels;
@@ -375,72 +386,142 @@ public class UiUtils{
}
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
resultCallback.accept(result);
if(!currentlyBlocked){
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
}
if(!currentlyBlocked){
new BlockAccountConfirmationSheet(activity, account, (onSuccess, onError)->{
new SetAccountBlocked(account.id, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
resultCallback.accept(result);
onSuccess.run();
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
});
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
onError.run();
}
})
.exec(accountID);
}).show();
}else{
new SetAccountBlocked(account.id, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
resultCallback.accept(result);
new Snackbar.Builder(activity)
.setText(activity.getString(R.string.unblocked_user_x, account.getDisplayUsername()))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
}
}
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
resultCallback.run();
}
public static void confirmToggleBlockDomain(Activity activity, String accountID, Account account, boolean currentlyBlocked, Runnable resultCallback, Consumer<Relationship> callbackInCaseUserWasBlockedInstead){
if(!currentlyBlocked){
new BlockDomainConfirmationSheet(activity, account, (onSuccess, onError)->{
new SetDomainBlocked(account.getDomain(), true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
resultCallback.run();
onSuccess.run();
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
});
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
onError.run();
}
})
.exec(accountID);
}, (onSuccess, onError)->{
new SetAccountBlocked(account.id, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
callbackInCaseUserWasBlockedInstead.accept(result);
onSuccess.run();
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
onError.run();
}
})
.exec(accountID);
}).show();
}else{
new SetDomainBlocked(account.getDomain(), false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
resultCallback.run();
new Snackbar.Builder(activity)
.setText(activity.getString(R.string.unblocked_domain_x, account.getDomain()))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
}
}
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), ()->{
new SetAccountMuted(account.id, !currentlyMuted)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
resultCallback.accept(result);
if(!currentlyMuted){
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
}
if(!currentlyMuted){
new MuteAccountConfirmationSheet(activity, account, (onSuccess, onError)->{
new SetAccountMuted(account.id, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
resultCallback.accept(result);
onSuccess.run();
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
});
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
onError.run();
}
})
.exec(accountID);
}).show();
}else{
new SetAccountMuted(account.id, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
resultCallback.accept(result);
new Snackbar.Builder(activity)
.setText(activity.getString(R.string.unmuted_user_x, account.getDisplayUsername()))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, false)
.exec(accountID);
}
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
@@ -848,11 +929,15 @@ public class UiUtils{
public static void maybeShowTextCopiedToast(Context context){
//show toast, android from S_V2 on has built-in popup, as documented in
//https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications
if(Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2){
if(needShowClipboardToast()){
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
}
}
public static boolean needShowClipboardToast(){
return Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2;
}
public static void setAllPaddings(View view, int paddingDp){
int pad=V.dp(paddingDp);
view.setPadding(pad, pad, pad, pad);
@@ -866,4 +951,46 @@ public class UiUtils{
lp.setMarginEnd(V.dp(marginEnd));
return lp;
}
public static CharSequence fixBulletListInString(Context context, @StringRes int res){
SpannableStringBuilder msg=new SpannableStringBuilder(context.getText(res));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(context, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
return msg;
}
public static void showProgressForAlertButton(Button button, boolean show){
boolean shown=button.getTag(R.id.button_progress_orig_color)!=null;
if(shown==show)
return;
button.setEnabled(!show);
if(show){
ColorStateList origColor=button.getTextColors();
button.setTag(R.id.button_progress_orig_color, origColor);
button.setTextColor(0);
ProgressBar progressBar=(ProgressBar) LayoutInflater.from(button.getContext()).inflate(R.layout.progress_bar, null);
Drawable progress=progressBar.getIndeterminateDrawable().mutate();
progress.setTint(getThemeColor(button.getContext(), R.attr.colorM3OnSurface) & 0x60ffffff);
if(progress instanceof Animatable a)
a.start();
LayerDrawable layerList=new LayerDrawable(new Drawable[]{progress});
layerList.setLayerGravity(0, Gravity.CENTER);
layerList.setLayerSize(0, V.dp(24), V.dp(24));
layerList.setBounds(0, 0, button.getWidth(), button.getHeight());
button.getOverlay().add(layerList);
}else{
button.getOverlay().clear();
ColorStateList origColor=(ColorStateList) button.getTag(R.id.button_progress_orig_color);
button.setTag(R.id.button_progress_orig_color, null);
button.setTextColor(origColor);
}
}
}

View File

@@ -278,7 +278,7 @@ public class ComposeAutocompleteViewController{
@Override
public void onSuccess(SearchResults result){
currentRequest=null;
if(result.hashtags.isEmpty() || (result.hashtags.size()==1 && result.hashtags.get(0).name.equals(lastText.substring(1))))
if(result.hashtags.isEmpty() || (result.hashtags.size()==1 && result.hashtags.get(0).name.equals(lastText.substring(1))) || mode!=Mode.HASHTAGS)
return;
List<Hashtag> oldList=hashtags;
hashtags=result.hashtags;

View File

@@ -28,7 +28,6 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -90,14 +89,14 @@ public class ComposeLanguageAlertViewController{
if(previouslySelected!=null){
if(previouslySelected.index!=-1 && ((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale)))){
(previouslySelected.index<specialLocales.size()+allLocales.size() && previouslySelected.index>-1 && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale)))){
selectedIndex=previouslySelected.index;
selectedLocale=previouslySelected.locale;
}else{
int i=0;
boolean found=false;
for(SpecialLocaleInfo li:specialLocales){
if(li.locale.equals(previouslySelected.locale)){
if(null!=li.locale&&li.locale.equals(previouslySelected.locale)){
selectedLocale=li.locale;
selectedIndex=i;
found=true;

View File

@@ -44,6 +44,7 @@ import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import androidx.annotation.LayoutRes;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -53,7 +54,7 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username, followers, verifiedLink, bio;
private final ImageView avatar;
public final ImageView avatar;
private final ProgressBarButton button;
private final PopupMenu contextMenu;
private final View menuAnchor;
@@ -75,7 +76,11 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
private boolean checked;
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment.getActivity(), R.layout.item_account_list, list);
this(fragment, list, relationships, R.layout.item_account_list);
}
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships, @LayoutRes int layout){
super(fragment.getActivity(), layout, list);
this.fragment=fragment;
this.accountID=Objects.requireNonNull(fragment.getArguments().getString("account"));
this.relationships=relationships;
@@ -111,24 +116,28 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
public void onBind(AccountViewModel item){
name.setText(item.parsedName);
username.setText("@"+item.account.acct);
String followersStr=fragment.getResources().getQuantityString(R.plurals.x_followers, item.account.followersCount>1000 ? 999 : (int)item.account.followersCount);
String followersNum=UiUtils.abbreviateNumber(item.account.followersCount);
int index=followersStr.indexOf("%,d");
followersStr=followersStr.replace("%,d", followersNum);
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
if(index!=-1){
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
if(followers!=null){
String followersStr=fragment.getResources().getQuantityString(R.plurals.x_followers, item.account.followersCount>1000 ? 999 : (int)item.account.followersCount);
String followersNum=UiUtils.abbreviateNumber(item.account.followersCount);
int index=followersStr.indexOf("%,d");
followersStr=followersStr.replace("%,d", followersNum);
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
if(index!=-1){
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
}
followers.setText(followersFormatted);
}
if(verifiedLink!=null){
boolean hasVerifiedLink=item.verifiedLink!=null;
if(!hasVerifiedLink)
verifiedLink.setText(R.string.no_verified_link);
else
verifiedLink.setText(item.verifiedLink);
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_check_small_16px : R.drawable.ic_help_16px, 0, 0, 0);
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
verifiedLink.setTextColor(tintColor);
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
}
followers.setText(followersFormatted);
boolean hasVerifiedLink=item.verifiedLink!=null;
if(!hasVerifiedLink)
verifiedLink.setText(R.string.no_verified_link);
else
verifiedLink.setText(item.verifiedLink);
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_check_small_16px : R.drawable.ic_help_16px, 0, 0, 0);
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
verifiedLink.setTextColor(tintColor);
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
bindRelationship();
if(showBio){
bio.setText(item.parsedBio);
@@ -243,10 +252,10 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(fragment.getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(fragment.getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
UiUtils.confirmToggleBlockDomain(fragment.getActivity(), accountID, account, relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
bindRelationship();
});
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
.setCallback(new Callback<>(){
@@ -338,7 +347,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
menu.findItem(R.id.share).setTitle(R.string.share_user);
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));

View File

@@ -1,11 +1,14 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.ImageView;
import org.joinmastodon.android.R;
public class FixedAspectRatioImageView extends ImageView{
private float aspectRatio=1;
private float aspectRatio;
private boolean useHeight;
public FixedAspectRatioImageView(Context context){
@@ -18,6 +21,10 @@ public class FixedAspectRatioImageView extends ImageView{
public FixedAspectRatioImageView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FixedAspectRatioImageView);
aspectRatio=ta.getFloat(R.styleable.FixedAspectRatioImageView_aspectRatio, 1);
useHeight=ta.getBoolean(R.styleable.FixedAspectRatioImageView_useHeight, false);
ta.recycle();
}
@Override

View File

@@ -1,23 +1,42 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import org.joinmastodon.android.R;
public class ProgressBarButton extends Button{
private boolean textVisible=true;
private ProgressBar progressBar;
private int progressBarID;
public ProgressBarButton(Context context){
super(context);
this(context, null);
}
public ProgressBarButton(Context context, AttributeSet attrs){
super(context, attrs);
this(context, attrs, 0);
}
public ProgressBarButton(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
public ProgressBarButton(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.ProgressBarButton);
progressBarID=ta.getResourceId(R.styleable.ProgressBarButton_progressBar, 0);
ta.recycle();
}
@Override
protected void onAttachedToWindow(){
super.onAttachedToWindow();
if(progressBarID!=0){
progressBar=((ViewGroup)getParent()).findViewById(progressBarID);
}
}
public void setTextVisible(boolean textVisible){
@@ -29,6 +48,19 @@ public class ProgressBarButton extends Button{
return textVisible;
}
public void setProgressBarVisible(boolean visible){
if(progressBar==null)
throw new IllegalStateException("progressBar is not set");
if(visible){
setTextVisible(false);
progressBar.setIndeterminateTintList(getTextColors());
progressBar.setVisibility(View.VISIBLE);
}else{
setTextVisible(true);
progressBar.setVisibility(View.GONE);
}
}
@Override
protected void onDraw(Canvas canvas){
if(textVisible){

View File

@@ -0,0 +1,170 @@
package org.joinmastodon.android.ui.views;
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import me.grishka.appkit.utils.CustomViewHelper;
public class RippleAnimationTextView extends TextView implements CustomViewHelper{
private final Paint animationPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private CharacterAnimationState[] charStates;
private final ArgbEvaluator colorEvaluator=new ArgbEvaluator();
private int runningAnimCount=0;
private Runnable[] delayedAnimations1, delayedAnimations2;
public RippleAnimationTextView(Context context){
this(context, null);
}
public RippleAnimationTextView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public RippleAnimationTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter){
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if(charStates!=null){
for(CharacterAnimationState state:charStates){
state.colorAnimation.cancel();
state.shadowAnimation.cancel();
state.scaleAnimation.cancel();
}
for(Runnable r:delayedAnimations1){
if(r!=null)
removeCallbacks(r);
}
for(Runnable r:delayedAnimations2){
if(r!=null)
removeCallbacks(r);
}
}
charStates=new CharacterAnimationState[lengthAfter];
delayedAnimations1=new Runnable[lengthAfter];
delayedAnimations2=new Runnable[lengthAfter];
}
@Override
protected void onDraw(Canvas canvas){
if(runningAnimCount==0 && !areThereDelayedAnimations()){
super.onDraw(canvas);
return;
}
Layout layout=getLayout();
animationPaint.set(getPaint());
CharSequence text=layout.getText();
for(int i=0;i<layout.getLineCount();i++){
int baseline=layout.getLineBaseline(i);
for(int offset=layout.getLineStart(i); offset<layout.getLineEnd(i); offset++){
float x=layout.getPrimaryHorizontal(offset);
CharacterAnimationState state=charStates[offset];
if(state==null || state.scaleAnimation==null){
animationPaint.setColor(getCurrentTextColor());
animationPaint.clearShadowLayer();
canvas.drawText(text, offset, offset+1, x, baseline, animationPaint);
}else{
animationPaint.setColor((int)colorEvaluator.evaluate(Math.max(0, Math.min(1, state.color.getValue())), getCurrentTextColor(), getLinkTextColors().getDefaultColor()));
float scale=state.scale.getValue();
int shadowAlpha=Math.round(255*Math.max(0, Math.min(1, state.shadowAlpha.getValue())));
animationPaint.setShadowLayer(dp(4), 0, dp(3), (getPaint().linkColor & 0xFFFFFF) | (shadowAlpha << 24));
canvas.save();
canvas.scale(scale, scale, x, baseline);
canvas.drawText(text, offset, offset+1, x, baseline, animationPaint);
canvas.restore();
}
}
}
invalidate();
}
public void animate(int startIndex, int endIndex){
for(int i=startIndex;i<endIndex;i++){
CharacterAnimationState _state=charStates[i];
if(_state==null){
_state=charStates[i]=new CharacterAnimationState();
}
CharacterAnimationState state=_state;
int finalI=i;
postOnAnimationDelayed(()->{
if(!state.colorAnimation.isRunning())
runningAnimCount++;
state.colorAnimation.animateToFinalPosition(1f);
if(!state.shadowAnimation.isRunning())
runningAnimCount++;
state.shadowAnimation.animateToFinalPosition(0.3f);
if(!state.scaleAnimation.isRunning())
runningAnimCount++;
state.scaleAnimation.animateToFinalPosition(1.2f);
invalidate();
if(delayedAnimations1[finalI]!=null)
removeCallbacks(delayedAnimations1[finalI]);
if(delayedAnimations2[finalI]!=null)
removeCallbacks(delayedAnimations2[finalI]);
Runnable delay1=()->{
if(!state.colorAnimation.isRunning())
runningAnimCount++;
state.colorAnimation.animateToFinalPosition(0f);
if(!state.shadowAnimation.isRunning())
runningAnimCount++;
state.shadowAnimation.animateToFinalPosition(0f);
invalidate();
delayedAnimations1[finalI]=null;
};
Runnable delay2=()->{
if(!state.scaleAnimation.isRunning())
runningAnimCount++;
state.scaleAnimation.animateToFinalPosition(1f);
delayedAnimations2[finalI]=null;
};
delayedAnimations1[finalI]=delay1;
delayedAnimations2[finalI]=delay2;
postOnAnimationDelayed(delay1, 2000);
postOnAnimationDelayed(delay2, 100);
}, 20L*(i-startIndex));
}
}
private boolean areThereDelayedAnimations(){
for(Runnable r:delayedAnimations1){
if(r!=null)
return true;
}
for(Runnable r:delayedAnimations2){
if(r!=null)
return true;
}
return false;
}
private class CharacterAnimationState extends FloatValueHolder{
private final SpringAnimation scaleAnimation, colorAnimation, shadowAnimation;
private final FloatValueHolder scale=new FloatValueHolder(1), color=new FloatValueHolder(), shadowAlpha=new FloatValueHolder();
public CharacterAnimationState(){
scaleAnimation=new SpringAnimation(scale);
colorAnimation=new SpringAnimation(color);
shadowAnimation=new SpringAnimation(shadowAlpha);
setupSpring(scaleAnimation);
setupSpring(colorAnimation);
setupSpring(shadowAnimation);
}
private void setupSpring(SpringAnimation anim){
anim.setMinimumVisibleChange(0.01f);
anim.setSpring(new SpringForce().setStiffness(500f).setDampingRatio(0.175f));
anim.addEndListener((animation, canceled, value, velocity)->runningAnimCount--);
}
}
}

View File

@@ -1,15 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<item android:bottom="-28dp">
<shape android:tint="@color/m3_primary_alpha5" android:tintMode="src_over">
<solid android:color="?colorM3Surface"/>
<corners android:topLeftRadius="28dp" android:topRightRadius="28dp"/>
</shape>
</item>
<item>
<shape android:tint="?colorM3Primary">
<solid android:color="#0D000000"/>
<corners android:topLeftRadius="28dp" android:topRightRadius="28dp"/>
<corners android:radius="28dp"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/m3_primary_overlay" xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:tint="@color/m3_primary_alpha5" android:tintMode="src_over">
<solid android:color="?colorM3Surface"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>

View File

@@ -5,7 +5,7 @@
<item>
<shape>
<solid android:color="?colorM3Primary"/>
<corners android:radius="20dp"/>
<corners android:radius="100dp"/>
</shape>
</item>
</ripple>
@@ -13,7 +13,7 @@
<item>
<shape>
<solid android:color="?colorM3DisabledBackground"/>
<corners android:radius="20dp"/>
<corners android:radius="100dp"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/bg_button_m3_filled" android:inset="4dp">
</inset>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/m3_primary_overlay" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="8dp"/>
</shape>
</item>
</ripple>

View File

@@ -5,7 +5,7 @@
<item>
<shape>
<solid android:color="?colorM3SecondaryContainer"/>
<corners android:radius="20dp"/>
<corners android:radius="100dp"/>
</shape>
</item>
</ripple>
@@ -13,7 +13,7 @@
<item>
<shape>
<solid android:color="?colorM3DisabledBackground"/>
<corners android:radius="20dp"/>
<corners android:radius="100dp"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/bg_button_m3_tonal" android:inset="4dp">
</inset>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:tint="@color/m3_primary_alpha11"
android:tintMode="src_over">
<solid android:color="?colorM3Surface" />
<corners android:radius="8dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:width="2dp" android:color="?colorM3OutlineVariant" android:dashWidth="5dp" android:dashGap="5dp"/>
<corners android:radius="8dp"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector>
<item android:state_enabled="true">
<shape>
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
<solid android:color="?colorM3SurfaceVariant"/>
</shape>
</item>
<item>
<shape android:tint="?colorM3OnSurface">
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
<solid android:color="#0a000000"/>
</shape>
</item>
</selector>
</item>
<item android:left="-3dp" android:top="-3dp" android:right="-3dp">
<selector>
<item android:state_focused="true">
<shape>
<stroke android:color="?colorM3Primary" android:width="2dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="?colorM3OnSurfaceVariant" android:width="1dp"/>
</shape>
</item>
</selector>
</item>
</layer-list>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<corners android:topLeftRadius="4dp" android:topRightRadius="4dp" />
<solid android:color="?colorM3SurfaceVariant" />
</shape>
</item>
<item android:left="-3dp" android:right="-3dp" android:top="-3dp">
<shape>
<stroke
android:width="2dp"
android:color="?colorM3Error" />
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_on_surface_overlay">
<item android:id="@android:id/mask">
<shape>
<corners android:radius="11dp"/>
<solid android:color="#000"/>
</shape>
</item>
<item>
<shape>
<corners android:radius="11dp"/>
<stroke android:color="?colorM3OutlineVariant" android:width="1dp"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="?colorM3OutlineVariant"/>
<corners android:radius="7dp"/>
</shape>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22Q9.95,22 8.125,21.212Q6.3,20.425 4.938,19.062Q3.575,17.7 2.788,15.875Q2,14.05 2,12Q2,9.925 2.788,8.113Q3.575,6.3 4.938,4.938Q6.3,3.575 8.125,2.787Q9.95,2 12,2Q14.075,2 15.887,2.787Q17.7,3.575 19.062,4.938Q20.425,6.3 21.212,8.113Q22,9.925 22,12V13.45Q22,14.925 20.988,15.962Q19.975,17 18.5,17Q17.6,17 16.825,16.6Q16.05,16.2 15.55,15.55Q14.875,16.225 13.963,16.613Q13.05,17 12,17Q9.925,17 8.463,15.537Q7,14.075 7,12Q7,9.925 8.463,8.462Q9.925,7 12,7Q14.075,7 15.538,8.462Q17,9.925 17,12V13.45Q17,14.175 17.45,14.587Q17.9,15 18.5,15Q19.1,15 19.55,14.587Q20,14.175 20,13.45V12Q20,8.725 17.637,6.362Q15.275,4 12,4Q8.725,4 6.362,6.362Q4,8.725 4,12Q4,15.275 6.362,17.637Q8.725,20 12,20H17V22ZM12,15Q13.25,15 14.125,14.125Q15,13.25 15,12Q15,10.75 14.125,9.875Q13.25,9 12,9Q10.75,9 9.875,9.875Q9,10.75 9,12Q9,13.25 9.875,14.125Q10.75,15 12,15Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,22Q3.175,22 2.588,21.413Q2,20.825 2,20V9Q2,8.175 2.588,7.587Q3.175,7 4,7H9V4Q9,3.175 9.588,2.587Q10.175,2 11,2H13Q13.825,2 14.413,2.587Q15,3.175 15,4V7H20Q20.825,7 21.413,7.587Q22,8.175 22,9V20Q22,20.825 21.413,21.413Q20.825,22 20,22ZM4,20H20Q20,20 20,20Q20,20 20,20V9Q20,9 20,9Q20,9 20,9H15Q15,9.825 14.413,10.412Q13.825,11 13,11H11Q10.175,11 9.588,10.412Q9,9.825 9,9H4Q4,9 4,9Q4,9 4,9V20Q4,20 4,20Q4,20 4,20ZM6,18H12V17.55Q12,17.125 11.762,16.762Q11.525,16.4 11.1,16.2Q10.6,15.975 10.088,15.863Q9.575,15.75 9,15.75Q8.425,15.75 7.913,15.863Q7.4,15.975 6.9,16.2Q6.475,16.4 6.238,16.762Q6,17.125 6,17.55ZM14,16.5H18V15H14ZM9,15Q9.625,15 10.062,14.562Q10.5,14.125 10.5,13.5Q10.5,12.875 10.062,12.438Q9.625,12 9,12Q8.375,12 7.938,12.438Q7.5,12.875 7.5,13.5Q7.5,14.125 7.938,14.562Q8.375,15 9,15ZM14,13.5H18V12H14ZM11,9H13V4H11ZM12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Q12,14.5 12,14.5Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM12,20Q15.35,20 17.675,17.675Q20,15.35 20,12Q20,10.65 19.562,9.4Q19.125,8.15 18.3,7.1L7.1,18.3Q8.15,19.125 9.4,19.562Q10.65,20 12,20ZM5.7,16.9 L16.9,5.7Q15.85,4.875 14.6,4.438Q13.35,4 12,4Q8.65,4 6.325,6.325Q4,8.65 4,12Q4,13.35 4.438,14.6Q4.875,15.85 5.7,16.9Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_bookmark_fill1_24px" android:state_selected="true"/>
<item android:drawable="@drawable/ic_bookmark_24px"/>
</selector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,21V5Q5,4.175 5.588,3.587Q6.175,3 7,3H17Q17.825,3 18.413,3.587Q19,4.175 19,5V21L12,18Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,20Q3.175,20 2.588,19.413Q2,18.825 2,18V14Q2.825,14 3.413,13.412Q4,12.825 4,12Q4,11.175 3.413,10.587Q2.825,10 2,10V6Q2,5.175 2.588,4.588Q3.175,4 4,4H20Q20.825,4 21.413,4.588Q22,5.175 22,6V10Q21.175,10 20.587,10.587Q20,11.175 20,12Q20,12.825 20.587,13.412Q21.175,14 22,14V18Q22,18.825 21.413,19.413Q20.825,20 20,20ZM4,18H20V15.45Q19.075,14.9 18.538,13.988Q18,13.075 18,12Q18,10.925 18.538,10.012Q19.075,9.1 20,8.55V6H4V8.55Q4.925,9.1 5.463,10.012Q6,10.925 6,12Q6,13.075 5.463,13.988Q4.925,14.9 4,15.45ZM12,17Q12.425,17 12.713,16.712Q13,16.425 13,16Q13,15.575 12.713,15.287Q12.425,15 12,15Q11.575,15 11.288,15.287Q11,15.575 11,16Q11,16.425 11.288,16.712Q11.575,17 12,17ZM12,13Q12.425,13 12.713,12.712Q13,12.425 13,12Q13,11.575 12.713,11.287Q12.425,11 12,11Q11.575,11 11.288,11.287Q11,11.575 11,12Q11,12.425 11.288,12.712Q11.575,13 12,13ZM12,9Q12.425,9 12.713,8.712Q13,8.425 13,8Q13,7.575 12.713,7.287Q12.425,7 12,7Q11.575,7 11.288,7.287Q11,7.575 11,8Q11,8.425 11.288,8.712Q11.575,9 12,9ZM12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M22,19.15 L20,17.15V9H11.85L10,7.15V5H7.85L5.85,3H12V7H22ZM20.5,23.3 L18.15,21H2V4.8L0.7,3.5L2.1,2.1L21.9,21.9ZM4,19H6V17H4ZM4,15H6V13H4ZM4,11H6V9H4ZM8,19H10V17H8ZM8,15H10V13H8ZM12,19H16.15L14.15,17H12ZM18,13H16V11H18Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@android:color/white"
android:pathData="M9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H32.1L42,15.9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V17.55H30.45V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM13.95,33.45H34.05V30.45H13.95ZM13.95,17.55H24V14.55H13.95ZM13.95,25.5H34.05V22.5H13.95ZM9,9V17.55V9V17.55V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,17H13V11H11ZM12,9Q12.425,9 12.713,8.712Q13,8.425 13,8Q13,7.575 12.713,7.287Q12.425,7 12,7Q11.575,7 11.288,7.287Q11,7.575 11,8Q11,8.425 11.288,8.712Q11.575,9 12,9ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM11,19.95V18Q10.175,18 9.588,17.413Q9,16.825 9,16V15L4.2,10.2Q4.125,10.65 4.062,11.1Q4,11.55 4,12Q4,15.025 5.988,17.3Q7.975,19.575 11,19.95ZM17.9,17.4Q18.925,16.275 19.462,14.887Q20,13.5 20,12Q20,9.55 18.638,7.525Q17.275,5.5 15,4.6V5Q15,5.825 14.413,6.412Q13.825,7 13,7H11V9Q11,9.425 10.713,9.712Q10.425,10 10,10H8V12H14Q14.425,12 14.713,12.287Q15,12.575 15,13V16H16Q16.65,16 17.175,16.387Q17.7,16.775 17.9,17.4Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,19V15Q19,13.75 18.125,12.875Q17.25,12 16,12H6.8L10.4,15.6L9,17L3,11L9,5L10.4,6.4L6.8,10H16Q18.075,10 19.538,11.462Q21,12.925 21,15V19Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_star_fill1_24px" android:state_selected="true"/>
<item android:drawable="@drawable/ic_star_24px"/>
</selector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19.8,22.6 L16.775,19.575Q16.15,19.975 15.45,20.263Q14.75,20.55 14,20.725V18.675Q14.35,18.55 14.688,18.425Q15.025,18.3 15.325,18.125L12,14.8V20L7,15H3V9H6.2L1.4,4.2L2.8,2.8L21.2,21.2ZM19.6,16.8 L18.15,15.35Q18.575,14.575 18.788,13.725Q19,12.875 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,13.3 20.638,14.525Q20.275,15.75 19.6,16.8ZM9.1,11.9ZM16.25,13.45 L14,11.2V7.95Q15.175,8.5 15.838,9.6Q16.5,10.7 16.5,12Q16.5,12.375 16.438,12.738Q16.375,13.1 16.25,13.45ZM12,9.2 L9.4,6.6 12,4ZM10,15.15V12.8L8.2,11H5V13H7.85Z"/>
</vector>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp"
android:paddingTop="24dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textAppearance="@style/m3_headline_small"
android:textColor="?colorM3OnSurface"
android:drawableTop="@drawable/ic_confirmation_number_24px"
android:drawableTint="?colorM3Secondary"
android:drawablePadding="16dp"
android:text="@string/enter_invite_link"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="@string/need_invite_to_join_server"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="76dp"
android:layout_marginTop="16dp">
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/bg_m3_filled_text_field"
android:paddingHorizontal="16dp"
android:textColorHint="?colorM3OnSurfaceVariant"
android:textColor="?colorM3OnSurface"
android:gravity="start|bottom"
android:paddingBottom="8dp"
android:singleLine="true"
android:inputType="textUri"
android:textAppearance="@style/m3_body_large"
android:hint="example.social/invite/AbC123"/>
<TextView
android:id="@+id/label"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_gravity="top"
android:paddingEnd="23dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:text="@string/server_url"/>
<TextView
android:id="@+id/supporting_text"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
android:layout_gravity="bottom"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:text=""/>
<ImageButton
android:id="@+id/clear"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:layout_gravity="end|top"
android:src="@drawable/ic_m3_cancel"
android:background="?android:actionBarItemBackground"
android:contentDescription="@string/clear"/>
</FrameLayout>
</LinearLayout>

View File

@@ -3,54 +3,75 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="8dp">
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout
<LinearLayout
android:id="@+id/inner"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:foreground="?android:selectableItemBackground"
android:orientation="vertical"
android:foreground="@drawable/fg_link_card"
android:padding="1dp"
android:maxWidth="400dp">
<ImageView
<org.joinmastodon.android.ui.views.FixedAspectRatioImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
app:aspectRatio="1.7777777778"
tools:src="#0f0"/>
<LinearLayout
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:padding="8dp"
android:orientation="vertical"
android:background="@drawable/window_bg_alpha95">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_body_large"
android:singleLine="true"
android:ellipsize="end"
android:textColor="?colorM3OnSurface"
tools:text="Link title"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
tools:text="Link description"/>
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_title_medium"
android:maxLines="3"
android:ellipsize="end"
android:textColor="?colorM3OnSurface"
tools:text="Link title"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:maxLines="2"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
tools:text="Link description"/>
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/domain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
<TextView
android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingBottom="8dp">
<RelativeLayout
android:id="@+id/inner"
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
android:foreground="@drawable/fg_link_card"
android:padding="1dp"
android:maxWidth="400dp">
<ImageView
android:id="@+id/photo"
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_alignParentEnd="true"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/photo"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_title_medium"
android:maxLines="3"
android:ellipsize="end"
android:textColor="?colorM3OnSurface"
tools:text="Link title"/>
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/photo"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/domain"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
<TextView
android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="example.com"/>
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
</RelativeLayout>
</FrameLayout>

View File

@@ -125,25 +125,15 @@
android:orientation="vertical"
android:background="@drawable/bg_onboarding_panel">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/signup_random_server_explain"/>
<Button
android:id="@+id/btn_random_instance"
android:id="@+id/btn_use_invite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:text="@string/pick_server_for_me"/>
android:text="@string/use_invite_link"/>
<Button
android:id="@+id/btn_next"

View File

@@ -37,16 +37,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:background="@drawable/bg_onboarding_panel">
android:background="@drawable/bg_m3_surface1">
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Tonal"
style="@style/Widget.Mastodon.M3.Button.Elevated"
android:text="@string/follow_all"/>
<Button
@@ -55,10 +55,10 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/skip"/>
android:text="@string/next"/>
</LinearLayout>

View File

@@ -61,7 +61,7 @@
android:layout_below="@id/cover"
android:layout_alignParentStart="true"
android:layout_marginStart="12dp"
android:layout_marginTop="-44dp"
android:layout_marginTop="-36dp"
android:background="@drawable/profile_ava_bg"
android:outlineProvider="@null">
@@ -76,64 +76,63 @@
</FrameLayout>
<FrameLayout
android:id="@+id/profile_action_btn_wrap"
android:layout_width="wrap_content"
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/cover"
android:layout_alignParentEnd="true"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp"
android:paddingStart="120dp">
android:layout_toEndOf="@id/avatar_border"
android:layout_marginTop="14dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="16dp"
android:fontFamily="sans-serif"
android:textAlignment="viewStart"
android:textAppearance="@style/m3_title_large"
android:textColor="?colorM3OnSurface"
android:maxLines="2"
android:ellipsize="end"
tools:text="Eugen" />
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/profile_action_btn"
android:layout_width="wrap_content"
android:layout_height="40dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:minWidth="156dp"
android:paddingHorizontal="16dp"
tools:text="@string/save_changes" />
<org.joinmastodon.android.ui.views.WrappingLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toEndOf="@id/avatar_border"
android:layout_marginStart="12dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="2dp"
android:horizontalGap="4dp"
android:verticalGap="0dp">
<ProgressBar
android:id="@+id/action_progress"
style="?android:progressBarStyleSmall"
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:elevation="10dp"
android:indeterminate="true"
android:outlineProvider="none"
android:visibility="gone" />
</FrameLayout>
android:layout_height="20dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="Gargron" />
<TextView
android:id="@+id/username_domain"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textAppearance="@style/m3_label_small"
android:gravity="center_vertical"
android:paddingHorizontal="4dp"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:background="@drawable/rect_4dp"
android:backgroundTint="?colorM3SurfaceVariant"
tools:text="mastodon.social"/>
</org.joinmastodon.android.ui.views.WrappingLinearLayout>
</RelativeLayout>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="12dp"
android:layout_marginRight="16dp"
android:fontFamily="sans-serif"
android:textAlignment="viewStart"
android:textAppearance="@style/m3_title_large"
android:textColor="?colorM3OnSurface"
tools:text="Eugen" />
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="2dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@Gargron" />
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/bio"
android:layout_width="match_parent"
@@ -192,18 +191,11 @@
tools:text="followers" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="28dp"
android:gravity="center"
android:text="·"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnSurfaceVariant" />
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginStart="-4dp"
android:background="@drawable/bg_button_borderless_rounded"
android:gravity="center_horizontal"
android:orientation="horizontal"
@@ -289,6 +281,41 @@
android:padding="16dp"
tools:text="Founder, CEO and lead developer @Mastodon, Germany." />
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp">
<FrameLayout
android:id="@+id/profile_action_btn_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/profile_action_btn"
android:layout_width="match_parent"
android:layout_height="40dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:minWidth="156dp"
android:paddingHorizontal="16dp"
tools:text="@string/save_changes" />
<ProgressBar
android:id="@+id/action_progress"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:elevation="10dp"
android:indeterminate="true"
android:outlineProvider="none"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/tabbar"

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CheckableRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/avatar"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
android:foreground="@drawable/fg_onboarding_ava"
android:padding="1dp"
tools:src="#0f0"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/accessory"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurface"
tools:text="User"/>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="@id/name"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/accessory"
android:layout_marginTop="-2dp"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical|start"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3Secondary"
android:textAlignment="viewStart"
tools:text="\@user@server"/>
<FrameLayout
android:id="@+id/accessory"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginStart="8dp"
android:duplicateParentState="true">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:paddingHorizontal="10dp"
android:minWidth="96dp"
android:maxWidth="150dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
tools:text="Follow back"/>
<ProgressBar
android:id="@+id/action_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
style="?android:progressBarStyleSmall"
android:elevation="10dp"
android:outlineProvider="none"
android:indeterminateTint="?colorM3OnPrimary"
android:visibility="gone"/>
<View
android:id="@+id/checkbox"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginHorizontal="4dp"
android:layout_marginTop="2dp"
android:duplicateParentState="true"
android:visibility="gone"/>
<ImageButton
android:id="@+id/options_btn"
android:layout_width="40dp"
android:layout_height="36dp"
android:layout_gravity="top"
android:background="?android:actionBarItemBackground"
android:tint="?colorM3OnSurfaceVariant"
android:contentDescription="@string/more_options"
android:src="@drawable/ic_more_vert_24px"
android:visibility="gone"/>
</FrameLayout>
<TextView
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/username"
android:layout_marginTop="2dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
android:maxLines="2"
android:paddingVertical="2dp"
tools:text="bla bla bla bla bla bla"/>
<View
android:id="@+id/menu_anchor"
android:layout_width="1px"
android:layout_height="1px"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="-16dp"/>
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/alt_text_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="noHideDescendants"
android:background="@drawable/bg_image_alt_overlay">
<TextView
android:id="@+id/alt_button"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textAppearance="@style/m3_label_large"
android:textColor="#FFF"
android:gravity="center"
android:includeFontPadding="false"
android:text="ALT"
tools:ignore="HardcodedText" />
<ImageButton
android:id="@+id/alt_text_close"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:src="@drawable/ic_baseline_close_24"
android:tint="#FFF"
android:background="?android:selectableItemBackgroundBorderless"/>
<org.joinmastodon.android.ui.views.NestableScrollView
android:id="@+id/alt_text_scroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="40dp"
android:requiresFadingEdge="vertical"
android:fadingEdgeLength="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/alt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="#FFF"
tools:text="Alt text goes here"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.NestableScrollView>
</FrameLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ProgressBar>

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