Compare commits

...

548 Commits

Author SHA1 Message Date
Grishka b2c797fb46 Fix #842 2024-05-21 22:23:15 +03:00
Gregory K 29ab502d2e
Merge pull request #833 from NorbiPeti/master
Display more user-friendly error messages
2024-05-13 01:49:34 +03:00
NorbiPeti 629e65edba Update error messages and remove the unknown error text 2024-05-13 00:08:18 +02:00
Grishka 97e16b9f73 Use `image_matrix_limit` from instance configuration if available 2024-05-07 23:04:43 +03:00
Grishka bc78c61009 Fix #835 2024-05-06 12:56:56 +03:00
Grishka 4a1b1e19e8 Fix #834 2024-05-04 21:20:20 +03:00
NorbiPeti c7b8cc72fc Display more user-friendly error messages
Instead of displaying the Java
exception, this change displays a more user-friendly message for some
common network-related issues.
Fixes mastodon/mastodon-android#667
2024-05-02 19:02:03 +02:00
Grishka 0e868f0be0 English proper nouns are a mess 2024-05-01 17:39:56 +03:00
Grishka 67847d90da Fix #832 2024-05-01 17:38:22 +03:00
Gregory K b0c77d42c0
Merge pull request #829 from Endeavour233/avoid-leak-response-body
Opti: avoid leaking response body when request is cancelled
2024-04-21 18:11:02 +03:00
shimura233 bfc0076429 opti: avoid leaking response body when it's not used. 2024-04-21 20:29:15 +08:00
Grishka 50760471d5 Add context menus to post footer buttons (AND-161) 2024-04-20 07:12:51 +03:00
Grishka a6df8cb62d Pressed state for alt badge (AND-157) 2024-04-20 06:57:14 +03:00
Grishka 873711939d Post header tap area thing (AND-160) 2024-04-20 06:51:30 +03:00
Grishka 2bd13eb3ba Label unlabeled things for accessibility 2024-04-20 06:34:21 +03:00
Grishka d423f17342 Animate privacy button changes (AND-158) 2024-04-20 06:27:47 +03:00
Grishka 09be5b3f97 Add descriptions to post visibility options 2024-04-20 04:48:57 +03:00
Grishka 1124bc48c2 Update CI Ruby to 3.3.0 and add Gemfile.lock 2024-04-12 18:31:43 +03:00
Grishka 69b7484a4a Merge commit '4dd5a80ef27bd4abf1eaa272d1e3c67b7d9a3a13' 2024-04-12 17:59:55 +03:00
Grishka 19939e457b Prepare for release 2024-04-12 17:57:14 +03:00
Grishka 3e256d41d2 Fix alert text size 2024-04-11 22:36:55 +03:00
Grishka 72f546ed15 update string 2024-04-11 17:42:18 +03:00
Gregory K 0b48414715
Merge pull request #824 from FineFindus/fix/featured-hastag-crash
fix: include account when opening FeaturedHashtags
2024-04-10 17:25:56 +03:00
FineFindus ca67c1eaca
fix: include account when opening FeaturedHashtags
Closes https://github.com/mastodon/mastodon-android/issues/803.
2024-04-10 16:16:11 +02:00
Eugen Rochko 4dd5a80ef2 New translations strings.xml (Persian) 2024-04-09 12:29:17 +02:00
Eugen Rochko c9f211807a New translations strings.xml (Lithuanian) 2024-04-09 05:53:52 +02:00
Eugen Rochko 440c15d9fa New translations full_description.txt (Lithuanian) 2024-04-07 22:44:40 +02:00
Eugen Rochko 86a443c39f New translations strings.xml (Lithuanian) 2024-04-07 22:44:39 +02:00
Eugen Rochko a8d86db57f New translations strings.xml (Lithuanian) 2024-04-07 21:46:44 +02:00
Eugen Rochko 8359b48285 New translations strings.xml (Persian) 2024-04-04 13:08:30 -04:00
Grishka 36f4770cae Media layout: improve the case of two horizontal attachments 2024-04-02 05:09:55 +03:00
Eugen Rochko 932cf91800 New translations strings.xml (Vietnamese) 2024-03-29 22:27:06 -04:00
Eugen Rochko 5cf02e66b7 New translations strings.xml (Armenian) 2024-03-29 03:51:21 -04:00
Grishka b7251972a8 Show the profile header view if we know the username 2024-03-28 23:00:03 +03:00
Eugen Rochko 006a423d5c New translations strings.xml (Greek) 2024-03-27 16:29:16 -04:00
Eugen Rochko 374b1edc81 New translations strings.xml (Basque) 2024-03-27 13:16:30 -04:00
Eugen Rochko 0894549687 New translations strings.xml (Basque) 2024-03-27 11:50:23 -04:00
Eugen Rochko cc5963cc34 New translations strings.xml (Persian) 2024-03-26 14:19:20 +01:00
Eugen Rochko fffe77501d New translations strings.xml (Greek) 2024-03-26 12:17:29 +01:00
Eugen Rochko 3443c2ae82 New translations strings.xml (Slovenian) 2024-03-25 13:25:24 +01:00
Eugen Rochko 01324af544 New translations strings.xml (Icelandic) 2024-03-25 11:23:46 +01:00
Eugen Rochko d56b1fe89b New translations strings.xml (Icelandic) 2024-03-25 09:14:16 +01:00
Eugen Rochko 92457d54df New translations strings.xml (Icelandic) 2024-03-24 22:36:45 +01:00
Eugen Rochko 4d2e30ff85 New translations strings.xml (French) 2024-03-24 17:29:05 +01:00
Eugen Rochko 1d0c279956 New translations strings.xml (Greek) 2024-03-24 16:28:56 +01:00
Eugen Rochko 0202ca5b23 New translations strings.xml (Hungarian) 2024-03-23 21:34:53 +01:00
Eugen Rochko 5e8ebeadc3 New translations strings.xml (Thai) 2024-03-23 19:14:41 +01:00
Eugen Rochko 8bfb0c45a8 New translations strings.xml (Thai) 2024-03-23 18:05:28 +01:00
Eugen Rochko 2c1ecf32ad New translations strings.xml (Thai) 2024-03-23 17:07:59 +01:00
Eugen Rochko 9cc4bd722d New translations full_description.txt (Indonesian) 2024-03-23 08:38:04 +01:00
Eugen Rochko d5f1e091b8 New translations strings.xml (Indonesian) 2024-03-23 08:38:03 +01:00
Eugen Rochko 439f3b44cb New translations strings.xml (Indonesian) 2024-03-23 07:33:06 +01:00
Eugen Rochko e07bd39e95 New translations strings.xml (Chinese Simplified) 2024-03-23 05:48:47 +01:00
Eugen Rochko 11fe2ba2a4 New translations strings.xml (Hungarian) 2024-03-22 20:50:32 +01:00
Eugen Rochko 2b0e507f45 New translations strings.xml (Dutch) 2024-03-22 18:48:03 +01:00
Eugen Rochko 84bad0aa6c New translations strings.xml (Dutch) 2024-03-22 17:50:30 +01:00
Eugen Rochko b41cda84e4 New translations strings.xml (Hungarian) 2024-03-22 14:19:35 +01:00
Eugen Rochko 0ae6fb2833 New translations strings.xml (Hungarian) 2024-03-22 11:26:06 +01:00
Eugen Rochko 21b6a1f4ef New translations strings.xml (Hungarian) 2024-03-22 09:52:08 +01:00
Eugen Rochko 69f9da4be4 New translations strings.xml (Indonesian) 2024-03-22 03:15:21 +01:00
Eugen Rochko 032db0921d New translations strings.xml (Italian) 2024-03-22 02:20:19 +01:00
Eugen Rochko eef4601ce2 New translations strings.xml (Chinese Traditional) 2024-03-22 01:20:13 +01:00
Eugen Rochko 71701048f5 New translations strings.xml (Italian) 2024-03-22 01:20:12 +01:00
Eugen Rochko 2361bb7682 New translations strings.xml (Chinese Traditional) 2024-03-21 22:55:40 +01:00
Eugen Rochko 0b1b8c5c5a New translations strings.xml (Slovenian) 2024-03-21 22:55:36 +01:00
Eugen Rochko fb885b0e00 New translations strings.xml (Hungarian) 2024-03-21 22:55:18 +01:00
Grishka a2dec4f7cf Notification requests tweaks 2024-03-22 00:49:42 +03:00
Eugen Rochko 521797d070 New translations strings.xml (Hungarian) 2024-03-21 20:51:30 +01:00
Eugen Rochko d2922dc226 New translations strings.xml (Hungarian) 2024-03-21 19:20:59 +01:00
Eugen Rochko ecc2b675d5 New translations strings.xml (Persian) 2024-03-21 14:09:21 +01:00
Eugen Rochko 5a49b650b0 New translations strings.xml (Chinese Traditional) 2024-03-21 02:31:23 +01:00
Eugen Rochko 5fd57caabf New translations strings.xml (Slovenian) 2024-03-21 00:57:01 +01:00
Eugen Rochko bad6afc543 New translations strings.xml (Slovenian) 2024-03-20 23:53:49 +01:00
Eugen Rochko ffaa036115 New translations strings.xml (Urdu (India)) 2024-03-20 21:23:27 +01:00
Eugen Rochko 65133e969e New translations strings.xml (Kabyle) 2024-03-20 21:23:26 +01:00
Eugen Rochko 267ee4e03e New translations strings.xml (Igbo) 2024-03-20 21:23:25 +01:00
Eugen Rochko db972ae421 New translations strings.xml (Occitan) 2024-03-20 21:23:24 +01:00
Eugen Rochko ea93dd5b2d New translations strings.xml (Scottish Gaelic) 2024-03-20 21:23:23 +01:00
Eugen Rochko c4d738844e New translations strings.xml (Sinhala) 2024-03-20 21:23:22 +01:00
Eugen Rochko f124d2cabc New translations strings.xml (Bosnian) 2024-03-20 21:23:21 +01:00
Eugen Rochko 022038878b New translations strings.xml (Filipino) 2024-03-20 21:23:20 +01:00
Eugen Rochko c6a846c602 New translations strings.xml (Burmese) 2024-03-20 21:23:18 +01:00
Eugen Rochko 31986a1ce5 New translations strings.xml (Croatian) 2024-03-20 21:23:17 +01:00
Eugen Rochko 04eea3b6e4 New translations strings.xml (Thai) 2024-03-20 21:23:16 +01:00
Eugen Rochko 09a5482df5 New translations strings.xml (Bengali) 2024-03-20 21:23:15 +01:00
Eugen Rochko ba8e5a03ea New translations strings.xml (Indonesian) 2024-03-20 21:23:14 +01:00
Eugen Rochko ddb3c34078 New translations strings.xml (Portuguese, Brazilian) 2024-03-20 21:23:13 +01:00
Eugen Rochko 8d4daa5d00 New translations strings.xml (Icelandic) 2024-03-20 21:23:12 +01:00
Eugen Rochko 65e1787987 New translations strings.xml (Galician) 2024-03-20 21:23:11 +01:00
Eugen Rochko 202f41b34b New translations strings.xml (Vietnamese) 2024-03-20 21:23:09 +01:00
Eugen Rochko 10497f358e New translations strings.xml (Chinese Traditional) 2024-03-20 21:23:08 +01:00
Eugen Rochko ffe0dafbdc New translations strings.xml (Chinese Simplified) 2024-03-20 21:23:06 +01:00
Eugen Rochko 5828be28e8 New translations strings.xml (Ukrainian) 2024-03-20 21:23:05 +01:00
Eugen Rochko b4e0605016 New translations strings.xml (Turkish) 2024-03-20 21:23:04 +01:00
Eugen Rochko da7686b9b3 New translations strings.xml (Swedish) 2024-03-20 21:23:03 +01:00
Eugen Rochko 651d5ae56a New translations strings.xml (Slovenian) 2024-03-20 21:23:02 +01:00
Eugen Rochko 570d8ce7eb New translations strings.xml (Portuguese) 2024-03-20 21:23:01 +01:00
Eugen Rochko a9491e22e4 New translations strings.xml (Norwegian) 2024-03-20 21:23:00 +01:00
Eugen Rochko 1ba31afa23 New translations strings.xml (Lithuanian) 2024-03-20 21:22:58 +01:00
Eugen Rochko 8f30d0d468 New translations strings.xml (Korean) 2024-03-20 21:22:57 +01:00
Eugen Rochko 4a8cea262b New translations strings.xml (Georgian) 2024-03-20 21:22:56 +01:00
Eugen Rochko e25574ce9a New translations strings.xml (Japanese) 2024-03-20 21:22:55 +01:00
Eugen Rochko 9773c1cb98 New translations strings.xml (Italian) 2024-03-20 21:22:54 +01:00
Eugen Rochko 6d3eafe9e8 New translations strings.xml (Armenian) 2024-03-20 21:22:53 +01:00
Eugen Rochko 19acab9d18 New translations strings.xml (Hebrew) 2024-03-20 21:22:52 +01:00
Eugen Rochko bfcda1d73c New translations strings.xml (Irish) 2024-03-20 21:22:51 +01:00
Eugen Rochko 600fc7939e New translations strings.xml (Finnish) 2024-03-20 21:22:50 +01:00
Eugen Rochko 1189aaae4f New translations strings.xml (Basque) 2024-03-20 21:22:49 +01:00
Eugen Rochko e9c8e8d764 New translations strings.xml (German) 2024-03-20 21:22:48 +01:00
Eugen Rochko 3e90620fcc New translations strings.xml (Danish) 2024-03-20 21:22:47 +01:00
Eugen Rochko 62a364a110 New translations strings.xml (Czech) 2024-03-20 21:22:47 +01:00
Eugen Rochko fbbbe99bf4 New translations strings.xml (Arabic) 2024-03-20 21:22:45 +01:00
Eugen Rochko d078ccc78c New translations strings.xml (Spanish) 2024-03-20 21:22:44 +01:00
Eugen Rochko cbacb6568e New translations strings.xml (French) 2024-03-20 21:22:43 +01:00
Eugen Rochko 65f220b570 New translations strings.xml (Romanian) 2024-03-20 21:22:42 +01:00
Eugen Rochko 22d83d831d New translations strings.xml (Dutch) 2024-03-20 21:22:41 +01:00
Eugen Rochko ac564a67ca New translations strings.xml (Persian) 2024-03-20 21:22:40 +01:00
Eugen Rochko d27a8dc29c New translations strings.xml (Catalan) 2024-03-20 21:22:39 +01:00
Eugen Rochko 568dfe911e New translations strings.xml (Belarusian) 2024-03-20 21:22:38 +01:00
Eugen Rochko 64a647ca84 New translations strings.xml (Hindi) 2024-03-20 21:22:37 +01:00
Eugen Rochko 6b3c3ac9b0 New translations strings.xml (Greek) 2024-03-20 21:22:36 +01:00
Eugen Rochko 6438df92c6 New translations strings.xml (Russian) 2024-03-20 21:22:35 +01:00
Eugen Rochko 5f98fdfafc New translations strings.xml (Polish) 2024-03-20 21:22:34 +01:00
Eugen Rochko 4a1c8aadf8 New translations strings.xml (Hungarian) 2024-03-20 21:22:33 +01:00
Grishka 441567f9d2 Notification requests (AND-154) 2024-03-20 23:18:04 +03:00
Eugen Rochko 381fd434ad New translations strings.xml (Japanese) 2024-03-20 18:45:42 +01:00
Eugen Rochko 89f713899b New translations strings.xml (Japanese) 2024-03-20 17:16:49 +01:00
Eugen Rochko a82c61791e New translations strings.xml (Thai) 2024-03-19 19:16:37 +01:00
Eugen Rochko 734c1ddab6 New translations strings.xml (Dutch) 2024-03-19 19:16:35 +01:00
Eugen Rochko 1d0b31e9de New translations strings.xml (Chinese Traditional) 2024-03-19 17:56:57 +01:00
Eugen Rochko 1441036475 New translations strings.xml (French) 2024-03-19 13:19:22 +01:00
Eugen Rochko 7a84352723 New translations strings.xml (Hungarian) 2024-03-19 13:19:21 +01:00
Eugen Rochko 6f153f3879 New translations strings.xml (Icelandic) 2024-03-19 11:06:29 +01:00
Grishka f888091e22 Add unlisted visibility option as "quiet public"
closes #189, closes #103, closes #37
2024-03-18 20:34:23 +03:00
Grishka e59cf2afca Make alt text selectable 2024-03-18 20:25:28 +03:00
Eugen Rochko 7516b8e662 New translations strings.xml (Persian) 2024-03-18 11:01:25 +01:00
Eugen Rochko bb4a480250 New translations strings.xml (Persian) 2024-03-17 18:19:22 +01:00
Eugen Rochko c6df18c456 New translations strings.xml (Persian) 2024-03-17 17:09:46 +01:00
Eugen Rochko cb945998d3 New translations strings.xml (Persian) 2024-03-17 16:13:47 +01:00
Grishka 5622c93bd9 Fix fragment stack breaking after opening a notification 2024-03-17 04:24:12 +03:00
Eugen Rochko 7552227da0 New translations strings.xml (Dutch) 2024-03-16 19:18:36 +01:00
Eugen Rochko 1904fce32d New translations strings.xml (Greek) 2024-03-16 19:18:35 +01:00
Eugen Rochko f1e5e572f4 New translations strings.xml (Kabyle) 2024-03-16 13:44:30 +01:00
Eugen Rochko d8f83170be New translations strings.xml (Dutch) 2024-03-16 13:44:30 +01:00
Eugen Rochko 95c135b270 New translations strings.xml (Dutch) 2024-03-16 12:48:38 +01:00
Eugen Rochko 8408daf070 New translations strings.xml (Dutch) 2024-03-16 00:08:45 +01:00
Eugen Rochko bd8eb6a034 New translations strings.xml (Slovenian) 2024-03-15 23:03:06 +01:00
Eugen Rochko 9c4d0ef85e New translations strings.xml (Dutch) 2024-03-15 23:03:05 +01:00
Eugen Rochko 1ee441314f New translations strings.xml (Slovenian) 2024-03-15 22:04:35 +01:00
Eugen Rochko d5d1e51bbc New translations strings.xml (Dutch) 2024-03-15 19:48:48 +01:00
Eugen Rochko 84aa99ba88 New translations strings.xml (Dutch) 2024-03-15 18:23:19 +01:00
Eugen Rochko 23e02d2c24 New translations strings.xml (Dutch) 2024-03-15 17:10:51 +01:00
Eugen Rochko 3ea66c6c4c New translations strings.xml (Belarusian) 2024-03-15 11:12:27 +01:00
Eugen Rochko 50cf737db6 New translations strings.xml (Vietnamese) 2024-03-13 16:46:20 +01:00
Grishka bf55b5a802 Swap poll options around 2024-03-13 17:13:25 +03:00
Grishka 49bf04c6c6 Tweak line height for posts
#791
2024-03-13 17:05:57 +03:00
Eugen Rochko f5d64f3882 New translations strings.xml (Slovenian) 2024-03-12 12:31:29 +01:00
Eugen Rochko 48d7de53c0 New translations strings.xml (Georgian) 2024-03-12 10:00:34 +01:00
Eugen Rochko d53bace4ce New translations strings.xml (Thai) 2024-03-11 20:09:04 +01:00
Eugen Rochko 02c800496c New translations strings.xml (Icelandic) 2024-03-11 18:48:27 +01:00
Eugen Rochko 0ce39946cb New translations strings.xml (Chinese Traditional) 2024-03-11 18:48:26 +01:00
Eugen Rochko 52b573d20f New translations strings.xml (Russian) 2024-03-11 16:19:01 +01:00
Grishka 5be6faa07c New posts button tweaks 2024-03-11 18:09:29 +03:00
Eugen Rochko ee05e818d9 New translations strings.xml (Italian) 2024-03-11 13:45:02 +01:00
Eugen Rochko 29d5e4fa13 New translations strings.xml (Urdu (India)) 2024-03-11 12:06:58 +01:00
Eugen Rochko 9bd830b368 New translations strings.xml (Kabyle) 2024-03-11 12:06:56 +01:00
Eugen Rochko 7d70f816d1 New translations strings.xml (Igbo) 2024-03-11 12:06:55 +01:00
Eugen Rochko 0d0cf04b57 New translations strings.xml (Occitan) 2024-03-11 12:06:54 +01:00
Eugen Rochko 4a6e514b81 New translations strings.xml (Scottish Gaelic) 2024-03-11 12:06:53 +01:00
Eugen Rochko b74f9092e7 New translations strings.xml (Sinhala) 2024-03-11 12:06:52 +01:00
Eugen Rochko a061347d76 New translations strings.xml (Bosnian) 2024-03-11 12:06:50 +01:00
Eugen Rochko 71586b1100 New translations strings.xml (Filipino) 2024-03-11 12:06:49 +01:00
Eugen Rochko 809aa6afd2 New translations strings.xml (Burmese) 2024-03-11 12:06:48 +01:00
Eugen Rochko 0067a036ae New translations strings.xml (Croatian) 2024-03-11 12:06:47 +01:00
Eugen Rochko 794d3329fe New translations strings.xml (Thai) 2024-03-11 12:06:46 +01:00
Eugen Rochko f96ed6c56f New translations strings.xml (Bengali) 2024-03-11 12:06:45 +01:00
Eugen Rochko 92d44eebe6 New translations strings.xml (Indonesian) 2024-03-11 12:06:44 +01:00
Eugen Rochko 93fe734636 New translations strings.xml (Portuguese, Brazilian) 2024-03-11 12:06:43 +01:00
Eugen Rochko 71a5c132f4 New translations strings.xml (Icelandic) 2024-03-11 12:06:42 +01:00
Eugen Rochko 15a514aca5 New translations strings.xml (Galician) 2024-03-11 12:06:41 +01:00
Eugen Rochko 71f74ced7d New translations strings.xml (Vietnamese) 2024-03-11 12:06:40 +01:00
Eugen Rochko 5c86c911c1 New translations strings.xml (Chinese Traditional) 2024-03-11 12:06:38 +01:00
Eugen Rochko 7aebc44062 New translations strings.xml (Chinese Simplified) 2024-03-11 12:06:37 +01:00
Eugen Rochko 6f6e1f1009 New translations strings.xml (Ukrainian) 2024-03-11 12:06:36 +01:00
Eugen Rochko 2e7f17b823 New translations strings.xml (Turkish) 2024-03-11 12:06:35 +01:00
Eugen Rochko 5194dae9a6 New translations strings.xml (Swedish) 2024-03-11 12:06:34 +01:00
Eugen Rochko 2db03669ce New translations strings.xml (Slovenian) 2024-03-11 12:06:33 +01:00
Eugen Rochko a6c4f83973 New translations strings.xml (Portuguese) 2024-03-11 12:06:32 +01:00
Eugen Rochko f625cea183 New translations strings.xml (Norwegian) 2024-03-11 12:06:31 +01:00
Eugen Rochko d6ee9db6ff New translations strings.xml (Lithuanian) 2024-03-11 12:06:30 +01:00
Eugen Rochko 9ea94ce177 New translations strings.xml (Korean) 2024-03-11 12:06:29 +01:00
Eugen Rochko 8102163c5b New translations strings.xml (Georgian) 2024-03-11 12:06:27 +01:00
Eugen Rochko 1034c16bfb New translations strings.xml (Japanese) 2024-03-11 12:06:26 +01:00
Eugen Rochko 956f9547e7 New translations strings.xml (Italian) 2024-03-11 12:06:25 +01:00
Eugen Rochko 95fb241da1 New translations strings.xml (Armenian) 2024-03-11 12:06:24 +01:00
Eugen Rochko 7b06af4c8b New translations strings.xml (Hebrew) 2024-03-11 12:06:23 +01:00
Eugen Rochko 1ca0ff53c8 New translations strings.xml (Irish) 2024-03-11 12:06:22 +01:00
Eugen Rochko 1e0bdf44c2 New translations strings.xml (Finnish) 2024-03-11 12:06:21 +01:00
Eugen Rochko c611d6386a New translations strings.xml (Basque) 2024-03-11 12:06:20 +01:00
Eugen Rochko b326dc3bc2 New translations strings.xml (German) 2024-03-11 12:06:18 +01:00
Eugen Rochko 7f6e21450a New translations strings.xml (Danish) 2024-03-11 12:06:17 +01:00
Eugen Rochko ee911a15c6 New translations strings.xml (Czech) 2024-03-11 12:06:16 +01:00
Eugen Rochko aa0e05f085 New translations strings.xml (Arabic) 2024-03-11 12:06:15 +01:00
Eugen Rochko f1fe078cf2 New translations strings.xml (Spanish) 2024-03-11 12:06:14 +01:00
Eugen Rochko e53dcf27ec New translations strings.xml (French) 2024-03-11 12:06:13 +01:00
Eugen Rochko 6e9ce8d5a5 New translations strings.xml (Romanian) 2024-03-11 12:06:12 +01:00
Eugen Rochko 686d88557b New translations strings.xml (Dutch) 2024-03-11 12:06:11 +01:00
Eugen Rochko b07f14d01b New translations strings.xml (Persian) 2024-03-11 12:06:09 +01:00
Eugen Rochko c200a72031 New translations strings.xml (Catalan) 2024-03-11 12:06:08 +01:00
Eugen Rochko 2a8ff3e50a New translations strings.xml (Belarusian) 2024-03-11 12:06:07 +01:00
Eugen Rochko 06cde138c1 New translations strings.xml (Hindi) 2024-03-11 12:06:06 +01:00
Eugen Rochko 9fae62f289 New translations strings.xml (Greek) 2024-03-11 12:06:04 +01:00
Eugen Rochko a1333929e9 New translations strings.xml (Russian) 2024-03-11 12:06:03 +01:00
Eugen Rochko 6a05fafe04 New translations strings.xml (Polish) 2024-03-11 12:06:02 +01:00
Eugen Rochko 13c6fc60f8 New translations strings.xml (Hungarian) 2024-03-11 12:06:01 +01:00
Grishka ff7948ad83 Add conversation muting 2024-03-11 13:31:32 +03:00
Grishka 3972ab207c New post notifications (AND-151) 2024-03-11 13:18:45 +03:00
Gregory K 33a8f1dab4
Merge pull request #795 from Arthur-GYT/per-app-language
Enable auto generated per-app language file
2024-03-11 12:06:02 +03:00
Eugen Rochko 27f261ae4a New translations strings.xml (Armenian) 2024-03-10 20:52:20 +01:00
Eugen Rochko ef59331dd3 New translations strings.xml (Armenian) 2024-03-10 19:14:55 +01:00
Eugen Rochko b019731249 New translations strings.xml (French) 2024-03-10 10:28:55 +01:00
Arthur-GYT 47aa7fc191 Enable auto generated per-app language file 2024-03-09 19:07:04 +01:00
Eugen Rochko 9356b26dfd New translations strings.xml (Scottish Gaelic) 2024-03-09 11:27:32 +01:00
Eugen Rochko 41b626ddbd New translations strings.xml (Polish) 2024-03-08 18:16:47 +01:00
Eugen Rochko f76b41581b New translations strings.xml (Polish) 2024-03-08 17:10:58 +01:00
Eugen Rochko 14f08b7759 New translations strings.xml (French) 2024-03-08 15:45:22 +01:00
Eugen Rochko 8c1cec09d6 New translations strings.xml (Russian) 2024-03-08 13:20:37 +01:00
Eugen Rochko 15c10cb14c New translations strings.xml (Armenian) 2024-03-07 22:56:57 +01:00
Eugen Rochko 3b2f68a400 New translations strings.xml (Armenian) 2024-03-07 21:17:50 +01:00
Eugen Rochko 2a9e4e0b82 New translations strings.xml (Turkish) 2024-03-07 17:30:18 +01:00
Grishka 7b25628f7a Fix more things 2024-03-06 20:58:59 +03:00
Grishka e06dd1e465 Google can't stop breaking shit, can it 2024-03-06 20:24:14 +03:00
Grishka a42ab10aed Merge branch 'l10n_master' 2024-03-06 19:45:58 +03:00
Grishka cd2ea90006 Prepare a new release 2024-03-06 19:45:37 +03:00
Grishka f0295edd83 Merge branch 'ci_setup' 2024-03-06 15:59:19 +03:00
Grishka eee0a40224 Filter posts by account when opening a featured hashtag
closes #786
2024-03-06 15:39:36 +03:00
Grishka bd189ae322 Why did I put it there 2024-03-06 15:24:27 +03:00
Grishka 714aa7a20b Fix #788 2024-03-06 15:23:20 +03:00
Eugen Rochko 76c6c65018 New translations strings.xml (French) 2024-03-05 22:32:45 +01:00
Eugen Rochko c511b54de2 New translations strings.xml (French) 2024-03-05 21:19:55 +01:00
Eugen Rochko 16b4a1f1a3 New translations strings.xml (Portuguese, Brazilian) 2024-03-05 19:04:15 +01:00
Eugen Rochko afb5743d31 New translations strings.xml (Portuguese, Brazilian) 2024-03-05 17:16:06 +01:00
Grishka 7e91c311d4 Crash fix 2024-03-05 05:04:50 +03:00
Eugen Rochko eef9f61e20 New translations strings.xml (Japanese) 2024-03-03 06:27:26 +01:00
Grishka 0cc55891c1 Specify Java distribution 2024-03-02 01:30:12 +03:00
Grishka e892eaa3d5 Update action versions & separate artifacts 2024-03-02 01:28:53 +03:00
Grishka bcdce2a880 Skip changelogs too (fastlane/fastlane#21905) 2024-03-02 01:16:32 +03:00
Eugen Rochko 9f38809730 New translations strings.xml (Turkish) 2024-03-01 18:48:01 +01:00
Eugen Rochko 77b416a52d New translations full_description.txt (Turkish) 2024-03-01 17:08:11 +01:00
Eugen Rochko 0684b0ec79 New translations strings.xml (Portuguese, Brazilian) 2024-03-01 17:08:10 +01:00
Eugen Rochko 20057a55f0 New translations strings.xml (Turkish) 2024-03-01 17:08:09 +01:00
Eugen Rochko a668dee567 New translations strings.xml (Vietnamese) 2024-03-01 15:22:53 +01:00
Grishka 11bda2fe07 Skip metadata for now 2024-03-01 10:43:48 +03:00
Grishka a7b83bc058 Yaml is hard 2024-03-01 10:35:14 +03:00
Grishka 19950e5115 And Gradle needs to know where it is 2024-03-01 10:29:12 +03:00
Grishka 11be65c6fe Unsurprisingly, you need Android SDK to build an Android app 2024-03-01 10:23:45 +03:00
Grishka b30ce84468 Maybe it will run this time? 2024-03-01 09:48:01 +03:00
Grishka c242c7ec82 please work 2024-03-01 09:41:36 +03:00
Grishka f128556a49 Add github action 2024-03-01 09:38:31 +03:00
Grishka 3cebc78443 Add Fastlane 2024-03-01 08:09:15 +03:00
Eugen Rochko c640501430 New translations strings.xml (Basque) 2024-02-29 17:48:30 +01:00
Eugen Rochko 48100a19e1 New translations strings.xml (Thai) 2024-02-29 12:03:06 +01:00
Eugen Rochko 102ca8034a New translations strings.xml (Icelandic) 2024-02-29 12:03:05 +01:00
Eugen Rochko c1d8f5904b New translations strings.xml (Italian) 2024-02-29 12:03:04 +01:00
Eugen Rochko 361086fc48 New translations strings.xml (French) 2024-02-29 12:03:03 +01:00
Eugen Rochko 10ebacf9cf New translations strings.xml (French) 2024-02-29 09:06:54 +01:00
Grishka 02bfb34665 Android Gradle Plugin can't JUST WORK, can it?! 2024-02-29 10:04:15 +03:00
Eugen Rochko 9f9ba7c367 New translations strings.xml (Chinese Traditional) 2024-02-29 00:40:36 +01:00
Eugen Rochko 4430822768 New translations strings.xml (Slovenian) 2024-02-28 23:33:09 +01:00
Grishka b4904024c6 Add post pinning
closes #346
2024-02-29 00:57:47 +03:00
Eugen Rochko 0d047e5cf9 New translations strings.xml (Dutch) 2024-02-28 22:33:24 +01:00
Grishka 58259d9fb0 Add QR code scanner button to search tab 2024-02-28 23:48:00 +03:00
Grishka e9dde114b7 Add haptic effect (AND-150) 2024-02-28 23:30:08 +03:00
Eugen Rochko 03c6f4130b New translations strings.xml (French) 2024-02-28 21:29:54 +01:00
Eugen Rochko bb94c7fff4 New translations strings.xml (Dutch) 2024-02-28 21:29:53 +01:00
Grishka 217884aaec Increase line spacing 2024-02-28 23:23:42 +03:00
Eugen Rochko 17508e1f36 New translations strings.xml (Icelandic) 2024-02-28 20:34:17 +01:00
Eugen Rochko 95b33b35df New translations strings.xml (Icelandic) 2024-02-28 18:57:19 +01:00
Eugen Rochko 4c0ae74f2d New translations strings.xml (French) 2024-02-28 14:02:26 +01:00
Eugen Rochko 9ef9110297 New translations strings.xml (French) 2024-02-28 12:30:21 +01:00
Eugen Rochko 71e0115cd6 New translations strings.xml (Lithuanian) 2024-02-27 10:10:47 +01:00
Eugen Rochko ec045abf53 New translations strings.xml (Basque) 2024-02-27 01:11:16 +01:00
Grishka b8ae79faa1 Remove "www." from more places 2024-02-26 23:58:57 +03:00
Grishka b51cccf1d7 Fix invite link detection 2024-02-26 23:54:35 +03:00
Eugen Rochko 87b78acd39 New translations strings.xml (Armenian) 2024-02-26 19:12:57 +01:00
Eugen Rochko a965862c1b New translations strings.xml (Armenian) 2024-02-26 17:25:34 +01:00
Grishka ce427d2a0b AND-149 2024-02-26 19:09:29 +03:00
Grishka 8ea4c84a29 AND-148 2024-02-26 17:59:41 +03:00
Eugen Rochko 0bb66708c6 New translations strings.xml (Armenian) 2024-02-26 06:58:11 +01:00
Eugen Rochko bf4b04ef5f New translations strings.xml (Armenian) 2024-02-26 05:58:19 +01:00
Eugen Rochko eea2f9c0cd New translations strings.xml (Japanese) 2024-02-26 03:36:15 +01:00
Grishka 3cdc27eb94 Remove dependency metadata blob from apks (#777) 2024-02-26 03:50:23 +03:00
Eugen Rochko 75237ab7dc New translations strings.xml (Chinese Traditional) 2024-02-25 22:55:01 +01:00
Eugen Rochko 7601a7de16 New translations strings.xml (Russian) 2024-02-25 19:36:34 +01:00
Eugen Rochko 3745240eea New translations strings.xml (Czech) 2024-02-25 17:36:47 +01:00
Gregory K 963b80cce9
Merge pull request #782 from FineFindus/fix/invisible-menu
fix: disable group divider in ProfileFragment on EMUI
2024-02-25 18:57:37 +03:00
Eugen Rochko 8812500734 New translations strings.xml (Czech) 2024-02-25 16:26:45 +01:00
FineFindus bd41807f3b
fix: disable group divider on EMUI 2024-02-25 16:25:42 +01:00
Eugen Rochko 90ee102eaa New translations strings.xml (Greek) 2024-02-25 13:24:23 +01:00
Eugen Rochko 0bac414151 New translations strings.xml (Greek) 2024-02-25 11:36:04 +01:00
Eugen Rochko 36bd1db67e New translations strings.xml (Hungarian) 2024-02-25 09:17:09 +01:00
Eugen Rochko d15f3e5daf New translations strings.xml (Hungarian) 2024-02-25 08:21:23 +01:00
Eugen Rochko 4c9c444dfc New translations strings.xml (Thai) 2024-02-25 06:48:54 +01:00
Grishka 0dc23789f5 Merge branch 'l10n_master' 2024-02-24 23:58:29 +03:00
Grishka 3f0ebd4aed Bump version 2024-02-24 23:58:09 +03:00
Grishka 1e501c707c Add labels and animations to the tab bar 2024-02-24 23:03:18 +03:00
Eugen Rochko d57f97b492 New translations strings.xml (Chinese Traditional) 2024-02-24 17:03:43 +01:00
Eugen Rochko 29f9214869 New translations strings.xml (German) 2024-02-24 14:17:55 +01:00
Eugen Rochko c6c1fe3595 New translations strings.xml (German) 2024-02-24 12:59:50 +01:00
Eugen Rochko 2ba4c6f443 New translations strings.xml (German) 2024-02-24 11:52:04 +01:00
Eugen Rochko a9ff948818 New translations full_description.txt (Thai) 2024-02-24 09:44:12 +01:00
Eugen Rochko 7dd9cfa7f0 New translations strings.xml (Thai) 2024-02-24 08:35:56 +01:00
Eugen Rochko 15a415558b New translations strings.xml (Thai) 2024-02-24 07:18:50 +01:00
Grishka b3e53bc48d AND-133 2024-02-24 03:57:43 +03:00
Grishka d0e33c8a12 AND-127 2024-02-24 03:48:06 +03:00
Eugen Rochko 060db42e47 New translations strings.xml (Italian) 2024-02-24 01:31:16 +01:00
Grishka 6bcf259de9 AND-129 2024-02-24 03:20:22 +03:00
Grishka 4d0a673209 AND-130 2024-02-24 03:14:41 +03:00
Grishka be9be6dc35 AND-126 2024-02-24 03:05:21 +03:00
Grishka 3f47497c12 AND-132 2024-02-24 03:04:35 +03:00
Eugen Rochko 268e0af7cb New translations strings.xml (Chinese Traditional) 2024-02-24 00:31:53 +01:00
Eugen Rochko c611f723b7 New translations strings.xml (Italian) 2024-02-24 00:31:52 +01:00
Eugen Rochko 4c9f500122 New translations strings.xml (Basque) 2024-02-24 00:31:50 +01:00
Eugen Rochko 15147dddf5 New translations strings.xml (Basque) 2024-02-23 23:31:42 +01:00
Eugen Rochko 747c81c269 New translations strings.xml (Thai) 2024-02-23 21:31:03 +01:00
Eugen Rochko 63ad076046 New translations strings.xml (Urdu (India)) 2024-02-23 18:49:13 +01:00
Eugen Rochko f109033ed2 New translations strings.xml (Kabyle) 2024-02-23 18:49:12 +01:00
Eugen Rochko 9445d3383a New translations strings.xml (Igbo) 2024-02-23 18:49:11 +01:00
Eugen Rochko 5db5537685 New translations strings.xml (Occitan) 2024-02-23 18:49:10 +01:00
Eugen Rochko 1149d1c37f New translations strings.xml (Scottish Gaelic) 2024-02-23 18:49:09 +01:00
Eugen Rochko e4b77551f7 New translations strings.xml (Sinhala) 2024-02-23 18:49:08 +01:00
Eugen Rochko 401fd298f5 New translations strings.xml (Bosnian) 2024-02-23 18:49:08 +01:00
Eugen Rochko 6a1f7f7238 New translations strings.xml (Filipino) 2024-02-23 18:49:07 +01:00
Eugen Rochko 7c1c0894a8 New translations strings.xml (Burmese) 2024-02-23 18:49:06 +01:00
Eugen Rochko cb96ae6cbc New translations strings.xml (Croatian) 2024-02-23 18:49:05 +01:00
Eugen Rochko c15d972359 New translations strings.xml (Thai) 2024-02-23 18:49:04 +01:00
Eugen Rochko c010e2371c New translations strings.xml (Bengali) 2024-02-23 18:49:03 +01:00
Eugen Rochko f88a244594 New translations strings.xml (Indonesian) 2024-02-23 18:49:02 +01:00
Eugen Rochko e2dccda205 New translations strings.xml (Portuguese, Brazilian) 2024-02-23 18:49:01 +01:00
Eugen Rochko bc6c6bc9a5 New translations strings.xml (Icelandic) 2024-02-23 18:48:59 +01:00
Eugen Rochko 3281eabbe1 New translations strings.xml (Galician) 2024-02-23 18:48:58 +01:00
Eugen Rochko 92de091228 New translations strings.xml (Vietnamese) 2024-02-23 18:48:57 +01:00
Eugen Rochko 94ab8a4e8b New translations strings.xml (Chinese Traditional) 2024-02-23 18:48:56 +01:00
Eugen Rochko 799987205e New translations strings.xml (Chinese Simplified) 2024-02-23 18:48:55 +01:00
Eugen Rochko f5dfe70ac6 New translations strings.xml (Ukrainian) 2024-02-23 18:48:54 +01:00
Eugen Rochko 69b03e2e59 New translations strings.xml (Turkish) 2024-02-23 18:48:53 +01:00
Eugen Rochko 588ebe11f4 New translations strings.xml (Swedish) 2024-02-23 18:48:52 +01:00
Eugen Rochko 4fe0d6e893 New translations strings.xml (Slovenian) 2024-02-23 18:48:51 +01:00
Eugen Rochko 787c20794f New translations strings.xml (Portuguese) 2024-02-23 18:48:50 +01:00
Eugen Rochko c462e72279 New translations strings.xml (Norwegian) 2024-02-23 18:48:49 +01:00
Eugen Rochko be7e6afce2 New translations strings.xml (Lithuanian) 2024-02-23 18:48:48 +01:00
Eugen Rochko feb9920829 New translations strings.xml (Korean) 2024-02-23 18:48:47 +01:00
Eugen Rochko 4d6a1a705d New translations strings.xml (Georgian) 2024-02-23 18:48:46 +01:00
Eugen Rochko 59d54469dc New translations strings.xml (Japanese) 2024-02-23 18:48:45 +01:00
Eugen Rochko 9cccd28447 New translations strings.xml (Italian) 2024-02-23 18:48:44 +01:00
Eugen Rochko da13b4602c New translations strings.xml (Armenian) 2024-02-23 18:48:43 +01:00
Eugen Rochko 2084a33192 New translations strings.xml (Hebrew) 2024-02-23 18:48:42 +01:00
Eugen Rochko 542ce40c1a New translations strings.xml (Irish) 2024-02-23 18:48:41 +01:00
Eugen Rochko ddd4ea329c New translations strings.xml (Finnish) 2024-02-23 18:48:40 +01:00
Eugen Rochko b5c3875ce9 New translations strings.xml (Basque) 2024-02-23 18:48:39 +01:00
Eugen Rochko ec8387d9db New translations strings.xml (German) 2024-02-23 18:48:38 +01:00
Eugen Rochko cf464e8eea New translations strings.xml (Danish) 2024-02-23 18:48:37 +01:00
Eugen Rochko 7b66f1c398 New translations strings.xml (Czech) 2024-02-23 18:48:36 +01:00
Eugen Rochko 814e11c1f4 New translations strings.xml (Arabic) 2024-02-23 18:48:35 +01:00
Eugen Rochko 647277444b New translations strings.xml (Spanish) 2024-02-23 18:48:34 +01:00
Eugen Rochko bd4ff9c7ec New translations strings.xml (French) 2024-02-23 18:48:33 +01:00
Eugen Rochko 7ed6195cca New translations strings.xml (Romanian) 2024-02-23 18:48:32 +01:00
Eugen Rochko 43397f069a New translations strings.xml (Dutch) 2024-02-23 18:48:31 +01:00
Eugen Rochko 8e34e31b35 New translations strings.xml (Persian) 2024-02-23 18:48:30 +01:00
Eugen Rochko 7ef1bdfc2a New translations strings.xml (Catalan) 2024-02-23 18:48:29 +01:00
Eugen Rochko 726f91ec75 New translations strings.xml (Belarusian) 2024-02-23 18:48:28 +01:00
Eugen Rochko 55a5268a3c New translations strings.xml (Hindi) 2024-02-23 18:48:27 +01:00
Eugen Rochko 4777e3d0b7 New translations strings.xml (Greek) 2024-02-23 18:48:26 +01:00
Eugen Rochko bfc970abe5 New translations strings.xml (Russian) 2024-02-23 18:48:24 +01:00
Eugen Rochko d2d35f4f39 New translations strings.xml (Polish) 2024-02-23 18:48:23 +01:00
Eugen Rochko 49cef5ea0a New translations strings.xml (Hungarian) 2024-02-23 18:48:22 +01:00
Grishka f2fbf55c53 AND-134 2024-02-23 20:45:17 +03:00
Grishka eacfd2fa4f AND-125 2024-02-23 20:00:35 +03:00
Grishka 51f87848f4 AND-128 2024-02-23 19:36:37 +03:00
Grishka c3f4637ddc AND-137 2024-02-23 19:31:23 +03:00
Grishka 2361394391 Share sheet previews (AND-139) 2024-02-23 19:25:21 +03:00
Eugen Rochko 38fdf8d53e New translations strings.xml (Italian) 2024-02-23 17:25:02 +01:00
Eugen Rochko 0cd3cab65d New translations strings.xml (Greek) 2024-02-23 15:31:39 +01:00
Eugen Rochko 142c8b55a1 New translations strings.xml (Hungarian) 2024-02-23 09:55:26 +01:00
Eugen Rochko 3274acaba8 New translations strings.xml (Hungarian) 2024-02-23 08:04:37 +01:00
Eugen Rochko 99b7d46ddf New translations full_description.txt (Armenian) 2024-02-23 06:54:22 +01:00
Eugen Rochko 4f7e45211f New translations strings.xml (Chinese Traditional) 2024-02-23 02:02:20 +01:00
Eugen Rochko 221773bf71 New translations strings.xml (Urdu (India)) 2024-02-22 23:36:54 +01:00
Eugen Rochko 305ce2ca42 New translations strings.xml (Kabyle) 2024-02-22 23:36:53 +01:00
Eugen Rochko 7dc2480400 New translations strings.xml (Igbo) 2024-02-22 23:36:52 +01:00
Eugen Rochko 9634ec6616 New translations strings.xml (Occitan) 2024-02-22 23:36:51 +01:00
Eugen Rochko 6f29540170 New translations strings.xml (Scottish Gaelic) 2024-02-22 23:36:50 +01:00
Eugen Rochko 18485c1cf8 New translations strings.xml (Sinhala) 2024-02-22 23:36:49 +01:00
Eugen Rochko 7c36c32e4b New translations strings.xml (Bosnian) 2024-02-22 23:36:48 +01:00
Eugen Rochko 1d0af51813 New translations strings.xml (Filipino) 2024-02-22 23:36:47 +01:00
Eugen Rochko d50060d602 New translations strings.xml (Burmese) 2024-02-22 23:36:46 +01:00
Eugen Rochko 9d455b2331 New translations strings.xml (Croatian) 2024-02-22 23:36:45 +01:00
Eugen Rochko 5f3b537f7d New translations strings.xml (Thai) 2024-02-22 23:36:44 +01:00
Eugen Rochko 1d43ed4c8a New translations strings.xml (Bengali) 2024-02-22 23:36:43 +01:00
Eugen Rochko e839323575 New translations strings.xml (Indonesian) 2024-02-22 23:36:42 +01:00
Eugen Rochko 7ee657012e New translations strings.xml (Portuguese, Brazilian) 2024-02-22 23:36:41 +01:00
Eugen Rochko 847fbed956 New translations strings.xml (Icelandic) 2024-02-22 23:36:40 +01:00
Eugen Rochko a89d5df313 New translations strings.xml (Galician) 2024-02-22 23:36:39 +01:00
Eugen Rochko 5f58789008 New translations strings.xml (Vietnamese) 2024-02-22 23:36:38 +01:00
Eugen Rochko 7b64cad668 New translations strings.xml (Chinese Traditional) 2024-02-22 23:36:37 +01:00
Eugen Rochko 70f60f094d New translations strings.xml (Chinese Simplified) 2024-02-22 23:36:36 +01:00
Eugen Rochko a5eff14552 New translations strings.xml (Ukrainian) 2024-02-22 23:36:35 +01:00
Eugen Rochko 36fb929387 New translations strings.xml (Turkish) 2024-02-22 23:36:34 +01:00
Eugen Rochko 66aabfb386 New translations strings.xml (Swedish) 2024-02-22 23:36:33 +01:00
Eugen Rochko f503973db1 New translations strings.xml (Slovenian) 2024-02-22 23:36:32 +01:00
Eugen Rochko 7cec835509 New translations strings.xml (Portuguese) 2024-02-22 23:36:31 +01:00
Eugen Rochko e300942455 New translations strings.xml (Norwegian) 2024-02-22 23:36:30 +01:00
Eugen Rochko 79476eff85 New translations strings.xml (Lithuanian) 2024-02-22 23:36:29 +01:00
Eugen Rochko b4ec470691 New translations strings.xml (Korean) 2024-02-22 23:36:28 +01:00
Eugen Rochko 4f72a0c74e New translations strings.xml (Georgian) 2024-02-22 23:36:27 +01:00
Eugen Rochko bcbb41aa43 New translations strings.xml (Japanese) 2024-02-22 23:36:27 +01:00
Eugen Rochko 5cc38d845a New translations strings.xml (Italian) 2024-02-22 23:36:26 +01:00
Eugen Rochko 36acc1588a New translations strings.xml (Armenian) 2024-02-22 23:36:25 +01:00
Eugen Rochko 349c5200ed New translations strings.xml (Hebrew) 2024-02-22 23:36:24 +01:00
Eugen Rochko ff71f6a092 New translations strings.xml (Irish) 2024-02-22 23:36:23 +01:00
Eugen Rochko 8863284970 New translations strings.xml (Finnish) 2024-02-22 23:36:22 +01:00
Eugen Rochko d5feb4e9f9 New translations strings.xml (Basque) 2024-02-22 23:36:21 +01:00
Eugen Rochko 4c284226e5 New translations strings.xml (German) 2024-02-22 23:36:20 +01:00
Eugen Rochko fef9cf5e64 New translations strings.xml (Danish) 2024-02-22 23:36:19 +01:00
Eugen Rochko 93f3c9a9eb New translations strings.xml (Czech) 2024-02-22 23:36:18 +01:00
Eugen Rochko 5827c77b0c New translations strings.xml (Arabic) 2024-02-22 23:36:17 +01:00
Eugen Rochko 1b36866fba New translations strings.xml (Spanish) 2024-02-22 23:36:16 +01:00
Eugen Rochko ebc10d5052 New translations strings.xml (French) 2024-02-22 23:36:15 +01:00
Eugen Rochko 8953fa48c7 New translations strings.xml (Romanian) 2024-02-22 23:36:14 +01:00
Eugen Rochko 59e7a296ca New translations strings.xml (Dutch) 2024-02-22 23:36:13 +01:00
Eugen Rochko 86432228a3 New translations strings.xml (Persian) 2024-02-22 23:36:12 +01:00
Eugen Rochko 4518566c37 New translations strings.xml (Catalan) 2024-02-22 23:36:11 +01:00
Eugen Rochko b392a89350 New translations strings.xml (Belarusian) 2024-02-22 23:36:10 +01:00
Eugen Rochko 225682f35d New translations strings.xml (Hindi) 2024-02-22 23:36:09 +01:00
Eugen Rochko b41ff2e18f New translations strings.xml (Greek) 2024-02-22 23:36:08 +01:00
Eugen Rochko 6ef76fb5bb New translations strings.xml (Russian) 2024-02-22 23:36:07 +01:00
Eugen Rochko a36679b032 New translations strings.xml (Polish) 2024-02-22 23:36:06 +01:00
Eugen Rochko dde91778a2 New translations strings.xml (Hungarian) 2024-02-22 23:36:05 +01:00
Grishka 1cdc6f4fcf Tweaks to QR code and media viewer 2024-02-23 01:20:24 +03:00
Grishka b8a5346631 Crash fix 2024-02-22 23:15:03 +03:00
Grishka 5cf222379a QR codes for profiles 2024-02-22 21:35:46 +03:00
Eugen Rochko 8f4ff49b32 New translations strings.xml (Basque) 2024-02-21 19:34:39 +01:00
Eugen Rochko 3a68ca3cc0 New translations strings.xml (Basque) 2024-02-21 18:37:46 +01:00
Eugen Rochko 116dc68a38 New translations strings.xml (Basque) 2024-02-21 15:47:30 +01:00
Eugen Rochko df84b0ac34 New translations strings.xml (Basque) 2024-02-21 14:12:43 +01:00
Eugen Rochko 5ef737766c New translations strings.xml (Lithuanian) 2024-02-21 13:00:51 +01:00
Eugen Rochko be17ba870b New translations strings.xml (Basque) 2024-02-21 13:00:50 +01:00
Eugen Rochko 747b2d5801 New translations strings.xml (Lithuanian) 2024-02-21 11:13:33 +01:00
Eugen Rochko b84c9bf948 New translations strings.xml (Icelandic) 2024-02-19 23:55:39 +01:00
Grishka b1e999cc9c Fix crash in post language selector once again 2024-02-20 00:39:58 +03:00
Eugen Rochko 89951a8547 New translations strings.xml (Icelandic) 2024-02-19 22:29:02 +01:00
Eugen Rochko 1bb0ac1110 New translations strings.xml (Icelandic) 2024-02-19 14:05:01 +01:00
Eugen Rochko 5bb51901f7 New translations strings.xml (Czech) 2024-02-19 14:05:00 +01:00
Eugen Rochko 18562cd3ee New translations strings.xml (Icelandic) 2024-02-19 12:34:48 +01:00
Eugen Rochko 9e01270b1e New translations strings.xml (Persian) 2024-02-19 06:27:29 +01:00
Eugen Rochko 0b1ff9730c New translations strings.xml (Lithuanian) 2024-02-18 00:14:32 +01:00
Eugen Rochko 845cfde58e New translations strings.xml (Dutch) 2024-02-17 22:27:21 +01:00
Eugen Rochko f81f264b37 New translations strings.xml (Dutch) 2024-02-17 21:28:35 +01:00
Eugen Rochko 5e14270c47 New translations strings.xml (Dutch) 2024-02-17 19:19:32 +01:00
Eugen Rochko 6d2427b336 New translations strings.xml (Dutch) 2024-02-17 18:24:02 +01:00
Eugen Rochko d89562a4c0 New translations strings.xml (Dutch) 2024-02-17 17:15:00 +01:00
Eugen Rochko 08389b023d New translations strings.xml (Turkish) 2024-02-17 12:50:42 +01:00
Eugen Rochko 797c4d6baa New translations strings.xml (Greek) 2024-02-17 12:50:41 +01:00
Eugen Rochko 9458ddd490 New translations strings.xml (Vietnamese) 2024-02-17 05:24:24 +01:00
Eugen Rochko 8f3a8af35e New translations strings.xml (Vietnamese) 2024-02-17 04:26:46 +01:00
Eugen Rochko d0f927c8d2 New translations strings.xml (Lithuanian) 2024-02-16 12:54:29 +01:00
Eugen Rochko 2de44c8d7f New translations strings.xml (Slovenian) 2024-02-15 23:54:13 +01:00
Eugen Rochko 9a3ab2f4d2 New translations strings.xml (Slovenian) 2024-02-15 22:29:25 +01:00
Eugen Rochko 7cecd689bb New translations strings.xml (Lithuanian) 2024-02-15 22:29:24 +01:00
Eugen Rochko b79cf4e087 New translations strings.xml (Lithuanian) 2024-02-15 21:11:35 +01:00
Eugen Rochko 63a9ce6eb6 New translations strings.xml (Hindi) 2024-02-15 13:02:59 +01:00
Eugen Rochko 1f960e8631 New translations strings.xml (Hindi) 2024-02-15 11:34:35 +01:00
Eugen Rochko 6dc059646e New translations strings.xml (Italian) 2024-02-15 00:39:26 +01:00
Eugen Rochko 5b37db0f8e New translations strings.xml (Italian) 2024-02-14 23:35:53 +01:00
Eugen Rochko 2789dd9fd1 New translations strings.xml (Belarusian) 2024-02-14 15:35:06 +01:00
Eugen Rochko a2a72a4aee New translations strings.xml (Belarusian) 2024-02-14 14:02:19 +01:00
Eugen Rochko b79fc8132a New translations strings.xml (Japanese) 2024-02-14 06:46:50 +01:00
Eugen Rochko 91b8735a4c New translations strings.xml (Japanese) 2024-02-14 05:28:17 +01:00
Eugen Rochko 313d81ffe1 New translations strings.xml (Japanese) 2024-02-14 01:33:19 +01:00
Eugen Rochko 192b32c1c6 New translations strings.xml (Lithuanian) 2024-02-13 22:53:46 +01:00
Eugen Rochko 2ef8be7c59 New translations strings.xml (Lithuanian) 2024-02-13 21:49:15 +01:00
Eugen Rochko 94e8d5e6d9 New translations strings.xml (Thai) 2024-02-13 20:40:01 +01:00
Eugen Rochko 445600653e New translations strings.xml (Lithuanian) 2024-02-13 20:40:00 +01:00
Eugen Rochko 4349c7a9e7 New translations full_description.txt (Chinese Traditional) 2024-02-13 19:24:53 +01:00
Eugen Rochko c2b2c39c8a New translations strings.xml (Thai) 2024-02-13 19:24:52 +01:00
Eugen Rochko 9dee6eea24 New translations strings.xml (Chinese Traditional) 2024-02-13 19:24:51 +01:00
Eugen Rochko 53355b31ea New translations strings.xml (Thai) 2024-02-13 17:47:14 +01:00
Eugen Rochko c2f55675a8 New translations strings.xml (Thai) 2024-02-13 15:07:09 +01:00
Eugen Rochko 42f3c58d02 New translations strings.xml (Swedish) 2024-02-13 09:32:18 +01:00
Eugen Rochko dac2c413d6 New translations strings.xml (Belarusian) 2024-02-13 09:32:17 +01:00
Eugen Rochko a539eb3768 New translations strings.xml (Chinese Traditional) 2024-02-13 05:00:33 +01:00
Eugen Rochko 481610cd10 New translations strings.xml (Belarusian) 2024-02-13 02:38:49 +01:00
Eugen Rochko fa78a0f6ca New translations strings.xml (Chinese Traditional) 2024-02-12 19:39:20 +01:00
Eugen Rochko bcc96ff329 New translations strings.xml (Slovenian) 2024-02-12 19:39:16 +01:00
Eugen Rochko bb3028fff6 New translations strings.xml (Japanese) 2024-02-12 19:39:12 +01:00
Eugen Rochko 3e66ce8949 New translations strings.xml (Spanish) 2024-02-12 19:39:04 +01:00
Eugen Rochko 5f89fb1e49 New translations strings.xml (Spanish) 2024-02-12 10:43:18 +01:00
Eugen Rochko b5b3c2671a New translations strings.xml (Spanish) 2024-02-12 09:04:43 +01:00
Eugen Rochko 6a8c09c113 New translations strings.xml (Slovenian) 2024-02-11 22:10:41 +01:00
Eugen Rochko 9660a2a019 New translations strings.xml (Japanese) 2024-02-11 13:03:46 +01:00
Eugen Rochko f667b657f6 New translations strings.xml (Japanese) 2024-02-11 11:07:22 +01:00
Eugen Rochko 9db309634e New translations strings.xml (Chinese Traditional) 2024-02-09 10:00:17 +01:00
Eugen Rochko 8a96762bcc New translations strings.xml (Chinese Traditional) 2024-02-09 08:08:32 +01:00
Eugen Rochko d58b24722e New translations strings.xml (Kabyle) 2024-02-09 01:31:56 +01:00
Eugen Rochko fe8904b7a5 New translations strings.xml (Scottish Gaelic) 2024-02-09 01:31:54 +01:00
Eugen Rochko 6f3404aac9 New translations strings.xml (Bosnian) 2024-02-09 01:31:52 +01:00
Eugen Rochko 9a81f720c2 New translations strings.xml (Filipino) 2024-02-09 01:31:51 +01:00
Eugen Rochko 3605ad4616 New translations strings.xml (Burmese) 2024-02-09 01:31:50 +01:00
Eugen Rochko 490ecfcb43 New translations strings.xml (Croatian) 2024-02-09 01:31:49 +01:00
Eugen Rochko 02b8ac55d5 New translations strings.xml (Thai) 2024-02-09 01:31:48 +01:00
Eugen Rochko fd71f04ca5 New translations strings.xml (Bengali) 2024-02-09 01:31:47 +01:00
Eugen Rochko 091953ada8 New translations strings.xml (Indonesian) 2024-02-09 01:31:46 +01:00
Eugen Rochko 8707db891a New translations strings.xml (Portuguese, Brazilian) 2024-02-09 01:31:45 +01:00
Eugen Rochko 1c67cb5edb New translations strings.xml (Icelandic) 2024-02-09 01:31:44 +01:00
Eugen Rochko a96567c329 New translations strings.xml (Galician) 2024-02-09 01:31:43 +01:00
Eugen Rochko 691372119a New translations strings.xml (Vietnamese) 2024-02-09 01:31:42 +01:00
Eugen Rochko 25734af54e New translations strings.xml (Chinese Traditional) 2024-02-09 01:31:41 +01:00
Eugen Rochko b37f9abeae New translations strings.xml (Chinese Simplified) 2024-02-09 01:31:40 +01:00
Eugen Rochko f502374533 New translations strings.xml (Ukrainian) 2024-02-09 01:31:39 +01:00
Eugen Rochko f10da18272 New translations strings.xml (Turkish) 2024-02-09 01:31:38 +01:00
Eugen Rochko 940f2ca73f New translations strings.xml (Swedish) 2024-02-09 01:31:37 +01:00
Eugen Rochko db3192e75a New translations strings.xml (Slovenian) 2024-02-09 01:31:36 +01:00
Eugen Rochko 1841568e7e New translations strings.xml (Portuguese) 2024-02-09 01:31:35 +01:00
Eugen Rochko f4ce0e67ac New translations strings.xml (Norwegian) 2024-02-09 01:31:34 +01:00
Eugen Rochko 92b34f085e New translations strings.xml (Lithuanian) 2024-02-09 01:31:33 +01:00
Eugen Rochko 4e4eb05526 New translations strings.xml (Korean) 2024-02-09 01:31:32 +01:00
Eugen Rochko 4dc707871d New translations strings.xml (Georgian) 2024-02-09 01:31:31 +01:00
Eugen Rochko e9562378b4 New translations strings.xml (Japanese) 2024-02-09 01:31:30 +01:00
Eugen Rochko c1b9aa7826 New translations strings.xml (Italian) 2024-02-09 01:31:29 +01:00
Eugen Rochko e979a348be New translations strings.xml (Armenian) 2024-02-09 01:31:28 +01:00
Eugen Rochko 89c5787ad6 New translations strings.xml (Finnish) 2024-02-09 01:31:26 +01:00
Eugen Rochko f0b2329656 New translations strings.xml (Basque) 2024-02-09 01:31:25 +01:00
Eugen Rochko 6cf9969220 New translations strings.xml (German) 2024-02-09 01:31:24 +01:00
Eugen Rochko 1f37e7605e New translations strings.xml (Danish) 2024-02-09 01:31:23 +01:00
Eugen Rochko cf6af6f912 New translations strings.xml (Czech) 2024-02-09 01:31:22 +01:00
Eugen Rochko 2c91adb03e New translations strings.xml (Arabic) 2024-02-09 01:31:21 +01:00
Eugen Rochko 911da90854 New translations strings.xml (Spanish) 2024-02-09 01:31:20 +01:00
Eugen Rochko 27f0235055 New translations strings.xml (French) 2024-02-09 01:31:19 +01:00
Eugen Rochko f3764222d8 New translations strings.xml (Dutch) 2024-02-09 01:31:17 +01:00
Eugen Rochko 64cb8c4a9a New translations strings.xml (Persian) 2024-02-09 01:31:16 +01:00
Eugen Rochko de78356f5a New translations strings.xml (Catalan) 2024-02-09 01:31:16 +01:00
Eugen Rochko 8a9d39397c New translations strings.xml (Belarusian) 2024-02-09 01:31:15 +01:00
Eugen Rochko 2d89fd0cf0 New translations strings.xml (Hindi) 2024-02-09 01:31:14 +01:00
Eugen Rochko 402620dbe4 New translations strings.xml (Greek) 2024-02-09 01:31:13 +01:00
Eugen Rochko 32776db395 New translations strings.xml (Russian) 2024-02-09 01:31:12 +01:00
Eugen Rochko 4523ab8a67 New translations strings.xml (Polish) 2024-02-09 01:31:11 +01:00
Eugen Rochko 760106bf5b New translations strings.xml (Hungarian) 2024-02-09 01:31:09 +01:00
Eugen Rochko f705afcafc New translations strings.xml (Dutch) 2024-02-08 23:16:32 +01:00
251 changed files with 9070 additions and 2009 deletions

79
.github/workflows/build_and_deploy.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Build and deploy
on:
push:
branches:
- 'ci_setup'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v2
with:
java-version: 21
distribution: temurin
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Decode keystore
uses: timheuer/base64-to-file@v1
id: android_keystore
with:
fileName: "release.jks"
encodedString: ${{ secrets.KEYSTORE_FILE }}
- name: Prepare Gradle environment
run: |
echo "apply from: 'ci_signing.gradle'" >> mastodon/build.gradle
echo "sdk.dir=$ANDROID_SDK_ROOT" > local.properties
- name: Build and deploy to Google Play
run: bundle exec fastlane deploy
env:
KEYSTORE_FILE: ${{ steps.android_keystore.outputs.filePath }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
SUPPLY_SKIP_UPLOAD_METADATA: true
SUPPLY_SKIP_UPLOAD_CHANGELOGS: true
- name: Build release apk
run: ./gradlew assembleRelease
env:
KEYSTORE_FILE: ${{ steps.android_keystore.outputs.filePath }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: mastodon-release.apk
path: mastodon/build/outputs/apk/release/mastodon-release.apk
- name: Build githubRelease apk
run: ./gradlew assembleGithubRelease
env:
KEYSTORE_FILE: ${{ steps.android_keystore.outputs.filePath }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
- name: Upload githubRelease apk
uses: actions/upload-artifact@v4
with:
name: mastodon-githubRelease.apk
path: mastodon/build/outputs/apk/githubRelease/mastodon-githubRelease.apk
- name: Upload mappings
uses: actions/upload-artifact@v4
with:
name: mappings
path: mastodon/build/outputs/mapping/*/mapping.txt

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
.cxx
local.properties
*.jks
/fastlane/report.xml

3
Gemfile Normal file
View File

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

218
Gemfile.lock Normal file
View File

@ -0,0 +1,218 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.913.0)
aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.78.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.4.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.5)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-22
ruby
DEPENDENCIES
fastlane
BUNDLED WITH
2.5.4

View File

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

2
fastlane/Appfile Normal file
View File

@ -0,0 +1,2 @@
json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("org.joinmastodon.android") # e.g. com.krausefx.app

36
fastlane/Fastfile Normal file
View File

@ -0,0 +1,36 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(
task: "bundle",
build_type: "release",
)
upload_to_play_store(
changes_not_sent_for_review: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
end

40
fastlane/README.md Normal file
View File

@ -0,0 +1,40 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android test
```sh
[bundle exec] fastlane android test
```
Runs all the tests
### android deploy
```sh
[bundle exec] fastlane android deploy
```
Deploy a new version to the Google Play
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@ -0,0 +1,3 @@
- You can now easily share and scan QR codes to quickly find each other
- We've updated the look of the tab bar to better match current platform guidelines
- Various minor usability improvements

View File

@ -0,0 +1,3 @@
- QR code scanner can now be accessed from the explore page
- You can now pin posts to your profile
- Fixed featured hashtags on profiles not showing the profile's own posts exclusively

View File

@ -0,0 +1,8 @@
- Adjust filters in the Notifications tab to silence unwanted alerts*
- Opt into push notifications when a user posts by tapping the bell 🔔 in the top corner of a user's profile.
- Mute overly active conversation notifications via the More button ⋮ on your posts
- When writing a post, choose Quiet public 🌙 visibility to avoid appearing in feeds or searches
- Improved post legibility with adjusted line height
- Bug fixes
*Your server must support filtered notifications to see this option.

View File

@ -4,12 +4,12 @@
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Զգայուն կամ հրահրող թեմաներով գրառումները կարելի է թաքցնել նախազգուշացումներով։ Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Ավելին՝
Ավելին.
• Մուգ տարբերակ՝ կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
• Հարցումներ՝ իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
• Մուգ տարբերակ. կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
• Հարցումներ. իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
• 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

View File

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

View File

@ -6,11 +6,11 @@ Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk
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
• 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 perrašymus.
• Bendrinti: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje.
• Mielumas: mūsų talismanas ž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.
Mastodon 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

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

View File

@ -1,4 +1,4 @@
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbiri ile kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğunuz önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsiniz!
Mastodon, internetteki merkezi olmayan en büyük sosyal ağdır. Tek bir web siteye bağlı kalmaksızın, milyonlarca kullanıcının bağımsız olarak birbirleriyle kolayca etkileşebileceği bir ağdır. Hangi konuyla ilgili olduğunuz önemli değil, Mastodon'da onunla ilgili gönderi paylaşan tutkulu insanlarla tanışabilirsiniz!
Bir topluluğa katılın ve profilinizi oluşturun. Olağanüstü kişileri bulun ve takip edin; gönderilerini reklamsız ve kronolojik bir zaman çizelgesinde okuyun. Gönderilerinizde şimdilik 500 karakter sınırlamasıyla kendinizi emojiler, görseller, GIFler, videolar ve sesler ile ifade edin. Harika içerikler paylaşmak için başlıklara yanıt yazın, insanların gönderilerini yineleyin. Ağınızı genişletmek için takip edilecek yeni hesaplar ve hashtagler bulun.
@ -13,4 +13,4 @@ Diğer özellikler:
• Paylaşım: Doğrudan Mastodon'a herhangi bir türde gönderi paylaş
• Sevimlilik: Maskotumuz şirin bir fil ve onu uygulamada zaman zaman göreceksiniz
Mastodon kar amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam yok, para kazanma güdüsü yok, risk sermayesi yok ve bu şekilde devam etmeyi planlıyoruz.
Mastodon kâr amacı gütmeyen bir kuruluştur ve geliştirilmesi doğrudan bağışlarınızla sağlanmaktadır. Reklam yok, para kazanma güdüsü yok, risk sermayesi yok ve bu şekilde devam etmeyi planlıyoruz.

View File

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

View File

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

View File

@ -4,15 +4,18 @@ plugins {
}
android {
androidResources {
generateLocaleConfig = true
}
compileSdk 33
defaultConfig {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 84
versionName "2.3.0"
versionCode 97
versionName "2.5.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", "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 {
@ -64,6 +67,17 @@ android {
checkReleaseBuilds false
abortOnError false
}
buildFeatures{
aidl true
buildConfig true
}
dependenciesInfo{
// Disables dependency metadata when building APKs.
includeInApk false
// Disables dependency metadata when building Android App Bundles.
includeInBundle false
}
namespace 'org.joinmastodon.android'
}
dependencies {
@ -76,11 +90,13 @@ 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.16'
implementation 'me.grishka.appkit:appkit:1.2.17'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'
implementation 'com.google.zxing:core:3.5.3'
implementation 'org.microg:safe-parcel:1.5.0'
implementation 'org.parceler:parceler-api:1.1.12'
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'

View File

@ -0,0 +1,20 @@
// Included into build.gradle when running in a CI pipeline
android{
signingConfigs{
release{
keyAlias "key0"
keyPassword System.getenv("KEYSTORE_PASSWORD")
storeFile file(System.getenv("KEYSTORE_FILE"))
storePassword System.getenv("KEYSTORE_PASSWORD")
}
}
buildTypes{
release{
signingConfig signingConfigs.release
}
githubRelease{
signingConfig signingConfigs.release
}
}
}

View File

@ -22,7 +22,7 @@
# Keep all model classes as they're used with gson and their names are shown in errors
-keep public class org.joinmastodon.android.model.**{
<fields>;
*;
}
# Inner classes in api requests are used with gson
@ -50,4 +50,8 @@
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable
-keepattributes LineNumberTable
-keepattributes Signature
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep public class * implements java.lang.reflect.Type

View File

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
@ -19,6 +20,10 @@
<intent>
<action android:name="android.intent.action.TRANSLATE" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
</queries>
<application
@ -26,11 +31,14 @@
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:largeHeap="true">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@ -84,6 +92,14 @@
</intent-filter>
</receiver>
<provider
android:authorities="${applicationId}.fileprovider"
android:name=".TweakedFileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths"/>
</provider>
</application>
</manifest>

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.api;
parcelable Status;

View File

@ -0,0 +1,7 @@
package com.google.android.gms.common.api.internal;
import com.google.android.gms.common.api.Status;
interface IStatusCallback {
void onResult(in Status status);
}

View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.common.internal;
parcelable ConnectionInfo;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.internal;
parcelable GetServiceRequest;

View File

@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.common.internal;
import android.os.Bundle;
import com.google.android.gms.common.internal.ConnectionInfo;
interface IGmsCallbacks {
void onPostInitComplete(int statusCode, IBinder binder, in Bundle params);
void onAccountValidationComplete(int statusCode, in Bundle params);
void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, in ConnectionInfo info);
}

View File

@ -0,0 +1,10 @@
package com.google.android.gms.common.internal;
import android.os.Bundle;
import com.google.android.gms.common.internal.IGmsCallbacks;
import com.google.android.gms.common.internal.GetServiceRequest;
interface IGmsServiceBroker {
void getService(IGmsCallbacks callback, in GetServiceRequest request) = 45;
}

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleAvailabilityResponse;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallIntentResponse;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallResponse;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallStatusUpdate;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall.internal;
parcelable ApiFeatureRequest;

View File

@ -0,0 +1,13 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallResponse;
interface IModuleInstallCallbacks {
void onModuleAvailabilityResponse(in Status status, in ModuleAvailabilityResponse response) = 0;
void onModuleInstallResponse(in Status status, in ModuleInstallResponse response) = 1;
void onModuleInstallIntentResponse(in Status status, in ModuleInstallIntentResponse response) = 2;
void onStatus(in Status status) = 3;
}

View File

@ -0,0 +1,14 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener;
interface IModuleInstallService {
void areModulesAvailable(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 0;
void installModules(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request, IModuleInstallStatusListener listener) = 1;
void getInstallModulesIntent(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 2;
void releaseModules(IStatusCallback callback, in ApiFeatureRequest request) = 3;
void unregisterListener(IStatusCallback callback, IModuleInstallStatusListener listener) = 5;
}

View File

@ -0,0 +1,7 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate;
interface IModuleInstallStatusListener {
void onModuleInstallStatusUpdate(in ModuleInstallStatusUpdate statusUpdate) = 0;
}

View File

@ -0,0 +1,15 @@
package com.google.android.gms.common;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Feature extends AutoSafeParcelable{
@SafeParceled(1)
public String name;
@SafeParceled(2)
public int oldVersion;
@SafeParceled(3)
public long version=-1;
public static final Creator<Feature> CREATOR=new AutoCreator<>(Feature.class);
}

View File

@ -0,0 +1,13 @@
package com.google.android.gms.common.api;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Scope extends AutoSafeParcelable{
@SafeParceled(1)
public int versionCode=1;
@SafeParceled(2)
public String scopeUri;
public static final Creator<Scope> CREATOR=new AutoCreator<>(Scope.class);
}

View File

@ -0,0 +1,33 @@
package com.google.android.gms.common.api;
import android.app.PendingIntent;
import org.joinmastodon.android.googleservices.ConnectionResult;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Status extends AutoSafeParcelable{
@SafeParceled(1000)
public int versionCode;
@SafeParceled(1)
public int statusCode;
@SafeParceled(2)
public String statusMessage;
@SafeParceled(3)
public PendingIntent pendingIntent;
@SafeParceled(4)
public ConnectionResult connectionResult;
public static final Creator<Status> CREATOR=new AutoCreator<>(Status.class);
@Override
public String toString(){
return "Status{"+
"versionCode="+versionCode+
", statusCode="+statusCode+
", statusMessage='"+statusMessage+'\''+
", pendingIntent="+pendingIntent+
", connectionResult="+connectionResult+
'}';
}
}

View File

@ -0,0 +1,19 @@
package com.google.android.gms.common.internal;
import android.os.Bundle;
import com.google.android.gms.common.Feature;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ConnectionInfo extends AutoSafeParcelable{
@SafeParceled(1)
public Bundle params;
@SafeParceled(2)
public Feature[] features;
@SafeParceled(3)
public int unknown3;
public static final Creator<ConnectionInfo> CREATOR=new AutoCreator<>(ConnectionInfo.class);
}

View File

@ -0,0 +1,47 @@
package com.google.android.gms.common.internal;
import android.os.Bundle;
import android.os.IBinder;
import android.accounts.Account;
import com.google.android.gms.common.Feature;
import com.google.android.gms.common.api.Scope;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class GetServiceRequest extends AutoSafeParcelable{
@SafeParceled(1)
int versionCode=6;
@SafeParceled(2)
public int serviceId;
@SafeParceled(3)
public int gmsVersion;
@SafeParceled(4)
public String packageName;
@SafeParceled(5)
public IBinder accountAccessor;
@SafeParceled(6)
public Scope[] scopes;
@SafeParceled(7)
public Bundle extras;
@SafeParceled(8)
public Account account;
@SafeParceled(9)
@Deprecated
long field9;
@SafeParceled(10)
public Feature[] defaultFeatures;
@SafeParceled(11)
public Feature[] apiFeatures;
@SafeParceled(12)
boolean supportsConnectionInfo;
@SafeParceled(13)
int field13;
@SafeParceled(14)
boolean field14;
@SafeParceled(15)
String attributionTag;
public static final Creator<GetServiceRequest> CREATOR=new AutoCreator<>(GetServiceRequest.class);
}

View File

@ -0,0 +1,13 @@
package com.google.android.gms.common.moduleinstall;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleAvailabilityResponse extends AutoSafeParcelable{
@SafeParceled(1)
public boolean modulesAvailable;
@SafeParceled(2)
public int availabilityStatus;
public static final Creator<ModuleAvailabilityResponse> CREATOR=new AutoCreator<>(ModuleAvailabilityResponse.class);
}

View File

@ -0,0 +1,13 @@
package com.google.android.gms.common.moduleinstall;
import android.app.PendingIntent;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleInstallIntentResponse extends AutoSafeParcelable{
@SafeParceled(1)
public PendingIntent pendingIntent;
public static final Creator<ModuleInstallIntentResponse> CREATOR=new AutoCreator<>(ModuleInstallIntentResponse.class);
}

View File

@ -0,0 +1,21 @@
package com.google.android.gms.common.moduleinstall;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleInstallResponse extends AutoSafeParcelable{
@SafeParceled(1)
public int sessionID;
@SafeParceled(2)
public boolean shouldUnregisterListener;
public static final Creator<ModuleInstallResponse> CREATOR=new AutoCreator<>(ModuleInstallResponse.class);
@Override
public String toString(){
return "ModuleInstallResponse{"+
"sessionID="+sessionID+
", shouldUnregisterListener="+shouldUnregisterListener+
'}';
}
}

View File

@ -0,0 +1,63 @@
package com.google.android.gms.common.moduleinstall;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleInstallStatusUpdate extends AutoSafeParcelable{
public static final int STATE_UNKNOWN = 0;
/**
* The request is pending and will be processed soon.
*/
public static final int STATE_PENDING = 1;
/**
* The optional module download is in progress.
*/
public static final int STATE_DOWNLOADING = 2;
/**
* The optional module download has been canceled.
*/
public static final int STATE_CANCELED = 3;
/**
* Installation is completed; the optional modules are available to the client app.
*/
public static final int STATE_COMPLETED = 4;
/**
* The optional module download or installation has failed.
*/
public static final int STATE_FAILED = 5;
/**
* The optional modules have been downloaded and the installation is in progress.
*/
public static final int STATE_INSTALLING = 6;
/**
* The optional module download has been paused.
* <p>
* This usually happens when connectivity requirements can't be met during download. Once the connectivity requirements
* are met, the download will be resumed automatically.
*/
public static final int STATE_DOWNLOAD_PAUSED = 7;
@SafeParceled(1)
public int sessionID;
@SafeParceled(2)
public int installState;
@SafeParceled(3)
public Long bytesDownloaded;
@SafeParceled(4)
public Long totalBytesToDownload;
@SafeParceled(5)
public int errorCode;
@Override
public String toString(){
return "ModuleInstallStatusUpdate{"+
"sessionID="+sessionID+
", installState="+installState+
", bytesDownloaded="+bytesDownloaded+
", totalBytesToDownload="+totalBytesToDownload+
", errorCode="+errorCode+
'}';
}
public static final Creator<ModuleInstallStatusUpdate> CREATOR=new AutoCreator<>(ModuleInstallStatusUpdate.class);
}

View File

@ -0,0 +1,21 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.Feature;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
import java.util.List;
public class ApiFeatureRequest extends AutoSafeParcelable{
@SafeParceled(value=1, subClass=Feature.class)
public List<Feature> features;
@SafeParceled(2)
public boolean urgent;
@SafeParceled(3)
public String sessionId;
@SafeParceled(4)
public String callingPackage;
public static final Creator<ApiFeatureRequest> CREATOR=new AutoCreator<>(ApiFeatureRequest.class);
}

View File

@ -0,0 +1,841 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.content.ClipData;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing
* of files associated with an app by creating a <code>content://</code> {@link Uri} for a file
* instead of a <code>file:///</code> {@link Uri}.
* <p>
* A content URI allows you to grant read and write access using
* temporary access permissions. When you create an {@link Intent} containing
* a content URI, in order to send the content URI
* to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add
* permissions. These permissions are available to the client app for as long as the stack for
* a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a
* {@link android.app.Service}, the permissions are available as long as the
* {@link android.app.Service} is running.
* <p>
* In comparison, to control access to a <code>file:///</code> {@link Uri} you have to modify the
* file system permissions of the underlying file. The permissions you provide become available to
* <em>any</em> app, and remain in effect until you change them. This level of access is
* fundamentally insecure.
* <p>
* The increased level of file access security offered by a content URI
* makes FileProvider a key part of Android's security infrastructure.
* <p>
* This overview of FileProvider includes the following topics:
* </p>
* <ol>
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
* <li><a href="#GetUri">Retrieving the Content URI for a File</li>
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
* </ol>
* <h3 id="ProviderDefinition">Defining a FileProvider</h3>
* <p>
* Since the default functionality of FileProvider includes content URI generation for files, you
* don't need to define a subclass in code. Instead, you can include a FileProvider in your app
* by specifying it entirely in XML. To specify the FileProvider component itself, add a
* <code><a href="{@docRoot}guide/topics/manifest/provider-element.html">&lt;provider&gt;</a></code>
* element to your app manifest. Set the <code>android:name</code> attribute to
* <code>androidx.core.content.FileProvider</code>. Set the <code>android:authorities</code>
* attribute to a URI authority based on a domain you control; for example, if you control the
* domain <code>mydomain.com</code> you should use the authority
* <code>com.mydomain.fileprovider</code>. Set the <code>android:exported</code> attribute to
* <code>false</code>; the FileProvider does not need to be public. Set the
* <a href="{@docRoot}guide/topics/manifest/provider-element.html#gprmsn"
* >android:grantUriPermissions</a> attribute to <code>true</code>, to allow you
* to grant temporary access to files. For example:
* <pre class="prettyprint">
*&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider
* android:name="androidx.core.content.FileProvider"
* android:authorities="com.mydomain.fileprovider"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* ...
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
* <p>
* If you want to override any of the default behavior of FileProvider methods, extend
* the FileProvider class and use the fully-qualified class name in the <code>android:name</code>
* attribute of the <code>&lt;provider&gt;</code> element.
* <h3 id="SpecifyFiles">Specifying Available Files</h3>
* A FileProvider can only generate a content URI for files in directories that you specify
* beforehand. To specify a directory, specify the its storage area and path in XML, using child
* elements of the <code>&lt;paths&gt;</code> element.
* For example, the following <code>paths</code> element tells FileProvider that you intend to
* request content URIs for the <code>images/</code> subdirectory of your private file area.
* <pre class="prettyprint">
*&lt;paths xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;files-path name="my_images" path="images/"/&gt;
* ...
*&lt;/paths&gt;
*</pre>
* <p>
* The <code>&lt;paths&gt;</code> element must contain one or more of the following child elements:
* </p>
* <dl>
* <dt>
* <pre class="prettyprint">
*&lt;files-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the <code>files/</code> subdirectory of your app's internal storage
* area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
* Context.getFilesDir()}.
* </dd>
* <dt>
* <pre>
*&lt;cache-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* <dt>
* <dd>
* Represents files in the cache subdirectory of your app's internal storage area. The root path
* of this subdirectory is the same as the value returned by {@link Context#getCacheDir()
* getCacheDir()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of the external storage area. The root path of this subdirectory
* is the same as the value returned by
* {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-files-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external storage area. The root path of this
* subdirectory is the same as the value returned by
* {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-cache-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external cache area. The root path of this
* subdirectory is the same as the value returned by
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-media-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external media area. The root path of this
* subdirectory is the same as the value returned by the first result of
* {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
* <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
* </dd>
* </dl>
* <p>
* These child elements all use the same attributes:
* </p>
* <dl>
* <dt>
* <code>name="<i>name</i>"</code>
* </dt>
* <dd>
* A URI path segment. To enforce security, this value hides the name of the subdirectory
* you're sharing. The subdirectory name for this value is contained in the
* <code>path</code> attribute.
* </dd>
* <dt>
* <code>path="<i>path</i>"</code>
* </dt>
* <dd>
* The subdirectory you're sharing. While the <code>name</code> attribute is a URI path
* segment, the <code>path</code> value is an actual subdirectory name. Notice that the
* value refers to a <b>subdirectory</b>, not an individual file or files. You can't
* share a single file by its file name, nor can you specify a subset of files using
* wildcards.
* </dd>
* </dl>
* <p>
* You must specify a child element of <code>&lt;paths&gt;</code> for each directory that contains
* files for which you want content URIs. For example, these XML elements specify two directories:
* <pre class="prettyprint">
*&lt;paths xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;files-path name="my_images" path="images/"/&gt;
* &lt;files-path name="my_docs" path="docs/"/&gt;
*&lt;/paths&gt;
*</pre>
* <p>
* Put the <code>&lt;paths&gt;</code> element and its children in an XML file in your project.
* For example, you can add them to a new file called <code>res/xml/file_paths.xml</code>.
* To link this file to the FileProvider, add a
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">&lt;meta-data&gt;</a> element
* as a child of the <code>&lt;provider&gt;</code> element that defines the FileProvider. Set the
* <code>&lt;meta-data&gt;</code> element's "android:name" attribute to
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
* to <code>&#64;xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
* extension). For example:
* <pre class="prettyprint">
*&lt;provider
* android:name="androidx.core.content.FileProvider"
* android:authorities="com.mydomain.fileprovider"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* &lt;meta-data
* android:name="android.support.FILE_PROVIDER_PATHS"
* android:resource="&#64;xml/file_paths" /&gt;
*&lt;/provider&gt;
*</pre>
* <h3 id="GetUri">Generating the Content URI for a File</h3>
* <p>
* To share a file with another app using a content URI, your app has to generate the content URI.
* To generate the content URI, create a new {@link File} for the file, then pass the {@link File}
* to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an
* {@link Intent}. The client app that receives the content URI can open the file
* and access its contents by calling
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}.
* <p>
* For example, suppose your app is offering files to other apps with a FileProvider that has the
* authority <code>com.mydomain.fileprovider</code>. To get a content URI for the file
* <code>default_image.jpg</code> in the <code>images/</code> subdirectory of your internal storage
* add the following code:
* <pre class="prettyprint">
*File imagePath = new File(Context.getFilesDir(), "images");
*File newFile = new File(imagePath, "default_image.jpg");
*Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
*</pre>
* As a result of the previous snippet,
* {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI
* <code>content://com.mydomain.fileprovider/my_images/default_image.jpg</code>.
* <h3 id="Permissions">Granting Temporary Permissions to a URI</h3>
* To grant an access permission to a content URI returned from
* {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following:
* <ul>
* <li>
* Call the method
* {@link Context#grantUriPermission(String, Uri, int)
* Context.grantUriPermission(package, Uri, mode_flags)} for the <code>content://</code>
* {@link Uri}, using the desired mode flags. This grants temporary access permission for the
* content URI to the specified package, according to the value of the
* the <code>mode_flags</code> parameter, which you can set to
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
* or both. The permission remains in effect until you revoke it by calling
* {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
* reboots.
* </li>
* <li>
* Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}.
* </li>
* <li>
* Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both.
* </li>
* <li>
* Finally, send the {@link Intent} to
* another app. Most often, you do this by calling
* {@link android.app.Activity#setResult(int, Intent) setResult()}.
* <p>
* Permissions granted in an {@link Intent} remain in effect while the stack of the receiving
* {@link android.app.Activity} is active. When the stack finishes, the permissions are
* automatically removed. Permissions granted to one {@link android.app.Activity} in a client
* app are automatically extended to other components of that app.
* </p>
* </li>
* </ul>
* <h3 id="ServeUri">Serving a Content URI to Another App</h3>
* <p>
* There are a variety of ways to serve the content URI for a file to a client app. One common way
* is for the client app to start your app by calling
* {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()},
* which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app.
* In response, your app can immediately return a content URI to the client app or present a user
* interface that allows the user to pick a file. In the latter case, once the user picks the file
* your app can return its content URI. In both cases, your app returns the content URI in an
* {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}.
* </p>
* <p>
* You can also put the content URI in a {@link android.content.ClipData} object and then add the
* object to an {@link Intent} you send to a client app. To do this, call
* {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can
* add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own
* content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent}
* to set temporary access permissions, the same permissions are applied to all of the content
* URIs.
* </p>
* <p class="note">
* <strong>Note:</strong> The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is
* only available in platform version 16 (Android 4.1) and later. If you want to maintain
* compatibility with previous versions, you should send one content URI at a time in the
* {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling
* {@link Intent#setData setData()}.
* </p>
* <h3 id="">More Information</h3>
* <p>
* To learn more about FileProvider, see the Android training class
* <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files Securely with URIs</a>.
* </p>
*/
public class FileProvider extends ContentProvider {
private static final String[] COLUMNS = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
private static final String
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
private static final File DEVICE_ROOT = new File("/");
@GuardedBy("sCache")
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
private PathStrategy mStrategy;
/**
* The default FileProvider implementation does not need to be initialized. If you want to
* override this method, you must provide your own subclass of FileProvider.
*/
@Override
public boolean onCreate() {
return true;
}
/**
* After the FileProvider is instantiated, this method is called to provide the system with
* information about the provider.
*
* @param context A {@link Context} for the current component.
* @param info A {@link ProviderInfo} for the new provider.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Sanity check our security
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority);
}
/**
* Return a content URI for a given {@link File}. Specific temporary
* permissions for the content URI can be set with
* {@link Context#grantUriPermission(String, Uri, int)}, or added
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
* <code>content</code> {@link Uri} for file paths defined in their <code>&lt;paths&gt;</code>
* meta-data element. See the Class Overview for more information.
*
* @param context A {@link Context} for the current component.
* @param authority The authority of a {@link FileProvider} defined in a
* {@code <provider>} element in your app's manifest.
* @param file A {@link File} pointing to the filename for which you want a
* <code>content</code> {@link Uri}.
* @return A content URI for the file.
* @throws IllegalArgumentException When the given {@link File} is outside
* the paths supported by the provider.
*/
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
/**
* Use a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
* managed by the FileProvider.
* FileProvider reports the column names defined in {@link OpenableColumns}:
* <ul>
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
* <li>{@link OpenableColumns#SIZE}</li>
* </ul>
* For more information, see
* {@link ContentProvider#query(Uri, String[], String, String[], String)
* ContentProvider.query()}.
*
* @param uri A content URI returned by {@link #getUriForFile}.
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
* included.
* @param selection Selection criteria to apply. If null then all data that matches the content
* URI is returned.
* @param selectionArgs An array of {@link String}, containing arguments to bind to
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
* values are bound to <i>selection</i> as {@link String} values.
* @param sortOrder A {@link String} containing the column name(s) on which to sort
* the resulting {@link Cursor}.
* @return A {@link Cursor} containing the results of the query.
*
*/
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
/**
* Returns the MIME type of a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
*
* @param uri A content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @return If the associated file has an extension, the MIME type associated with that
* extension; otherwise <code>application/octet-stream</code>.
*/
@Override
public String getType(@NonNull Uri uri) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
/**
* Deletes the file associated with the specified content URI, as
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
*
* @param uri A content URI for a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param selection Ignored. Set to {@code null}.
* @param selectionArgs Ignored. Set to {@code null}.
* @return 1 if the delete succeeds; otherwise, 0.
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
return file.delete() ? 1 : 0;
}
/**
* By default, FileProvider automatically returns the
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor}.
*
* To override this method, you must provide your own subclass of FileProvider.
*
* @param uri A content URI associated with a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
* write access, or "rwt" for read and write access that truncates any existing file.
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
*/
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
/**
* Return {@link PathStrategy} for given authority, either by parsing or
* returning from cache.
*/
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
/**
* Parse and return {@link PathStrategy} for given authority as defined in
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
*
* @see #getPathStrategy(Context, String)
*/
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
if (info == null) {
throw new IllegalArgumentException(
"Couldn't find meta-data for provider with authority " + authority);
}
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = context.getExternalFilesDirs(null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = context.getExternalCacheDirs();
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
/**
* Strategy for mapping between {@link File} and {@link Uri}.
* <p>
* Strategies must be symmetric so that mapping a {@link File} to a
* {@link Uri} and then back to a {@link File} points at the original
* target.
* <p>
* Strategies must remain consistent across app launches, and not rely on
* dynamic state. This ensures that any generated {@link Uri} can still be
* resolved if your process is killed and later restarted.
*
* @see SimplePathStrategy
*/
interface PathStrategy {
/**
* Return a {@link Uri} that represents the given {@link File}.
*/
Uri getUriForFile(File file);
/**
* Return a {@link File} that represents the given {@link Uri}.
*/
File getFileForUri(Uri uri);
}
/**
* Strategy that provides access to files living under a narrow whitelist of
* filesystem roots. It will throw {@link SecurityException} if callers try
* accessing files outside the configured roots.
* <p>
* For example, if configured with
* {@code addRoot("myfiles", context.getFilesDir())}, then
* {@code context.getFileStreamPath("foo.txt")} would map to
* {@code content://myauthority/myfiles/foo.txt}.
*/
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<String, File>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
/**
* Copied from ContentResolver.java
*/
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
} else if ("w".equals(mode) || "wt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else if ("wa".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_APPEND;
} else if ("rw".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE;
} else if ("rwt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
return modeBits;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
}

View File

@ -152,6 +152,11 @@ public class MainActivity extends FragmentStackActivity{
}
fragment.setArguments(args);
showFragment(fragment);
Intent intent=getIntent();
intent.removeExtra("fromNotification");
intent.removeExtra("notification");
intent.removeExtra("accountID");
setIntent(intent);
}
private void showCompose(){

View File

@ -75,7 +75,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
new GetNotificationByID(pn.notificationId+"")
new GetNotificationByID(pn.notificationId)
.setCallback(new Callback<>(){
@Override
public void onSuccess(org.joinmastodon.android.model.Notification result){

View File

@ -0,0 +1,38 @@
package org.joinmastodon.android;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.FileNotFoundException;
import java.util.Arrays;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class TweakedFileProvider extends FileProvider{
private static final String TAG="TweakedFileProvider";
@Override
public String getType(@NonNull Uri uri){
Log.d(TAG, "getType() called with: uri = ["+uri+"]");
if(uri.getPathSegments().get(0).equals("image_cache")){
Log.i(TAG, "getType: HERE!");
return "image/jpeg"; // might as well be a png but image decoding APIs don't care, needs to be image/* though
}
return super.getType(uri);
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
Log.d(TAG, "query() called with: uri = ["+uri+"], projection = ["+Arrays.toString(projection)+"], selection = ["+selection+"], selectionArgs = ["+Arrays.toString(selectionArgs)+"], sortOrder = ["+sortOrder+"]");
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
Log.d(TAG, "openFile() called with: uri = ["+uri+"], mode = ["+mode+"]");
return super.openFile(uri, mode);
}
}

View File

@ -113,8 +113,10 @@ public class MastodonAPIController{
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
if(req.canceled)
if(req.canceled){
response.close();
return;
}
if(BuildConfig.DEBUG)
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
synchronized(req){

View File

@ -5,31 +5,62 @@ import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.R;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{
public final String error;
public final int httpStatus;
public final Throwable underlyingException;
public final int messageResource;
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
this.error=error;
this.httpStatus=httpStatus;
this.underlyingException=exception;
if(exception instanceof UnknownHostException){
this.messageResource=R.string.could_not_reach_server;
}else if(exception instanceof SocketTimeoutException){
this.messageResource=R.string.connection_timed_out;
}else if(exception instanceof JsonSyntaxException || exception instanceof JsonIOException || httpStatus>=500){
this.messageResource=R.string.server_error;
}else if(httpStatus==404){
this.messageResource=R.string.not_found;
}else{
this.messageResource=0;
}
}
@Override
public void bindErrorView(View view){
TextView text=view.findViewById(R.id.error_text);
text.setText(error);
String message;
if(messageResource>0){
message=view.getContext().getString(messageResource, error);
}else{
message=error;
}
text.setText(message);
}
@Override
public void showToast(Context context){
if(context==null)
return;
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
String message;
if(messageResource>0){
message=context.getString(messageResource, error);
}else{
message=error;
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
}

View File

@ -1,5 +1,7 @@
package org.joinmastodon.android.api.requests.accounts;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
@ -10,7 +12,7 @@ import java.util.List;
import androidx.annotation.NonNull;
public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
public GetAccountStatuses(String id, String maxID, String minID, int limit, @NonNull Filter filter){
public GetAccountStatuses(String id, String maxID, String minID, int limit, @NonNull Filter filter, String hashtag){
super(HttpMethod.GET, "/accounts/"+id+"/statuses", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@ -29,6 +31,8 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
case PINNED -> addQueryParameter("pinned", "true");
}
if(!TextUtils.isEmpty(hashtag))
addQueryParameter("tagged", hashtag);
}
public enum Filter{

View File

@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
if(followed)
setRequestBody(new Request(showReblogs, null));
setRequestBody(new Request(showReblogs, notify));
else
setRequestBody(new Object());
}

View File

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

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.annotations.SerializedName;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
@ -12,6 +13,10 @@ import java.util.List;
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
this(maxID, limit, includeTypes, null);
}
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@ -25,6 +30,8 @@ public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
addQueryParameter("exclude_types[]", type);
}
}
if(!TextUtils.isEmpty(onlyAccountID))
addQueryParameter("account_id", onlyAccountID);
removeUnsupportedItems=true;
}
}

View File

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

View File

@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class RespondToNotificationRequest extends ResultlessMastodonAPIRequest{
public RespondToNotificationRequest(String id, boolean allow){
super(HttpMethod.POST, "/notifications/requests/"+id+(allow ? "/accept" : "/dismiss"));
setRequestBody(new Object());
}
}

View File

@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationsPolicy;
public class SetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
public SetNotificationsPolicy(NotificationsPolicy policy){
super(HttpMethod.PUT, "/notifications/policy", NotificationsPolicy.class);
setRequestBody(policy);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,181 @@
package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class AccountNotificationsListFragment extends BaseNotificationsListFragment{
private Account account;
private String requestID;
private TextView expandedTitle;
private boolean choiceMade, allowed;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
requestID=getArguments().getString("requestID");
setTitleMarqueeEnabled(false);
loadData();
setTitle(getString(R.string.notifications_from_user, account.displayName));
setHasOptionsMenu(true);
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Notification> result){
onDataLoaded(result, !result.isEmpty());
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=list.getAdapter().getItemCount());
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
expandedTitle=(TextView) LayoutInflater.from(getActivity()).inflate(R.layout.expanded_title_medium, list, false);
expandedTitle.setText(getTitle());
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(expandedTitle));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(recyclerView.getChildCount()==0)
return;
float fraction;
View topChild=recyclerView.getChildAt(0);
if(recyclerView.getChildAdapterPosition(topChild)>0){
fraction=1;
}else{
fraction=(-topChild.getTop())/(float)(topChild.getHeight()-topChild.getPaddingBottom());
}
expandedTitle.setAlpha(1f-fraction);
toolbarTitleView.setAlpha(fraction);
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notification_request, menu);
MenuItem mute=menu.findItem(R.id.mute);
MenuItem allow=menu.findItem(R.id.allow);
if(choiceMade && allowed){
allow.setIcon(R.drawable.ic_check_wght700_24px);
tintMenuIcon(allow, R.attr.colorM3Primary);
}else{
tintMenuIcon(allow, R.attr.colorM3OnSurfaceVariant);
}
if(choiceMade && !allowed){
mute.setIcon(R.drawable.ic_delete_wght700_24px);
tintMenuIcon(mute, R.attr.colorM3Primary);
}else{
tintMenuIcon(mute, R.attr.colorM3OnSurfaceVariant);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(choiceMade)
return true;
allowed=item.getItemId()==R.id.allow;
new RespondToNotificationRequest(requestID, allowed)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
choiceMade=true;
invalidateOptionsMenu();
E.post(new NotificationRequestRespondedEvent(accountID, requestID));
new Snackbar.Builder(getActivity())
.setText(getString(allowed ? R.string.notifications_allowed : R.string.notifications_muted, account.displayName))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
return true;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
}
return super.buildDisplayItems(n);
}
@Override
protected boolean wantsToolbarMenuIconsTinted(){
return false;
}
private void tintMenuIcon(MenuItem item, int color){
int tintColor=UiUtils.getThemeColor(getActivity(), color);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
Drawable icon=item.getIcon();
if(icon!=null && icon.getColorFilter()==null){
icon=icon.mutate();
icon.setTintList(ColorStateList.valueOf(tintColor));
item.setIcon(icon);
}
}else{
item.setIconTintList(ColorStateList.valueOf(tintColor));
}
}
}

View File

@ -10,7 +10,6 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
@ -58,7 +57,7 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
@ -141,11 +140,6 @@ public class AccountTimelineFragment extends StatusListFragment{
return mergeAdapter;
}
@Override
protected int getMainAdapterOffset(){
return super.getMainAdapterOffset()+1;
}
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
return switch(filter){
case DEFAULT -> defaultFilter;

View File

@ -0,0 +1,120 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
protected String maxID;
protected View endMark;
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
private Notification getNotificationByID(String id){
for(Notification n : data){
if(n.id.equals(id))
return n;
}
return null;
}
protected void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0; i<displayItems.size(); i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
}

View File

@ -325,7 +325,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
toolbar.setNavigationContentDescription(R.string.back);
}
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}

View File

@ -5,6 +5,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.Intent;
import android.content.res.Configuration;
@ -22,6 +23,10 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@ -31,15 +36,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -65,7 +69,9 @@ import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.ExtendedPopupMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PopupKeyboard;
@ -86,10 +92,12 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@ -130,7 +138,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, languageBtn;
private TextView replyText;
private Button visibilityBtn;
private LinearLayout visibilityBtn;
private TextView visibilityText1, visibilityText2, visibilityCurrentText;
private LinearLayout bottomBar;
private View autocompleteDivider;
@ -270,6 +279,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
visibilityText1=view.findViewById(R.id.visibility_text1);
visibilityText2=view.findViewById(R.id.visibility_text2);
visibilityCurrentText=visibilityText1;
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text);
@ -279,9 +291,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setOnClickListener(v->toggleSpoiler());
languageBtn.setOnClickListener(v->showLanguageAlert());
visibilityBtn.setOnClickListener(this::onVisibilityClick);
visibilityBtn.setAccessibilityDelegate(new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName("android.widget.Spinner");
}
});
Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate();
arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
visibilityBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrow, null);
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
public void onIconChanged(int icon){
@ -322,7 +340,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
}
updateVisibilityIcon();
updateVisibilityIcon(false);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
@ -908,22 +926,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void onVisibilityClick(View v){
PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility);
menu.setOnMenuItemClickListener(item->{
int id=item.getItemId();
if(id==R.id.vis_public){
statusVisibility=StatusPrivacy.PUBLIC;
}else if(id==R.id.vis_followers){
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT;
ArrayList<ListItem<StatusPrivacy>> items=new ArrayList<>();
ExtendedPopupMenu menu=new ExtendedPopupMenu(getActivity(), items);
Consumer<ListItem<StatusPrivacy>> onClick=i->{
if(statusVisibility!=i.parentObject){
statusVisibility=i.parentObject;
updateVisibilityIcon(true);
}
item.setChecked(true);
updateVisibilityIcon();
return true;
});
menu.show();
menu.dismiss();
};
items.add(new ListItem<>(R.string.visibility_public, R.string.visibility_subtitle_public, R.drawable.ic_public_24px, StatusPrivacy.PUBLIC, onClick));
items.add(new ListItem<>(R.string.visibility_unlisted, R.string.visibility_subtitle_unlisted, R.drawable.ic_clear_night_24px, StatusPrivacy.UNLISTED, onClick));
items.add(new ListItem<>(R.string.visibility_followers_only, R.string.visibility_subtitle_followers, R.drawable.ic_lock_24px, StatusPrivacy.PRIVATE, onClick));
items.add(new ListItem<>(R.string.visibility_private, R.string.visibility_subtitle_private, R.drawable.ic_alternate_email_24px, StatusPrivacy.DIRECT, onClick));
menu.showAsDropDown(v);
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState){
@ -949,12 +965,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void applyPreferencesForPostVisibility(Preferences prefs, Bundle savedInstanceState){
// Only override the reply visibility if our preference is more private
if(prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)){
// Map unlisted from the API onto public, because we don't have unlisted in the UI
statusVisibility=switch(prefs.postingDefaultVisibility){
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
statusVisibility=prefs.postingDefaultVisibility;
}
// A saved privacy setting from a previous compose session wins over all
@ -962,28 +973,45 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
updateVisibilityIcon();
updateVisibilityIcon(false);
}
private void updateVisibilityIcon(){
private void updateVisibilityIcon(boolean animated){
if(getActivity()==null)
return;
if(statusVisibility==null){ // TODO find out why this happens
statusVisibility=StatusPrivacy.PUBLIC;
}
visibilityBtn.setText(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.string.visibility_public;
TextView visibilityText;
if(!animated){
visibilityText=visibilityCurrentText;
}else{
TransitionManager.beginDelayedTransition(visibilityBtn, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds().excludeTarget(TextView.class, true))
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
visibilityText=visibilityCurrentText==visibilityText1 ? visibilityText2 : visibilityText1;
visibilityText.setVisibility(View.VISIBLE);
visibilityCurrentText.setVisibility(View.GONE);
visibilityCurrentText=visibilityText;
}
visibilityText.setText(switch(statusVisibility){
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
});
Drawable icon=getResources().getDrawable(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.drawable.ic_public_20px;
case PUBLIC -> R.drawable.ic_public_20px;
case UNLISTED -> R.drawable.ic_clear_night_20px;
case PRIVATE -> R.drawable.ic_group_20px;
case DIRECT -> R.drawable.ic_alternate_email_20px;
}, getActivity().getTheme()).mutate();
icon.setBounds(0, 0, V.dp(18), V.dp(18));
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
visibilityBtn.setCompoundDrawablesRelative(icon, null, visibilityBtn.getCompoundDrawablesRelative()[2], null);
visibilityText.setCompoundDrawablesRelative(icon, null, null, null);
}
@Override
@ -1120,12 +1148,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void showLanguageAlert(){
Preferences prefs=AccountSessionManager.getInstance().getAccount(accountID).preferences;
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), prefs!=null ? prefs.postingDefaultLanguage : null, postLang, mainEditText.getText().toString());
new M3AlertDialogBuilder(getActivity())
final AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.language)
.setView(vc.getView())
.setPositiveButton(R.string.ok, (dialog, which)->setPostLanguage(vc.getSelectedOption()))
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.cancel, null)
.show();
vc.setSelectionListener(opt->{
setPostLanguage(opt);
dlg.dismiss();
});
}
private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){

View File

@ -10,7 +10,6 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
@ -19,6 +18,7 @@ import java.util.Objects;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
private Account account;
@ -45,7 +45,11 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
Bundle args=new Bundle();
args.putParcelable("targetAccount", Parcels.wrap(account));
args.putParcelable("hashtag", Parcels.wrap(Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag));
args.putString("account", accountID);
Nav.go(getActivity(), HashtagFeaturedTimelineFragment.class, args);
}
@Override

View File

@ -0,0 +1,47 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
// The difference between this and HashtagTimelineFragment is that this opens from the featured hashtags
// and only shows posts by that account.
public class HashtagFeaturedTimelineFragment extends StatusListFragment{
private Account targetAccount;
private Hashtag hashtag;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
targetAccount=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag"));
setTitle("#"+hashtag.name);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(targetAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.DEFAULT, hashtag.name)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null)
return;
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.ACCOUNT);
onDataLoaded(result, !empty);
}
})
.exec(accountID);
}
}

View File

@ -34,7 +34,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
@ -155,7 +154,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
}
@Override
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
return 1;
}

View File

@ -112,7 +112,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
content.setOrientation(LinearLayout.VERTICAL);
FrameLayout fragmentContainer=new FrameLayout(getActivity());
fragmentContainer.setId(R.id.fragment_wrap);
fragmentContainer.setId(me.grishka.appkit.R.id.fragment_wrap);
content.addView(fragmentContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
inflater.inflate(R.layout.tab_bar, content);
@ -131,10 +131,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(savedInstanceState==null){
getChildFragmentManager().beginTransaction()
.add(R.id.fragment_wrap, homeTimelineFragment)
.add(R.id.fragment_wrap, searchFragment).hide(searchFragment)
.add(R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(R.id.fragment_wrap, profileFragment).hide(profileFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, homeTimelineFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit();
String defaultTab=getArguments().getString("tab");

View File

@ -1,12 +1,17 @@
package org.joinmastodon.android.fragments;
import android.os.Build;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
@ -19,7 +24,7 @@ import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.AccountAddedToListEvent;
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
@ -33,24 +38,31 @@ import org.parceler.Parcels;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ListMembersFragment extends PaginatedAccountListFragment{
private static final int ADD_MEMBER_RESULT=600;
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{
private ImageButton fab;
private FollowList followList;
private boolean inSelectionMode;
private Set<String> selectedAccounts=new HashSet<>();
private ActionMode actionMode;
private MenuItem deleteItem;
private FrameLayout searchFragmentContainer;
private FrameLayout fragmentContentWrap;
private AddNewListMembersFragment searchFragment;
private FragmentRootLinearLayout rootView;
private WindowInsets lastInsets;
private HashSet<String> accountIDsInList=new HashSet<>();
private boolean dismissingSearchFragment;
public ListMembersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@ -76,6 +88,26 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
return new GetListAccounts(followList.id, maxID, count);
}
@Override
protected void onDataLoaded(List<AccountViewModel> d, boolean more){
if(refreshing)
accountIDsInList.clear();
for(AccountViewModel a:d){
accountIDsInList.add(a.account.id);
}
super.onDataLoaded(d, more);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=super.onCreateView(inflater, container, savedInstanceState);
FrameLayout wrapper=new FrameLayout(getActivity());
wrapper.addView(view);
rootView=(FragmentRootLinearLayout) view;
fragmentContentWrap=wrapper;
return wrapper;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
@ -132,16 +164,19 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
lastInsets=insets;
if(searchFragment!=null)
searchFragment.onApplyWindowInsets(insets);
UiUtils.applyBottomInsetToFAB(fab, insets);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==ADD_MEMBER_RESULT && success){
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
addAccounts(List.of(acc));
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, 0, 0, 0);
}
rootView.onApplyWindowInsets(insets);
}
@Subscribe
@ -160,9 +195,25 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
}
private void onFabClick(){
searchFragmentContainer=new FrameLayout(getActivity());
searchFragmentContainer.setId(R.id.search_fragment);
fragmentContentWrap.addView(searchFragmentContainer);
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
args.putParcelable("list", Parcels.wrap(followList));
args.putBoolean("_can_go_back", true);
searchFragment=new AddNewListMembersFragment(this);
searchFragment.setArguments(args);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
if(lastInsets!=null)
searchFragment.onApplyWindowInsets(lastInsets);
searchFragmentContainer.setTranslationX(V.dp(100));
searchFragmentContainer.setAlpha(0f);
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
}
private void onItemClick(AccountViewHolder holder){
@ -198,7 +249,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
if(id==R.id.remove_from_list){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_member)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id), null))
.setNegativeButton(R.string.cancel, null)
.show();
}
@ -229,7 +280,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_members)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts), null))
.setNegativeButton(R.string.cancel, null)
.show();
return true;
@ -251,13 +302,16 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
}
private void removeAccounts(Set<String> ids){
private void removeAccounts(Set<String> ids, Runnable onDone){
new RemoveAccountsFromList(followList.id, ids)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
if(onDone!=null)
onDone.run();
if(inSelectionMode)
actionMode.finish();
accountIDsInList.removeAll(ids);
removeAccountRows(ids);
}
@ -270,12 +324,15 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
.exec(accountID);
}
private void addAccounts(Collection<Account> accounts){
private void addAccounts(Collection<Account> accounts, Runnable onDone){
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
if(onDone!=null)
onDone.run();
for(Account acc:accounts){
accountIDsInList.add(acc.id);
data.add(new AccountViewModel(acc, accountID));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
@ -298,4 +355,54 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
}
}
}
@Override
public boolean isAccountInList(AccountViewModel account){
return accountIDsInList.contains(account.account.id);
}
@Override
public void addAccountToList(AccountViewModel account, Runnable onDone){
addAccounts(Set.of(account.account), onDone);
}
@Override
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
removeAccounts(Set.of(account.account.id), onDone);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
dismissingSearchFragment=true;
rootView.setVisibility(View.VISIBLE);
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
fragmentContentWrap.removeView(searchFragmentContainer);
searchFragmentContainer=null;
searchFragment=null;
dismissingSearchFragment=false;
}).start();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}
@Override
protected void setStatusBarColor(int color){
rootView.setStatusBarColor(color);
}
@Override
protected void setNavigationBarColor(int color){
rootView.setNavigationBarColor(color);
}
}

View File

@ -0,0 +1,250 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotificationRequests;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.NotificationRequest;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.Snackbar;
import org.parceler.Parcels;
import java.util.HashMap;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class NotificationRequestsFragment extends MastodonRecyclerFragment<NotificationRequest>{
private String accountID;
private String maxID;
private HashMap<String, AccountViewModel> accountViewModels=new HashMap<>();
private View endMark;
private NotificationRequestsAdapter adapter;
public NotificationRequestsFragment(){
super(50);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.filtered_notifications);
loadData();
E.register(this);
}
@Override
public void onDestroy(){
E.unregister(this);
super.onDestroy();
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotificationRequests(offset==0 ? null : maxID)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<NotificationRequest> result){
if(data.isEmpty() || refreshing)
accountViewModels.clear();
maxID=result.getNextPageMaxID();
for(NotificationRequest req:result){
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
}
onDataLoaded(result, !TextUtils.isEmpty(maxID));
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
return adapter=new NotificationRequestsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof NotificationRequestViewHolder).setDrawBelowLastItem(true));
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Subscribe
public void onNotificationRequestResponded(NotificationRequestRespondedEvent ev){
if(adapter==null || !ev.accountID.equals(accountID))
return;
for(int i=0;i<data.size();i++){
if(data.get(i).id.equals(ev.requestID)){
data.remove(i);
adapter.notifyItemRemoved(i);
return;
}
}
for(NotificationRequest nr:preloadedData){
if(nr.id.equals(ev.requestID)){
preloadedData.remove(nr);
break;
}
}
}
private class NotificationRequestsAdapter extends UsableRecyclerView.Adapter<NotificationRequestViewHolder> implements ImageLoaderRecyclerAdapter{
public NotificationRequestsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public NotificationRequestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new NotificationRequestViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public void onBindViewHolder(NotificationRequestViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return Objects.requireNonNull(accountViewModels.get(data.get(position).account.id)).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(data.get(position).account.id));
return switch(image){
case 0 -> model.avaRequest;
default -> model.emojiHelper.getImageRequest(image-1);
};
}
}
private class NotificationRequestViewHolder extends BindableViewHolder<NotificationRequest> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, badge;
private final ImageView ava;
private final ImageButton allow, mute;
public NotificationRequestViewHolder(){
super(getActivity(), R.layout.item_notification_request, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
badge=findViewById(R.id.badge);
ava=findViewById(R.id.ava);
allow=findViewById(R.id.btn_allow);
mute=findViewById(R.id.btn_mute);
ava.setOutlineProvider(OutlineProviders.roundedRect(8));
ava.setClipToOutline(true);
allow.setOnClickListener(this::onAllowClick);
mute.setOnClickListener(this::onMuteClick);
}
@SuppressLint("DefaultLocale")
@Override
public void onBind(NotificationRequest item){
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
name.setText(model.parsedName);
username.setText(item.account.getDisplayUsername());
badge.setText(item.notificationsCount>99 ? String.format("%d+", 99) : String.format("%d", item.notificationsCount));
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
if(image==null)
ava.setImageResource(R.drawable.image_placeholder);
else
ava.setImageDrawable(image);
}else{
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
model.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(item.account));
args.putString("requestID", item.id);
Nav.go(getActivity(), AccountNotificationsListFragment.class, args);
}
private void onAllowClick(View v){
acceptOrDecline(true);
}
private void onMuteClick(View v){
acceptOrDecline(false);
}
private void acceptOrDecline(boolean accept){
new RespondToNotificationRequest(item.id, accept)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
int pos=data.indexOf(item);
data.remove(pos);
adapter.notifyItemRemoved(pos);
new Snackbar.Builder(getActivity())
.setText(getString(accept ? R.string.notifications_allowed : R.string.notifications_muted, item.account.displayName))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
}
}

View File

@ -1,57 +1,70 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.GetNotificationsPolicy;
import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolicy;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationsPolicy;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.GenericListItemsViewController;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
public class NotificationsListFragment extends BaseNotificationsListFragment{
private boolean onlyMentions;
private String maxID;
private View tabBar;
private View mentionsTab, allTab;
private View endMark;
private String unreadMarker, realUnreadMarker;
private MenuItem markAllReadItem;
private boolean reloadingFromCache;
private ListItem<Void> requestsItem=new ListItem<>(R.string.filtered_notifications, 0, R.drawable.ic_inventory_2_24px, i->openNotificationRequests());
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
private NotificationsPolicy lastPolicy;
@Override
public void onCreate(Bundle savedInstanceState){
@ -74,43 +87,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
setTitle(R.string.notifications);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && !reloadingFromCache)
endMark.setVisibility(View.GONE);
if(offset==0)
reloadPolicy();
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@ -142,30 +124,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
resetUnreadBackground();
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
tabBar=view.findViewById(R.id.tabbar);
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
View tabBarItself=view.findViewById(R.id.tabbar_inner);
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
@ -215,14 +177,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return views;
}
private Notification getNotificationByID(String id){
for(Notification n:data){
if(n.id.equals(id))
return n;
}
return null;
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
@ -249,25 +203,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
private void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount()) || holder.getAbsoluteAdapterPosition()<requestsItems.size();
}
private void onTabClick(View v){
@ -285,34 +223,34 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
markAllReadItem=menu.findItem(R.id.mark_all_read);
MenuItem filters=menu.findItem(R.id.filters);
filters.setVisible(lastPolicy!=null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.mark_all_read){
int id=item.getItemId();
if(id==R.id.mark_all_read){
markAsRead();
resetUnreadBackground();
}else if(id==R.id.filters){
showFiltersAlert();
}
return true;
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(requestsRowAdapter);
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void markAsRead(){
if(data.isEmpty())
return;
@ -366,4 +304,93 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
return true;
}
private void updatePolicy(NotificationsPolicy policy){
int count=policy.summary==null ? 0 : policy.summary.pendingRequestsCount;
boolean isShown=!requestsItems.isEmpty();
boolean needShow=count>0;
if(isShown && !needShow){
requestsItems.clear();
requestsRowAdapter.notifyItemRemoved(0);
}else if(!isShown && needShow){
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
requestsItems.add(requestsItem);
requestsRowAdapter.notifyItemInserted(0);
}else if(isShown){
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
requestsRowAdapter.notifyItemChanged(0);
}
lastPolicy=policy;
invalidateOptionsMenu();
}
private void reloadPolicy(){
new GetNotificationsPolicy()
.setCallback(new Callback<>(){
@Override
public void onSuccess(NotificationsPolicy policy){
updatePolicy(policy);
}
@Override
public void onError(ErrorResponse errorResponse){
}
})
.exec(accountID);
}
private void showFiltersAlert(){
GenericListItemsViewController<Void> controller=new GenericListItemsViewController<>(getActivity());
Consumer<CheckableListItem<Void>> toggler=item->{
item.toggle();
controller.rebindItem(item);
};
CheckableListItem<Void> followingItem, followersItem, newAccountsItem, mentionsItem;
List<ListItem<Void>> items=List.of(
followingItem=new CheckableListItem<>(R.string.notification_filter_following, R.string.notification_filter_following_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowing, toggler, true),
followersItem=new CheckableListItem<>(R.string.notification_filter_followers, R.string.notification_filter_followers_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowers, toggler, true),
newAccountsItem=new CheckableListItem<>(R.string.notification_filter_new_accounts, R.string.notification_filter_new_accounts_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNewAccounts, toggler, true),
mentionsItem=new CheckableListItem<>(R.string.notification_filter_mentions, R.string.notification_filter_mentions_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterPrivateMentions, toggler, true)
);
controller.setItems(items);
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.filter_notifications)
.setView(controller.getView())
.setPositiveButton(R.string.save, null)
.show();
Button btn=dlg.getButton(Dialog.BUTTON_POSITIVE);
btn.setOnClickListener(v->{
UiUtils.showProgressForAlertButton(btn, true);
NotificationsPolicy newPolicy=new NotificationsPolicy();
newPolicy.filterNotFollowing=followingItem.checked;
newPolicy.filterNotFollowers=followersItem.checked;
newPolicy.filterNewAccounts=newAccountsItem.checked;
newPolicy.filterPrivateMentions=mentionsItem.checked;
new SetNotificationsPolicy(newPolicy)
.setCallback(new Callback<>(){
@Override
public void onSuccess(NotificationsPolicy policy){
updatePolicy(policy);
dlg.dismiss();
}
@Override
public void onError(ErrorResponse errorResponse){
Activity activity=getActivity();
if(activity==null)
return;
UiUtils.showProgressForAlertButton(btn, false);
errorResponse.showToast(activity);
}
})
.exec(accountID);
});
}
private void openNotificationRequests(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), NotificationRequestsFragment.class, args);
}
}

View File

@ -25,7 +25,7 @@ public class PinnedPostsListFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED)
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){

View File

@ -310,7 +310,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
super.onSelectedChanged(viewHolder, actionState);
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.setTag(me.grishka.appkit.R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
}
}

View File

@ -18,7 +18,6 @@ import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SectionHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
@ -93,7 +92,13 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag);
case HASHTAG -> {
Bundle args=new Bundle();
args.putParcelable("targetAccount", Parcels.wrap(profileAccount));
args.putParcelable("hashtag", Parcels.wrap(res.hashtag));
args.putString("account", accountID);
Nav.go(getActivity(), HashtagFeaturedTimelineFragment.class, args);
}
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
@ -109,7 +114,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
@Override
protected void doLoadData(int offset, int count){
if(!statusesLoaded){
new GetAccountStatuses(profileAccount.id, null, null, 2, GetAccountStatuses.Filter.PINNED)
new GetAccountStatuses(profileAccount.id, null, null, 2, GetAccountStatuses.Filter.PINNED, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
@ -193,6 +198,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
private void showAllFeaturedHashtags(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("hashtags", tags);
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);

View File

@ -18,11 +18,14 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.LayoutInflater;
@ -36,6 +39,7 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
@ -61,6 +65,7 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
@ -129,6 +134,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View tabsDivider;
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private ImageButton qrCodeButton;
private ProgressBar innerProgress;
private View actions;
private Account account;
private String accountID;
@ -211,6 +219,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabsDivider=content.findViewById(R.id.tabs_divider);
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
qrCodeButton=content.findViewById(R.id.qr_code);
innerProgress=content.findViewById(R.id.profile_progress);
actions=content.findViewById(R.id.profile_actions);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@ -298,6 +309,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
if(account==null)
return true;
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
@ -323,7 +336,19 @@ 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());
usernameDomain.setOnClickListener(v->{
if(account==null)
return;
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
});
qrCodeButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
ProfileQrCodeFragment qf=new ProfileQrCodeFragment();
qf.setArguments(args);
qf.show(getChildFragmentManager(), "qrDialog");
});
return sizeWrapper;
}
@ -446,6 +471,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return true;
}
});
if(!loaded)
bindHeaderViewForPreviewMaybe();
}
@Override
@ -490,7 +517,41 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private void bindHeaderViewForPreviewMaybe(){
if(loaded)
return;
String username=getArguments().getString("accountUsername");
String domain=getArguments().getString("accountDomain");
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(domain))
return;
content.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
innerProgress.setVisibility(View.VISIBLE);
this.username.setText(username);
name.setText(username);
usernameDomain.setText(domain);
avatar.setImageResource(R.drawable.image_placeholder);
cover.setImageResource(R.drawable.image_placeholder);
actions.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
tabsDivider.setVisibility(View.GONE);
}
private void bindHeaderView(){
if(innerProgress.getVisibility()==View.VISIBLE){
TransitionManager.beginDelayedTransition(contentView, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.excludeChildren(actions, true)
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
innerProgress.setVisibility(View.GONE);
countersLayout.setVisibility(View.VISIBLE);
actions.setVisibility(View.VISIBLE);
tabsDivider.setVisibility(View.VISIBLE);
}
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
@ -583,6 +644,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private CharSequence makeRedString(CharSequence s){
int color=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
SpannableString ss=new SpannableString(s);
ss.setSpan(new ForegroundColorSpan(color), 0, ss.length(), 0);
return ss;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){
@ -599,28 +667,36 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(isOwnProfile)
return;
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.displayName));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.displayName));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.displayName));
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(makeRedString(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername())));
menu.findItem(R.id.report).setTitle(makeRedString(getString(R.string.report_user, account.getDisplayUsername())));
if(relationship.following)
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.displayName));
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user));
else
menu.findItem(R.id.hide_boosts).setVisible(false);
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
menu.findItem(R.id.block_domain).setTitle(makeRedString(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain())));
else
menu.findItem(R.id.block_domain).setVisible(false);
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
if(relationship.following){
MenuItem notifications=menu.findItem(R.id.notifications);
notifications.setVisible(true);
notifications.setIcon(relationship.notifying ? R.drawable.ic_notifications_fill1_24px : R.drawable.ic_notifications_24px);
notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername()));
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){
menu.setGroupDividerEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
UiUtils.openSystemShareSheet(getActivity(), account);
}else if(id==R.id.mute){
confirmToggleMuted();
}else if(id==R.id.block){
@ -639,7 +715,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
@ -669,6 +745,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
}else if(id==R.id.notifications){
new SetAccountFollowed(account.id, true, relationship.showingReblogs, !relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
updateRelationship(result);
new Snackbar.Builder(getActivity())
.setText(result.notifying ? R.string.new_post_notifications_enabled : R.string.new_post_notifications_disabled)
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
@ -836,17 +930,48 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
toolbar.setNavigationContentDescription(R.string.discard);
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
Runnable updater=new Runnable(){
@Override
public void run(){
// setPadding() calls nullLayouts() internally, forcing the text layout to update
actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0);
actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0);
actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY);
actionButton.postOnAnimation(this);
}
};
actionButton.postOnAnimation(updater);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.addListener(new Transition.TransitionListener(){
@Override
public void onTransitionStart(Transition transition){}
@Override
public void onTransitionEnd(Transition transition){
actionButton.removeCallbacks(updater);
}
@Override
public void onTransitionCancel(Transition transition){}
@Override
public void onTransitionPause(Transition transition){}
@Override
public void onTransitionResume(Transition transition){}
})
);
name.setVisibility(View.GONE);
username.setVisibility(View.GONE);
name.setVisibility(View.INVISIBLE);
username.setVisibility(View.INVISIBLE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
qrCodeButton.setVisibility(View.GONE);
usernameDomain.setVisibility(View.INVISIBLE);
nameEditWrap.setVisibility(View.VISIBLE);
nameEdit.setText(account.displayName);
@ -875,7 +1000,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
Toolbar toolbar=getToolbar();
if(canGoBack()){
Drawable back=getToolbarContext().getDrawable(R.drawable.ic_arrow_back).mutate();
Drawable back=getToolbarContext().getDrawable(me.grishka.appkit.R.drawable.ic_arrow_back).mutate();
back.setTint(UiUtils.getThemeColor(getToolbarContext(), R.attr.colorM3OnSurfaceVariant));
toolbar.setNavigationIcon(back);
toolbar.setNavigationContentDescription(0);
@ -885,11 +1010,40 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
editSaveMenuItem=null;
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
Runnable updater=new Runnable(){
@Override
public void run(){
// setPadding() calls nullLayouts() internally, forcing the text layout to update
actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0);
actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0);
actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY);
actionButton.postOnAnimation(this);
}
};
actionButton.postOnAnimation(updater);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.addListener(new Transition.TransitionListener(){
@Override
public void onTransitionStart(Transition transition){}
@Override
public void onTransitionEnd(Transition transition){
actionButton.removeCallbacks(updater);
}
@Override
public void onTransitionCancel(Transition transition){}
@Override
public void onTransitionPause(Transition transition){}
@Override
public void onTransitionResume(Transition transition){}
})
);
nameEditWrap.setVisibility(View.GONE);
bioEditWrap.setVisibility(View.GONE);
@ -898,6 +1052,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
refreshLayout.setEnabled(true);
usernameDomain.setVisibility(View.VISIBLE);
qrCodeButton.setVisibility(View.VISIBLE);
bindHeaderView();
V.setVisibilityAnimated(fab, View.VISIBLE);
@ -972,6 +1128,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void onAvatarClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(AVATAR_RESULT);
}else{
@ -985,6 +1143,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void onCoverClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(COVER_RESULT);
}else{

View File

@ -0,0 +1,602 @@
package org.joinmastodon.android.fragments;
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.Dialog;
import android.app.DownloadManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.googleservices.GmsClient;
import org.joinmastodon.android.googleservices.barcodescanner.Barcode;
import org.joinmastodon.android.googleservices.barcodescanner.BarcodeScanner;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.drawables.FancyQrCodeDrawable;
import org.joinmastodon.android.ui.drawables.RadialParticleSystemDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioFrameLayout;
import org.parceler.Parcels;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.CustomViewHelper;
import me.grishka.appkit.utils.V;
public class ProfileQrCodeFragment extends AppKitFragment{
private static final String TAG="ProfileQrCodeFragment";
private static final int PERMISSION_RESULT=388;
private static final int SCAN_RESULT=439;
private Context themeWrapper;
private GradientDrawable scrim=new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xE6000000, 0xD9000000});
private RadialParticleSystemDrawable particles;
private View codeContainer;
private View particleAnimContainer;
private Animator currentTransition;
private View saveBtn;
private TextView saveBtnText;
private View content;
private String accountID;
private Account account;
private String accountDomain;
private Intent scannerIntent;
private boolean dismissing;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, 0);
setHasOptionsMenu(true);
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setCancelable(false);
scannerIntent=BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true);
}
@Override
public void onStart(){
super.onStart();
Dialog dlg=getDialog();
dlg.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
dlg.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
dlg.getWindow().setNavigationBarColor(0);
dlg.getWindow().setStatusBarColor(0);
WindowManager.LayoutParams lp=dlg.getWindow().getAttributes();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){
lp.layoutInDisplayCutoutMode=WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
dlg.getWindow().setAttributes(lp);
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
dlg.setOnKeyListener((dialog, keyCode, event)->{
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
dismiss();
}
return true;
});
}
@Override
public void onDismiss(DialogInterface dialog){
super.onDismiss(dialog);
Activity activity=getActivity();
if(activity!=null)
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
}
@SuppressLint("ClickableViewAccessibility")
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
content=View.inflate(themeWrapper, R.layout.fragment_profile_qr, container);
View decor=getDialog().getWindow().getDecorView();
decor.setOnApplyWindowInsetsListener((v, insets)->{
content.setPadding(insets.getStableInsetLeft(), insets.getStableInsetTop(), insets.getStableInsetRight(), insets.getStableInsetBottom());
return insets.consumeStableInsets();
});
int flags=decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
flags&=~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
decor.setSystemUiVisibility(flags);
content.setBackground(scrim);
String url=account.url;
QRCodeWriter writer=new QRCodeWriter();
BitMatrix code;
try{
code=writer.encode(url, BarcodeFormat.QR_CODE, 0, 0, Map.of(EncodeHintType.MARGIN, 0, EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H));
}catch(WriterException e){
throw new RuntimeException(e);
}
View codeView=content.findViewById(R.id.code);
ImageView avatar=content.findViewById(R.id.avatar);
TextView username=content.findViewById(R.id.username);
TextView domain=content.findViewById(R.id.domain);
View share=content.findViewById(R.id.share_btn);
saveBtn=content.findViewById(R.id.save_btn);
saveBtnText=content.findViewById(R.id.save_text);
View cornerAnimContainer=content.findViewById(R.id.corner_animation_container);
particleAnimContainer=content.findViewById(R.id.particle_animation_container);
codeContainer=content.findViewById(R.id.code_container);
if(!TextUtils.isEmpty(account.avatar)){
ViewImageLoader.loadWithoutAnimation(avatar, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(24), V.dp(24), List.of(), Uri.parse(account.avatarStatic)));
}
username.setText(account.username);
String accDomain=account.getDomain();
domain.setText(accountDomain=TextUtils.isEmpty(accDomain) ? AccountSessionManager.get(accountID).domain : accDomain);
Drawable logo=getResources().getDrawable(R.drawable.ic_ntf_logo, themeWrapper.getTheme()).mutate();
logo.setTint(UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnPrimary));
codeView.setBackground(new FancyQrCodeDrawable(code, UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnPrimary), logo));
share.setOnClickListener(v->{
UiUtils.openSystemShareSheet(getActivity(), account);
});
saveBtn.setOnClickListener(v->saveCodeAsFile());
cornerAnimContainer.setBackground(new AnimatedCornersDrawable(themeWrapper));
int particleColor=UiUtils.getThemeColor(themeWrapper, R.attr.colorM3Primary);
particles=new RadialParticleSystemDrawable(5000, 200, (particleColor & 0xFFFFFF) | 0x80000000, particleColor & 0xFFFFFF, V.dp(65), V.dp(50), getResources().getDisplayMetrics().density);
particleAnimContainer.setBackground(particles);
content.setOnTouchListener(new TouchDismissListener());
int buttonExtraWidth=saveBtn.getPaddingLeft()+saveBtn.getPaddingRight()+saveBtnText.getCompoundDrawablesRelative()[0].getIntrinsicWidth()+saveBtnText.getCompoundDrawablePadding();
saveBtn.getLayoutParams().width=(int)Math.max(saveBtnText.getPaint().measureText(getString(R.string.save)), saveBtnText.getPaint().measureText(getString(R.string.saved)))+buttonExtraWidth;
return content;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if(savedInstanceState==null){
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(scrim, "alpha", 0, 255),
ObjectAnimator.ofFloat(particleAnimContainer, View.TRANSLATION_Y, V.dp(50), 0),
ObjectAnimator.ofFloat(particleAnimContainer, View.ALPHA, 0, 1),
ObjectAnimator.ofFloat(getToolbar(), View.ALPHA, 0, 1)
);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.setDuration(350);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentTransition=null;
}
});
currentTransition=set;
set.start();
}
}
@Override
public void dismiss(){
dismissWithAnimation(super::dismiss, true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(GmsClient.isGooglePlayServicesAvailable(getActivity())){
MenuItem item=menu.add(0, 0, 0, R.string.scan_qr_code);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
item.setIcon(R.drawable.ic_qr_code_scanner_24px);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(scannerIntent.resolveActivity(getActivity().getPackageManager())!=null){
startActivityForResult(scannerIntent, SCAN_RESULT);
}else{
BarcodeScanner.installScannerModule(themeWrapper, ()->startActivityForResult(scannerIntent, SCAN_RESULT));
}
return true;
}
@Override
protected boolean canGoBack(){
return true;
}
@Override
public void onToolbarNavigationClick(){
dismiss();
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24;
}
@Override
protected LayoutInflater getToolbarLayoutInflater(){
return LayoutInflater.from(themeWrapper);
}
@Override
protected int getToolbarResource(){
return R.layout.profile_qr_toolbar;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
if(requestCode==PERMISSION_RESULT){
if(grantResults[0]==PackageManager.PERMISSION_GRANTED){
doSaveCodeAsFile();
}else if(!getActivity().shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.permission_required)
.setMessage(R.string.storage_permission_to_download)
.setPositiveButton(R.string.open_settings, (dialog, which)->getActivity().startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null))))
.setNegativeButton(R.string.cancel, null)
.show();
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK && BarcodeScanner.isValidResult(data)){
Barcode code=BarcodeScanner.getResult(data);
if(code!=null){
if(code.rawValue.startsWith("https:") || code.rawValue.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code.rawValue), accountID);
dismiss();
}else{
Toast.makeText(themeWrapper, R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}
}
}
}
private void dismissWithAnimation(Runnable onDone, boolean animateTranslationDown){
if(dismissing)
return;
dismissing=true;
content.setOnTouchListener(null);
if(currentTransition!=null)
currentTransition.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(scrim, "alpha", 0),
ObjectAnimator.ofFloat(particleAnimContainer, View.TRANSLATION_Y, particleAnimContainer.getTranslationY()+V.dp(animateTranslationDown ? 50 : -50)),
ObjectAnimator.ofFloat(particleAnimContainer, View.ALPHA, 0),
ObjectAnimator.ofFloat(getToolbar(), View.ALPHA, 0)
);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.setDuration(200);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
onDone.run();
}
});
currentTransition=set;
set.start();
}
private void saveCodeAsFile(){
if(Build.VERSION.SDK_INT>=29){
doSaveCodeAsFile();
}else{
if(getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_RESULT);
}else{
doSaveCodeAsFile();
}
}
}
private void doSaveCodeAsFile(){
Bitmap bmp=Bitmap.createBitmap(1080, 1080, Bitmap.Config.ARGB_8888);
Canvas c=new Canvas(bmp);
float factor=1080f/codeContainer.getWidth();
c.scale(factor, factor);
codeContainer.draw(c);
Activity activity=getActivity();
MastodonAPIController.runInBackground(()->{
String fileName=account.username+"_"+accountDomain+".png";
try(OutputStream os=destinationStreamForFile(fileName)){
bmp.compress(Bitmap.CompressFormat.PNG, 100, os);
activity.runOnUiThread(()->{
saveBtn.setEnabled(false);
saveBtnText.setText(R.string.saved);
saveBtnText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check_20px, 0, 0, 0);
new Snackbar.Builder(activity)
.setText(R.string.image_saved)
.setAction(R.string.view_file, ()->startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)))
.show();
});
if(Build.VERSION.SDK_INT<29){
File dstFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
MediaScannerConnection.scanFile(activity, new String[]{dstFile.getAbsolutePath()}, new String[]{"image/png"}, null);
}
}catch(IOException x){
activity.runOnUiThread(()->{
new Snackbar.Builder(activity)
.setText(R.string.error_saving_file)
.show();
});
}
});
}
private OutputStream destinationStreamForFile(String fileName) throws IOException{
if(Build.VERSION.SDK_INT>=29){
ContentValues values=new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
ContentResolver cr=getActivity().getContentResolver();
Uri itemUri=cr.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
return cr.openOutputStream(itemUri);
}else{
return new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName));
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
codeContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
codeContainer.getViewTreeObserver().removeOnPreDrawListener(this);
updateParticleEmitter();
return true;
}
});
}
private void updateParticleEmitter(){
int[] loc={0, 0};
particleAnimContainer.getLocationInWindow(loc);
int x=loc[0], y=loc[1];
codeContainer.getLocationInWindow(loc);
int cx=loc[0]-x+codeContainer.getWidth()/2;
int cy=loc[1]-y+codeContainer.getHeight()/2;
int r=codeContainer.getWidth()/2-V.dp(10);
particles.setEmitterPosition(cx, cy);
particles.setClipOutBounds(cx-r, cy-r, cx+r, cy+r);
}
public static class CustomizedLinearLayout extends LinearLayout implements CustomViewHelper{
public CustomizedLinearLayout(Context context){
this(context, null);
}
public CustomizedLinearLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CustomizedLinearLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int maxW=dp(400);
FixedAspectRatioFrameLayout aspectLayout=(FixedAspectRatioFrameLayout) getChildAt(0);
if(MeasureSpec.getSize(widthMeasureSpec)>maxW){
widthMeasureSpec=MeasureSpec.getMode(widthMeasureSpec) | maxW;
aspectLayout.setUseHeight(MeasureSpec.getSize(heightMeasureSpec)<dp(464));
}else{
aspectLayout.setUseHeight(false);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private class AnimatedCornersDrawable extends Drawable{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF tmpRect=new RectF();
public AnimatedCornersDrawable(Context context){
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Primary));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(V.dp(4));
}
@Override
public void draw(@NonNull Canvas canvas){
float inset=V.dp(24);
float radius=V.dp(40);
float animProgress=((float)Math.sin(Math.toRadians(SystemClock.uptimeMillis()/16.6%360.0))+1f)/2f;
tmpRect.set(getBounds());
tmpRect.inset(inset, inset);
canvas.save();
float factor=1f+0.025f*animProgress;
paint.setStrokeWidth(V.dp(4)/factor);
canvas.scale(factor, factor, tmpRect.centerX(), tmpRect.centerY());
canvas.drawRoundRect(tmpRect, radius, radius, paint);
canvas.restore();
invalidateSelf();
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
float inset=V.dp(24);
float radius=V.dp(40);
float additionalLength=V.dp(40);
tmpRect.set(getBounds());
tmpRect.inset(inset, inset);
float[] intervals=new float[]{3.1415f*radius*0.5f+additionalLength*2f, tmpRect.width()-radius*2f-additionalLength*2f};
paint.setPathEffect(new DashPathEffect(intervals, intervals[0]-additionalLength));
updateParticleEmitter();
}
}
private class TouchDismissListener implements View.OnTouchListener{
private Rect tmpRect=new Rect();
private int[] xy={0, 0};
private boolean dragging;
private float dragDownY;
private VelocityTracker velocityTracker;
private SpringAnimation springBackAnim;
@Override
public boolean onTouch(View v, MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
codeContainer.getLocationInWindow(xy);
tmpRect.set(xy[0], xy[1], xy[0]+codeContainer.getWidth(), xy[1]+codeContainer.getHeight());
if(springBackAnim!=null){
springBackAnim.skipToEnd();
}
if(tmpRect.contains((int)ev.getX(), (int)ev.getY())){
dragging=true;
dragDownY=ev.getY();
velocityTracker=VelocityTracker.obtain();
velocityTracker.addMovement(ev);
}else{
dismiss();
}
}else if(dragging){
if(ev.getAction()==MotionEvent.ACTION_MOVE){
float transY=ev.getY()-dragDownY;
particleAnimContainer.setTranslationY(transY);
float alpha=1f-Math.abs(transY)/particleAnimContainer.getHeight();
scrim.setAlpha(Math.round(alpha*255));
getToolbar().setAlpha(alpha);
velocityTracker.addMovement(ev);
}else if(ev.getAction()==MotionEvent.ACTION_UP){
dragging=false;
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000);
float velocity=velocityTracker.getYVelocity();
if(Math.abs(velocity)>=V.dp(1000) || Math.abs(particleAnimContainer.getTranslationY())>particleAnimContainer.getHeight()/4f){
dismissWithAnimation(ProfileQrCodeFragment.super::dismiss, velocity>0);
}else{
springBack(velocity);
}
velocityTracker.recycle();
velocityTracker=null;
}else if(ev.getAction()==MotionEvent.ACTION_CANCEL){
dragging=false;
springBack(velocityTracker.getYVelocity());
velocityTracker.recycle();
velocityTracker=null;
}
}
return true;
}
private void springBack(float velocityY){
SpringAnimation anim=new SpringAnimation(particleAnimContainer, DynamicAnimation.TRANSLATION_Y, 0);
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW).setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
anim.setStartVelocity(velocityY);
anim.addEndListener((animation, canceled, value, velocity)->springBackAnim=null);
anim.addUpdateListener((animation, value, velocity)->{
float alpha=1f-Math.abs(particleAnimContainer.getTranslationY())/particleAnimContainer.getHeight();
scrim.setAlpha(Math.round(alpha*255));
getToolbar().setAlpha(alpha);
});
springBackAnim=anim;
anim.start();
}
}
}

View File

@ -34,6 +34,7 @@ import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import org.parceler.Parcels;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import androidx.annotation.Nullable;
@ -260,10 +261,10 @@ public class SplashFragment extends AppKitFragment{
private void loadAndChooseDefaultServer(){
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()));
String clipText=clipData.getItemAt(0).coerceToText(getActivity()).toString();
if(HtmlParser.isValidInviteUrl(clipText)){
currentInviteLink=Uri.parse(clipText);
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, HtmlParser.normalizeDomain(Objects.requireNonNull(currentInviteLink.getHost()))));
}
}else{
loadingDefaultServer=true;
@ -315,7 +316,7 @@ public class SplashFragment extends AppKitFragment{
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
defaultServerButton.setText(getString(R.string.join_default_server, HtmlParser.normalizeDomain(chosenDefaultServer)));
}
}
}

View File

@ -151,7 +151,7 @@ public class ThreadFragment extends StatusListFragment{
replyButton.setOnClickListener(v->openReply());
Account self=AccountSessionManager.get(accountID).self;
if(!TextUtils.isEmpty(self.avatar)){
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
}
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
showContent();

View File

@ -1,37 +0,0 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
import org.joinmastodon.android.model.Account;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class AddListMembersFragment extends AccountSearchFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Account> result){
AddListMembersFragment.this.onSuccess(result);
}
})
.exec(accountID);
}
@Override
protected String getSearchViewPlaceholder(){
return getString(R.string.search_among_people_you_follow);
}
}

View File

@ -1,6 +1,9 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@ -11,9 +14,14 @@ import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.googleservices.GmsClient;
import org.joinmastodon.android.googleservices.barcodescanner.Barcode;
import org.joinmastodon.android.googleservices.barcodescanner.BarcodeScanner;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
@ -33,6 +41,7 @@ import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
private static final int QUERY_RESULT=937;
private static final int SCAN_RESULT=456;
private TabLayout tabLayout;
private ViewPager2 pager;
@ -40,7 +49,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private TabLayoutMediator tabLayoutMediator;
private boolean searchActive;
private FrameLayout searchView;
private ImageButton searchBack;
private ImageButton searchBack, searchScanQR;
private TextView searchText;
private View tabsDivider;
@ -52,6 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private String accountID;
private String currentQuery;
private Intent scannerIntent;
@Override
public void onCreate(Bundle savedInstanceState){
@ -60,6 +70,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
setRetainInstance(true);
accountID=getArguments().getString("account");
scannerIntent=BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true);
}
@Nullable
@ -169,11 +180,17 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
searchBack.setOnClickListener(v->exitSearch());
if(searchActive){
searchBack.setImageResource(R.drawable.ic_arrow_back);
searchBack.setImageResource(me.grishka.appkit.R.drawable.ic_arrow_back);
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
}
searchScanQR=view.findViewById(R.id.search_scan_qr);
if(!GmsClient.isGooglePlayServicesAvailable(getActivity())){
searchScanQR.setVisibility(View.GONE);
}else{
searchScanQR.setOnClickListener(v->openQrScanner());
}
View searchWrap=view.findViewById(R.id.search_wrap);
searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
@ -211,7 +228,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
searchBack.setImageResource(R.drawable.ic_arrow_back);
searchBack.setImageResource(me.grishka.appkit.R.drawable.ic_arrow_back);
searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
@ -268,6 +285,28 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK && BarcodeScanner.isValidResult(data)){
Barcode code=BarcodeScanner.getResult(data);
if(code!=null){
if(code.rawValue.startsWith("https:") || code.rawValue.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code.rawValue), accountID);
}else{
Toast.makeText(getActivity(), R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}
}
}
}
private void openQrScanner(){
if(scannerIntent.resolveActivity(getActivity().getPackageManager())!=null){
startActivityForResult(scannerIntent, SCAN_RESULT);
}else{
BarcodeScanner.installScannerModule(getActivity(), ()->startActivityForResult(scannerIntent, SCAN_RESULT));
}
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@ -18,6 +19,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.HorizontalScrollingTouchListener;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
@ -81,6 +83,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
.exec(accountID);
}
@SuppressLint("ClickableViewAccessibility")
@Override
protected RecyclerView.Adapter getAdapter(){
cardsList=new UsableRecyclerView(getActivity());
@ -98,6 +101,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> im
});
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
cardsList.setDrawSelectorOnTop(true);
cardsList.setOnTouchListener(new HorizontalScrollingTouchListener(getActivity()));
mergeAdapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, mergeAdapter);

View File

@ -166,7 +166,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
navigationIcon=new LayerDrawable(new Drawable[]{
searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_search_24px, getToolbarContext().getTheme()).mutate(),
backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_arrow_back, getToolbarContext().getTheme()).mutate()
backIcon=getToolbarContext().getResources().getDrawable(me.grishka.appkit.R.drawable.ic_arrow_back, getToolbarContext().getTheme()).mutate()
}){
@Override
public Drawable mutate(){

View File

@ -316,8 +316,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
if(a==null) {
response.close();
return;
}
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->{

View File

@ -370,7 +370,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
protected void proceedWithAuthOrSignup(Instance instance){
if(currentInviteLinkAlert!=null){
currentInviteLinkAlert.dismiss();
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.isValidInviteUrl(currentSearchQueryButWithCasePreserved)){
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
new CheckInviteLink(inviteLink.getPath())
@ -543,8 +543,8 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
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()){
String clipText=clipData.getItemAt(0).coerceToText(getActivity()).toString();
if(HtmlParser.isValidInviteUrl(clipText)){
edit.setText(clipText);
supportingText.setText(R.string.invite_link_pasted);
}

View File

@ -133,7 +133,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
}
numRunningFollowRequests++;
String id=accountIdsToFollow.remove(0);
new SetAccountFollowed(id, true, true)
new SetAccountFollowed(id, true, true, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@ -81,7 +81,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES)
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
@ -134,7 +134,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
@Override
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
return 1;
}

View File

@ -34,7 +34,6 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ReportDoneFragment extends MastodonToolbarFragment{
@ -177,7 +176,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
}
private void onUnfollowClick(){
new SetAccountFollowed(reportAccount.id, false, false)
new SetAccountFollowed(reportAccount.id, false, false, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@ -125,41 +125,40 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
ArrayList<String> options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList<String>::new));
options.add(0, getString(R.string.filter_duration_forever));
options.add(getString(R.string.filter_duration_custom));
Instant[] newEnd={null};
boolean[] isCustom={false};
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_filter_duration_title)
.setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
.setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
AlertDialog a=(AlertDialog) dlg;
if(item==options.size()-1){ // custom
showCustomDurationAlert(isCustom[0] ? newEnd[0] : null, date->{
showCustomDurationAlert(null, date->{
if(date==null){
a.getListView().setItemChecked(item, false);
}else{
isCustom[0]=true;
newEnd[0]=date;
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
Instant newEnd=date;
if(!Objects.equals(endsAt, newEnd)){
endsAt=newEnd;
updateDurationItem();
dirty=true;
}
a.dismiss();
}
});
}else{
isCustom[0]=false;
Instant newEnd;
if(item==0){
newEnd[0]=null;
newEnd=null;
}else{
newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
newEnd=Instant.now().plusSeconds(durationOptions[item-1]);
}
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
if(!Objects.equals(endsAt, newEnd)){
endsAt=newEnd;
updateDurationItem();
dirty=true;
}
a.dismiss();
}
})
.setPositiveButton(R.string.ok, (dlg, item)->{
if(!Objects.equals(endsAt, newEnd[0])){
endsAt=newEnd[0];
updateDurationItem();
dirty=true;
}
})
.setNegativeButton(R.string.cancel, null)
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}

View File

@ -37,14 +37,14 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
private ImageButton fab;
private Button fab;
private ActionMode actionMode;
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
private ArrayList<String> deletedItemIDs=new ArrayList<>();
private MenuItem deleteItem;
public FilterWordsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
}
@Override
@ -92,18 +92,14 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_add_24px);
fab.setContentDescription(getString(R.string.add_muted_word));
fab.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_add_24px, 0, 0, 0);
fab.setText(R.string.add_muted_word_short);
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
int fabInset=0;
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
fabInset=insets.getSystemWindowInsetBottom();
}
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
UiUtils.applyBottomInsetToFAB(fab, insets);
super.onApplyWindowInsets(insets);
}

View File

@ -1,6 +1,15 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
@ -15,9 +24,12 @@ import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewContr
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
private ListItem<Void> languageItem;
private CheckableListItem<Void> altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
private ListItem<Void> languageItem, customTabsItem;
private CheckableListItem<Void> altTextItem, playGifsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
private Locale postLanguage;
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
@ -33,9 +45,9 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
onDataLoaded(List.of(
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
customTabsItem=new ListItem<>(R.string.settings_custom_tabs, GlobalUserPreferences.useCustomTabs ? R.string.in_app_browser : R.string.system_browser, R.drawable.ic_open_in_browser_24px, this::onCustomTabsClick),
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, this::toggleCheckableItem),
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, this::toggleCheckableItem),
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, this::toggleCheckableItem),
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, this::toggleCheckableItem),
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, this::toggleCheckableItem),
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, this::toggleCheckableItem)
@ -46,19 +58,61 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
protected void doLoadData(int offset, int count){}
private void onDefaultLanguageClick(ListItem<?> item){
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
new M3AlertDialogBuilder(getActivity())
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, newPostLanguage==null ? new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage, null) : newPostLanguage, null);
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.default_post_language)
.setView(vc.getView())
.setPositiveButton(R.string.ok, (dlg, which)->{
ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
if(!opt.locale.equals(postLanguage)){
newPostLanguage=opt;
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
rebindItem(languageItem);
}
.setPositiveButton(R.string.cancel, null)
.show();
vc.setSelectionListener(opt->{
if(!opt.locale.equals(postLanguage)){
newPostLanguage=opt;
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
rebindItem(languageItem);
}
dlg.dismiss();
});
}
private void onCustomTabsClick(ListItem<?> item){
// GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("http://example.com"));
ResolveInfo info=getActivity().getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
final String browserName;
if(info==null){
browserName="??";
}else{
browserName=info.loadLabel(getActivity().getPackageManager()).toString();
}
ArrayAdapter<CharSequence> adapter=new ArrayAdapter<>(getActivity(), R.layout.item_alert_single_choice_2lines_but_different, R.id.text,
new String[]{getString(R.string.in_app_browser), getString(R.string.system_browser)}){
@Override
public boolean hasStableIds(){
return true;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent){
View view=super.getView(position, convertView, parent);
TextView subtitle=view.findViewById(R.id.subtitle);
if(position==0){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(browserName);
}
return view;
}
};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_custom_tabs)
.setSingleChoiceItems(adapter, GlobalUserPreferences.useCustomTabs ? 0 : 1, (dlg, which)->{
GlobalUserPreferences.useCustomTabs=which==0;
customTabsItem.subtitleRes=GlobalUserPreferences.useCustomTabs ? R.string.in_app_browser : R.string.system_browser;
rebindItem(customTabsItem);
dlg.dismiss();
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@ -66,9 +120,8 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
protected void onHidden(){
super.onHidden();
GlobalUserPreferences.playGifs=playGifsItem.checked;
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
GlobalUserPreferences.altTextReminders=altTextItem.checked;
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
GlobalUserPreferences.confirmUnfollow=confirmUnfollowItem.checked;
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
GlobalUserPreferences.save();

View File

@ -86,28 +86,26 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
case DARK -> 1;
case AUTO -> 2;
};
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme)
.setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
};
if(pref!=GlobalUserPreferences.theme){
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
GlobalUserPreferences.theme=pref;
GlobalUserPreferences.save();
themeItem.subtitleRes=getAppearanceValue();
rebindItem(themeItem);
maybeApplyNewThemeRightNow(prev);
}
})
.setNegativeButton(R.string.cancel, null)
selected, (dlg, item)->{
GlobalUserPreferences.ThemePreference pref=switch(item){
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
default -> throw new IllegalStateException("Unexpected value: "+item);
};
if(pref!=GlobalUserPreferences.theme){
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
GlobalUserPreferences.theme=pref;
GlobalUserPreferences.save();
themeItem.subtitleRes=getAppearanceValue();
rebindItem(themeItem);
maybeApplyNewThemeRightNow(prev);
}
dlg.dismiss();
})
.show();
}

View File

@ -1,6 +1,9 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import com.squareup.otto.Subscribe;
@ -12,6 +15,7 @@ import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
@ -24,6 +28,12 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
private Button fab;
public SettingsFiltersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -44,6 +54,8 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Filter> result){
if(getActivity()==null)
return;
onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
}
})
@ -51,13 +63,12 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(super.getAdapter());
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
)));
return adapter;
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setText(R.string.settings_add_filter);
fab.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_add_24px, 0, 0, 0);
fab.setOnClickListener(v->onAddFilterClick());
}
private void onFilterClick(ListItem<Filter> filter){
@ -67,7 +78,7 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
Nav.go(getActivity(), EditFilterFragment.class, args);
}
private void onAddFilterClick(ListItem<?> item){
private void onAddFilterClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), EditFilterFragment.class, args);
@ -107,4 +118,10 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
data.add(makeListItem(ev.filter));
itemsAdapter.notifyItemInserted(data.size()-1);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
UiUtils.applyBottomInsetToFAB(fab, insets);
super.onApplyWindowInsets(insets);
}
}

View File

@ -192,18 +192,13 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
3*24*3600,
7*24*3600
};
int[] selectedOption={0};
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.pause_all_notifications_title)
.setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
.setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
if(selectedOption[0]==0){
((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
selectedOption[0]=durationOptions[item];
AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+durationOptions[item]*1000L);
dlg.dismiss();
})
.setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
.setNegativeButton(R.string.cancel, null)
.show();
alert.setOnDismissListener(dialog->updatePauseItem());
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
@ -216,20 +211,18 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
R.string.notifications_policy_follower,
R.string.notifications_policy_no_one
).map(this::getString).toArray(String[]::new);
int[] selectedItem={getPushSubscription().policy.ordinal()};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_notifications_policy)
.setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
.setPositiveButton(R.string.ok, (dlg, which)->{
.setSingleChoiceItems(items, getPushSubscription().policy.ordinal(), (dlg, which)->{
dlg.dismiss();
PushSubscription.Policy prevValue=getPushSubscription().policy;
PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
PushSubscription.Policy newValue=PushSubscription.Policy.values()[which];
if(prevValue==newValue)
return;
getPushSubscription().policy=newValue;
updatePolicyItem(prevValue);
needUpdateNotificationSettings=true;
})
.setNegativeButton(R.string.cancel, null)
.show();
}

View File

@ -100,15 +100,19 @@ public class SettingsServerAboutFragment extends LoaderFragment{
scroller.setClipToPadding(false);
scroller.addView(scrollingLayout);
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
if(!TextUtils.isEmpty(instance.thumbnail)){
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
}else{
scrollingLayout.setPadding(0, V.dp(24), 0, 0);
}
boolean needDivider=false;
if(instance.contactAccount!=null){

View File

@ -0,0 +1,47 @@
package org.joinmastodon.android.googleservices;
import android.app.PendingIntent;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ConnectionResult extends AutoSafeParcelable{
public static final int UNKNOWN = -1;
public static final int SUCCESS = 0;
public static final int SERVICE_MISSING = 1;
public static final int SERVICE_VERSION_UPDATE_REQUIRED = 2;
public static final int SERVICE_DISABLED = 3;
public static final int SIGN_IN_REQUIRED = 4;
public static final int INVALID_ACCOUNT = 5;
public static final int RESOLUTION_REQUIRED = 6;
public static final int NETWORK_ERROR = 7;
public static final int INTERNAL_ERROR = 8;
public static final int SERVICE_INVALID = 9;
public static final int DEVELOPER_ERROR = 10;
public static final int LICENSE_CHECK_FAILED = 11;
public static final int CANCELED = 13;
public static final int TIMEOUT = 14;
public static final int INTERRUPTED = 15;
public static final int API_UNAVAILABLE = 16;
public static final int SIGN_IN_FAILED = 17;
public static final int SERVICE_UPDATING = 18;
public static final int SERVICE_MISSING_PERMISSION = 19;
public static final int RESTRICTED_PROFILE = 20;
public static final int RESOLUTION_ACTIVITY_NOT_FOUND = 22;
public static final int API_DISABLED = 23;
public static final int API_DISABLED_FOR_CONNECTION = 24;
@Deprecated
public static final int DRIVE_EXTERNAL_STORAGE_REQUIRED = 1500;
@SafeParceled(1)
public int versionCode;
@SafeParceled(2)
public int errorCode;
@SafeParceled(3)
public PendingIntent resolution;
@SafeParceled(4)
public String errorMessage;
public static final Creator<ConnectionResult> CREATOR=new AutoCreator<>(ConnectionResult.class);
}

View File

@ -0,0 +1,116 @@
package org.joinmastodon.android.googleservices;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;
import com.google.android.gms.common.internal.ConnectionInfo;
import com.google.android.gms.common.internal.GetServiceRequest;
import com.google.android.gms.common.internal.IGmsCallbacks;
import com.google.android.gms.common.internal.IGmsServiceBroker;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService;
import java.util.function.Function;
public class GmsClient{
private static final String TAG="GmsClient";
private static final SparseArray<ServiceConnection> currentConnections=new SparseArray<>();
private static int nextConnectionID=0;
public static <I extends IInterface> void connectToService(Context context, String action, int id, boolean useDynamicLookup, ServiceConnectionCallback<I> callback, Function<IBinder, I> asInterface){
Intent intent;
if(useDynamicLookup){
try{
Bundle args=new Bundle();
args.putString("serviceActionBundleKey", action);
Bundle result=context.getContentResolver().call(new Uri.Builder().scheme("content").authority("com.google.android.gms.chimera").build(), "serviceIntentCall", null, args);
if(result==null)
throw new IllegalStateException("Dynamic lookup failed");
intent=result.getParcelable("serviceResponseIntentKey");
if(intent==null)
throw new IllegalStateException("Dynamic lookup returned null");
}catch(Exception x){
callback.onError(x);
return;
}
}else{
intent=new Intent(action);
}
intent.setPackage("com.google.android.gms");
ServiceConnection conn=new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service){
IGmsServiceBroker broker=IGmsServiceBroker.Stub.asInterface(service);
GetServiceRequest req=new GetServiceRequest();
req.serviceId=id;
req.packageName=context.getPackageName();
ServiceConnection serviceConnectionThis=this;
try{
broker.getService(new IGmsCallbacks.Stub(){
@Override
public void onPostInitComplete(int statusCode, IBinder binder, Bundle params) throws RemoteException{
int connectionID=nextConnectionID++;
currentConnections.put(connectionID, serviceConnectionThis);
callback.onSuccess(asInterface.apply(binder), connectionID);
}
@Override
public void onAccountValidationComplete(int statusCode, Bundle params) throws RemoteException{}
@Override
public void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, ConnectionInfo info) throws RemoteException{
onPostInitComplete(statusCode, binder, info!=null ? info.params : null);
}
}, req);
}catch(Exception x){
callback.onError(x);
context.unbindService(this);
}
}
@Override
public void onServiceDisconnected(ComponentName name){}
};
boolean res=context.bindService(intent, conn, Context.BIND_AUTO_CREATE | Context.BIND_DEBUG_UNBIND | Context.BIND_ADJUST_WITH_ACTIVITY);
if(!res){
context.unbindService(conn);
callback.onError(new IllegalStateException("Service connection failed"));
}
}
public static void disconnectFromService(Context context, int connectionID){
ServiceConnection conn=currentConnections.get(connectionID);
if(conn!=null){
currentConnections.remove(connectionID);
context.unbindService(conn);
}
}
public static boolean isGooglePlayServicesAvailable(Context context){
PackageManager pm=context.getPackageManager();
try{
pm.getPackageInfo("com.google.android.gms", 0);
return true;
}catch(PackageManager.NameNotFoundException e){
return false;
}
}
public static void getModuleInstallerService(Context context, ServiceConnectionCallback<IModuleInstallService> callback){
connectToService(context, "com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START", 308, true, callback, IModuleInstallService.Stub::asInterface);
}
public interface ServiceConnectionCallback<I extends IInterface>{
void onSuccess(I service, int connectionID);
void onError(Exception error);
}
}

View File

@ -0,0 +1,253 @@
package org.joinmastodon.android.googleservices.barcodescanner;
import android.graphics.Point;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Barcode extends AutoSafeParcelable{
public static final int FORMAT_UNKNOWN = -1;
public static final int FORMAT_ALL_FORMATS = 0;
public static final int FORMAT_CODE_128 = 1;
public static final int FORMAT_CODE_39 = 2;
public static final int FORMAT_CODE_93 = 4;
public static final int FORMAT_CODABAR = 8;
public static final int FORMAT_DATA_MATRIX = 16;
public static final int FORMAT_EAN_13 = 32;
public static final int FORMAT_EAN_8 = 64;
public static final int FORMAT_ITF = 128;
public static final int FORMAT_QR_CODE = 256;
public static final int FORMAT_UPC_A = 512;
public static final int FORMAT_UPC_E = 1024;
public static final int FORMAT_PDF417 = 2048;
public static final int FORMAT_AZTEC = 4096;
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_CONTACT_INFO = 1;
public static final int TYPE_EMAIL = 2;
public static final int TYPE_ISBN = 3;
public static final int TYPE_PHONE = 4;
public static final int TYPE_PRODUCT = 5;
public static final int TYPE_SMS = 6;
public static final int TYPE_TEXT = 7;
public static final int TYPE_URL = 8;
public static final int TYPE_WIFI = 9;
public static final int TYPE_GEO = 10;
public static final int TYPE_CALENDAR_EVENT = 11;
public static final int TYPE_DRIVER_LICENSE = 12;
@SafeParceled(1)
public int format;
@SafeParceled(2)
public String displayValue;
@SafeParceled(3)
public String rawValue;
@SafeParceled(4)
public byte[] rawBytes;
@SafeParceled(5)
public Point[] cornerPoints;
@SafeParceled(6)
public int valueType;
@SafeParceled(7)
public Email emailValue;
@SafeParceled(8)
public Phone phoneValue;
@SafeParceled(9)
public SMS smsValue;
@SafeParceled(10)
public WiFi wifiValue;
@SafeParceled(11)
public UrlBookmark urlBookmarkValue;
@SafeParceled(12)
public GeoPoint geoPointValue;
@SafeParceled(13)
public CalendarEvent calendarEventValue;
@SafeParceled(14)
public ContactInfo contactInfoValue;
@SafeParceled(15)
public DriverLicense driverLicenseValue;
public static final Creator<Barcode> CREATOR=new AutoCreator<>(Barcode.class);
// None of the following is needed or used in the Mastodon app and its use cases for QR code scanning,
// but I'm putting it out there in case someone else is crazy enough to want to use Google Services without their libraries
public static class Email extends AutoSafeParcelable{
@SafeParceled(1)
public int type;
@SafeParceled(2)
public String address;
@SafeParceled(3)
public String subject;
@SafeParceled(4)
public String body;
public static final Creator<Email> CREATOR=new AutoCreator<>(Email.class);
}
public static class Phone extends AutoSafeParcelable{
@SafeParceled(1)
public int type;
@SafeParceled(2)
public String number;
public static final Creator<Phone> CREATOR=new AutoCreator<>(Phone.class);
}
public static class SMS extends AutoSafeParcelable{
@SafeParceled(1)
public String message;
@SafeParceled(2)
public String phoneNumber;
public static final Creator<SMS> CREATOR=new AutoCreator<>(SMS.class);
}
public static class WiFi extends AutoSafeParcelable{
@SafeParceled(1)
public String ssid;
@SafeParceled(2)
public String password;
@SafeParceled(3)
public int encryptionType;
public static final Creator<WiFi> CREATOR=new AutoCreator<>(WiFi.class);
}
public static class UrlBookmark extends AutoSafeParcelable{
@SafeParceled(1)
public String title;
@SafeParceled(2)
public String url;
public static final Creator<UrlBookmark> CREATOR=new AutoCreator<>(UrlBookmark.class);
}
public static class GeoPoint extends AutoSafeParcelable{
@SafeParceled(1)
public double lat;
@SafeParceled(2)
public double lng;
public static final Creator<GeoPoint> CREATOR=new AutoCreator<>(GeoPoint.class);
}
public static class EventDateTime extends AutoSafeParcelable{
@SafeParceled(1)
public int year;
@SafeParceled(2)
public int month;
@SafeParceled(3)
public int day;
@SafeParceled(4)
public int hours;
@SafeParceled(5)
public int minutes;
@SafeParceled(6)
public int seconds;
@SafeParceled(7)
public boolean isUtc;
@SafeParceled(8)
public String rawValue;
public static final Creator<EventDateTime> CREATOR=new AutoCreator<>(EventDateTime.class);
}
public static class CalendarEvent extends AutoSafeParcelable{
@SafeParceled(1)
public String summary;
@SafeParceled(2)
public String description;
@SafeParceled(3)
public String location;
@SafeParceled(4)
public String organizer;
@SafeParceled(5)
public String status;
@SafeParceled(6)
public EventDateTime start;
@SafeParceled(7)
public EventDateTime end;
public static final Creator<CalendarEvent> CREATOR=new AutoCreator<>(CalendarEvent.class);
}
public static class Address extends AutoSafeParcelable{
@SafeParceled(1)
public int type;
@SafeParceled(2)
public String[] addressLines;
public static final Creator<Address> CREATOR=new AutoCreator<>(Address.class);
}
public static class PersonName extends AutoSafeParcelable{
@SafeParceled(1)
public String formattedName;
@SafeParceled(2)
public String pronunciation;
@SafeParceled(3)
public String prefix;
@SafeParceled(4)
public String first;
@SafeParceled(5)
public String middle;
@SafeParceled(6)
public String last;
@SafeParceled(7)
public String suffix;
public static final Creator<PersonName> CREATOR=new AutoCreator<>(PersonName.class);
}
public static class ContactInfo extends AutoSafeParcelable{
@SafeParceled(1)
public PersonName name;
@SafeParceled(2)
public String organization;
@SafeParceled(3)
public String title;
@SafeParceled(4)
public Phone[] phones;
@SafeParceled(5)
public Email[] emails;
@SafeParceled(6)
public String[] urls;
@SafeParceled(7)
public Address[] addresses;
public static final Creator<ContactInfo> CREATOR=new AutoCreator<>(ContactInfo.class);
}
public static class DriverLicense extends AutoSafeParcelable{
@SafeParceled(1)
public String documentType;
@SafeParceled(2)
public String firstName;
@SafeParceled(3)
public String middleName;
@SafeParceled(4)
public String lastName;
@SafeParceled(5)
public String gender;
@SafeParceled(6)
public String addressStreet;
@SafeParceled(7)
public String addressCity;
@SafeParceled(8)
public String addressState;
@SafeParceled(9)
public String addressZip;
@SafeParceled(10)
public String licenseNumber;
@SafeParceled(11)
public String issueDate;
@SafeParceled(12)
public String expiryDate;
@SafeParceled(13)
public String birthDate;
@SafeParceled(14)
public String issuingCountry;
public static final Creator<DriverLicense> CREATOR=new AutoCreator<>(DriverLicense.class);
}
}

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