Compare commits

...

324 Commits

Author SHA1 Message Date
LucasGGamerM
f6ea0404ef Revert "fix: allows for editing timeline badges again. fixes sk22#800 (#804)"
This reverts commit f14df2bb0f.
2023-09-03 18:26:55 -03:00
FineFindus
8cd55fc365 feat: display icon for bots (#793)
* feat(profile): display bot icon for bots

* feat(status): display bot icon

* use 16sp size and 4sp spacing

* don't add lock/bot icon as image spans

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-09-03 21:34:22 +02:00
LucasGGamerM
f14df2bb0f fix: allows for editing timeline badges again. fixes sk22#800 (#804) 2023-09-03 20:49:29 +02:00
Jacoco
53369eb2d4 Fix unreliable Preferences from Account Source (#798)
* Create empty Preferences object on error

* Update prefs from account when preferences fails
2023-09-01 21:28:02 +02:00
sk
807010893a support static url for emoji reacts and keyboard 2023-09-01 14:38:55 +02:00
sk
ea01b14ffb more options for showing emoji reactions
closes sk22#796
re: sk22#788
2023-09-01 14:23:38 +02:00
arnav
3fd9dc1dcd Translated using Weblate (Hindi)
Currently translated at 4.3% (16 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/hi/
2023-08-31 23:18:52 +00:00
sk
02a4a77885 Merge remote-tracking branch 'weblate/main' 2023-09-01 01:18:30 +02:00
sk
651090a504 Merge remote-tracking branch 'upstream/master' 2023-09-01 01:17:18 +02:00
sk
203254c9f4 fix crash in onEmojiReactionsChanged 2023-09-01 01:16:52 +02:00
Gregory K
16ef577a7a Merge pull request #678 from LucasGGamerM/mastodon-android
fix: fix alt texts not being able to be edited
2023-08-31 20:31:39 +03:00
LucasGGamerM
734b3bced6 fix: fix alt texts not being able to be edited
fixes #70 cc: @sk22
2023-08-31 14:18:32 -03:00
sk
e26c641dc7 fix drafts being all on the same-ish day 2023-08-31 18:07:43 +02:00
sk
9295cf4e9c fix emoji reaction spacing not updating
closes sk22#784
2023-08-31 17:24:29 +02:00
sk
dd9237e9ca enable emojiReactionsInTimelines by default 2023-08-31 17:20:44 +02:00
sk
ea81c1fad6 Merge remote-tracking branch 'upstream/master' 2023-08-31 17:09:18 +02:00
sk
d334703c65 adhere to showEmojiReactionsInLists setting
closes sk22#788
2023-08-31 17:04:22 +02:00
Gregory K
5f6f3c94c9 Merge pull request #677 from tinsukE/gap-local-filter
When loading gap posts, apply filters before building display items.
2023-08-31 15:06:07 +03:00
Angelo Suzuki
09ba42a974 When loading gap posts, apply filters before building display items.
This will make sure that items that are filtered out don't show up on the interface.
Fixes #675
2023-08-31 14:01:58 +02:00
ihor_ck
b1e43d6f97 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-08-30 18:04:05 +00:00
ppnplus
ea92a61d13 Translated using Weblate (Thai)
Currently translated at 20.5% (76 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/th/
2023-08-30 18:04:05 +00:00
Linerly
7fda69a6aa Translated using Weblate (Indonesian)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-30 18:04:05 +00:00
gallegonovato
cf8b9ac649 Translated using Weblate (Spanish)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-30 18:04:05 +00:00
ppnplus
3c122b005d Translated using Weblate (Thai)
Currently translated at 3.7% (14 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/th/
2023-08-29 22:37:15 +00:00
edxkl
f9dc6105f4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.5% (361 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-08-29 22:37:15 +00:00
Oliebol
d7fe3c80e6 Translated using Weblate (Dutch)
Currently translated at 82.9% (307 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-08-29 22:37:15 +00:00
Choukajohn
e996deea0b Translated using Weblate (French)
Currently translated at 100.0% (370 of 370 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-08-29 22:37:14 +00:00
S1m
580ae15af6 Fix unifiedpush (#785)
* [UnifiedPush] Register the new endpoint

* [UnifiedPush] Get account linked to instance
2023-08-29 12:22:12 +02:00
sk
e66078e52e fix poll options outline provider
closes sk22#702
2023-08-29 00:53:02 +02:00
FineFindus
f84356f5d0 fix: display 'Lists with' with username (#780) 2023-08-29 00:19:34 +02:00
sk
1fa5a8436b highlight verified links again!
closes sk22#713
2023-08-29 00:18:12 +02:00
sk
2a471ffa96 fix broken "edit avatar" overlay.. again!!
closes sk22#727
closes sk22#738
2023-08-28 23:48:18 +02:00
sk
1ee5e1ab3a fix cut-off settings item titles
closes sk22#723
2023-08-28 23:42:34 +02:00
sk
4d90cad034 Merge remote-tracking branch 'weblate/main' 2023-08-28 23:35:18 +02:00
sk
45615b1fc5 Merge remote-tracking branch 'upstream/l10n_master' 2023-08-28 23:35:04 +02:00
sk
9fd0e7fea4 return if fcm device token is empty
closes sk22#779
2023-08-28 23:33:28 +02:00
sk
696016bd8f use hasSpoiler method 2023-08-28 23:29:18 +02:00
sk
cc46e09853 fix issue with visibility button
closes sk22#740
2023-08-28 23:24:56 +02:00
sk
83e84836b5 fix slightly wrong margins
closes sk22#778
2023-08-28 22:47:43 +02:00
sk
14bb544344 replace donation link 2023-08-28 22:35:17 +02:00
sk
ceec18ff5e fix divider not being full-width 2023-08-28 22:33:19 +02:00
sk
d36ad43700 fix reporting crashing the app 2023-08-28 22:26:53 +02:00
sk
fb39f74ba5 fix bottom padding for above emoji reactions 2023-08-28 20:22:16 +02:00
sk
9bfc73d6ee fix wrong bottom padding
hopefully, lol
2023-08-28 20:04:52 +02:00
EndermanCo
efb72eace9 Translated using Weblate (Persian)
Currently translated at 50.0% (9 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-08-28 14:53:05 +00:00
edxkl
84b15f4a49 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.5% (360 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-08-28 14:53:05 +00:00
ca
2c793fd83b Translated using Weblate (Catalan)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ca/
2023-08-28 14:53:05 +00:00
Eugen Rochko
900b204bb0 New translations strings.xml (Portuguese, Brazilian) 2023-08-27 16:51:00 +02:00
Eugen Rochko
17d679901a New translations strings.xml (Japanese) 2023-08-27 15:42:52 +02:00
sk
c3d9147705 fix wrong time/username reference when replying 2023-08-27 14:46:33 +02:00
Eugen Rochko
d8036779f8 New translations strings.xml (Japanese) 2023-08-27 14:46:15 +02:00
sk
2fafdcc4f8 fix unifiedpush settings item
closes sk22#770
2023-08-27 14:33:57 +02:00
sk
ef3f96ba74 fix timestamp being hidden for long usernames
closes sk22#772
2023-08-27 14:12:23 +02:00
sk
91bf1b277f fix null-pointer exception in filter result
closes sk22#774
2023-08-27 13:52:31 +02:00
sk
c6acd35ceb fix "blocked" button style
closes sk22#775
2023-08-27 13:45:44 +02:00
sk
a85a0b7d78 don't show blocked_by status
restoring fix for sk22#526
2023-08-27 13:38:33 +02:00
0que
3a35674ea2 Translated using Weblate (Russian)
Currently translated at 77.7% (14 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2023-08-27 05:53:05 +00:00
0que
3235cf1c4f Translated using Weblate (Russian)
Currently translated at 89.7% (331 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-08-27 05:53:04 +00:00
Eugen Rochko
3f6bda28b3 New translations strings.xml (Vietnamese) 2023-08-26 17:43:48 +02:00
Eugen Rochko
c0b4f4dd79 New translations strings.xml (Vietnamese) 2023-08-26 16:06:27 +02:00
sk
ad96031aeb rounded corners for inset images! 2023-08-25 23:26:27 +02:00
sk
aa9e66e6a2 tweak account card design
closes sk22#731
2023-08-25 17:20:54 +02:00
sk
86081654fb remove unused import 2023-08-25 17:19:42 +02:00
sk
cac030ffed fix inconsistent avatar border radius 2023-08-25 17:19:29 +02:00
starstuff
2a913e26e7 Translated using Weblate (Swedish)
Currently translated at 41.4% (153 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sv/
2023-08-25 14:53:06 +00:00
edxkl
f60375cd5f Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.0% (358 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-08-25 14:53:06 +00:00
poesty
5920270899 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-08-25 14:53:06 +00:00
Eugen Rochko
f36aee44c6 New translations strings.xml (Scottish Gaelic) 2023-08-25 09:54:23 +02:00
Eugen Rochko
cd24526a9d New translations strings.xml (Swedish) 2023-08-24 17:48:37 +02:00
Eugen Rochko
a345ac1390 New translations strings.xml (Icelandic) 2023-08-24 17:48:36 +02:00
sk
71f4f089b6 scroll to added emoji; improve loading animation 2023-08-24 13:26:42 +02:00
sk
0f72809342 scroll to existing reaction when adding 2023-08-24 13:15:53 +02:00
sk
40a033d692 no emoji reactions for akkoma announcements 2023-08-24 13:03:24 +02:00
sk
d44df2c23c change behavior when adding reactions
closes sk22#769
2023-08-24 13:00:16 +02:00
sk
47fde1e08b undo spacing "fix" 2023-08-24 12:41:17 +02:00
sk
0688521ae8 make emoji reactions not click-through-able 2023-08-24 12:17:15 +02:00
sk
bdf0f21647 fix spacing between text and footer 2023-08-24 12:13:20 +02:00
sk
bb03342ff2 boop versinom 2023-08-24 12:12:55 +02:00
sk
937304f27b improve profile metadata editor styles 2023-08-24 00:05:36 +02:00
sk
6b4ce0ea69 hide roles when in edit mode 2023-08-23 23:48:42 +02:00
sk
7f0c4860f8 clean up code 2023-08-23 23:47:06 +02:00
sk
9b4c70a5ed fix jumping profile layout
closes sk22#656
2023-08-23 23:46:23 +02:00
sk
49137273ae clean up code 2023-08-23 23:38:45 +02:00
sk
647e3e5e85 progress indicator for emoji reactions 2023-08-23 23:38:12 +02:00
sk
4920bf63e3 oops. accidentally removed a setting 2023-08-23 23:12:11 +02:00
sk
0afcdb2cdf emoji reactions for announcements! 2023-08-23 22:56:11 +02:00
sk
d96c3c3c8a fix paddings around dummys, text and spoilers
closes sk22#638
2023-08-23 22:12:14 +02:00
Eugen Rochko
3d987b8e1d New translations strings.xml (Thai) 2023-08-23 21:39:07 +02:00
sk
f5e5408d70 Merge remote-tracking branch 'upstream/master' 2023-08-23 20:02:06 +02:00
sk
d62899c990 fix reaction data and binding inconsistencies 2023-08-23 19:49:07 +02:00
Eugen Rochko
57043912e0 New translations strings.xml (Chinese Traditional) 2023-08-23 19:16:40 +02:00
Eugen Rochko
00aef5ea6b New translations full_description.txt (Arabic) 2023-08-23 13:02:31 +02:00
Eugen Rochko
369b69668c New translations strings.xml (Arabic) 2023-08-23 13:02:29 +02:00
Eugen Rochko
65245f4560 New translations strings.xml (Arabic) 2023-08-23 09:39:50 +02:00
Eugen Rochko
4d4fdc97d4 New translations strings.xml (French) 2023-08-23 09:39:49 +02:00
Eugen Rochko
c96577891c New translations strings.xml (Scottish Gaelic) 2023-08-23 07:00:18 +02:00
Eugen Rochko
f48b2fc9cb New translations strings.xml (Thai) 2023-08-23 07:00:13 +02:00
Eugen Rochko
2fca2580ed New translations strings.xml (Bengali) 2023-08-23 07:00:12 +02:00
Eugen Rochko
7adc1da361 New translations strings.xml (Persian) 2023-08-23 07:00:11 +02:00
Eugen Rochko
6dc24dde43 New translations strings.xml (Indonesian) 2023-08-23 07:00:10 +02:00
Eugen Rochko
4929e0e6ec New translations strings.xml (Portuguese, Brazilian) 2023-08-23 07:00:09 +02:00
Eugen Rochko
16a8b8ed71 New translations strings.xml (Icelandic) 2023-08-23 07:00:08 +02:00
Eugen Rochko
ad1412817e New translations strings.xml (Galician) 2023-08-23 07:00:07 +02:00
Eugen Rochko
d9e6bb3bea New translations strings.xml (Vietnamese) 2023-08-23 07:00:06 +02:00
Eugen Rochko
8970404638 New translations strings.xml (Chinese Traditional) 2023-08-23 07:00:05 +02:00
Eugen Rochko
2a2241d7f9 New translations strings.xml (Chinese Simplified) 2023-08-23 07:00:04 +02:00
Eugen Rochko
db1a47e8eb New translations strings.xml (Ukrainian) 2023-08-23 07:00:03 +02:00
Eugen Rochko
3e57061cef New translations strings.xml (Turkish) 2023-08-23 07:00:02 +02:00
Eugen Rochko
cd200f8450 New translations strings.xml (Slovenian) 2023-08-23 07:00:00 +02:00
Eugen Rochko
782013079f New translations strings.xml (Russian) 2023-08-23 06:59:59 +02:00
Eugen Rochko
e5db8acd66 New translations strings.xml (Polish) 2023-08-23 06:59:57 +02:00
Eugen Rochko
1a6a8019c8 New translations strings.xml (Norwegian) 2023-08-23 06:59:57 +02:00
Eugen Rochko
e935eef29f New translations strings.xml (Dutch) 2023-08-23 06:59:56 +02:00
Eugen Rochko
381defda51 New translations strings.xml (Japanese) 2023-08-23 06:59:54 +02:00
Eugen Rochko
02ae80c204 New translations strings.xml (Italian) 2023-08-23 06:59:53 +02:00
Eugen Rochko
82214b30e8 New translations strings.xml (Armenian) 2023-08-23 06:59:52 +02:00
Eugen Rochko
33a1f48602 New translations strings.xml (Greek) 2023-08-23 06:59:48 +02:00
Eugen Rochko
aee845e5cc New translations strings.xml (German) 2023-08-23 06:59:47 +02:00
Eugen Rochko
cd780f6006 New translations strings.xml (Danish) 2023-08-23 06:59:46 +02:00
Eugen Rochko
d4741fefa0 New translations strings.xml (Czech) 2023-08-23 06:59:46 +02:00
Eugen Rochko
7e1e8a2616 New translations strings.xml (Belarusian) 2023-08-23 06:59:44 +02:00
Eugen Rochko
d73c05cdfc New translations strings.xml (Arabic) 2023-08-23 06:59:43 +02:00
Eugen Rochko
78323023cb New translations strings.xml (Spanish) 2023-08-23 06:59:42 +02:00
Eugen Rochko
2cf084c98f New translations strings.xml (French) 2023-08-23 06:59:41 +02:00
Grishka
e5bdeba1d7 Fix strings 2023-08-23 00:44:40 +03:00
Grishka
8d7db7774f Merge branch 'l10n_master' 2023-08-23 00:40:45 +03:00
Grishka
78d22c670c Fix reporting 2023-08-23 00:40:38 +03:00
Eugen Rochko
0a679109f5 New translations strings.xml (Arabic) 2023-08-22 17:14:41 +02:00
Eugen Rochko
e843142b7e New translations strings.xml (Arabic) 2023-08-22 13:15:25 +02:00
Eugen Rochko
72e728f655 New translations strings.xml (French) 2023-08-22 13:15:23 +02:00
Eugen Rochko
ef56792f56 New translations strings.xml (Arabic) 2023-08-22 11:39:27 +02:00
Eugen Rochko
504a6959e8 New translations strings.xml (French) 2023-08-22 11:39:25 +02:00
sk
6054a3d65c Merge remote-tracking branch 'weblate/main' 2023-08-21 22:43:23 +02:00
sk
f50eac02d8 move adding reactions to reactions item 2023-08-21 22:40:19 +02:00
snerk
9634db9061 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 20.8% (75 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nn/
2023-08-21 22:40:19 +02:00
snerk
97e3e283dd Translated using Weblate (Norwegian Nynorsk)
Currently translated at 18.8% (68 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nn/
2023-08-21 22:40:19 +02:00
snerk
f1e233569b Added translation using Weblate (Norwegian Nynorsk) 2023-08-21 22:40:19 +02:00
alextecplayz
04dd637fa9 Translated using Weblate (Romanian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-08-21 22:40:19 +02:00
gallegonovato
c48a4105a9 Translated using Weblate (Spanish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-21 22:40:19 +02:00
EndermanCo
aac53d949b Translated using Weblate (Persian)
Currently translated at 38.8% (7 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-08-21 22:40:19 +02:00
EndermanCo
9bb4e5b467 Translated using Weblate (Persian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-21 22:40:19 +02:00
0eoc
fb0391d5cd Translated using Weblate (Russian)
Currently translated at 61.1% (11 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2023-08-21 22:40:19 +02:00
ihor_ck
e4d898c903 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-08-21 22:40:19 +02:00
Linerly
da222f75bb Translated using Weblate (Indonesian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-21 22:40:19 +02:00
Choukajohn
25fbd91eb3 Translated using Weblate (French)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-08-21 22:40:19 +02:00
0eoc
d458cca7bf Translated using Weblate (Russian)
Currently translated at 96.1% (323 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-08-21 22:40:19 +02:00
gallegonovato
3933a61b5a Translated using Weblate (Spanish)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-21 22:40:19 +02:00
Codeberg Translate
29092bbf36 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/
2023-08-21 22:40:19 +02:00
edxkl
a33d2578c9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.4% (17 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2023-08-21 22:40:19 +02:00
edxkl
9afe4b5ac6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.3% (327 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-08-21 22:40:19 +02:00
EndermanCo
6782006b05 Translated using Weblate (Persian)
Currently translated at 27.7% (5 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-08-21 22:40:19 +02:00
0eoc
90bdbefd48 Translated using Weblate (Russian)
Currently translated at 61.1% (11 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2023-08-21 22:40:19 +02:00
gicorada
b7bcf1082e Translated using Weblate (Italian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-08-21 22:40:19 +02:00
EndermanCo
ba9bbc5b6e Translated using Weblate (Persian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-21 22:40:19 +02:00
0eoc
0fecfbd50c Translated using Weblate (Russian)
Currently translated at 81.2% (273 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-08-21 22:40:19 +02:00
gallegonovato
0031dc6119 Translated using Weblate (Spanish)
Currently translated at 99.1% (333 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-21 22:40:19 +02:00
EndermanCo
79e606698e Translated using Weblate (Persian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-21 22:40:19 +02:00
EndermanCo
3c9fc43780 Translated using Weblate (Persian)
Currently translated at 98.5% (331 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-21 22:40:19 +02:00
ihor_ck
adb9b7394a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-08-21 22:40:19 +02:00
Eryk Michalak
6191fdfaef Translated using Weblate (Polish)
Currently translated at 91.3% (307 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-08-21 22:40:19 +02:00
Linerly
446754e8a6 Translated using Weblate (Indonesian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-21 22:40:19 +02:00
Choukajohn
30c67b0b39 Translated using Weblate (French)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-08-21 22:40:19 +02:00
Hudobni Volk
1043ea7b11 Translated using Weblate (Slovenian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-08-21 22:40:19 +02:00
alextecplayz
b449bcd006 Translated using Weblate (Romanian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-08-21 22:40:19 +02:00
EndermanCo
a9d513b564 Translated using Weblate (Persian)
Currently translated at 98.5% (330 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-21 22:40:19 +02:00
Hudobni Volk
5cef527810 Translated using Weblate (Slovenian)
Currently translated at 61.1% (11 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-08-21 22:40:19 +02:00
ganjibaiemade
8bb907747d Translated using Weblate (Chinese (Traditional))
Currently translated at 18.2% (61 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-08-21 22:40:19 +02:00
Hudobni Volk
9c889f8df3 Translated using Weblate (Slovenian)
Currently translated at 100.0% (335 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-08-21 22:40:19 +02:00
Linerly
cbc164d844 Translated using Weblate (Indonesian)
Currently translated at 100.0% (335 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-21 22:40:19 +02:00
Eugen Rochko
b8e3060887 New translations strings.xml (Arabic) 2023-08-21 21:00:00 +02:00
Eugen Rochko
1aa1ede421 New translations strings.xml (Arabic) 2023-08-21 20:03:05 +02:00
Eugen Rochko
480dba7629 New translations strings.xml (Arabic) 2023-08-21 19:05:39 +02:00
Eugen Rochko
9b9c66a149 New translations strings.xml (Arabic) 2023-08-21 17:43:00 +02:00
Eugen Rochko
0f5eb923ee New translations strings.xml (Arabic) 2023-08-21 16:13:06 +02:00
snerk
7f521b3129 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nn/
2023-08-21 09:41:52 +00:00
EndermanCo
9ce217d1f2 Translated using Weblate (Persian)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-21 09:41:51 +00:00
ihor_ck
7b1bd3ccad Translated using Weblate (Ukrainian)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-08-21 09:41:51 +00:00
Oliebol
13b1cbde6b Translated using Weblate (Dutch)
Currently translated at 82.9% (306 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-08-21 09:41:51 +00:00
Linerly
8d898a1a78 Translated using Weblate (Indonesian)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-21 09:41:51 +00:00
Choukajohn
51c2890ede Translated using Weblate (French)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-08-21 09:41:51 +00:00
gallegonovato
03642faa9c Translated using Weblate (Spanish)
Currently translated at 100.0% (369 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-21 09:41:51 +00:00
sk22
084c6e1e59 Translated using Weblate (German)
Currently translated at 94.5% (349 of 369 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-08-21 09:41:51 +00:00
Eugen Rochko
90ed28e7a0 New translations strings.xml (French) 2023-08-20 17:34:05 +01:00
Eugen Rochko
d2b45c1c84 New translations strings.xml (Swedish) 2023-08-20 12:43:05 +01:00
Eugen Rochko
a119ba5f80 New translations strings.xml (Danish) 2023-08-19 17:08:25 +01:00
Eugen Rochko
8c1191a08f New translations strings.xml (Danish) 2023-08-19 16:09:27 +01:00
Eugen Rochko
4275d596e6 New translations strings.xml (Thai) 2023-08-19 12:46:09 +01:00
snerk
2709d5226d Translated using Weblate (Norwegian Nynorsk)
Currently translated at 21.6% (78 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nn/
2023-08-18 16:18:22 +00:00
snerk
8f613e3255 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 20.8% (75 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nn/
2023-08-18 16:18:22 +00:00
snerk
6831e846cf Translated using Weblate (Norwegian Nynorsk)
Currently translated at 18.8% (68 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nn/
2023-08-18 16:18:22 +00:00
snerk
034eb9427d Added translation using Weblate (Norwegian Nynorsk) 2023-08-18 16:18:22 +00:00
alextecplayz
f73c325db3 Translated using Weblate (Romanian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-08-18 16:18:22 +00:00
gallegonovato
5e2b11c504 Translated using Weblate (Spanish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-18 16:18:22 +00:00
EndermanCo
ec13133431 Translated using Weblate (Persian)
Currently translated at 38.8% (7 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-08-18 16:18:22 +00:00
EndermanCo
a8a56a3ed8 Translated using Weblate (Persian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-18 16:18:22 +00:00
0eoc
4e9c7c4de2 Translated using Weblate (Russian)
Currently translated at 61.1% (11 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2023-08-18 16:18:22 +00:00
ihor_ck
ffb7894098 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-08-18 16:18:22 +00:00
Linerly
0d9520ac45 Translated using Weblate (Indonesian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-18 16:18:22 +00:00
Choukajohn
be852e57df Translated using Weblate (French)
Currently translated at 100.0% (360 of 360 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-08-18 16:18:22 +00:00
0eoc
157b38b8ae Translated using Weblate (Russian)
Currently translated at 96.1% (323 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-08-18 16:18:22 +00:00
gallegonovato
83196a1a0d Translated using Weblate (Spanish)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-18 16:18:22 +00:00
Codeberg Translate
306225b054 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/
2023-08-18 16:18:22 +00:00
edxkl
6efc71d8d2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.4% (17 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/pt_BR/
2023-08-18 16:18:21 +00:00
edxkl
cc4cd4d3f8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.3% (327 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pt_BR/
2023-08-18 16:18:21 +00:00
EndermanCo
00e3292205 Translated using Weblate (Persian)
Currently translated at 27.7% (5 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/fa/
2023-08-18 16:18:21 +00:00
0eoc
316952423c Translated using Weblate (Russian)
Currently translated at 61.1% (11 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/ru/
2023-08-18 16:18:21 +00:00
gicorada
61c2abd014 Translated using Weblate (Italian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-08-18 16:18:21 +00:00
EndermanCo
ee5f299b90 Translated using Weblate (Persian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-18 16:18:21 +00:00
0eoc
f153846381 Translated using Weblate (Russian)
Currently translated at 81.2% (273 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ru/
2023-08-18 16:18:21 +00:00
gallegonovato
0656db0858 Translated using Weblate (Spanish)
Currently translated at 99.1% (333 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-08-18 16:18:21 +00:00
EndermanCo
7f250cb8df Translated using Weblate (Persian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-18 16:18:21 +00:00
EndermanCo
a1e73eca89 Translated using Weblate (Persian)
Currently translated at 98.5% (331 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-18 16:18:21 +00:00
ihor_ck
1dc6936da6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-08-18 16:18:21 +00:00
Eryk Michalak
0431d80a8d Translated using Weblate (Polish)
Currently translated at 91.3% (307 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/pl/
2023-08-18 16:18:21 +00:00
Linerly
eaa78093f7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-18 16:18:21 +00:00
Choukajohn
25b7151fde Translated using Weblate (French)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-08-18 16:18:21 +00:00
Hudobni Volk
0438b579b6 Translated using Weblate (Slovenian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-08-18 16:18:21 +00:00
alextecplayz
afa50a4e8c Translated using Weblate (Romanian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-08-18 16:18:21 +00:00
EndermanCo
85bdb0067b Translated using Weblate (Persian)
Currently translated at 98.5% (330 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-08-18 16:18:21 +00:00
Hudobni Volk
760cbc7f9a Translated using Weblate (Slovenian)
Currently translated at 61.1% (11 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/sl/
2023-08-18 16:18:21 +00:00
ganjibaiemade
d0e34fcd90 Translated using Weblate (Chinese (Traditional))
Currently translated at 18.2% (61 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-08-18 16:18:21 +00:00
Hudobni Volk
da434b9a9b Translated using Weblate (Slovenian)
Currently translated at 100.0% (335 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/sl/
2023-08-18 16:18:21 +00:00
Linerly
48863dd510 Translated using Weblate (Indonesian)
Currently translated at 100.0% (335 of 335 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-08-18 16:18:21 +00:00
FineFindus
2ca34278f9 build: add F-Droid flavor (#760)
* build: add fdroid flavor

* feat: disable fcm registration in f-droid flavor
2023-08-18 18:15:57 +02:00
Jacoco
a79779f813 Emoji Reactions Support (#645)
* Display Pleroma emoji reactions

* Interact with existing Pleroma emoji reactions

* Setting for emoji reaction support

* Setting for displaying reactions in timelines

* More horizontal padding on reactions display item

* List accounts who reacted

* Arbitrary emoji reaction from status footer

* Hide custom emoji keyboard when emoji is selected

* Clear preferences before applying
All preferences get written anyways so nothing will be lost

* Reset react visibility state on bind

* Fix custom emoji turning black when reacting

* Load reactions when a new one is added

* Emoji reactions grid

* Load custom emoji in reactions list fragment

* New reaction toast messages and Unicode emoji regex

* Make custom emoji picker for reactions scrollable

* Scroll down to show custom emoji picker when reacting

* Divider after reaction custom emoji picker

* Animate react button opacity back in

* fix plural strings

* re-implement reactions using horizontal recycler view

* update reactions with event

* tweak emoji font size

* tweak button styles (a tiny bit)

* change footer react button behavior

* fix emoji reaction status item padding

* move emoji reactions below content items

* add content description and tooltip

* use custom emoji keyboard to enter unicode emoji

* fix reactions clearing on status counter updates

* fix space next to emoji reactions not clickable

* make compatible with glitch-soc

* Remove now unused EmojiReactionsView class

* improve handling of reaction padding

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-08-18 18:14:33 +02:00
Eugen Rochko
cc83f2baf3 New translations strings.xml (Danish) 2023-08-18 01:51:02 +01:00
Eugen Rochko
728496b831 New translations strings.xml (Danish) 2023-08-18 00:06:12 +01:00
Eugen Rochko
bbc99162c6 New translations strings.xml (Danish) 2023-08-17 16:47:55 +01:00
Eugen Rochko
eed3af9e3e New translations strings.xml (Arabic) 2023-08-16 22:54:50 +02:00
Eugen Rochko
50187ff376 New translations strings.xml (Arabic) 2023-08-16 21:48:52 +02:00
Eugen Rochko
5f30919fb4 New translations strings.xml (Arabic) 2023-08-16 18:38:58 +02:00
Eugen Rochko
14c3cfac85 New translations strings.xml (French) 2023-08-16 15:50:45 +02:00
Eugen Rochko
e978f02765 New translations strings.xml (French) 2023-08-16 14:09:02 +02:00
Eugen Rochko
8d877c480f New translations strings.xml (French) 2023-08-16 12:20:32 +02:00
Eugen Rochko
c53efee9a1 New translations strings.xml (Thai) 2023-08-15 22:47:32 +02:00
Eugen Rochko
148c461e86 New translations strings.xml (Danish) 2023-08-15 16:40:55 +02:00
Eugen Rochko
fcadb9883d New translations strings.xml (Danish) 2023-08-15 14:59:58 +02:00
Eugen Rochko
bb6491e10a New translations full_description.txt (Danish) 2023-08-15 14:59:57 +02:00
Eugen Rochko
6248ccf376 New translations full_description.txt (Danish) 2023-08-15 13:53:21 +02:00
Eugen Rochko
c9e08f36fa New translations full_description.txt (Danish) 2023-08-15 03:19:07 +02:00
Eugen Rochko
10b95d753b New translations full_description.txt (Danish) 2023-08-15 02:15:33 +02:00
Eugen Rochko
c3989083cf New translations strings.xml (Portuguese) 2023-08-14 15:33:01 +02:00
Eugen Rochko
01db585094 New translations strings.xml (Basque) 2023-08-14 01:42:56 +02:00
Eugen Rochko
cc67cb330c New translations full_description.txt (Basque) 2023-08-14 00:10:18 +02:00
Eugen Rochko
52ed3c5a04 New translations strings.xml (Basque) 2023-08-14 00:10:17 +02:00
Eugen Rochko
5976f6230a New translations strings.xml (Chinese Simplified) 2023-08-13 18:58:06 +02:00
Eugen Rochko
3553f03a95 New translations strings.xml (Slovenian) 2023-08-13 11:06:06 +02:00
Eugen Rochko
d6e2d889c3 New translations strings.xml (Slovenian) 2023-08-12 16:02:40 +02:00
Eugen Rochko
a777b3b450 New translations strings.xml (Slovenian) 2023-08-12 14:07:49 +02:00
Eugen Rochko
9957efbea0 New translations strings.xml (Norwegian) 2023-08-12 01:06:29 +02:00
Eugen Rochko
22e7b9730f New translations strings.xml (Vietnamese) 2023-08-11 17:50:37 +02:00
Eugen Rochko
91470b8509 New translations strings.xml (Vietnamese) 2023-08-11 16:21:15 +02:00
Eugen Rochko
c9d5327328 New translations strings.xml (Russian) 2023-08-11 12:41:37 +02:00
Eugen Rochko
1aa61b72e5 New translations strings.xml (Russian) 2023-08-11 10:45:11 +02:00
Eugen Rochko
3ca5edc3fc New translations strings.xml (Russian) 2023-08-11 09:19:15 +02:00
Eugen Rochko
a092ebaeb3 New translations strings.xml (Russian) 2023-08-11 08:03:02 +02:00
Eugen Rochko
5b9e84c255 New translations strings.xml (Russian) 2023-08-11 07:07:00 +02:00
Eugen Rochko
9c058b926f New translations strings.xml (Russian) 2023-08-10 12:30:16 +02:00
Eugen Rochko
4f2d2ae6e8 New translations strings.xml (Russian) 2023-08-10 11:10:22 +02:00
Eugen Rochko
75aa26a018 New translations strings.xml (Russian) 2023-08-09 14:50:26 +02:00
Eugen Rochko
0f795254e5 New translations strings.xml (Russian) 2023-08-09 13:28:03 +02:00
Eugen Rochko
33592f0a83 New translations strings.xml (Swedish) 2023-08-07 12:43:38 +02:00
Eugen Rochko
d6fd01eaca New translations strings.xml (Swedish) 2023-08-07 10:56:51 +02:00
Mark Hansen
1cdc58378a Sort Hashtags you follow menu (#695)
* Sort Hashtags you follow menu

* change sorting method

---------

Co-authored-by: Mark Hansen <mhansen@accusoft.com>
Co-authored-by: sk <sk22@mailbox.org>
2023-08-05 20:43:23 +02:00
FineFindus
584b11fce3 feat: allow playing multiple media (#755)
* feat: allow playing multiple media

* replace audio overlay icon

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-08-05 20:35:57 +02:00
FineFindus
fe2039062b fix(remote): send remoteAccount (#754)
closes sk22#664

* fix(remote): send remoteAccount

* fix(server-about): set adming account as remote

* fix(remote): show remote local accounts as remote
2023-08-05 20:30:41 +02:00
FineFindus
0269756b52 feat: change mute duration (#751)
* feat: add mute timer

* fix: missing ressources

* refactor(add-mute-timer): change of the mute timer popup layout

* tweak mute dialog

---------

Co-authored-by: LucasGGamerM <lucassggabriel@gmail.com>
Co-authored-by: sk <sk22@mailbox.org>
2023-08-05 20:17:40 +02:00
FineFindus
df1a6cf764 feat(openURL): open about in app (#750) 2023-08-05 19:44:18 +02:00
FineFindus
6d2385b6b3 feat: support UnifiedPush notifications (#749)
* build: add unified push dependency

* feat(notification): allow arbitrary push notification endpoint

* feat(notification/unified-push): show notification

* refactor(unifiedPush): use more consise null check

* feat(settings/notification): add UnifiedPush toggle

* feat(settings/notification): show no distributor message

* feat(settings/notification): disable unifiedpush when no distributor is available

* change icon name

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-08-05 19:42:10 +02:00
FineFindus
44eaa36cef fix: copy link invisible on EMUI (#753) 2023-08-05 19:33:36 +02:00
sk
50b40c4a07 Merge remote-tracking branch 'upstream/master' 2023-08-05 19:32:28 +02:00
Eugen Rochko
ee6e0ff26c New translations strings.xml (Russian) 2023-08-04 19:26:18 +02:00
Eugen Rochko
4d9574bf38 New translations strings.xml (Russian) 2023-08-04 17:51:30 +02:00
Eugen Rochko
813be9a2be New translations strings.xml (Russian) 2023-08-04 16:30:43 +02:00
Eugen Rochko
cc76ebfafb New translations strings.xml (Russian) 2023-08-04 14:58:02 +02:00
Eugen Rochko
7989ee0243 New translations strings.xml (Russian) 2023-08-04 13:58:08 +02:00
Eugen Rochko
3aa1997cfd New translations strings.xml (Russian) 2023-08-04 12:51:54 +02:00
Grishka
c3da15552e Merge branch 'l10n_master' 2023-08-03 17:22:04 +03:00
Grishka
a014fe9443 Fix media layout with unknown sizes 2023-08-03 17:21:40 +03:00
Eugen Rochko
92551d4ca3 New translations strings.xml (Portuguese, Brazilian) 2023-08-03 03:27:17 +02:00
Eugen Rochko
8010858e85 New translations strings.xml (Portuguese, Brazilian) 2023-08-03 02:26:22 +02:00
Eugen Rochko
4efb4875b0 New translations strings.xml (Swedish) 2023-08-02 09:16:24 +02:00
Eugen Rochko
c5d041e46d New translations strings.xml (Portuguese, Brazilian) 2023-08-01 23:11:09 +02:00
Eugen Rochko
53c2223aae New translations strings.xml (Portuguese, Brazilian) 2023-08-01 21:25:07 +02:00
Eugen Rochko
25034ac0ae New translations strings.xml (Portuguese, Brazilian) 2023-08-01 01:41:52 +02:00
Eugen Rochko
ac9de72b75 New translations strings.xml (Portuguese, Brazilian) 2023-08-01 00:42:41 +02:00
Eugen Rochko
1f48ad93f2 New translations strings.xml (Portuguese, Brazilian) 2023-07-31 23:46:36 +02:00
Eugen Rochko
38f7f7aa00 New translations strings.xml (Czech) 2023-07-31 14:32:20 +02:00
Eugen Rochko
fe8175c63a New translations strings.xml (French) 2023-07-30 19:39:54 +02:00
Eugen Rochko
2d9e01bbc1 New translations strings.xml (French) 2023-07-30 18:44:39 +02:00
Eugen Rochko
022a227b08 New translations strings.xml (Turkish) 2023-07-28 00:29:16 +02:00
Eugen Rochko
a2228259f1 New translations full_description.txt (Turkish) 2023-07-27 23:33:53 +02:00
Eugen Rochko
a61af7c56f New translations strings.xml (Turkish) 2023-07-27 23:33:52 +02:00
Eugen Rochko
5d6a646976 New translations strings.xml (French) 2023-07-27 23:33:51 +02:00
Eugen Rochko
628d0d7492 New translations strings.xml (French) 2023-07-27 22:05:11 +02:00
Eugen Rochko
b76c8745ec New translations strings.xml (Norwegian) 2023-07-25 22:57:26 +02:00
Eugen Rochko
51e67bc441 New translations strings.xml (Norwegian) 2023-07-25 21:52:29 +02:00
Gregory K
8887f75b70 Merge pull request #655 from nilathedragon/patch-1
Do not assume languages array will contain entries
2023-07-25 19:12:56 +03:00
Nila
9436a838c0 Do not assume languages array will contain entries 2023-07-25 17:28:41 +02:00
Eugen Rochko
ef120fa36f New translations strings.xml (Swedish) 2023-07-25 00:55:54 +02:00
Eugen Rochko
8c6385e2c5 New translations strings.xml (Swedish) 2023-07-24 23:57:42 +02:00
Eugen Rochko
0bd85d9905 New translations strings.xml (Persian) 2023-07-24 13:40:53 +02:00
Eugen Rochko
ce0dab7b28 New translations strings.xml (Czech) 2023-07-24 09:55:45 +02:00
Eugen Rochko
ddcc5670ce New translations strings.xml (Bengali) 2023-07-22 17:48:06 +02:00
Eugen Rochko
86afa184e2 New translations strings.xml (Bengali) 2023-07-22 16:26:35 +02:00
Eugen Rochko
77f341f139 New translations strings.xml (German) 2023-07-22 12:27:40 +02:00
Eugen Rochko
918b5d99c2 New translations strings.xml (Swedish) 2023-07-21 22:09:32 +02:00
Eugen Rochko
7a098d6eff New translations strings.xml (Swedish) 2023-07-21 21:06:04 +02:00
Eugen Rochko
71f81283f5 New translations strings.xml (Polish) 2023-07-20 13:51:45 +02:00
Eugen Rochko
058c7c3c33 New translations strings.xml (Danish) 2023-07-20 10:31:41 +02:00
Eugen Rochko
870e33879b New translations strings.xml (Indonesian) 2023-07-19 07:48:32 +02:00
Eugen Rochko
3ca82bdfc5 New translations strings.xml (Japanese) 2023-07-19 05:08:28 +02:00
Eugen Rochko
4721bad286 New translations short_description.txt (Armenian) 2023-07-18 16:30:06 +02:00
Eugen Rochko
f040cf2f07 New translations full_description.txt (Armenian) 2023-07-18 16:30:05 +02:00
Eugen Rochko
8d50717c90 New translations strings.xml (Armenian) 2023-07-18 16:30:04 +02:00
Eugen Rochko
2512ad3c95 New translations strings.xml (Armenian) 2023-07-18 14:49:00 +02:00
Eugen Rochko
bc7e007634 New translations strings.xml (Swedish) 2023-07-18 12:03:42 +02:00
Eugen Rochko
1f3c87e0c7 New translations strings.xml (Swedish) 2023-07-18 10:31:42 +02:00
Eugen Rochko
73e08faee9 New translations strings.xml (Persian) 2023-07-13 20:45:58 +02:00
Eugen Rochko
02dc7711e4 New translations strings.xml (Persian) 2023-07-13 19:38:29 +02:00
Eugen Rochko
67b4d80e5b New translations strings.xml (Spanish) 2023-07-13 16:08:11 +02:00
Eugen Rochko
5168d2bb39 New translations strings.xml (Spanish) 2023-07-13 14:59:49 +02:00
Eugen Rochko
57190a75bf New translations strings.xml (Indonesian) 2023-07-13 11:54:32 +02:00
Eugen Rochko
f10e865895 New translations strings.xml (Indonesian) 2023-07-13 10:48:50 +02:00
178 changed files with 6541 additions and 937 deletions

1
.github/FUNDING.yml vendored
View File

@@ -3,7 +3,6 @@
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # mastodon
open_collective: # Replace with a single Open Collective username e.g., user1
ko_fi: xsk22
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username e.g., user1

View File

@@ -3,6 +3,12 @@ buildscript {
repositories {
google()
mavenCentral()
maven {
url "https://www.jitpack.io"
content {
includeModule 'com.github.UnifiedPush', 'android-connector'
}
}
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 97
versionName "2.0.1+fork.97"
versionCode 98
versionName "2.0.3+fork.98"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
}
@@ -34,6 +34,7 @@ android {
}
githubRelease { initWith release }
playRelease { initWith release }
fdroidRelease { initWith release }
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -78,6 +79,7 @@ dependencies {
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'com.github.UnifiedPush:android-connector:2.1.1'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -87,6 +88,15 @@
<category android:name="me.grishka.fcmtest"/>
</intent-filter>
</receiver>
<receiver android:exported="true" android:enabled="true" android:name=".UnifiedPushNotificationReceiver"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
</intent-filter>
</receiver>
</application>

View File

@@ -169,7 +169,8 @@ public class AudioPlayerService extends Service{
}
updateNotification(false, false);
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
int audiofocus = GlobalUserPreferences.overlayMedia ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN;
getSystemService(AudioManager.class).requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, audiofocus);
player=new MediaPlayer();
player.setOnPreparedListener(this::onPlayerPrepared);

View File

@@ -59,6 +59,7 @@ public class GlobalUserPreferences{
public static boolean disableM3PillActiveIndicator;
public static boolean showNavigationLabels;
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
public static boolean overlayMedia;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
@@ -117,6 +118,7 @@ public class GlobalUserPreferences{
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
overlayMedia=prefs.getBoolean("overlayMedia", false);
if (prefs.contains("prefixRepliesWithRe")) {
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
@@ -174,6 +176,7 @@ public class GlobalUserPreferences{
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
.putBoolean("overlayMedia", overlayMedia)
.apply();
}

View File

@@ -14,6 +14,7 @@ import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log;
@@ -32,6 +33,7 @@ import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -58,7 +60,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
private static final int SUMMARY_ID = 791;
private static int notificationId = 0;
private static Map<String, Integer> notificationIdsForAccounts = new HashMap<>();
private static final Map<String, Integer> notificationIdsForAccounts = new HashMap<>();
@Override
public void onReceive(Context context, Intent intent){
@@ -148,6 +150,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
}
public void notifyUnifiedPush(Context context, String accountID, org.joinmastodon.android.model.Notification notification) {
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
}
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
NotificationManager nm=context.getSystemService(NotificationManager.class);
AccountSession session=AccountSessionManager.get(accountID);
@@ -318,7 +325,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
req.visibility = preferences.postingDefaultVisibility;
req.inReplyToId = notification.status.id;
if (!notification.status.spoilerText.isEmpty() &&
if (notification.status.hasSpoiler() &&
(GlobalUserPreferences.prefixReplies == ALWAYS
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
&& !notification.status.spoilerText.startsWith("re: ")) {

View File

@@ -0,0 +1,84 @@
package org.joinmastodon.android;
import android.content.Context;
import android.util.Log;
import org.jetbrains.annotations.NotNull;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.unifiedpush.android.connector.MessagingReceiver;
import java.util.List;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class UnifiedPushNotificationReceiver extends MessagingReceiver{
private static final String TAG="UnifiedPushNotificationReceiver";
public UnifiedPushNotificationReceiver() {
super();
}
@Override
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) {
// Called when a new endpoint be used for sending push messages
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance);
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null, endpoint);
}
@Override
public void onRegistrationFailed(@NotNull Context context, @NotNull String instance) {
// called when the registration is not possible, eg. no network
Log.d(TAG, "onRegistrationFailed: " + instance);
//re-register for gcm
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null);
}
@Override
public void onUnregistered(@NotNull Context context, @NotNull String instance) {
// called when this application is unregistered from receiving push messages
Log.d(TAG, "onUnregistered: " + instance);
//re-register for gcm
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
if (account != null)
account.getPushSubscriptionManager().registerAccountForPush(null);
}
@Override
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) {
// Called when a new message is received. The message contains the full POST body of the push message
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
if (account == null)
return;
//this is stupid
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
// thus it is not possible to decrypt them. SO we need to re-request them from the server and transform them later on
// The official uses fcm and moves the headers to extra data, see
// https://github.com/mastodon/webpush-fcm-relay/blob/cac95b28d5364b0204f629283141ac3fb749e0c5/webpush-fcm-relay.go#L116
// https://github.com/tuskyapp/Tusky/pull/2303#issue-1112080540
account.getCacheController().getNotifications(null, 1, false, false, true, new Callback<>(){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
result.items
.stream()
.findFirst()
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
}
@Override
public void onError(ErrorResponse error){
//professional error handling
}
});
}
}

View File

@@ -121,13 +121,13 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
.orElseGet(() -> this.execNoAuth(domain));
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null);
public MastodonAPIRequest<T> wrapProgress(Context context, @StringRes int message, boolean cancelable){
return wrapProgress(context, message, cancelable, null);
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
progressDialog=new ProgressDialog(activity);
progressDialog.setMessage(activity.getString(message));
public MastodonAPIRequest<T> wrapProgress(Context context, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
progressDialog=new ProgressDialog(context);
progressDialog.setMessage(context.getString(message));
progressDialog.setCancelable(cancelable);
if (transform != null) transform.accept(progressDialog);
if(cancelable){

View File

@@ -120,9 +120,22 @@ public class PushSubscriptionManager{
return !TextUtils.isEmpty(deviceToken);
}
public void registerAccountForPush(PushSubscription subscription){
if(TextUtils.isEmpty(deviceToken))
throw new IllegalStateException("No device push token available");
// this function is used for registering push notifications using FCM
// to avoid NonFreeNet in F-Droid, this registration is disabled in it
// see https://github.com/LucasGGamerM/moshidon/issues/206 for more context
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease") || TextUtils.isEmpty(deviceToken)){
Log.d(TAG, "Skipping registering for FCM push notifications");
return;
}
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
registerAccountForPush(subscription, endpoint);
}
public void registerAccountForPush(PushSubscription subscription, String endpoint){
MastodonAPIController.runInBackground(()->{
Log.d(TAG, "registerAccountForPush: started for "+accountID);
String encodedPublicKey, encodedAuthKey, pushAccountID;
@@ -151,12 +164,11 @@ public class PushSubscriptionManager{
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
return;
}
new RegisterForPushNotifications(deviceToken,
new RegisterForPushNotifications(endpoint,
encodedPublicKey,
encodedAuthKey,
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
subscription==null ? PushSubscription.Policy.ALL : subscription.policy,
pushAccountID)
subscription==null ? PushSubscription.Policy.ALL : subscription.policy)
.setCallback(new Callback<>(){
@Override
public void onSuccess(PushSubscription result){

View File

@@ -4,8 +4,15 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
public SetAccountMuted(String id, boolean muted){
public SetAccountMuted(String id, boolean muted, long duration){
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
setRequestBody(new Object());
setRequestBody(new Request(duration));
}
private static class Request{
public long duration;
public Request(long duration){
this.duration=duration;
}
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.announcements;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class AddAnnouncementReaction extends MastodonAPIRequest<Object> {
public AddAnnouncementReaction(String id, String emoji) {
super(HttpMethod.PUT, "/announcements/" + id + "/reactions/" + emoji, Object.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.api.requests.announcements;
import org.joinmastodon.android.api.MastodonAPIRequest;
public class DeleteAnnouncementReaction extends MastodonAPIRequest<Object> {
public DeleteAnnouncementReaction(String id, String emoji) {
super(HttpMethod.DELETE, "/announcements/" + id + "/reactions/" + emoji, Object.class);
}
}

View File

@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription;
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy, String accountID){
public RegisterForPushNotifications(String endpoint, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
Request r=new Request();
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
r.subscription.endpoint=endpoint;
r.data.alerts=alerts;
r.policy=policy;
r.subscription.keys.p256dh=encryptionKey;

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 AddStatusReaction extends MastodonAPIRequest<Status> {
public AddStatusReaction(String id, String emoji) {
super(HttpMethod.POST, "/statuses/" + id + "/react/" + emoji, Status.class);
setRequestBody(new Object());
}
}

View File

@@ -11,13 +11,11 @@ import java.util.ArrayList;
import java.util.List;
public class CreateStatus extends MastodonAPIRequest<Status>{
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
public static long EPOCH_OF_THE_YEAR_FIVE_THOUSAND=95617584000000L;
public static final Instant DRAFTS_AFTER_INSTANT=Instant.ofEpochMilli(EPOCH_OF_THE_YEAR_FIVE_THOUSAND - 1) /* end of 4999 */;
public static Instant getDraftInstant() {
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
// yes, this is a weird implementation for something that hardly matters
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
return DRAFTS_AFTER_INSTANT.plusMillis(System.currentTimeMillis());
}
public CreateStatus(CreateStatus.Request req, String uuid){
@@ -36,6 +34,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public static class Request{
public String status;
public List<MediaAttribute> mediaAttributes;
public List<String> mediaIds;
public Poll poll;
public String inReplyToId;
@@ -55,5 +54,17 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public boolean multiple;
public boolean hideTotals;
}
public static class MediaAttribute{
public String id;
public String description;
public String focus;
public MediaAttribute(String id, String description, String focus){
this.id=id;
this.description=description;
this.focus=focus;
}
}
}
}

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 DeleteStatusReaction extends MastodonAPIRequest<Status> {
public DeleteStatusReaction(String id, String emoji) {
super(HttpMethod.POST, "/statuses/" + id + "/unreact/" + emoji, 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 PleromaAddStatusReaction extends MastodonAPIRequest<Status> {
public PleromaAddStatusReaction(String id, String emoji) {
super(HttpMethod.PUT, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class PleromaDeleteStatusReaction extends MastodonAPIRequest<Status> {
public PleromaDeleteStatusReaction(String id, String emoji) {
super(HttpMethod.DELETE, "/pleroma/statuses/" + id + "/reactions/" + emoji, Status.class);
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.EmojiReaction;
import java.util.List;
public class PleromaGetStatusReactions extends MastodonAPIRequest<List<EmojiReaction>> {
public PleromaGetStatusReactions(String id, String emoji) {
super(HttpMethod.GET, "/pleroma/statuses/" + id + "/reactions/" + (emoji != null ? emoji : ""), new TypeToken<>(){});
}
}

View File

@@ -13,7 +13,6 @@ import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class AccountLocalPreferences{
private final SharedPreferences prefs;
@@ -38,6 +37,9 @@ public class AccountLocalPreferences{
public String timelineReplyVisibility; // akkoma-only
public boolean keepOnlyLatestNotification;
public boolean emojiReactionsEnabled;
public ShowEmojiReactions showEmojiReactions;
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
@@ -62,6 +64,8 @@ public class AccountLocalPreferences{
publishButtonText=prefs.getString("publishButtonText", null);
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name()));
}
public long getNotificationsPauseEndTime(){
@@ -93,6 +97,14 @@ public class AccountLocalPreferences{
.putString("publishButtonText", publishButtonText)
.putString("timelineReplyVisibility", timelineReplyVisibility)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
.putString("showEmojiReactions", showEmojiReactions.name())
.apply();
}
public enum ShowEmojiReactions{
HIDE_EMPTY,
ONLY_OPENED,
ALWAYS
}
}

View File

@@ -146,6 +146,9 @@ public class AccountSession{
@Override
public void onError(ErrorResponse error){
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
if (preferences==null)
preferences=new Preferences();
preferencesFromAccountSource(self);
}
})
.exec(getID());

View File

@@ -0,0 +1,19 @@
package org.joinmastodon.android.events;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.model.EmojiReaction;
import java.util.List;
public class EmojiReactionsUpdatedEvent{
public final String id;
public final List<EmojiReaction> reactions;
public final boolean updateTextPadding;
public RecyclerView.ViewHolder viewHolder;
public EmojiReactionsUpdatedEvent(String id, List<EmojiReaction> reactions, boolean updateTextPadding, RecyclerView.ViewHolder viewHolder){
this.id=id;
this.reactions=reactions;
this.updateTextPadding=updateTextPadding;
this.viewHolder=viewHolder;
}
}

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.model.Status;
public class StatusCountersUpdatedEvent{

View File

@@ -26,6 +26,8 @@ import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
@@ -69,10 +71,12 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
Status fakeStatus = a.toStatus();
TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus, true);
textItem.textSelectable = true;
return List.of(
HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead),
textItem
);
List<StatusDisplayItem> items=new ArrayList<>();
items.add(HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead));
items.add(textItem);
if(!isInstanceAkkoma()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true));
return items;
}
public void onMarkAsRead(String id) {

View File

@@ -31,6 +31,7 @@ import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
@@ -607,6 +608,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if (header != null) header.rebind();
}
public void updateEmojiReactions(Status status, String itemID){
EmojiReactionsStatusDisplayItem.Holder reactions=findHolderOfType(itemID, EmojiReactionsStatusDisplayItem.Holder.class);
if(reactions != null){
reactions.getItem().status.reactions.clear();
reactions.getItem().status.reactions.addAll(status.reactions);
reactions.rebind();
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
@@ -782,6 +792,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
public void scrollBy(int x, int y) {
list.scrollBy(x, y);
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){

View File

@@ -300,6 +300,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
onCustomEmojiClick(emoji);
}
@Override
public void onEmojiSelected(String emoji){
if(getActivity().getCurrentFocus() instanceof EditText edit && edit == mainEditText){
edit.getText().replace(edit.getSelectionStart(), edit.getSelectionEnd(), emoji);
}
}
@Override
public void onBackspace(){
getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
@@ -412,7 +419,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
hasSpoiler=true;
spoilerWrap.setVisibility(View.VISIBLE);
spoilerBtn.setSelected(true);
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
}else if(editingStatus!=null && editingStatus.hasSpoiler()){
hasSpoiler=true;
spoilerWrap.setVisibility(View.VISIBLE);
spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText));
@@ -666,11 +673,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
? UiUtils.formatRelativeTimestamp(getContext(), status.createdAt)
: getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), status.editedAt));
String sepp = getString(R.string.sk_separator);
String username = status.account.getDisplayUsername();
((TextView) view.findViewById(R.id.time_and_username)).setText(time == null ? username :
username + " " + sepp + " " + time);
if (status.spoilerText != null && !status.spoilerText.isBlank()) {
((TextView) view.findViewById(R.id.username)).setText(status.account.getDisplayUsername());
view.findViewById(R.id.separator).setVisibility(time==null ? View.GONE : View.VISIBLE);
view.findViewById(R.id.time).setVisibility(time==null ? View.GONE : View.VISIBLE);
if(time!=null) ((TextView) view.findViewById(R.id.time)).setText(time);
if (status.hasSpoiler()) {
TextView replyToSpoiler = view.findViewById(R.id.reply_to_spoiler);
replyToSpoiler.setVisibility(View.VISIBLE);
replyToSpoiler.setText(status.spoilerText);
@@ -1058,6 +1066,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
req.scheduledAt=scheduledAt;
if(!mediaViewController.isEmpty()){
req.mediaIds=mediaViewController.getAttachmentIDs();
if(editingStatus != null){
req.mediaAttributes=mediaViewController.getAttachmentAttributes();
}
}
// ask whether to publish now when editing an existing draft
if (!force && editingStatus != null && scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {

View File

@@ -45,8 +45,8 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
}
@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, id, data.stream().filter(h -> Objects.equals(h.name, id)).findAny().map(h -> h.following).orElse(null));
public void onItemClick(String hashtag){
UiUtils.openHashtagTimeline(getActivity(), accountID, hashtag, data.stream().filter(h -> Objects.equals(h.name, hashtag)).findAny().map(h -> h.following).orElse(null));
}
@Override

View File

@@ -201,7 +201,7 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
}
// literally the same as AccountCardStatusDisplayItem and DiscoverAccountsFragment. code should be generalized
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.DisableableClickable{
private final ImageView cover, avatar;
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
private final ProgressBarButton actionButton, acceptButton, rejectButton;
@@ -233,15 +233,24 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
rejectProgress=findViewById(R.id.reject_progress);
rejectWrap=findViewById(R.id.reject_btn_wrap);
itemView.setOutlineProvider(OutlineProviders.roundedRect(6));
itemView.setClipToOutline(true);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setOutlineProvider(OutlineProviders.roundedRect(15));
avatar.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
View border=findViewById(R.id.avatar_border);
border.setOutlineProvider(OutlineProviders.roundedRect(17));
border.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(9));
cover.setClipToOutline(true);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
actionButton.setOnClickListener(this::onActionButtonClick);
acceptButton.setOnClickListener(this::onFollowRequestButtonClick);
rejectButton.setOnClickListener(this::onFollowRequestButtonClick);
itemView.setOnClickListener(v->this.onClick());
}
@Override
public boolean isEnabled(){
return false;
}
@Override
@@ -254,26 +263,23 @@ public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowR
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
postsLabel.setText(getResources().getQuantityString(R.plurals.sk_posts_count_label, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id);
if(relationship == null || !relationship.followedBy){
UiUtils.setExtraTextInfo(getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account);
if(relationship==null || !relationship.followedBy){
actionWrap.setVisibility(View.GONE);
acceptWrap.setVisibility(View.VISIBLE);
rejectWrap.setVisibility(View.VISIBLE);
// i hate that i wasn't able to do this in xml
acceptButton.setCompoundDrawableTintList(acceptButton.getTextColors());
acceptProgress.setIndeterminateTintList(acceptButton.getTextColors());
rejectButton.setCompoundDrawableTintList(rejectButton.getTextColors());
rejectProgress.setIndeterminateTintList(rejectButton.getTextColors());
}else if(relationship==null){
actionWrap.setVisibility(View.GONE);
acceptWrap.setVisibility(View.GONE);
rejectWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
acceptWrap.setVisibility(View.GONE);

View File

@@ -62,6 +62,7 @@ import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -336,11 +337,13 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
hashtagsMenu.clear();
hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0);
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(hashtagsMenu));
hashtagsItems.forEach((id, hashtag) -> {
MenuItem item = hashtagsMenu.add(Menu.NONE, id, Menu.NONE, hashtag.name);
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item);
});
hashtagsItems.entrySet().stream()
.sorted(Comparator.comparing(x -> x.getValue().name, String.CASE_INSENSITIVE_ORDER))
.forEach(entry -> {
MenuItem item = hashtagsMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().name);
item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
UiUtils.insetPopupMenuIcon(ctx, item);
});
}
public void updateToolbarLogo(){

View File

@@ -12,9 +12,10 @@ import android.view.View;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
@@ -22,14 +23,15 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
@@ -43,7 +45,6 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
private boolean onlyMentions;
@@ -99,7 +100,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return items;
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); // | StatusDisplayItem.FLAG_NO_HEADER);
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS); // | StatusDisplayItem.FLAG_NO_HEADER);
if (GlobalUserPreferences.spectatorMode)
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
if(titleItem!=null)
items.add(0, titleItem);
@@ -244,8 +247,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Subscribe
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
for(Notification n:data){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
for(int i=0;i<list.getChildCount();i++){
@@ -259,8 +261,31 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
for(Notification n:preloadedData){
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
}
}
}
@Subscribe
public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){
for(Notification n : data){
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
for(int i=0; i<list.getChildCount(); i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && reactions.getItem().status==n.status.getContentStatus() && ev.viewHolder!=holder){
reactions.rebind();
}else if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(n.getID())){
text.rebind();
}
}
}
}
for(Notification n : preloadedData){
if(n.status!=null && n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
}

View File

@@ -4,10 +4,12 @@ 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.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -131,6 +133,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private ImageView lockIcon, botIcon;
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
@@ -230,6 +233,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name);
username=content.findViewById(R.id.username);
lockIcon=content.findViewById(R.id.lock_icon);
botIcon=content.findViewById(R.id.bot_icon);
bio=content.findViewById(R.id.bio);
followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label);
@@ -258,6 +263,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
list=content.findViewById(R.id.metadata);
rolesView=content.findViewById(R.id.roles);
avatarBorder.setOutlineProvider(OutlineProviders.roundedRect(26));
avatarBorder.setClipToOutline(true);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@@ -345,7 +352,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnClickListener(v->{
content.findViewById(R.id.username_wrap).setOnClickListener(v->{
try {
new GetInstance()
.setCallback(new Callback<>(){
@@ -366,7 +373,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
.execRemote(Uri.parse(account.url).getHost());
} catch (NullPointerException ignored) {
// maybe the url was malformed?
Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT);
Toast.makeText(getContext(), R.string.error, Toast.LENGTH_SHORT).show();
}
});
@@ -590,6 +597,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
@SuppressLint("SetTextI18n")
private void bindHeaderView(){
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
@@ -622,19 +630,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
String acct = ((isSelf || account.isRemote)
? account.getFullyQualifiedName()
: account.acct);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(acct);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
lock.setTint(username.getCurrentTextColor());
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BASELINE), 0);
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+acct);
}
username.setText('@'+acct);
lockIcon.setVisibility(account.locked ? View.VISIBLE : View.GONE);
lockIcon.setImageTintList(ColorStateList.valueOf(username.getCurrentTextColor()));
botIcon.setVisibility(account.bot ? View.VISIBLE : View.GONE);
botIcon.setImageTintList(ColorStateList.valueOf(username.getCurrentTextColor()));
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
@@ -1057,7 +1061,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
actionButton.setText(R.string.save_changes);
pager.setVisibility(View.GONE);
tabbar.setVisibility(View.GONE);
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay, getActivity().getTheme()).mutate();
avatar.setForeground(overlay);
updateMetadataHeight();
@@ -1076,6 +1080,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
);
name.setVisibility(View.GONE);
rolesView.setVisibility(View.GONE);
username.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
@@ -1124,6 +1129,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEditWrap.setVisibility(View.GONE);
bioEditWrap.setVisibility(View.GONE);
name.setVisibility(View.VISIBLE);
rolesView.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
@@ -1424,16 +1430,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder {
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
private final TextView title;
private final LinkedTextView value;
// private final ImageView verifiedIcon;
public AboutViewHolder(){
super(R.layout.item_profile_about);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
// verifiedIcon=findViewById(R.id.verified_icon);
}
@Override
@@ -1441,7 +1445,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
super.onBind(item);
title.setText(item.parsedName);
value.setText(item.parsedValue);
// verifiedIcon.setVisibility(item.verifiedAt!=null ? View.VISIBLE : View.GONE);
if(item.verifiedAt!=null){
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate();
check.setTint(textColor);
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
}else{
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
value.setCompoundDrawables(null, null, null, null);
}
}
@Override

View File

@@ -80,7 +80,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, true, null);
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, null,
StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS |
StatusDisplayItem.FLAG_NO_FOOTER |
StatusDisplayItem.FLAG_NO_TRANSLATE);
}
@Override

View File

@@ -57,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -143,9 +143,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
}
}
String sep = getString(R.string.sk_separator);
ReblogOrReplyLineStatusDisplayItem line=new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null);
line.needBottomPadding=true;
items.add(0, line);
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null));
}
return items;
}

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ShowEmojiReactions.ONLY_OPENED;
import android.content.res.Configuration;
import android.os.Bundle;
@@ -7,7 +9,9 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
@@ -16,9 +20,11 @@ import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.parceler.Parcels;
import java.util.ArrayList;
@@ -33,9 +39,14 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), addFooter ? 0 : StatusDisplayItem.FLAG_NO_FOOTER);
boolean isMainThreadStatus = this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id);
int flags = 0;
AccountLocalPreferences lp=getLocalPrefs();
if (GlobalUserPreferences.spectatorMode)
flags |= StatusDisplayItem.FLAG_NO_FOOTER;
if (!lp.emojiReactionsEnabled || lp.showEmojiReactions==ONLY_OPENED)
flags |= StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS;
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), isMainThreadStatus ? 0 : flags);
}
protected abstract FilterContext getFilterContext();
@@ -230,6 +241,30 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
}
}
@Subscribe
public void onEmojiReactionsChanged(EmojiReactionsUpdatedEvent ev){
for(Status s:data){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof EmojiReactionsStatusDisplayItem.Holder reactions && reactions.getItem().status==s.getContentStatus() && ev.viewHolder!=holder){
reactions.rebind();
}else if(holder instanceof TextStatusDisplayItem.Holder text && text.getItem().parentID.equals(s.getID())){
text.rebind();
}
}
}
}
for(Status s:preloadedData){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
}
}
}
@Subscribe
public void onStatusDeleted(StatusDeletedEvent ev){
if(!ev.accountID.equals(accountID))

View File

@@ -417,6 +417,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
adapter.notifyDataSetChanged();
}
public Status getMainStatus(){
return mainStatus;
}
@Override
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;

View File

@@ -14,6 +14,7 @@ import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
@@ -136,6 +137,10 @@ public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFra
List<AccountViewModel> items = result.stream()
.filter(a -> d.size() > 1000 || d.stream()
.noneMatch(i -> i.account.url.equals(a.url)))
.peek(account ->{
if (account.getDomainFromURL().equals(getRemoteDomain()))
account.acct=account.getFullyQualifiedName();
})
.map(a->new AccountViewModel(a, accountID))
.collect(Collectors.toList());

View File

@@ -0,0 +1,97 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.PleromaGetStatusReactions;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiReaction;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEmojiReactionsListFragment extends BaseAccountListFragment {
private String id;
private String emojiName;
private String url;
private int count;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id = getArguments().getString("statusID");
emojiName = getArguments().getString("emoji");
url = getArguments().getString("url");
count = getArguments().getInt("count");
SpannableStringBuilder title = new SpannableStringBuilder(getResources().getQuantityString(R.plurals.sk_users_reacted_with, count,
count, url == null ? emojiName : ":"+emojiName+":"));
if (url != null) {
Emoji emoji = new Emoji();
emoji.shortcode = emojiName;
emoji.url = url;
HtmlParser.parseCustomEmoji(title, Collections.singletonList(emoji));
}
setTitle(title);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (url != null) {
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
}
}
@Override
public void dataLoaded() {
super.dataLoaded();
footerProgress.setVisibility(View.GONE);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest = new PleromaGetStatusReactions(id, emojiName)
.setCallback(new SimpleCallback<>(StatusEmojiReactionsListFragment.this){
@Override
public void onSuccess(List<EmojiReaction> result) {
if (getActivity() == null)
return;
List<AccountViewModel> items = result.get(0).accounts.stream()
.map(a -> new AccountViewModel(a, accountID))
.collect(Collectors.toList());
onDataLoaded(items);
}
@Override
public void onError(ErrorResponse error) {
super.onError(error);
}
})
.exec(accountID);
}
@Override
public void onResume(){
super.onResume();
if(!loaded && !dataLoading)
loadData();
}
@Override
public Uri getWebUri(Uri.Builder base) {
return null;
}
}

View File

@@ -197,7 +197,7 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
}
}
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.DisableableClickable{
private final ImageView cover, avatar;
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
private final ProgressBarButton actionButton;
@@ -223,13 +223,22 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
actionProgress=findViewById(R.id.action_progress);
actionWrap=findViewById(R.id.action_btn_wrap);
itemView.setOutlineProvider(OutlineProviders.roundedRect(6));
itemView.setClipToOutline(true);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setOutlineProvider(OutlineProviders.roundedRect(15));
avatar.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
View border=findViewById(R.id.avatar_border);
border.setOutlineProvider(OutlineProviders.roundedRect(17));
border.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(9));
cover.setClipToOutline(true);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
actionButton.setOnClickListener(this::onActionButtonClick);
itemView.setOnClickListener(v->this.onClick());
}
@Override
public boolean isEnabled(){
return false;
}
@Override
@@ -242,12 +251,14 @@ public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverA
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
postsLabel.setText(getResources().getQuantityString(R.plurals.sk_posts_count_label, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id);
UiUtils.setExtraTextInfo(getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account);
if(relationship==null){
actionWrap.setVisibility(View.GONE);
}else{

View File

@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;

View File

@@ -139,7 +139,9 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
headerView.findViewById(R.id.more).setVisibility(View.GONE);
headerView.findViewById(R.id.visibility).setVisibility(View.GONE);
headerView.findViewById(R.id.unread_indicator).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.time_and_username)).setText(R.string.sk_app_username);
headerView.findViewById(R.id.separator).setVisibility(View.GONE);
headerView.findViewById(R.id.time).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.username)).setText(R.string.sk_app_username);
((TextView) headerView.findViewById(R.id.name)).setText(R.string.sk_app_name);
((ImageView) headerView.findViewById(R.id.avatar)).setImageDrawable(getActivity().getDrawable(R.mipmap.ic_launcher));
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(this);

View File

@@ -102,7 +102,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
selectedIDs.remove(id);
else
selectedIDs.add(id);
btn.setEnabled(!selectedIDs.isEmpty());
CheckableHeaderStatusDisplayItem.Holder holder=findHolderOfType(id, CheckableHeaderStatusDisplayItem.Holder.class);
if(holder!=null)
holder.rebind();
@@ -112,7 +111,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
import java.util.Objects;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ImageCache;
@@ -32,7 +33,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
setTitle(getString(R.string.about_app, getString(R.string.sk_app_name)));
AccountSession s=AccountSessionManager.get(accountID);
onDataLoaded(List.of(
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.donate_url))),
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, ()->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag), null)),
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))),
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),

View File

@@ -30,7 +30,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
// MEGALODON
private MastodonLanguage.LanguageResolver languageResolver;
private ListItem<Void> prefixRepliesItem, replyVisibilityItem;
private CheckableListItem<Void> forwardReportsItem, remoteLoadingItem, showBoostsItem, showRepliesItem, loadNewPostsItem, seeNewPostsBtnItem;
private CheckableListItem<Void> forwardReportsItem, remoteLoadingItem, showBoostsItem, showRepliesItem, loadNewPostsItem, seeNewPostsBtnItem, overlayMediaItem;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -47,6 +47,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick),
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_fluent_image_alt_text_24_regular, ()->toggleCheckableItem(altTextItem)),
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_fluent_gif_24_regular, ()->toggleCheckableItem(playGifsItem)),
overlayMediaItem=new CheckableListItem<>(R.string.sk_settings_continues_playback, R.string.sk_settings_continues_playback_summary, CheckableListItem.Style.SWITCH, GlobalUserPreferences.overlayMedia, R.drawable.ic_fluent_play_circle_hint_24_regular, ()->toggleCheckableItem(overlayMediaItem)),
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_fluent_link_24_regular, ()->toggleCheckableItem(customTabsItem)),
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_fluent_person_delete_24_regular, ()->toggleCheckableItem(confirmUnfollowItem)),
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(confirmBoostItem)),
@@ -162,6 +163,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> impleme
protected void onHidden(){
super.onHidden();
GlobalUserPreferences.playGifs=playGifsItem.checked;
GlobalUserPreferences.overlayMedia=overlayMediaItem.checked;
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
GlobalUserPreferences.altTextReminders=altTextItem.checked;
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;

View File

@@ -2,10 +2,14 @@ package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import androidx.annotation.StringRes;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
@@ -15,12 +19,13 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import me.grishka.appkit.Nav;
public class SettingsInstanceFragment extends BaseSettingsFragment<Void> implements HasAccountID{
private CheckableListItem<Void> contentTypesItem, localOnlyItem, glitchModeItem;
private ListItem<Void> defaultContentTypeItem;
private CheckableListItem<Void> contentTypesItem, emojiReactionsItem, localOnlyItem, glitchModeItem;
private ListItem<Void> defaultContentTypeItem, showEmojiReactionsItem;
private AccountLocalPreferences lp;
@Override
@@ -35,12 +40,16 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
new ListItem<>(R.string.sk_settings_posting, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/preferences/other")),
new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit"), 0, true),
contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, this::onContentTypeClick),
defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick),
defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick, 0, true),
emojiReactionsItem=new CheckableListItem<>(R.string.sk_settings_emoji_reactions, R.string.sk_settings_emoji_reactions_explanation, CheckableListItem.Style.SWITCH, lp.emojiReactionsEnabled, R.drawable.ic_fluent_emoji_laugh_24_regular, this::onEmojiReactionsClick),
showEmojiReactionsItem=new ListItem<>(R.string.sk_settings_show_emoji_reactions, getShowEmojiReactionsString(), R.drawable.ic_fluent_emoji_24_regular, this::onShowEmojiReactionsClick, 0, true),
localOnlyItem=new CheckableListItem<>(R.string.sk_settings_support_local_only, R.string.sk_settings_local_only_explanation, CheckableListItem.Style.SWITCH, lp.localOnlySupported, R.drawable.ic_fluent_eye_24_regular, this::onLocalOnlyClick),
glitchModeItem=new CheckableListItem<>(R.string.sk_settings_glitch_instance, R.string.sk_settings_glitch_mode_explanation, CheckableListItem.Style.SWITCH, lp.glitchInstance, R.drawable.ic_fluent_eye_24_filled, ()->toggleCheckableItem(glitchModeItem))
));
contentTypesItem.checkedChangeListener=checked->onContentTypeClick();
defaultContentTypeItem.isEnabled=contentTypesItem.checked;
emojiReactionsItem.checkedChangeListener=checked->onEmojiReactionsClick();
showEmojiReactionsItem.isEnabled=emojiReactionsItem.checked;
localOnlyItem.checkedChangeListener=checked->onLocalOnlyClick();
glitchModeItem.isEnabled=localOnlyItem.checked;
}
@@ -52,9 +61,11 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
protected void onHidden(){
super.onHidden();
lp.contentTypesEnabled=contentTypesItem.checked;
lp.emojiReactionsEnabled=emojiReactionsItem.checked;
lp.localOnlySupported=localOnlyItem.checked;
lp.glitchInstance=glitchModeItem.checked;
lp.save();
E.post(new StatusDisplaySettingsChangedEvent(accountID));
}
private void onServerClick(){
@@ -101,6 +112,36 @@ public class SettingsInstanceFragment extends BaseSettingsFragment<Void> impleme
.show();
}
private void onShowEmojiReactionsClick(){
int selected=lp.showEmojiReactions.ordinal();
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_show_emoji_reactions)
.setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_show_emoji_reactions_hide_empty, R.string.sk_settings_show_emoji_reactions_only_opened, R.string.sk_settings_show_emoji_reactions_always).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
lp.showEmojiReactions=AccountLocalPreferences.ShowEmojiReactions.values()[newSelected[0]];
showEmojiReactionsItem.subtitleRes=getShowEmojiReactionsString();
rebindItem(showEmojiReactionsItem);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private @StringRes int getShowEmojiReactionsString(){
return switch(lp.showEmojiReactions){
case HIDE_EMPTY -> R.string.sk_settings_show_emoji_reactions_hide_empty;
case ONLY_OPENED -> R.string.sk_settings_show_emoji_reactions_only_opened;
case ALWAYS -> R.string.sk_settings_show_emoji_reactions_always;
};
}
private void onEmojiReactionsClick(){
toggleCheckableItem(emojiReactionsItem);
showEmojiReactionsItem.isEnabled=emojiReactionsItem.checked;
rebindItem(showEmojiReactionsItem);
}
private void onLocalOnlyClick(){
toggleCheckableItem(localOnlyItem);
glitchModeItem.checked=localOnlyItem.checked && !isInstanceAkkoma();

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.fragments.settings;
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.content.Intent;
@@ -25,8 +27,11 @@ import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.unifiedpush.android.connector.RegistrationDialogContent;
import org.unifiedpush.android.connector.UnifiedPush;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
@@ -52,7 +57,8 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
private boolean notificationsAllowed=true;
// MEGALODON
private CheckableListItem<Void> uniformIconItem, deleteItem, onlyLatestItem;
private boolean useUnifiedPush = false;
private CheckableListItem<Void> uniformIconItem, deleteItem, onlyLatestItem, unifiedPushItem;
private CheckableListItem<Void> postsItem, updateItem;
private AccountLocalPreferences lp;
@@ -64,6 +70,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
lp=AccountSessionManager.get(accountID).getLocalPreferences();
getPushSubscription();
useUnifiedPush=!getDistributor(getContext()).isEmpty();
onDataLoaded(List.of(
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, ()->onPauseNotificationsClick(false)),
@@ -79,11 +86,19 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
uniformIconItem=new CheckableListItem<>(R.string.sk_settings_uniform_icon_for_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.uniformNotificationIcon, R.drawable.ic_ntf_logo, ()->toggleCheckableItem(uniformIconItem)),
deleteItem=new CheckableListItem<>(R.string.sk_settings_enable_delete_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enableDeleteNotifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, ()->toggleCheckableItem(deleteItem)),
onlyLatestItem=new CheckableListItem<>(R.string.sk_settings_single_notification, 0, CheckableListItem.Style.SWITCH, lp.keepOnlyLatestNotification, R.drawable.ic_fluent_convert_range_24_regular, ()->toggleCheckableItem(onlyLatestItem), true)
onlyLatestItem=new CheckableListItem<>(R.string.sk_settings_single_notification, 0, CheckableListItem.Style.SWITCH, lp.keepOnlyLatestNotification, R.drawable.ic_fluent_convert_range_24_regular, ()->toggleCheckableItem(onlyLatestItem), true),
unifiedPushItem=new CheckableListItem<>(R.string.sk_settings_unifiedpush, 0, CheckableListItem.Style.SWITCH, useUnifiedPush, R.drawable.ic_fluent_alert_arrow_up_24_regular, this::onUnifiedPush, true)
));
//only enable when distributors, who can receive notifications, are available
unifiedPushItem.isEnabled=!UnifiedPush.getDistributors(getContext(), new ArrayList<>()).isEmpty();
if (!unifiedPushItem.isEnabled) {
unifiedPushItem.subtitleRes=R.string.sk_settings_unifiedpush_no_distributor_body;
}
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem, updateItem, postsItem);
pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
unifiedPushItem.checkedChangeListener=checked->onUnifiedPush();
updatePolicyItem(null);
updatePauseItem();
}
@@ -312,4 +327,38 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
bannerAdapter.setVisible(false);
}
}
}
private void onUnifiedPush(){
if(getDistributor(getContext()).isEmpty()){
List<String> distributors = UnifiedPush.getDistributors(getContext(), new ArrayList<>());
showUnifiedPushRegisterDialog(distributors);
return;
}
UnifiedPush.unregisterApp(
getContext(),
accountID
);
//re-register to fcm
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
unifiedPushItem.toggle();
rebindItem(unifiedPushItem);
}
private void showUnifiedPushRegisterDialog(List<String> distributors){
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_unifiedpush_choose).setItems(distributors.toArray(String[]::new),
(dialog, which)->{
String userDistrib = distributors.get(which);
UnifiedPush.saveDistributor(getContext(), userDistrib);
UnifiedPush.registerApp(
getContext(),
accountID,
new ArrayList<>(),
getContext().getPackageName()
);
unifiedPushItem.toggle();
rebindItem(unifiedPushItem);
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();
}
}

View File

@@ -8,6 +8,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -24,6 +25,7 @@ import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.instance.GetInstanceExtendedDescription;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.model.viewmodel.ListItem;
@@ -126,6 +128,8 @@ public class SettingsServerAboutFragment extends LoaderFragment{
hlp.leftMargin=hlp.rightMargin=V.dp(16);
scrollingLayout.addView(heading, hlp);
// if a remote instance is shown, the account is remote and need to be loaded accordingly when shown
instance.contactAccount.isRemote=!AccountSessionManager.get(accountID).domain.equals(instance.normalizedUri);
AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID);
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);

View File

@@ -1,9 +1,11 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Parcel
@@ -20,6 +22,7 @@ public class Announcement extends BaseModel implements DisplayItemsParent {
public Instant updatedAt;
public boolean read;
public List<Emoji> emojis;
public List<EmojiReaction> reactions;
public List<Mention> mentions;
public List<Hashtag> tags;
@@ -41,10 +44,17 @@ public class Announcement extends BaseModel implements DisplayItemsParent {
'}';
}
public Status toStatus() {
Status s = Status.ofFake(id, content, publishedAt);
s.createdAt = startsAt != null ? startsAt : publishedAt;
if (updatedAt != null) s.editedAt = updatedAt;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
if(reactions==null) reactions=new ArrayList<>();
}
public Status toStatus() {
Status s=Status.ofFake(id, content, publishedAt);
s.createdAt=startsAt != null ? startsAt : publishedAt;
s.reactions=reactions;
if(updatedAt != null) s.editedAt=updatedAt;
return s;
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.model;
import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import com.google.gson.annotations.SerializedName;
@@ -45,26 +46,26 @@ public class Attachment extends BaseModel{
public int getWidth(){
if(meta==null)
return 1920;
return 0;
if(meta.width>0)
return meta.width;
if(meta.original!=null && meta.original.width>0)
return meta.original.width;
if(meta.small!=null && meta.small.width>0)
return meta.small.width;
return 1920;
return 0;
}
public int getHeight(){
if(meta==null)
return 1080;
return 0;
if(meta.height>0)
return meta.height;
if(meta.original!=null && meta.original.height>0)
return meta.original.height;
if(meta.small!=null && meta.small.height>0)
return meta.small.height;
return 1080;
return 0;
}
public double getDuration(){
@@ -77,6 +78,13 @@ public class Attachment extends BaseModel{
return 0;
}
public boolean hasSound() {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(url);
String hasAudioStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO);
return "yes".equals(hasAudioStr);
}
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();

View File

@@ -41,6 +41,12 @@ public class Emoji extends BaseModel{
this.staticUrl = staticUrl;
}
public String getUrl(boolean playGifs){
String idealUrl=playGifs ? url : staticUrl;
if(idealUrl==null) return url==null ? staticUrl : url;
return idealUrl;
}
@Override
public String toString(){
return "Emoji{"+

View File

@@ -0,0 +1,63 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.GlobalUserPreferences;
import org.parceler.Parcel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@Parcel
public class EmojiReaction {
public List<Account> accounts;
public List<String> accountIds;
public int count;
public boolean me;
public String name;
public String url;
public String staticUrl;
public transient ImageLoaderRequest request;
public String getUrl(boolean playGifs){
String idealUrl=playGifs ? url : staticUrl;
if(idealUrl==null) return url==null ? staticUrl : url;
return idealUrl;
}
public static EmojiReaction of(Emoji info, Account me){
EmojiReaction reaction=new EmojiReaction();
reaction.me=true;
reaction.count=1;
reaction.name=info.shortcode;
reaction.url=info.url;
reaction.staticUrl=info.staticUrl;
reaction.accounts=new ArrayList<>(Collections.singleton(me));
reaction.accountIds=new ArrayList<>(Collections.singleton(me.id));
reaction.request=new UrlImageLoaderRequest(info.url, V.sp(24), V.sp(24));
return reaction;
}
public static EmojiReaction of(String emoji, Account me){
EmojiReaction reaction=new EmojiReaction();
reaction.me=true;
reaction.count=1;
reaction.name=emoji;
reaction.accounts=new ArrayList<>(Collections.singleton(me));
reaction.accountIds=new ArrayList<>(Collections.singleton(me.id));
return reaction;
}
public void add(Account self){
if(accounts==null) accounts=new ArrayList<>();
if(accountIds==null) accountIds=new ArrayList<>();
count++;
me=true;
accounts.add(self);
accountIds.add(self.id);
}
}

View File

@@ -14,6 +14,7 @@ public class FilterResult extends BaseModel {
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if (filter != null) filter.postprocess();
if(filter!=null) filter.postprocess();
if(keywordMatches==null) keywordMatches=List.of();
}
}

View File

@@ -131,7 +131,7 @@ public class Instance extends BaseModel{
ci.domain=uri;
ci.normalizedDomain=IDN.toUnicode(uri);
ci.description=Html.fromHtml(shortDescription).toString().trim();
if(languages!=null && languages.size() > 0){
if(languages!=null&&languages.size()>0){
ci.language=languages.get(0);
ci.languages=languages;
}else{

View File

@@ -1,9 +1,12 @@
package org.joinmastodon.android.model;
import android.content.Context;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.StringRes;
@@ -20,6 +23,40 @@ public class PushNotification extends BaseModel{
@RequiredField
public String body;
public static PushNotification fromNotification(Context context, Notification notification){
PushNotification pushNotification = new PushNotification();
pushNotification.notificationType = switch(notification.type) {
case FOLLOW -> PushNotification.Type.FOLLOW;
case MENTION -> PushNotification.Type.MENTION;
case REBLOG -> PushNotification.Type.REBLOG;
case FAVORITE -> PushNotification.Type.FAVORITE;
case POLL -> PushNotification.Type.POLL;
case STATUS -> PushNotification.Type.STATUS;
case UPDATE -> PushNotification.Type.UPDATE;
case SIGN_UP -> PushNotification.Type.SIGN_UP;
case REPORT -> PushNotification.Type.REPORT;
//Follow request, and reactions are not supported by the API
default -> throw new IllegalStateException("Unexpected value: "+notification.type);
};
String notificationTitle = context.getString(switch(notification.type){
case FOLLOW -> R.string.user_followed_you;
case MENTION -> R.string.sk_notification_mention;
case REBLOG -> R.string.notification_boosted;
case FAVORITE -> R.string.user_favorited;
case POLL -> R.string.poll_ended;
case UPDATE -> R.string.sk_post_edited;
case SIGN_UP -> R.string.sk_signed_up;
case REPORT -> R.string.sk_reported;
default -> throw new IllegalStateException("Unexpected value: "+notification.type);
});
pushNotification.title = UiUtils.generateFormattedString(notificationTitle, notification.account.displayName).toString();
pushNotification.icon = notification.status.account.avatarStatic;
pushNotification.body = notification.status.getStrippedText();
return pushNotification;
}
@Override
public String toString(){
return "PushNotification{"+

View File

@@ -13,16 +13,17 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcel;
import java.lang.reflect.Type;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Parcel
@@ -74,6 +75,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public Status quote; // can be boolean in calckey
public List<EmojiReaction> reactions;
protected List<EmojiReaction> emojiReactions; // akkoma
public transient boolean filterRevealed;
public transient boolean spoilerRevealed;
public transient boolean sensitiveRevealed;
@@ -96,7 +100,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
t.postprocess();
for(Emoji e:emojis)
e.postprocess();
if (mediaAttachments == null) mediaAttachments = List.of();
if (mediaAttachments == null) mediaAttachments=List.of();
for(Attachment a:mediaAttachments)
a.postprocess();
account.postprocess();
@@ -110,10 +114,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
for(FilterResult fr:filtered)
fr.postprocess();
if (!TextUtils.isEmpty(spoilerText)) sensitive = true;
spoilerRevealed=TextUtils.isEmpty(spoilerText);
spoilerRevealed=!hasSpoiler();
if(!spoilerRevealed) sensitive=true;
sensitiveRevealed=!sensitive;
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
if(visibility.equals(StatusPrivacy.LOCAL)) localOnly=true;
if(emojiReactions!=null) reactions=emojiReactions;
if(reactions==null) reactions=new ArrayList<>();
}
@Override
@@ -171,6 +177,10 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
pinned=ev.pinned;
}
public void update(EmojiReactionsUpdatedEvent ev){
reactions=ev.reactions;
}
public Status getContentStatus(){
return reblog!=null ? reblog : this;
}
@@ -181,6 +191,10 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
return strippedText;
}
public boolean hasSpoiler(){
return !TextUtils.isEmpty(spoilerText);
}
@NonNull
@Override
public Status clone(){
@@ -194,17 +208,18 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
}
public static Status ofFake(String id, String text, Instant createdAt) {
Status s = new Status();
s.id = id;
s.mediaAttachments = List.of();
s.createdAt = createdAt;
s.content = s.text = text;
s.spoilerText = "";
s.visibility = StatusPrivacy.PUBLIC;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
s.filtered = List.of();
Status s=new Status();
s.id=id;
s.mediaAttachments=List.of();
s.createdAt=createdAt;
s.content=s.text=text;
s.spoilerText="";
s.visibility=StatusPrivacy.PUBLIC;
s.reactions=List.of();
s.mentions=List.of();
s.tags =List.of();
s.emojis=List.of();
s.filtered=List.of();
return s;
}
@@ -216,21 +231,21 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public static class StatusDeserializer implements JsonDeserializer<Status> {
@Override
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
JsonObject obj=json.getAsJsonObject();
Status quote = null;
Status quote=null;
if (obj.has("quote") && obj.get("quote").isJsonObject())
quote = gson.fromJson(obj.get("quote"), Status.class);
quote=gson.fromJson(obj.get("quote"), Status.class);
obj.remove("quote");
Status reblog = null;
Status reblog=null;
if (obj.has("reblog"))
reblog = gson.fromJson(obj.get("reblog"), Status.class);
reblog=gson.fromJson(obj.get("reblog"), Status.class);
obj.remove("reblog");
Status status = gsonWithoutDeserializer.fromJson(json, Status.class);
status.quote = quote;
status.reblog = reblog;
Status status=gsonWithoutDeserializer.fromJson(json, Status.class);
status.quote=quote;
status.reblog=reblog;
return status;
}

File diff suppressed because one or more lines are too long

View File

@@ -37,6 +37,16 @@ public class OutlineProviders{
}
};
private final static int BUTTON_BG_HEIGHT=V.dp(40);
public static final ViewOutlineProvider M3_BUTTON=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
int viewHeight=view.getHeight();
int top=Math.floorDiv(viewHeight - BUTTON_BG_HEIGHT, 2);
outline.setRoundRect(0, top, view.getWidth(), top + BUTTON_BG_HEIGHT, V.dp(20));
}
};
public static ViewOutlineProvider roundedRect(int dp){
ViewOutlineProvider provider=roundedRects.get(dp);
if(provider!=null)

View File

@@ -111,15 +111,24 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
rejectWrap=findViewById(R.id.reject_btn_wrap);
View card=findViewById(R.id.card);
card.setOutlineProvider(OutlineProviders.roundedRect(6));
card.setOutlineProvider(OutlineProviders.roundedRect(12));
card.setClipToOutline(true);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setOutlineProvider(OutlineProviders.roundedRect(15));
avatar.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
View border=findViewById(R.id.avatar_border);
border.setOutlineProvider(OutlineProviders.roundedRect(17));
border.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(9));
cover.setClipToOutline(true);
actionButton.setOnClickListener(this::onActionButtonClick);
acceptButton.setOnClickListener(this::onFollowRequestButtonClick);
rejectButton.setOnClickListener(this::onFollowRequestButtonClick);
card.setOnClickListener(v->onClick());
}
@Override
public boolean isEnabled(){
return false;
}
@Override
@@ -132,18 +141,19 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.sk_posts_count_label, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=item.parentFragment.getRelationship(item.account.id);
if(item.notification.type == Notification.Type.FOLLOW_REQUEST && (relationship == null || !relationship.followedBy)){
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), null, findViewById(R.id.pronouns), true, false, false, item.account);
if(item.notification.type==Notification.Type.FOLLOW_REQUEST && (relationship==null || !relationship.followedBy)){
actionWrap.setVisibility(View.GONE);
acceptWrap.setVisibility(View.VISIBLE);
rejectWrap.setVisibility(View.VISIBLE);
// i hate that i wasn't able to do this in xml
acceptButton.setCompoundDrawableTintList(acceptButton.getTextColors());
acceptProgress.setIndeterminateTintList(acceptButton.getTextColors());
rejectButton.setCompoundDrawableTintList(rejectButton.getTextColors());
@@ -163,7 +173,7 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), null, v == acceptButton, relationship, rel -> {
if(v.getContext()==null) return;
if(v.getContext()==null || rel==null) return;
itemView.setHasTransientState(false);
item.parentFragment.putRelationship(item.account.id, rel);
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();

View File

@@ -4,16 +4,16 @@ import android.content.Context;
import android.view.ViewGroup;
import android.widget.Space;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import me.grishka.appkit.utils.V;
public class DummyStatusDisplayItem extends StatusDisplayItem {
private final boolean addMediaGridMargin;
public DummyStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, boolean addMediaGridMargin) {
public DummyStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment) {
super(parentID, parentFragment);
this.addMediaGridMargin = addMediaGridMargin;
}
@Override
@@ -22,19 +22,21 @@ public class DummyStatusDisplayItem extends StatusDisplayItem {
}
public static class Holder extends StatusDisplayItem.Holder<DummyStatusDisplayItem> {
private final RecyclerView.LayoutParams params;
public Holder(Context context) {
super(new Space(context));
}
params=new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
@Override
public void onBind(DummyStatusDisplayItem item) {
// BetterItemAnimator appears not to handle InsetStatusItemDecoration's getItemOffsets
// correctly, causing removed inset views to jump while animating. i don't quite
// understand it, but this workaround appears to work.
// see InsetStatusItemDecoration#getItemOffsets
ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
params.setMargins(0, item.addMediaGridMargin ? V.dp(0) : 0, 0, V.dp(16));
params.setMargins(0, 0, 0, V.dp(16));
itemView.setLayoutParams(params);
}
@Override
public void onBind(DummyStatusDisplayItem item) {}
}
}

View File

@@ -0,0 +1,417 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.announcements.AddAnnouncementReaction;
import org.joinmastodon.android.api.requests.announcements.DeleteAnnouncementReaction;
import org.joinmastodon.android.api.requests.statuses.AddStatusReaction;
import org.joinmastodon.android.api.requests.statuses.DeleteStatusReaction;
import org.joinmastodon.android.api.requests.statuses.PleromaAddStatusReaction;
import org.joinmastodon.android.api.requests.statuses.PleromaDeleteStatusReaction;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.account_list.StatusEmojiReactionsListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiReaction;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.utils.TextDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
public final Status status;
private final Drawable placeholder;
private final boolean hideEmpty, forAnnouncement, playGifs;
private final String accountID;
private static final float ALPHA_DISABLED=0.55f;
public EmojiReactionsStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, String accountID, boolean hideEmpty, boolean forAnnouncement) {
super(parentID, parentFragment);
this.status=status;
this.hideEmpty=hideEmpty;
this.forAnnouncement=forAnnouncement;
this.accountID=accountID;
placeholder=parentFragment.getContext().getDrawable(R.drawable.image_placeholder).mutate();
placeholder.setBounds(0, 0, V.sp(24), V.sp(24));
playGifs=GlobalUserPreferences.playGifs;
}
@Override
public int getImageCount(){
return (int) status.reactions.stream().filter(r->r.getUrl(playGifs)!=null).count();
}
@Override
public ImageLoaderRequest getImageRequest(int index){
return status.reactions.get(index).request;
}
@Override
public Type getType(){
return Type.EMOJI_REACTIONS;
}
public boolean isHidden(){
return status.reactions.isEmpty() && hideEmpty;
}
// borrowed from ProfileFragment
private void setActionProgressVisible(Holder.EmojiReactionViewHolder vh, boolean visible){
if(vh==null) return;
vh.progress.setVisibility(visible ? View.VISIBLE : View.GONE);
vh.btn.setClickable(!visible);
vh.btn.setAlpha(visible ? ALPHA_DISABLED : 1);
}
private MastodonAPIRequest<?> createRequest(String name, int count, boolean delete, Holder.EmojiReactionViewHolder vh, Runnable cb, Runnable err){
setActionProgressVisible(vh, true);
boolean ak=parentFragment.isInstanceAkkoma();
boolean keepSpinning=delete && count == 1;
if(forAnnouncement){
MastodonAPIRequest<Object> req=delete
? new DeleteAnnouncementReaction(status.id, name)
: new AddAnnouncementReaction(status.id, name);
return req.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
if(!keepSpinning) setActionProgressVisible(vh, false);
cb.run();
}
@Override
public void onError(ErrorResponse error){
setActionProgressVisible(vh, false);
error.showToast(parentFragment.getContext());
if(err!=null) err.run();
}
});
}else{
MastodonAPIRequest<Status> req=delete
? (ak ? new PleromaDeleteStatusReaction(status.id, name) : new DeleteStatusReaction(status.id, name))
: (ak ? new PleromaAddStatusReaction(status.id, name) : new AddStatusReaction(status.id, name));
return req.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
if(!keepSpinning) setActionProgressVisible(vh, false);
cb.run();
}
@Override
public void onError(ErrorResponse error){
setActionProgressVisible(vh, false);
error.showToast(parentFragment.getContext());
if(err!=null) err.run();
}
});
}
}
public static class Holder extends StatusDisplayItem.Holder<EmojiReactionsStatusDisplayItem> implements ImageLoaderViewHolder, CustomEmojiPopupKeyboard.Listener {
private final UsableRecyclerView list;
private final LinearLayout root, line;
private CustomEmojiPopupKeyboard emojiKeyboard;
private final View space;
private final ImageButton addButton;
private final ProgressBar progress;
private final EmojiReactionsAdapter adapter;
private final ListImageLoaderWrapper imgLoader;
public Holder(Activity activity, ViewGroup parent) {
super(activity, R.layout.display_item_emoji_reactions, parent);
root=(LinearLayout) itemView;
line=findViewById(R.id.line);
list=findViewById(R.id.list);
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
list.setAdapter(adapter=new EmojiReactionsAdapter(this, imgLoader));
addButton=findViewById(R.id.add_btn);
progress=findViewById(R.id.progress);
addButton.setOnClickListener(this::onReactClick);
space=findViewById(R.id.space);
list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
}
@Override
public void onBind(EmojiReactionsStatusDisplayItem item) {
if(emojiKeyboard != null) root.removeView(emojiKeyboard.getView());
AccountSession session=item.parentFragment.getSession();
item.status.reactions.forEach(r->r.request=r.getUrl(item.playGifs)!=null
? new UrlImageLoaderRequest(r.getUrl(item.playGifs), V.sp(24), V.sp(24))
: null);
emojiKeyboard=new CustomEmojiPopupKeyboard(
(Activity) item.parentFragment.getContext(),
AccountSessionManager.getInstance().getCustomEmojis(session.domain),
session.domain, true);
emojiKeyboard.setListener(this);
space.setVisibility(View.GONE);
root.addView(emojiKeyboard.getView());
boolean hidden=item.isHidden();
root.setVisibility(hidden ? View.GONE : View.VISIBLE);
line.setVisibility(hidden ? View.GONE : View.VISIBLE);
line.setPadding(
list.getPaddingLeft(),
hidden ? 0 : V.dp(8),
list.getPaddingRight(),
item.forAnnouncement ? V.dp(8) : 0
);
imgLoader.updateImages();
adapter.notifyDataSetChanged();
}
private void hideEmojiKeyboard(){
space.setVisibility(View.GONE);
addButton.setSelected(false);
if(emojiKeyboard.isVisible()) emojiKeyboard.toggleKeyboardPopup(null);
}
@Override
public void onEmojiSelected(Emoji emoji) {
addEmojiReaction(emoji.shortcode, emoji);
hideEmojiKeyboard();
}
@Override
public void onEmojiSelected(String emoji){
addEmojiReaction(emoji, null);
hideEmojiKeyboard();
}
private void addEmojiReaction(String emoji, Emoji info) {
int countBefore=item.status.reactions.size();
for(int i=0; i<item.status.reactions.size(); i++){
EmojiReaction r=item.status.reactions.get(i);
if(r.name.equals(emoji) && r.me){
RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext());
scroller.setTargetPosition(i);
list.getLayoutManager().startSmoothScroll(scroller);
return; // nothing to do, already added
}
}
progress.setVisibility(View.VISIBLE);
addButton.setClickable(false);
addButton.setAlpha(ALPHA_DISABLED);
Runnable resetBtn=()->{
progress.setVisibility(View.GONE);
addButton.setClickable(true);
addButton.setAlpha(1f);
};
Account me=AccountSessionManager.get(item.accountID).self;
EmojiReaction existing=null;
for(int i=0; i<item.status.reactions.size(); i++){
EmojiReaction r=item.status.reactions.get(i);
if(r.name.equals(emoji)){
existing=r;
break;
}
}
EmojiReaction finalExisting=existing;
item.createRequest(emoji, existing==null ? 1 : existing.count, false, null, ()->{
resetBtn.run();
if(finalExisting==null){
int pos=item.status.reactions.size();
item.status.reactions.add(pos, info!=null ? EmojiReaction.of(info, me) : EmojiReaction.of(emoji, me));
adapter.notifyItemRangeInserted(pos, 1);
RecyclerView.SmoothScroller scroller=new LinearSmoothScroller(list.getContext());
scroller.setTargetPosition(pos);
list.getLayoutManager().startSmoothScroll(scroller);
}else{
finalExisting.add(me);
adapter.notifyItemChanged(item.status.reactions.indexOf(finalExisting));
}
E.post(new EmojiReactionsUpdatedEvent(item.status.id, item.status.reactions, countBefore==0, adapter.parentHolder));
}, resetBtn).exec(item.accountID);
}
@Override
public void onBackspace() {}
private void onReactClick(View v){
emojiKeyboard.toggleKeyboardPopup(null);
v.setSelected(emojiKeyboard.isVisible());
space.setVisibility(emojiKeyboard.isVisible() ? View.VISIBLE : View.GONE);
DisplayMetrics displayMetrics = new DisplayMetrics();
int[] locationOnScreen = new int[2];
((Activity) v.getContext()).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
v.getLocationOnScreen(locationOnScreen);
double fromScreenTop = (double) locationOnScreen[1] / displayMetrics.heightPixels;
if (fromScreenTop > 0.75) {
item.parentFragment.scrollBy(0, (int) (displayMetrics.heightPixels * 0.3));
}
}
@Override
public void setImage(int index, Drawable image){
View child=list.getChildAt(index);
if(child==null) return;
((EmojiReactionViewHolder) list.getChildViewHolder(child)).setImage(index, image);
}
@Override
public void clearImage(int index){
if(item.status.reactions.get(index).getUrl(item.playGifs)==null) return;
setImage(index, item.placeholder);
}
private class EmojiReactionsAdapter extends UsableRecyclerView.Adapter<EmojiReactionViewHolder> implements ImageLoaderRecyclerAdapter{
ListImageLoaderWrapper imgLoader;
Holder parentHolder;
public EmojiReactionsAdapter(Holder parentHolder, ListImageLoaderWrapper imgLoader){
super(imgLoader);
this.parentHolder=parentHolder;
this.imgLoader=imgLoader;
}
@NonNull
@Override
public EmojiReactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new EmojiReactionViewHolder(parent.getContext(), list);
}
@Override
public void onBindViewHolder(EmojiReactionViewHolder holder, int position){
holder.bind(Pair.create(item, item.status.reactions.get(position)));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return item.status.reactions.size();
}
@Override
public int getImageCountForItem(int position){
return item.status.reactions.get(position).getUrl(item.playGifs)==null ? 0 : 1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return item.status.reactions.get(position).request;
}
}
private static class EmojiReactionViewHolder extends BindableViewHolder<Pair<EmojiReactionsStatusDisplayItem, EmojiReaction>> implements ImageLoaderViewHolder{
private final ProgressBarButton btn;
private final ProgressBar progress;
public EmojiReactionViewHolder(Context context, RecyclerView list){
super(context, R.layout.item_emoji_reaction, list);
btn=findViewById(R.id.btn);
progress=findViewById(R.id.progress);
itemView.setClickable(true);
}
@Override
public void setImage(int index, Drawable drawable){
drawable.setBounds(0, 0, V.sp(24), V.sp(24));
btn.setCompoundDrawablesRelative(drawable, null, null, null);
if(drawable instanceof Animatable) ((Animatable) drawable).start();
}
@Override
public void clearImage(int index){
setImage(index, item.first.placeholder);
}
@Override
public void onBind(Pair<EmojiReactionsStatusDisplayItem, EmojiReaction> item){
item.first.setActionProgressVisible(this, false);
EmojiReactionsStatusDisplayItem parent=item.first;
EmojiReaction reaction=item.second;
btn.setText(UiUtils.abbreviateNumber(reaction.count));
btn.setContentDescription(reaction.name);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) btn.setTooltipText(reaction.name);
if(reaction.getUrl(parent.playGifs)==null){
Paint p=new Paint();
p.setTextSize(V.sp(18));
TextDrawable drawable=new TextDrawable(p, reaction.name);
btn.setCompoundDrawablesRelative(drawable, null, null, null);
}else{
btn.setCompoundDrawablesRelative(item.first.placeholder, null, null, null);
}
btn.setSelected(reaction.me);
btn.setOnClickListener(e->{
boolean deleting=reaction.me;
parent.createRequest(reaction.name, reaction.count, deleting, this, ()->{
EmojiReactionsAdapter adapter = (EmojiReactionsAdapter) getBindingAdapter();
for(int i=0; i<parent.status.reactions.size(); i++){
EmojiReaction r=parent.status.reactions.get(i);
if(!r.name.equals(reaction.name)) continue;
if(deleting && r.count==1) {
parent.status.reactions.remove(i);
adapter.notifyItemRemoved(i);
break;
}
r.me=!deleting;
if(deleting) r.count--;
else r.count++;
adapter.notifyItemChanged(i);
break;
}
if(parent.isHidden()){
adapter.parentHolder.root.setVisibility(View.GONE);
adapter.parentHolder.line.setVisibility(View.GONE);
}
E.post(new EmojiReactionsUpdatedEvent(parent.status.id, parent.status.reactions, parent.status.reactions.isEmpty(), adapter.parentHolder));
adapter.parentHolder.imgLoader.updateImages();
}, null).exec(parent.parentFragment.getAccountID());
});
if (parent.parentFragment.isInstanceAkkoma()) {
// glitch-soc doesn't have this, afaik
btn.setOnLongClickListener(e->{
EmojiReaction emojiReaction=parent.status.reactions.get(getAbsoluteAdapterPosition());
Bundle args=new Bundle();
args.putString("account", parent.parentFragment.getAccountID());
args.putString("statusID", parent.status.id);
int atSymbolIndex = emojiReaction.name.indexOf("@");
args.putString("emoji", atSymbolIndex != -1 ? emojiReaction.name.substring(0, atSymbolIndex) : emojiReaction.name);
args.putString("url", emojiReaction.getUrl(parent.playGifs));
args.putInt("count", emojiReaction.count);
Nav.go(parent.parentFragment.getActivity(), StatusEmojiReactionsListFragment.class, args);
return true;
});
}
}
}
}
}

View File

@@ -77,18 +77,21 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
};
private static final float ALPHA_PRESSED=0.55f;
static {
opacityOut = new AlphaAnimation(1, 0.55f);
opacityOut = new AlphaAnimation(1, ALPHA_PRESSED);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(0.55f, 1);
opacityIn = new AlphaAnimation(ALPHA_PRESSED, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
}
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_footer, parent);
replies=findViewById(R.id.reply);
boosts=findViewById(R.id.boost);
favorites=findViewById(R.id.favorite);

View File

@@ -132,9 +132,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView name, timeAndUsername, extraText, pronouns;
private final View collapseBtn;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, markAsRead, collapseBtnIcon;
private final TextView name, time, username, extraText, pronouns;
private final View collapseBtn, timeUsernameSeparator;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, markAsRead, collapseBtnIcon, botIcon;
private final PopupMenu optionsMenu;
private Relationship relationship;
private APIRequest<?> currentRelationshipRequest;
@@ -146,7 +146,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
protected Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
super(activity, layout, parent);
name=findViewById(R.id.name);
timeAndUsername=findViewById(R.id.time_and_username);
time=findViewById(R.id.time);
username=findViewById(R.id.username);
botIcon=findViewById(R.id.bot_icon);
timeUsernameSeparator=findViewById(R.id.separator);
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
visibility=findViewById(R.id.visibility);
@@ -171,7 +174,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P)
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI())
optionsMenu.getMenu().setGroupDividerEnabled(true);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
@@ -190,7 +193,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
}
boolean isPixelfed = item.parentFragment.isInstancePixelfed();
boolean textEmpty = TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText);
boolean textEmpty = TextUtils.isEmpty(item.status.content) && !item.status.hasSpoiler();
if(!redraft && (isPixelfed || textEmpty)){
// pixelfed doesn't support /statuses/:id/source :/
if (isPixelfed) {
@@ -315,18 +318,26 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
else if (item.status != null && item.status.editedAt != null)
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
String sepp = item.parentFragment.getString(R.string.sk_separator);
String username = "@" + item.user.acct;
timeAndUsername.setText(time == null ? username :
username + " " + sepp + " " + time);
this.username.setText(item.user.getDisplayUsername());
this.timeUsernameSeparator.setVisibility(time==null ? View.GONE : View.VISIBLE);
this.time.setVisibility(time==null ? View.GONE : View.VISIBLE);
if(time!=null) this.time.setText(time);
botIcon.setVisibility(item.user.bot ? View.VISIBLE : View.GONE);
botIcon.setColorFilter(username.getCurrentTextColor());
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
if (item.hasVisibilityToggle){
boolean disabled = !item.status.sensitiveRevealed ||
(!TextUtils.isEmpty(item.status.spoilerText) &&
!item.status.spoilerRevealed);
visibility.setEnabled(!disabled);
V.setVisibilityAnimated(visibility, disabled ? View.INVISIBLE : View.VISIBLE);
boolean hidden = !item.status.sensitiveRevealed || (item.status.hasSpoiler() && !item.status.spoilerRevealed);
// doing this because V.setVisibilityAnimated ignores changes between INVISIBLE and GONE
int newVis=hidden ? View.INVISIBLE : View.VISIBLE;
if(newVis==View.INVISIBLE && visibility.getVisibility()==View.GONE)
visibility.setVisibility(newVis);
else
V.setVisibilityAnimated(visibility, newVis);
visibility.setEnabled(!hidden);
visibility.setContentDescription(item.parentFragment.getString(item.status.sensitiveRevealed ? R.string.spoiler_hide : R.string.spoiler_show));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
visibility.setTooltipText(visibility.getContentDescription());

View File

@@ -6,6 +6,7 @@ import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -13,12 +14,14 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
private final Status status;
@@ -51,6 +54,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView title, description, domain;
private final ImageView photo;
private final View inner;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
@@ -60,7 +64,8 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
description=findViewById(R.id.description);
domain=findViewById(R.id.domain);
photo=findViewById(R.id.photo);
findViewById(R.id.inner).setOnClickListener(this::onClick);
inner=findViewById(R.id.inner);
inner.setOnClickListener(this::onClick);
}
@Override
@@ -84,6 +89,15 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
photo.setImageDrawable(crossfadeDrawable);
didClear=false;
}
// if there's no image, we don't want to cover the inset borders
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) inner.getLayoutParams();
int margin=item.inset && item.imgRequest == null ? V.dp(1) : 0;
params.setMargins(margin, 0, margin, margin);
boolean insetAndLast=item.inset && isLastDisplayItemForStatus();
inner.setClipToOutline(insetAndLast);
inner.setOutlineProvider(insetAndLast ? OutlineProviders.bottomRoundedRect(12) : null);
}
@Override

View File

@@ -26,6 +26,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
@@ -210,6 +211,10 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
sensitiveText.setText(R.string.media_hidden);
else
sensitiveText.setText(R.string.sensitive_content_explain);
boolean insetAndLast=item.inset && isLastDisplayItemForStatus();
wrapper.setClipToOutline(insetAndLast);
wrapper.setOutlineProvider(insetAndLast ? OutlineProviders.bottomRoundedRect(12) : null);
}
@Override

View File

@@ -79,7 +79,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate();
progressBgInset=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted_inset, activity.getTheme()).mutate();
itemView.setOnClickListener(this::onButtonClick);
button.setOutlineProvider(OutlineProviders.roundedRect(24));
button.setOutlineProvider(OutlineProviders.M3_BUTTON);
button.setClipToOutline(true);
}

View File

@@ -92,7 +92,7 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
itemView.getPaddingLeft(),
itemView.getPaddingTop(),
itemView.getPaddingRight(),
item.inset || GlobalUserPreferences.spectatorMode ? itemView.getPaddingTop() : 0
item.inset ? itemView.getPaddingTop() : 0
);
}

View File

@@ -1,5 +1,8 @@
package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ShowEmojiReactions.ALWAYS;
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ShowEmojiReactions.ONLY_OPENED;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
@@ -12,6 +15,7 @@ import android.view.ViewGroup;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
@@ -49,8 +53,8 @@ import me.grishka.appkit.views.UsableRecyclerView;
public abstract class StatusDisplayItem{
public final String parentID;
public final BaseStatusListFragment parentFragment;
public boolean inset;
public final BaseStatusListFragment<?> parentFragment;
public boolean inset, insetPadding=true;
public int index;
public boolean
hasDescendantNeighbor = false,
@@ -64,6 +68,7 @@ public abstract class StatusDisplayItem{
public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3;
public static final int FLAG_NO_HEADER=1 << 4;
public static final int FLAG_NO_TRANSLATE=1 << 5;
public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6;
public void setAncestryInfo(
boolean hasDescendantNeighbor,
@@ -77,7 +82,7 @@ public abstract class StatusDisplayItem{
this.isDirectDescendant = isDirectDescendant;
}
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
public StatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment){
this.parentID=parentID;
this.parentFragment=parentFragment;
}
@@ -102,6 +107,7 @@ public abstract class StatusDisplayItem{
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
case EMOJI_REACTIONS -> new EmojiReactionsStatusDisplayItem.Holder(activity, parent);
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent);
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
@@ -118,17 +124,6 @@ public abstract class StatusDisplayItem{
};
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, boolean disableTranslate, FilterContext filterContext) {
int flags=0;
if(inset)
flags|=FLAG_INSET;
if(!addFooter)
flags|=FLAG_NO_FOOTER;
if (disableTranslate)
flags|=FLAG_NO_TRANSLATE;
return buildItems(fragment, status, accountID, parentObject, knownAccounts, filterContext, flags);
}
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
String parentID = parent.getID();
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
@@ -204,7 +199,7 @@ public abstract class StatusDisplayItem{
items.add(replyLine);
}
}
if((flags & FLAG_CHECKABLE)!=0)
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
else
@@ -222,7 +217,7 @@ public abstract class StatusDisplayItem{
}
ArrayList<StatusDisplayItem> contentItems;
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
if(statusForContent.hasSpoiler()){
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
items.add(spoilerItem);
@@ -250,7 +245,7 @@ public abstract class StatusDisplayItem{
}else if(!hasSpoiler && header!=null){
header.needBottomPadding=true;
}else if(hasSpoiler){
contentItems.add(new DummyStatusDisplayItem(parentID, fragment, true));
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
}
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
@@ -286,8 +281,16 @@ public abstract class StatusDisplayItem{
if(contentItems!=items && statusForContent.spoilerRevealed){
items.addAll(contentItems);
}
AccountLocalPreferences lp=fragment.getLocalPrefs();
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && lp.emojiReactionsEnabled &&
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment)){
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
}
FooterStatusDisplayItem footer=null;
if((flags & FLAG_NO_FOOTER)==0){
FooterStatusDisplayItem footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
footer.hideCounts=hideCounts;
items.add(footer);
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
@@ -296,10 +299,12 @@ public abstract class StatusDisplayItem{
int i=1;
boolean inset=(flags & FLAG_INSET)!=0;
// add inset dummy so last content item doesn't clip out of inset bounds
if (inset) {
items.add(new DummyStatusDisplayItem(parentID, fragment,
!contentItems.isEmpty() && contentItems
.get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0){
items.add(new DummyStatusDisplayItem(parentID, fragment));
// in case we ever need the dummy to display a margin for the media grid again:
// (i forgot why we apparently don't need this anymore)
// !contentItems.isEmpty() && contentItems
// .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
}
for(StatusDisplayItem item:items){
item.inset=inset;
@@ -340,6 +345,7 @@ public abstract class StatusDisplayItem{
POLL_OPTION,
POLL_FOOTER,
CARD,
EMOJI_REACTIONS,
FOOTER,
ACCOUNT_CARD,
ACCOUNT,
@@ -375,6 +381,35 @@ public abstract class StatusDisplayItem{
item.parentFragment.onItemClick(item.parentID);
}
public Optional<StatusDisplayItem> getNextVisibleDisplayItem(){
Optional<StatusDisplayItem> next=getNextDisplayItem();
for(int offset=1; next.isPresent(); next=getDisplayItemOffset(++offset)){
if(!next.map(n->
(n instanceof EmojiReactionsStatusDisplayItem e && e.isHidden()) ||
(n instanceof DummyStatusDisplayItem)
).orElse(false)) return next;
}
return Optional.empty();
}
public Optional<StatusDisplayItem> getNextDisplayItem(){
return getDisplayItemOffset(1);
}
public Optional<StatusDisplayItem> getDisplayItemOffset(int offset){
int nextPos=getAbsoluteAdapterPosition() + offset;
List<StatusDisplayItem> displayItems=item.parentFragment.getDisplayItems();
return displayItems.size() > nextPos
? Optional.of(displayItems.get(nextPos))
: Optional.empty();
}
public boolean isLastDisplayItemForStatus(){
return getNextVisibleDisplayItem()
.map(n->!n.parentID.equals(item.parentID))
.orElse(true);
}
@Override
public boolean isEnabled(){
return item.parentFragment.isItemEnabled(item.parentID);

View File

@@ -21,6 +21,7 @@ import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
@@ -193,12 +194,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
// remove additional padding when (transparently padded) translate button is visible
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
int bottomPadding = (translateVisible && nextIsFooter) ? 0
: nextIsFooter ? V.dp(6)
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
if(next!=null && !next.parentID.equals(item.parentID)) next=null;
int bottomPadding=next instanceof FooterStatusDisplayItem ? V.dp(6)
: item.inset ? V.dp(12)
: (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0
: V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
@@ -235,7 +235,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
// compensate for spoiler's bottom margin
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, (item.inset || GlobalUserPreferences.spectatorMode) && hasSpoiler ? V.dp(-16) : 0,
params.setMargins(params.leftMargin, item.inset && hasSpoiler ? V.dp(-16) : 0,
params.rightMargin, params.bottomMargin);
}

View File

@@ -99,11 +99,15 @@ public class BlurhashCrossfadeDrawable extends Drawable{
@Override
public int getIntrinsicWidth(){
if(width==0)
return imageDrawable==null ? 1920 : imageDrawable.getIntrinsicWidth();
return width;
}
@Override
public int getIntrinsicHeight(){
if(height==0)
return imageDrawable==null ? 1080 : imageDrawable.getIntrinsicHeight();
return height;
}

View File

@@ -45,6 +45,7 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Attachment;
@@ -418,7 +419,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
wlp.flags|=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
wm.updateViewLayout(windowView, wlp);
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
int audiofocus = GlobalUserPreferences.overlayMedia ? AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK : AudioManager.AUDIOFOCUS_GAIN;
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, audiofocus);
}
screenOnRefCount++;
}

View File

@@ -8,8 +8,6 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import java.util.List;
@@ -89,10 +87,10 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
pad=V.dp(16);
// else
// pad=V.dp(12);
boolean insetLeft=true, insetRight=true;
if(insetLeft)
boolean insetPadding=((StatusDisplayItem.Holder<?>) holder).getItem().insetPadding;
if(insetPadding)
outRect.left=pad;
if(insetRight)
if(insetPadding)
outRect.right=pad;
// had to comment this out because animations with offsets aren't handled properly.

View File

@@ -0,0 +1,242 @@
package org.joinmastodon.android.ui.utils;
/*
* Copyright 2016 Ali Muzaffar
* <p/>
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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.
*/
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.TextView;
import java.lang.ref.WeakReference;
public class TextDrawable extends Drawable implements TextWatcher {
private WeakReference<TextView> ref;
private String mText;
private Paint mPaint;
private Rect mHeightBounds;
private boolean mBindToViewPaint = false;
private float mPrevTextSize = 0;
private boolean mInitFitText = false;
private boolean mFitTextEnabled = false;
/**
* Create a TextDrawable using the given paint object and string
*
* @param paint
* @param s
*/
public TextDrawable(Paint paint, String s) {
mText = s;
mPaint = new Paint(paint);
mHeightBounds = new Rect();
init();
}
/**
* Create a TextDrawable. This uses the given TextView to initialize paint and has initial text
* that will be drawn. Initial text can also be useful for reserving space that may otherwise
* not be available when setting compound drawables.
*
* @param tv The TextView / EditText using to initialize this drawable
* @param initialText Optional initial text to display
* @param bindToViewsText Should this drawable mirror the text in the TextView
* @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
* Note, this will override any changes made using setColorFilter or setAlpha.
*/
public TextDrawable(TextView tv, String initialText, boolean bindToViewsText, boolean bindToViewsPaint) {
this(tv.getPaint(), initialText);
ref = new WeakReference<>(tv);
if (bindToViewsText || bindToViewsPaint) {
if (bindToViewsText) {
tv.addTextChangedListener(this);
}
mBindToViewPaint = bindToViewsPaint;
}
}
/**
* Create a TextDrawable. This uses the given TextView to initialize paint and the text that
* will be drawn.
*
* @param tv The TextView / EditText using to initialize this drawable
* @param bindToViewsText Should this drawable mirror the text in the TextView
* @param bindToViewsPaint Should this drawable mirror changes to Paint in the TextView, like textColor, typeface, alpha etc.
* Note, this will override any changes made using setColorFilter or setAlpha.
*/
public TextDrawable(TextView tv, boolean bindToViewsText, boolean bindToViewsPaint) {
this(tv, tv.getText().toString(), bindToViewsText, bindToViewsPaint);
}
/**
* Use the provided TextView/EditText to initialize the drawable.
* The Drawable will copy the Text and the Paint properties, however it will from that
* point on be independant of the TextView.
*
* @param tv a TextView or EditText or any of their children.
*/
public TextDrawable(TextView tv) {
this(tv, false, false);
}
/**
* Use the provided TextView/EditText to initialize the drawable.
* The Drawable will copy the Paint properties, and use the provided text to initialise itself.
*
* @param tv a TextView or EditText or any of their children.
* @param s The String to draw
*/
public TextDrawable(TextView tv, String s) {
this(tv, s, false, false);
}
@Override
public void draw(Canvas canvas) {
if (mBindToViewPaint && ref.get() != null) {
Paint p = ref.get().getPaint();
canvas.drawText(mText, 0, getBounds().height(), p);
} else {
if (mInitFitText) {
fitTextAndInit();
}
canvas.drawText(mText, 0, getBounds().height(), mPaint);
}
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
int alpha = mPaint.getAlpha();
if (alpha == 0) {
return PixelFormat.TRANSPARENT;
} else if (alpha == 255) {
return PixelFormat.OPAQUE;
} else {
return PixelFormat.TRANSLUCENT;
}
}
private void init() {
Rect bounds = getBounds();
//We want to use some character to determine the max height of the text.
//Otherwise if we draw something like "..." they will appear centered
//Here I'm just going to use the entire alphabet to determine max height.
mPaint.getTextBounds("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 0, 1, mHeightBounds);
//This doesn't account for leading or training white spaces.
//mPaint.getTextBounds(mText, 0, mText.length(), bounds);
float width = mPaint.measureText(mText);
bounds.top = mHeightBounds.top;
bounds.bottom = mHeightBounds.bottom;
bounds.right = (int) width;
bounds.left = 0;
setBounds(bounds);
}
public void setPaint(Paint paint) {
mPaint = new Paint(paint);
//Since this can change the font used, we need to recalculate bounds.
if (mFitTextEnabled && !mInitFitText) {
fitTextAndInit();
} else {
init();
}
invalidateSelf();
}
public Paint getPaint() {
return mPaint;
}
public void setText(String text) {
mText = text;
//Since this can change the bounds of the text, we need to recalculate.
if (mFitTextEnabled && !mInitFitText) {
fitTextAndInit();
} else {
init();
}
invalidateSelf();
}
public String getText() {
return mText;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
setText(s.toString());
}
/**
* Make the TextDrawable match the width of the View it's associated with.
* <p/>
* Note: While this option will not work if bindToViewPaint is true.
*
* @param fitText
*/
public void setFillText(boolean fitText) {
mFitTextEnabled = fitText;
if (fitText) {
mPrevTextSize = mPaint.getTextSize();
if (ref.get() != null) {
if (ref.get().getWidth() > 0) {
fitTextAndInit();
} else {
mInitFitText = true;
}
}
} else {
if (mPrevTextSize > 0) {
mPaint.setTextSize(mPrevTextSize);
}
init();
}
}
private void fitTextAndInit() {
float fitWidth = ref.get().getWidth();
float textWidth = mPaint.measureText(mText);
float multi = fitWidth / textWidth;
mPaint.setTextSize(mPaint.getTextSize() * multi);
mInitFitText = false;
init();
}
}

View File

@@ -45,7 +45,9 @@ import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.util.Pair;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
@@ -73,6 +75,7 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.lists.DeleteList;
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
@@ -93,6 +96,8 @@ import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerAboutFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Emoji;
@@ -103,7 +108,6 @@ import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Searchable;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -115,6 +119,7 @@ import java.lang.reflect.Method;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
@@ -132,6 +137,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -528,31 +534,70 @@ public class UiUtils {
.exec(accountID);
});
}
public static void confirmToggleMuteUser(Context context, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){
View durationView=LayoutInflater.from(context).inflate(R.layout.mute_user_dialog, null);
LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(0, V.dp(-12), 0, 0);
durationView.setLayoutParams(params);
Button button=durationView.findViewById(R.id.button);
((TextView) durationView.findViewById(R.id.message)).setText(context.getString(R.string.confirm_mute, account.displayName));
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute),
currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
() -> {
new SetAccountMuted(account.id, !currentlyMuted)
.setCallback(new Callback<>() {
AtomicReference<Duration> muteDuration=new AtomicReference<>(Duration.ZERO);
PopupMenu popupMenu=new PopupMenu(context, button, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.mute_duration);
popupMenu.setOnMenuItemClickListener(item->{
int id=item.getItemId();
if(id==R.id.duration_indefinite)
muteDuration.set(Duration.ZERO);
else if(id==R.id.duration_minutes_5){
muteDuration.set(Duration.ofMinutes(5));
}else if(id==R.id.duration_minutes_30){
muteDuration.set(Duration.ofMinutes(30));
}else if(id==R.id.duration_hours_1){
muteDuration.set(Duration.ofHours(1));
}else if(id==R.id.duration_hours_6){
muteDuration.set(Duration.ofHours(6));
}else if(id==R.id.duration_days_1){
muteDuration.set(Duration.ofDays(1));
}else if(id==R.id.duration_days_3){
muteDuration.set(Duration.ofDays(3));
}else if(id==R.id.duration_days_7){
muteDuration.set(Duration.ofDays(7));
}
button.setText(item.getTitle());
return true;
});
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
button.setText(popupMenu.getMenu().getItem(0).getTitle());
new M3AlertDialogBuilder(context)
.setTitle(context.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title))
.setMessage(currentlyMuted ? context.getString(R.string.confirm_unmute, account.displayName) : null)
.setView(currentlyMuted ? null : durationView)
.setPositiveButton(context.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), (dlg, i)->{
new SetAccountMuted(account.id, !currentlyMuted, muteDuration.get().getSeconds())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result) {
public void onSuccess(Relationship result){
resultCallback.accept(result);
if (!currentlyMuted) {
if(!currentlyMuted){
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
public void onError(ErrorResponse error){
error.showToast(context);
}
})
.wrapProgress(activity, R.string.loading, false)
.wrapProgress(context, R.string.loading, false)
.exec(accountID);
});
})
.setNegativeButton(R.string.cancel, null)
.setIcon(currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular)
.show();
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback) {
@@ -702,9 +747,6 @@ public class UiUtils {
if(relationship.blocking){
button.setText(R.string.button_blocked);
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
}else if(relationship.blockedBy){
button.setText(R.string.button_follow);
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
}else if(relationship.requested){
button.setText(R.string.button_follow_pending);
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
@@ -716,7 +758,6 @@ public class UiUtils {
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
}
button.setEnabled(!relationship.blockedBy);
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
@@ -1028,8 +1069,8 @@ public class UiUtils {
return back;
}
public static boolean setExtraTextInfo(Context ctx, TextView extraText, TextView pronouns, boolean displayPronouns, boolean mentionedOnly, boolean localOnly, @Nullable Account account) {
List<String> extraParts = new ArrayList<>();
public static boolean setExtraTextInfo(Context ctx, @Nullable TextView extraText, @Nullable TextView pronouns, boolean displayPronouns, boolean mentionedOnly, boolean localOnly, @Nullable Account account) {
List<String> extraParts = extraText!=null && (localOnly || mentionedOnly) ? new ArrayList<>() : null;
Optional<String> p=pronouns==null || !displayPronouns ? Optional.empty() : extractPronouns(ctx, account);
if(p.isPresent()) {
HtmlParser.setTextWithCustomEmoji(pronouns, p.get(), account.emojis);
@@ -1041,7 +1082,7 @@ public class UiUtils {
extraParts.add(ctx.getString(R.string.sk_inline_local_only));
if(mentionedOnly)
extraParts.add(ctx.getString(R.string.sk_inline_direct));
if(!extraParts.isEmpty()) {
if(extraText!=null && extraParts!=null && !extraParts.isEmpty()) {
String sepp = ctx.getString(R.string.sk_separator);
String text = String.join(" " + sepp + " ", extraParts);
if(account == null) extraText.setText(text);
@@ -1049,7 +1090,7 @@ public class UiUtils {
extraText.setVisibility(View.VISIBLE);
return true;
}else{
extraText.setVisibility(View.GONE);
if(extraText!=null) extraText.setVisibility(View.GONE);
return false;
}
}
@@ -1239,6 +1280,23 @@ public class UiUtils {
}
})
.exec(accountID));
} else if (uri.getPath() != null && uri.getPath().matches("^/about$")) {
return Optional.of(new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
Bundle args = new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
args.putString("account", accountID);
go.accept(SettingsServerFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
go.accept(null, bundleError(error));
}
})
.execNoAuth(uri.getHost()));
} else if (looksLikeFediverseUrl(url)) {
return Optional.of(new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {

View File

@@ -27,10 +27,14 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.ProgressListener;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
@@ -47,8 +51,11 @@ import org.parceler.Parcel;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
@@ -550,6 +557,14 @@ public class ComposeMediaViewController{
public List<String> getAttachmentIDs(){
return attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
}
public List<CreateStatus.Request.MediaAttribute> getAttachmentAttributes(){
List<CreateStatus.Request.MediaAttribute> mediaAttributes = new ArrayList<>();
for (DraftMediaAttachment att:attachments){
mediaAttributes.add(new CreateStatus.Request.MediaAttribute(att.serverAttachment.id, att.description, null));
}
return mediaAttributes;
}
public boolean isEmpty(){
return attachments.isEmpty();
@@ -592,7 +607,7 @@ public class ComposeMediaViewController{
public void saveAltTextsBeforePublishing(Runnable onSuccess, Consumer<ErrorResponse> onError){
ArrayList<UpdateAttachment> updateAltTextRequests=new ArrayList<>();
for(DraftMediaAttachment att:attachments){
if(!att.descriptionSaved){
if(!att.descriptionSaved && att.serverAttachment.description == null){
UpdateAttachment req=new UpdateAttachment(att.serverAttachment.id, att.description);
req.setCallback(new Callback<>(){
@Override

View File

@@ -190,7 +190,10 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
}
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
if (item.account.isRemote)
args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}
@@ -210,6 +213,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
Account account=item.account;
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
menu.findItem(R.id.manage_user_lists).setTitle(fragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));

View File

@@ -40,11 +40,9 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
if(TextUtils.isEmpty(item.subtitle) && item.subtitleRes==0){
subtitle.setVisibility(View.GONE);
title.setMaxLines(2);
view.setMinimumHeight(V.dp(56));
}else{
subtitle.setVisibility(View.VISIBLE);
title.setMaxLines(1);
view.setMinimumHeight(V.dp(72));
if(TextUtils.isEmpty(item.subtitle))
subtitle.setText(item.subtitleRes);

View File

@@ -0,0 +1,44 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import me.grishka.appkit.views.UsableRecyclerView;
public class EmojiReactionsRecyclerView extends UsableRecyclerView{
public EmojiReactionsRecyclerView(Context context){
super(context);
}
public EmojiReactionsRecyclerView(Context context, AttributeSet attrs){
super(context, attrs);
}
public EmojiReactionsRecyclerView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent e){
super.onTouchEvent(e);
// to pass through touch events (i.e. clicking the status) to the parent view
return false;
}
// https://stackoverflow.com/questions/55372837/is-there-a-way-to-make-recyclerview-requiresfadingedge-unaffected-by-paddingtop
@Override
protected boolean isPaddingOffsetRequired() {
return true;
}
@Override
protected int getLeftPaddingOffset(){
return -getPaddingLeft();
}
@Override
protected int getRightPaddingOffset() {
return getPaddingRight();
}
}

View File

@@ -2,7 +2,7 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true">
<ripple android:color="@color/m3_on_secondary_container_overlay">
<item>
<item android:gravity="center_vertical" android:height="40dp">
<shape>
<solid android:color="?colorM3ErrorContainer"/>
<corners android:radius="20dp"/>
@@ -11,9 +11,13 @@
</ripple>
</item>
<item>
<shape>
<solid android:color="?colorM3DisabledBackground"/>
<corners android:radius="20dp"/>
</shape>
<layer-list>
<item android:gravity="center_vertical" android:height="40dp">
<shape>
<solid android:color="?colorM3DisabledBackground"/>
<corners android:radius="20dp"/>
</shape>
</item>
</layer-list>
</item>
</selector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="?colorM3Surface"/>
</item>
<item>
<shape android:tint="?colorM3Primary">
<solid android:color="?colorFilledCardAlpha"/>
</shape>
</item>
<item>
<ripple android:color="@color/m3_on_surface_variant_overlay">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
</shape>
</item>
</ripple>
</item>
</layer-list>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,15V13H20V15ZM4,11V9H20V11Z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,1.996C16.05,1.996 19.357,5.191 19.496,9.245L19.5,9.496V13.593L20.88,16.749C20.949,16.907 20.985,17.077 20.985,17.25C20.985,17.94 20.425,18.5 19.735,18.5L15,18.501C15,20.158 13.657,21.501 12,21.501C10.402,21.501 9.096,20.252 9.005,18.678L9,18.499L4.275,18.5C4.104,18.5 3.934,18.465 3.777,18.396C3.144,18.121 2.853,17.385 3.128,16.752L4.5,13.594V9.496C4.501,5.341 7.852,1.996 12,1.996ZM13.5,18.499L10.5,18.501C10.5,19.33 11.172,20.001 12,20.001C12.78,20.001 13.421,19.406 13.493,18.646L13.5,18.499ZM12,3.496C8.68,3.496 6.001,6.17 6,9.496V13.906L4.656,17H19.353L18,13.907L18,9.509L17.997,9.284C17.885,6.05 15.242,3.496 12,3.496Z"
android:fillColor="@color/fluent_default_icon_tint"/>
<path
android:pathData="M8.083,9.969A0.508,0.508 0,0 0,8.807 10.682L11.494,7.955L11.494,14.897a0.508,0.508 0,1 0,1.016 0L12.509,7.958L15.193,10.682A0.508,0.508 45,0 0,15.917 9.969L12.452,6.453a0.635,0.635 0,0 0,-0.904 0z"
android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M14.69 11.503c1 0 1.81 0.81 1.81 1.81v0.689h-0.005c-0.034 0.78-0.248 1.757-1.123 2.555C14.416 17.43 12.765 18 10 18c-2.766 0-4.416-0.57-5.372-1.443-0.875-0.798-1.089-1.776-1.123-2.555H3.5v-0.69c0-0.999 0.81-1.809 1.81-1.809h9.38zM6.5 3C5.672 3 5 3.672 5 4.5v4C5 9.328 5.672 10 6.5 10h7c0.828 0 1.5-0.672 1.5-1.5v-4C15 3.672 14.328 3 13.5 3h-3V2.5C10.5 2.191 10.276 2 10 2S9.5 2.23 9.5 2.5V3h-3zM7 6.5c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1zm4 0c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M8.46 1.897l0.99 0.39c0.353 0.138 0.746 0.138 1.099 0l0.99-0.39c1.21-0.477 2.582 0.091 3.102 1.285l0.424 0.975c0.151 0.348 0.429 0.626 0.777 0.777l0.975 0.424c1.194 0.52 1.762 1.891 1.285 3.103l-0.39 0.99c-0.139 0.352-0.139 0.745 0 1.098l0.39 0.99c0.477 1.21-0.091 2.582-1.285 3.102l-0.975 0.424c-0.348 0.151-0.626 0.429-0.777 0.777l-0.424 0.975c-0.52 1.194-1.891 1.762-3.103 1.285l-0.99-0.39c-0.352-0.139-0.745-0.139-1.098 0l-0.99 0.39c-1.21 0.477-2.582-0.091-3.102-1.285l-0.424-0.975c-0.151-0.348-0.429-0.626-0.777-0.777l-0.975-0.424c-1.194-0.52-1.762-1.891-1.285-3.103l0.39-0.99c0.138-0.352 0.138-0.745 0-1.098l-0.39-0.99C1.42 7.25 1.988 5.878 3.182 5.358l0.975-0.424c0.348-0.151 0.626-0.429 0.777-0.777l0.424-0.975C5.878 1.988 7.25 1.42 8.461 1.897zm3.445 0.93l-0.99 0.39c-0.588 0.232-1.243 0.232-1.831 0l-0.99-0.39C7.384 2.549 6.58 2.881 6.275 3.582L5.851 4.556C5.599 5.136 5.136 5.6 4.556 5.851L3.581 6.275c-0.7 0.305-1.033 1.109-0.753 1.82l0.389 0.989c0.232 0.588 0.232 1.243 0 1.831l-0.39 0.99c-0.279 0.71 0.054 1.514 0.754 1.819l0.975 0.424c0.58 0.252 1.043 0.715 1.295 1.295l0.424 0.975c0.305 0.7 1.109 1.033 1.82 0.753l0.989-0.39c0.588-0.23 1.243-0.23 1.831 0l0.99 0.39c0.71 0.28 1.514-0.053 1.819-0.753l0.424-0.975c0.252-0.58 0.715-1.043 1.295-1.295l0.975-0.424c0.7-0.305 1.033-1.11 0.753-1.82l-0.39-0.989c-0.23-0.588-0.23-1.243 0-1.831l0.39-0.99c0.28-0.71-0.053-1.514-0.753-1.819l-0.975-0.424c-0.58-0.252-1.043-0.715-1.295-1.295l-0.424-0.975c-0.305-0.7-1.11-1.033-1.82-0.753zm-2.927 8.944l3.648-4.104c0.183-0.206 0.5-0.225 0.706-0.041 0.183 0.163 0.218 0.43 0.095 0.633l-0.054 0.073-4 4.5c-0.17 0.19-0.451 0.22-0.655 0.081l-0.072-0.06-2-2c-0.195-0.195-0.195-0.512 0-0.707 0.173-0.174 0.443-0.193 0.638-0.058l0.069 0.058 1.625 1.625 3.648-4.104-3.648 4.104z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 2c-0.667 0-1.32 0.065-1.95 0.19-0.407 0.08-0.671 0.475-0.591 0.882 0.08 0.406 0.475 0.67 0.881 0.59C10.877 3.556 11.431 3.5 12 3.5s1.123 0.056 1.66 0.162c0.406 0.08 0.8-0.184 0.881-0.59 0.08-0.407-0.184-0.801-0.59-0.882C13.319 2.065 12.667 2 12 2zM7.278 4.931c0.344-0.23 0.436-0.696 0.206-1.04-0.23-0.344-0.697-0.437-1.04-0.206-1.09 0.73-2.03 1.668-2.76 2.758-0.23 0.345-0.137 0.81 0.207 1.04 0.344 0.231 0.81 0.139 1.04-0.205 0.621-0.927 1.42-1.726 2.347-2.347zm10.279-1.246c-0.345-0.23-0.81-0.138-1.04 0.206-0.231 0.344-0.139 0.81 0.205 1.04 0.927 0.621 1.726 1.42 2.347 2.347 0.23 0.344 0.696 0.436 1.04 0.206 0.344-0.23 0.437-0.697 0.206-1.04-0.73-1.09-1.668-2.03-2.758-2.76zm4.253 6.364c-0.08-0.406-0.475-0.67-0.882-0.59-0.406 0.08-0.67 0.475-0.59 0.881 0.106 0.537 0.162 1.091 0.162 1.66s-0.056 1.123-0.162 1.66c-0.08 0.406 0.184 0.8 0.59 0.881 0.407 0.08 0.801-0.184 0.882-0.59C21.935 13.319 22 12.667 22 12s-0.065-1.32-0.19-1.95zM3.662 10.34c0.08-0.406-0.184-0.8-0.59-0.881-0.407-0.08-0.801 0.184-0.882 0.59C2.065 10.681 2 11.333 2 12s0.065 1.32 0.19 1.95c0.08 0.407 0.475 0.671 0.882 0.591 0.406-0.08 0.67-0.475 0.59-0.881C3.556 13.123 3.5 12.569 3.5 12s0.056-1.123 0.162-1.66zm1.27 6.382C4.7 16.378 4.234 16.286 3.89 16.516s-0.437 0.697-0.206 1.04c0.73 1.09 1.668 2.03 2.758 2.76 0.345 0.23 0.81 0.137 1.04-0.207 0.231-0.344 0.139-0.81-0.205-1.04-0.927-0.621-1.726-1.42-2.347-2.347zm15.383 0.835c0.23-0.345 0.138-0.81-0.206-1.04-0.344-0.231-0.81-0.139-1.04 0.205-0.621 0.927-1.42 1.726-2.347 2.347-0.344 0.23-0.436 0.696-0.206 1.04 0.23 0.344 0.697 0.437 1.04 0.206 1.09-0.73 2.03-1.668 2.76-2.758zm-9.975 2.781c-0.406-0.08-0.8 0.184-0.881 0.59-0.08 0.407 0.184 0.801 0.59 0.882C10.681 21.935 11.333 22 12 22s1.32-0.065 1.95-0.19c0.407-0.08 0.671-0.475 0.591-0.882-0.08-0.406-0.475-0.67-0.881-0.59C13.123 20.444 12.569 20.5 12 20.5s-1.123-0.056-1.66-0.162zM9 9.248c0-0.952 1.023-1.554 1.856-1.093l5.757 3.186C16.852 11.473 17 11.724 17 11.997c0 0.272-0.148 0.523-0.387 0.655l-5.757 3.187C10.023 16.299 9 15.697 9 14.745V9.247z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="16" android:viewportHeight="16">
<group android:translateX="-2" android:translateY="-1">
<path android:pathData="M10 2c1.657 0 3 1.343 3 3v1h1c1.105 0 2 0.895 2 2v7c0 1.105-0.895 2-2 2H6c-1.105 0-2-0.895-2-2V8c0-1.105 0.895-2 2-2h1V5c0-1.657 1.343-3 3-3zm0 8.5c-0.552 0-1 0.448-1 1s0.448 1 1 1 1-0.448 1-1-0.448-1-1-1zM10 4C9.448 4 9 4.448 9 5v1h2V5c0-0.552-0.448-1-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
</group>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M5.5,19Q4.875,19 4.438,18.562Q4,18.125 4,17.5V9.5Q4,8.875 4.438,8.438Q4.875,8 5.5,8H6V6Q6,4.333 7.167,3.167Q8.333,2 10,2Q11.667,2 12.833,3.167Q14,4.333 14,6V8H14.5Q15.125,8 15.562,8.438Q16,8.875 16,9.5V17.5Q16,18.125 15.562,18.562Q15.125,19 14.5,19ZM7.5,8H12.5V6Q12.5,4.958 11.771,4.229Q11.042,3.5 10,3.5Q8.958,3.5 8.229,4.229Q7.5,4.958 7.5,6ZM10,15Q10.625,15 11.062,14.562Q11.5,14.125 11.5,13.5Q11.5,12.875 11.062,12.438Q10.625,12 10,12Q9.375,12 8.938,12.438Q8.5,12.875 8.5,13.5Q8.5,14.125 8.938,14.562Q9.375,15 10,15Z"/>
</vector>

View File

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

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/line"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/add_btn_wrap"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginStart="2dp">
<ProgressBar
android:id="@+id/progress"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:elevation="10dp"
android:indeterminate="true"
android:outlineProvider="none"
android:visibility="gone"/>
<ImageButton
android:id="@+id/add_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:minWidth="48dp"
android:background="@drawable/bg_button_m3_tonal_circle"
android:tooltipText="@string/sk_button_react"
android:contentDescription="@string/sk_button_react"
android:src="@drawable/ic_fluent_add_24_filled" />
</FrameLayout>
<org.joinmastodon.android.ui.views.EmojiReactionsRecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginStart="-4dp"
android:paddingStart="8dp"
android:paddingEnd="3dp"
android:clipToPadding="false"
android:requiresFadingEdge="horizontal"
android:fadingEdgeLength="24dp" />
</LinearLayout>
<Space
android:id="@+id/space"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="8dp" />
</LinearLayout>

View File

@@ -7,156 +7,171 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="11sp">
<!-- avatar width (46sp) / 2 - button width (24sp) / 2 -->
android:rowCount="2"
android:columnCount="1">
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/reply_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_fluent_chat_multiple_24_selector_text"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical" />
<TextView
android:id="@+id/reply"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="11sp">
<!-- avatar width (46sp) / 2 - button width (24sp) / 2 -->
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/boost_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:foregroundTint="@color/boost_icon"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_boost"
android:tint="@color/boost_icon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/boost"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:duplicateParentState="true"
android:textColor="@color/boost_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/favorite_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_fluent_star_24_selector"
android:tint="@color/favorite_icon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/favorite"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:textColor="@color/favorite_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/reply_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_fluent_chat_multiple_24_selector_text"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical" />
<TextView
android:id="@+id/reply"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/boost_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:foregroundTint="@color/boost_icon"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_boost"
android:tint="@color/boost_icon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/boost"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:duplicateParentState="true"
android:textColor="@color/boost_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/favorite_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:duplicateParentState="true"
android:src="@drawable/ic_fluent_star_24_selector"
android:tint="@color/favorite_icon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/favorite"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:paddingStart="8dp"
android:minWidth="16dp"
android:textColor="@color/favorite_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large"
android:maxLines="1"
android:ellipsize="end"
tools:text="123"
tools:ignore="RtlSymmetry" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:id="@+id/bookmark"
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="16dp"
android:src="@drawable/ic_fluent_bookmark_24_selector"
android:tint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
</FrameLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/share_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:id="@+id/bookmark"
android:id="@+id/share"
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="16dp"
android:src="@drawable/ic_fluent_bookmark_24_selector"
android:tint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
android:src="@drawable/ic_fluent_share_24_regular"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical"/>
</FrameLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/share_btn"
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingVertical="12dp">
<ImageView
android:id="@+id/share"
android:layout_width="24sp"
android:layout_height="24sp"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="16dp"
android:src="@drawable/ic_fluent_share_24_regular"
android:tint="?android:textColorSecondary"
android:gravity="center_vertical"/>
</FrameLayout>
android:layout_height="wrap_content"
android:id="@+id/footer_emoji_keyboard_container"
android:orientation="vertical">
</LinearLayout>
</GridLayout>
</LinearLayout>
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>

View File

@@ -152,20 +152,62 @@
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
<TextView
android:id="@+id/time_and_username"
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/time_and_username"
android:layout_alignBottom="@id/avatar"
android:layout_marginEnd="8dp"
android:layout_toStartOf="@id/buttons"
android:layout_toEndOf="@id/avatar"
android:layout_marginBottom="3sp"
android:textAlignment="viewStart"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="9h ago · \@Gargron@mastodon.social"/>
android:layout_marginBottom="3sp">
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@Gargron@mastodon.social"/>
<ImageView
android:id="@+id/bot_icon"
android:layout_width="16sp"
android:layout_height="16sp"
android:layout_marginStart="4sp"
android:layout_gravity="center_vertical"
android:visibility="gone"
android:importantForAccessibility="no"
android:contentDescription="@string/sk_icon_bot"
android:src="@drawable/ic_fluent_bot_20_filled" />
<TextView
android:id="@+id/separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4sp"
android:textAlignment="viewStart"
android:importantForAccessibility="no"
android:singleLine="true"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/sk_separator"/>
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="9h ago"/>
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
</RelativeLayout>

View File

@@ -8,83 +8,33 @@
android:paddingLeft="16dp"
android:clipToPadding="false">
<View
android:id="@+id/checkbox"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="-4dp"
android:layout_marginTop="-8dp"
android:layout_marginEnd="12dp"
android:duplicateParentState="true"/>
<FrameLayout
android:id="@+id/checkbox_wrap"
android:layout_width="wrap_content"
android:layout_height="46sp"
android:duplicateParentState="true">
<ImageView
android:id="@+id/more"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="-2dp"
android:layout_marginEnd="-2dp"
android:background="?android:selectableItemBackgroundBorderless"
android:scaleType="center"
android:tint="?colorM3OnSurfaceVariant"
android:contentDescription="@string/more_options"
android:src="@drawable/ic_fluent_more_vertical_20_filled" />
<View
android:id="@+id/checkbox"
android:layout_gravity="center_vertical"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="-4dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="12dp"
android:duplicateParentState="true"/>
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentTop="true"
android:layout_toEndOf="@id/checkbox"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp" />
</FrameLayout>
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:id="@+id/name_wrap"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/more"
android:layout_marginEnd="8dp">
android:layout_height="wrap_content"
android:layout_marginTop="-16dp"
android:layout_marginStart="-16dp"
android:layout_toEndOf="@id/checkbox_wrap">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/m3_title_medium"
android:textColor="?colorM3OnSurface"
android:gravity="start|center_vertical"
tools:text="Eugen" />
<include layout="@layout/display_item_header" />
<TextView
android:id="@+id/extra_text"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/m3_title_medium"
android:fontFamily="sans-serif"
android:textAlignment="viewStart"
android:textColor="?colorM3OnSurface"
tools:text="boosted your cat picture" />
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
<TextView
android:id="@+id/time_and_username"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="@id/name_wrap"
android:layout_toEndOf="@id/avatar"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_title_small"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="9h ago · \@Gargron@mastodon.social"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>

View File

@@ -56,19 +56,18 @@
<FrameLayout
android:id="@+id/avatar_border"
android:layout_width="104dp"
android:layout_height="104dp"
android:layout_width="108dp"
android:layout_height="108dp"
android:layout_below="@id/cover"
android:layout_alignParentStart="true"
android:layout_marginStart="12dp"
android:layout_marginTop="-44dp"
android:background="@drawable/profile_ava_bg"
android:outlineProvider="@null">
android:background="?colorM3Surface">
<ImageView
android:id="@+id/avatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:contentDescription="@string/profile_picture"
android:scaleType="centerCrop"
@@ -168,17 +167,56 @@
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
<TextView
android:id="@+id/username"
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
android:id="@+id/username_wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:paddingTop="2dp"
android:paddingBottom="8dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@Gargron" />
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="4dp"
android:maxLines="1"
android:ellipsize="end"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@Gargron" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="2dp">
<ImageView
android:id="@+id/lock_icon"
android:layout_width="18sp"
android:layout_height="18sp"
android:layout_marginEnd="4dp"
android:layout_gravity="center_vertical"
android:visibility="gone"
android:importantForAccessibility="no"
android:contentDescription="@string/manually_approves_followers"
android:src="@drawable/ic_fluent_lock_closed_20_filled" />
<ImageView
android:id="@+id/bot_icon"
android:layout_width="18sp"
android:layout_height="18sp"
android:layout_marginEnd="4dp"
android:layout_gravity="center_vertical"
android:visibility="gone"
android:importantForAccessibility="no"
android:contentDescription="@string/sk_icon_bot"
android:src="@drawable/ic_fluent_bot_20_filled" />
</LinearLayout>
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/bio"

View File

@@ -91,50 +91,6 @@
</RelativeLayout>
<LinearLayout
android:id="@+id/forward_report"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingHorizontal="8dp"
android:paddingVertical="8dp"
android:gravity="center_vertical"
android:layoutDirection="locale"
android:background="?android:selectableItemBackground">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="32dp"
android:importantForAccessibility="no"
android:tint="?android:textColorPrimary"
android:src="@drawable/ic_fluent_arrow_forward_24_regular"/>
<TextView
android:id="@+id/forward_report_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingVertical="8dp"
android:textSize="16sp"
android:text="@string/sk_forward_report_to"
android:textColor="?android:textColorPrimary" />
<Switch
android:id="@+id/forward_report_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duplicateParentState="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp"
android:focusable="false"
android:clickable="false"/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -3,74 +3,114 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorM3SurfaceVariant"
android:elevation="2dp"
android:paddingBottom="0dp">
android:background="@drawable/bg_filled_card">
<ImageView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="128dp"
android:layout_marginTop="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:scaleType="centerCrop"
tools:src="#0f0"/>
<View
android:id="@+id/avatar_border"
android:layout_width="60dp"
android:layout_height="60dp"
<FrameLayout
android:id="@+id/avatar_wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/cover"
android:layout_alignParentStart="true"
android:layout_marginTop="-6dp"
android:layout_marginStart="14dp"
android:background="@drawable/discover_ava_bg"/>
android:layout_marginTop="-12dp"
android:layout_marginStart="13dp"
android:layout_marginEnd="12dp">
<ImageView
android:id="@+id/avatar"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_below="@id/cover"
android:layout_alignParentStart="true"
android:layout_marginStart="16dp"
android:layout_marginTop="-4dp"
android:scaleType="centerCrop"
tools:src="#f00" />
<View
android:id="@+id/avatar_border"
android:layout_gravity="center"
android:layout_width="66dp"
android:layout_height="66dp"
android:background="@drawable/bg_filled_card"/>
<TextView
android:id="@+id/name"
<ImageView
android:id="@+id/avatar"
android:layout_gravity="center"
android:layout_width="60dp"
android:layout_height="60dp"
android:scaleType="centerCrop"
tools:src="#f00" />
</FrameLayout>
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:id="@+id/name_wrap"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/cover"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/avatar_wrap"
android:layout_marginEnd="16dp"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_medium"
tools:text="Eugen"/>
android:layout_above="@+id/username">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/m3_title_medium"
android:textColor="?colorM3OnSurface"
android:gravity="start|center_vertical"
tools:text="Eugen" />
<TextView
android:id="@+id/pronouns"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8sp"
android:maxWidth="161sp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/m3_title_medium"
android:fontFamily="sans-serif"
android:textAlignment="viewStart"
android:textColor="?colorM3OnSurface"
tools:text="they/them" />
<TextView
android:id="@+id/extra_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8sp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/m3_title_medium"
android:fontFamily="sans-serif"
android:textAlignment="viewStart"
android:textColor="?colorM3OnSurface"
tools:text="boosted your cat picture" />
</org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@id/name"
android:layout_alignEnd="@id/name"
android:layout_below="@id/name"
android:layout_alignBottom="@id/avatar_wrap"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@id/avatar_wrap"
android:layout_marginBottom="3sp"
android:textAlignment="viewStart"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@Gargron@mastodon.social"/>
<TextView
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/avatar"
android:layout_below="@id/avatar_wrap"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
tools:ignore="RtlSymmetry">
<ProgressBar
android:id="@+id/progress"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_gravity="center"
android:elevation="10dp"
android:indeterminate="true"
android:outlineProvider="none"
android:visibility="gone"/>
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/btn"
style="@style/Widget.Mastodon.M3.Button.Outlined.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:drawableTint="@null"
android:drawableStart="@drawable/image_placeholder"
android:background="@drawable/bg_button_m3_tonal"/>
</FrameLayout>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="24dp">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:textColor="?android:textColorPrimary"
android:text="@string/confirm_mute"
android:textSize="16sp"/>
<org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="locale">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:paddingVertical="8dp"
android:gravity="center_vertical"
android:textColor="?android:textColorPrimary"
android:text="@string/sk_mute_label"
android:textSize="16sp"/>
<Button
android:id="@+id/button"
style="@style/Widget.Mastodon.M3.Button.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_m3_tonal"
android:ellipsize="none"
android:singleLine="true"
android:stateListAnimator="@null"
android:textColor="?android:textColorPrimary"
android:textSize="16sp" />
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>
</LinearLayout>

View File

@@ -1,62 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:clipToPadding="false"
android:background="?colorM3Background">
android:background="?colorM3Background"
tools:ignore="RtlSymmetry">
<ImageView
android:id="@+id/dragger_thingy"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:layout_marginEnd="8dp"
android:scaleType="center"
android:tint="?colorM3OnSurface"
android:contentDescription="@string/reorder"
android:src="@drawable/ic_drag_handle_24px"/>
android:src="@drawable/ic_fluent_re_order_dots_vertical_24_regular"/>
<ImageButton
android:id="@+id/delete"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:scaleType="center"
android:tint="?colorM3OnSurface"
android:background="?android:actionBarItemBackground"
android:contentDescription="@string/delete"
android:src="@drawable/ic_fluent_delete_24_regular"/>
<EditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_toEndOf="@id/dragger_thingy"
android:layout_toStartOf="@id/delete"
style="@style/Widget.Mastodon.M3.EditText"
android:inputType="textCapSentences"
android:hint="@string/field_content"
android:saveEnabled="false"
android:singleLine="true"/>
<EditText
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginBottom="6dp"
android:layout_toEndOf="@id/dragger_thingy"
android:layout_toStartOf="@id/delete"
android:layout_below="@id/content"
style="@style/Widget.Mastodon.M3.EditText"
android:inputType="textCapSentences"
android:hint="@string/field_label"
android:saveEnabled="false"
android:singleLine="true"/>
<EditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/dragger_thingy"
android:layout_toStartOf="@id/delete"
android:layout_below="@id/title"
style="@style/Widget.Mastodon.M3.EditText"
android:inputType="textCapSentences"
android:hint="@string/field_content"
android:saveEnabled="false"
android:singleLine="true"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/content"
android:layout_marginTop="8dp"
android:background="?colorM3SurfaceVariant"/>
</RelativeLayout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/duration_indefinite" android:title="@string/sk_duration_indefinite" />
<item android:id="@+id/duration_minutes_5" android:title="@string/sk_duration_minutes_5"/>
<item android:id="@+id/duration_minutes_30" android:title="@string/sk_duration_minutes_30"/>
<item android:id="@+id/duration_hours_1" android:title="@string/sk_duration_hours_1"/>
<item android:id="@+id/duration_hours_6" android:title="@string/sk_duration_hours_6"/>
<item android:id="@+id/duration_days_1" android:title="@string/sk_duration_days_1"/>
<item android:id="@+id/duration_days_3" android:title="@string/sk_duration_days_3"/>
<item android:id="@+id/duration_days_7" android:title="@string/sk_duration_days_7"/>
</menu>

View File

@@ -4,11 +4,18 @@
<string name="next">التالي</string>
<string name="loading_instance">جارٍ جلب معلومات الخادم…</string>
<string name="error">خطأ</string>
<string name="not_a_mastodon_instance">لا يبدو أنّ %s كخادم ماستدون.</string>
<string name="ok">حسنًا</string>
<string name="preparing_auth">جَارٍ الإعدَادُ لِلمُصادَقَة…</string>
<string name="finishing_auth">يُنهي المصادقة…</string>
<string name="user_boosted">%s إعادة نشر</string>
<string name="in_reply_to">ردًا على %s</string>
<string name="notifications">الإشعارات</string>
<string name="user_followed_you">%s بَدَأ بِمُتابَعَتِك</string>
<string name="user_sent_follow_request">%s أرسَلَ طَلَبًا لِمُتابَعَتِك</string>
<string name="user_favorited">%s أعجَبه منشورك</string>
<string name="notification_boosted">قام %s بإعادة نشر منشورك</string>
<string name="poll_ended">الاطلاع على نتائج استطلاع الرأي الذي صوّت فيه</string>
<string name="share_toot_title">شارك</string>
<string name="settings">الإعدادات</string>
<string name="publish">انشر</string>
@@ -37,7 +44,8 @@
<string name="profile_about">حَول</string>
<string name="button_follow">تابِع</string>
<string name="button_following">مُتابَع</string>
<string name="edit_profile">حرّر الملف الشخصي</string>
<string name="edit_profile">تعديل الملف الشخصي</string>
<string name="share_user">شارك الصفحة الشخصية</string>
<string name="mute_user">كَتمُ %s</string>
<string name="unmute_user">إلغاء الكَتم عن @%s</string>
<string name="block_user">حَظرُ %s</string>
@@ -117,8 +125,16 @@
<item quantity="many">تبقى %d يومًا</item>
<item quantity="other">تبقى %d يوم</item>
</plurals>
<string name="poll_closed">انتهى</string>
<string name="confirm_mute_title">اكتم الحساب</string>
<plurals name="x_votes">
<item quantity="zero">%,d صوت</item>
<item quantity="one">%,d صوت واحد</item>
<item quantity="two">صوتين</item>
<item quantity="few">%,d أصوات</item>
<item quantity="many">%,d صوتا</item>
<item quantity="other">%,d صوتا</item>
</plurals>
<string name="poll_closed">مغلق</string>
<string name="confirm_mute_title">كتم الحساب</string>
<string name="confirm_mute">أكّد كتم %s</string>
<string name="do_mute">اكتم</string>
<string name="confirm_unmute_title">ارفع الكتم عن الحساب</string>
@@ -135,17 +151,20 @@
<string name="button_blocked">محجوب</string>
<string name="action_vote">صَوّت</string>
<string name="delete">احذف</string>
<string name="confirm_delete_title">احذف المنشور</string>
<string name="confirm_delete">أمتأكد من حذف هذا المنشور؟</string>
<string name="deleting">يحذف…</string>
<string name="notification_channel_audio_player">تشغيل الصوت</string>
<string name="play">شغّل</string>
<string name="pause">ألبث</string>
<string name="log_out">الخروج</string>
<string name="add_account">أضف حساباً</string>
<string name="search_hint">ابحث</string>
<string name="hashtags">وُسُوم</string>
<string name="news">الأخبار</string>
<string name="for_you">لأجلك</string>
<string name="mentions">الذِكر</string>
<string name="all_notifications">كلها</string>
<string name="mentions">الإشارات</string>
<plurals name="x_people_talking">
<item quantity="zero">لا أحد يتحدث</item>
<item quantity="one">شخص واحد يتحدث</item>
@@ -155,11 +174,16 @@
<item quantity="other">%d شخص يتحدثون</item>
</plurals>
<string name="report_title">بلّغ عن %s</string>
<string name="report_choose_reason">ما المُشكِلَةُ فِي هَذَا المَنشُور؟</string>
<string name="report_choose_reason_account">ما المُشكِلَة مَعَ %s؟</string>
<string name="report_choose_reason_subtitle">اختر أفضل تطابق</string>
<string name="report_reason_personal">لا يعجبني</string>
<string name="report_reason_personal_subtitle">ألا ترغب برؤيته</string>
<string name="report_reason_spam">إنه غير مرغوب فيه</string>
<string name="report_reason_spam_subtitle">روابط خبيثة أو تفاعل كاذب أو ردود متكررة</string>
<string name="report_reason_violation">ينتهك قواعد الخادم</string>
<string name="report_reason_violation_subtitle">تعلم أنه ينتهك قواعد محددة</string>
<string name="report_reason_violation_subtitle">أنت مُدرك لانتهاكه قواعد مُحَدَّدَة</string>
<string name="report_reason_other">إنَّهُ شَيءٌ آخَر</string>
<string name="report_reason_other_subtitle">لا تندرج هذه المشكلة ضمن فئات أخرى</string>
<string name="report_choose_rule">ما هي القواعد المنتهكة؟</string>
<string name="report_choose_rule_subtitle">اختر كل ما ينطبق</string>
@@ -168,8 +192,14 @@
<string name="report_comment_title">هل لديك شيء آخر لتخبرنا به؟</string>
<string name="report_comment_hint">تعليقات إضافية</string>
<string name="sending_report">يرسل البلاغ…</string>
<string name="report_sent_title">شُكرًا لَكَ على الإبلاغ، سَوفَ نتحرى عن الأمر.</string>
<string name="report_sent_subtitle">في أثناء مراجعتنا للبلاغ، يمكنك اتخاذ إجراء ضد %s:</string>
<string name="unfollow_user">ألغ متابعة %s</string>
<string name="unfollow">ألغ المتابعة</string>
<string name="mute_user_explain">لن ترى مشاركاتهم. لكن لا يزال بإمكانهم متابعتك ورؤية مشاركاتك ولن يعرفوا أنه تم كتم صوتها.</string>
<string name="block_user_explain">لن ترى مشاركاتهم. ولن يتمكنوا من رؤية مشاركاتك أو متابعتك. سيكونون قادرين على معرفة أنهم محظورون.</string>
<string name="report_personal_title">لا تريد أن ترى هذا؟</string>
<string name="report_personal_subtitle">فيما يلي خياراتك للتحكم بما يُعرَض عليك في ماستدون:</string>
<string name="back">العودة</string>
<string name="search_communities">اسم الخادم أو عنوان URL</string>
<string name="instance_rules_title">قواعد الخادم</string>
@@ -196,23 +226,39 @@
<string name="category_tech">تقني</string>
<string name="confirm_email_title">تحقق من صندوق الوارد الخاص بك</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">اضغط على الرابط الذي أرسلناه إليك للتحقق من %s. سننتظر هنا.</string>
<string name="confirm_email_didnt_get">ألم تحصل على رابط؟</string>
<string name="resend">أعد الإرسال</string>
<string name="open_email_app">افتح تطبيق البريد الإلكتروني</string>
<string name="resent_email">أُرسلت رسالة التأكيد</string>
<string name="compose_hint">عَبِّر عَمَّ يَجُولُ فِي ذِهنِك</string>
<string name="content_warning">تحذير من المحتوى</string>
<string name="save">احفظ</string>
<string name="add_alt_text">أضف نصًا بديلًا</string>
<string name="visibility_public">علني</string>
<string name="visibility_followers_only">للمُتابِعينَ فقط</string>
<string name="visibility_private">للمشار إليهم فقط</string>
<string name="recent_searches">الحديثة</string>
<string name="skip">تخطى</string>
<string name="notification_type_follow">متابعُون جُدُد</string>
<string name="notification_type_favorite">المفضلة</string>
<string name="notification_type_mention">الذِكر</string>
<string name="notification_type_reblog">المشاركات</string>
<string name="notification_type_mention">الإشارات</string>
<string name="notification_type_poll">استطلاع رأي</string>
<string name="choose_account">اختر حسابًا</string>
<string name="err_not_logged_in">يرجى تسجيل الدخول إلى حساب ماستدون أولًا</string>
<plurals name="cant_add_more_than_x_attachments">
<item quantity="zero">لا يمكن إرفاق أكثر من ملف واحد</item>
<item quantity="one">لا يمكن إرفاق أكثر من ملف واحد</item>
<item quantity="two">لا يمكنك إرفاق أكثر من %d ملفين</item>
<item quantity="few">لا يمكن إرفاق أكثر من %d ملفات</item>
<item quantity="many">لا يمكن إرفاق أكثر من %d ملفات</item>
<item quantity="other">لا يمكن إرفاق أكثر من %d ملف</item>
</plurals>
<string name="media_attachment_unsupported_type">نوع الملف %s غير مدعوم</string>
<string name="media_attachment_too_big">الملف %1$s يتجاوز حدّ %2$s مب</string>
<string name="settings_theme">المظهر</string>
<string name="theme_auto">استخدام مظهر الجهاز</string>
<string name="theme_light">فاتح</string>
<string name="theme_dark">داكن</string>
<string name="settings_behavior">السلوك</string>
@@ -225,9 +271,13 @@
<string name="settings_clear_cache">امسح التخزين المؤقت للوسائط</string>
<string name="settings_app_version">تطبيق ماستدون لأندرويد نسخة %1$s (%2$d)</string>
<string name="media_cache_cleared">مُسح التخزين المؤقت للوسائط</string>
<string name="confirm_log_out">تسجيل الخروج من %s؟</string>
<string name="sensitive_content_explain">وصف المؤلف هذه الوسائط بأنها حساسة.</string>
<string name="avatar_description">انتقل إلى الصفحة الشخصية لـ %s</string>
<string name="more_options">مزيد من الخيارات</string>
<string name="new_post">منشور جديد</string>
<string name="button_reply">ردّ</string>
<string name="button_reblog">شارك</string>
<string name="button_favorite">فضّل</string>
<string name="button_share">شارك</string>
<string name="media_no_description">وسائط بدون وصف</string>
@@ -239,7 +289,11 @@
<string name="media_viewer">عارض الوسائط</string>
<string name="follow_user">تابع %s</string>
<string name="unfollowed_user">ألغ متابعة %s</string>
<string name="followed_user">أنت تتابع الآن %s</string>
<string name="following_user_requested">طَلَبَ %s مُتابَعتك</string>
<string name="open_in_browser">افتح في المتصفح</string>
<string name="hide_boosts_from_user">اخف مشاركات %s</string>
<string name="show_boosts_from_user">أظهر مشاركات %s</string>
<string name="signup_reason">لماذا تريد الانضمام؟</string>
<string name="signup_reason_note">هذا سوف يساعدنا في مراجعة تطبيقك.</string>
<string name="clear">امسح</string>
@@ -253,7 +307,13 @@
<string name="error_saving_file">خطأ أثناء حفظ الملف</string>
<string name="file_saved">حُفظ الملف</string>
<string name="downloading">ينزّل…</string>
<string name="no_app_to_handle_action">لا يوجد تطبيق لمعالجة هذا الإجراء</string>
<string name="local_timeline">المحلي</string>
<string name="trending_posts_info_banner">هذه هي المشاركات التي تكتسب شعبية عبر ماستدون.</string>
<string name="trending_links_info_banner">هذه هي القصص الإخبارية التي يُتحدّث عنها على ماستدون.</string>
<!-- %s is the server domain -->
<string name="local_timeline_info_banner">هذه هي جميع المشاركات من جميع المستخدمين في الخادم الخاص بك (%s).</string>
<string name="recommended_accounts_info_banner">قد تعجبك هذه الحسابات استنادا إلى حسابات أخرى تتابعها.</string>
<string name="see_new_posts">استعرض المنشورات الجديدة</string>
<string name="load_missing_posts">حمّل المَنشورات المَفقودَة</string>
<string name="follow_back">رُدّ المتابعة</string>
@@ -285,6 +345,14 @@
<item quantity="many">%,d تفضيلًا</item>
<item quantity="other">%,d تفضيل</item>
</plurals>
<plurals name="x_reblogs">
<item quantity="zero">%,d إعادة نشر</item>
<item quantity="one">إعادة نشر واحدة</item>
<item quantity="two">أعيد نشره مرّتان</item>
<item quantity="few">أعيد نشره %,d مرة</item>
<item quantity="many">أعيد نشره %,d مرات</item>
<item quantity="other">أعيد نشره %,d مرات</item>
</plurals>
<string name="timestamp_via_app">%1$s عبر %2$s</string>
<string name="time_now">الآن</string>
<string name="edit_history">تاريخ التعديل</string>
@@ -294,7 +362,7 @@
<item quantity="zero">منذ %d ثانية</item>
<item quantity="one">منذ ثانية</item>
<item quantity="two">منذ ثانيتان</item>
<item quantity="few">%d ثواني</item>
<item quantity="few">%d ثوانٍ</item>
<item quantity="many">منذ %d ثانية</item>
<item quantity="other">%d ثواني مضت</item>
</plurals>
@@ -347,6 +415,7 @@
<string name="login_title">مرحبا بك مجددًا</string>
<string name="login_subtitle">قم بتسجيل الدخول باستخدام الخادم حيث قمتَ بإنشاء حسابك فيه.</string>
<string name="server_url">رابط الخادم</string>
<string name="signup_random_server_explain">سوف نختار خادماً بناءً على لغتك إذا قمت بالمتابعة دون إجراء إختيار.</string>
<string name="server_filter_any_language">أي لغة</string>
<string name="server_filter_instant_signup">تسجيل فوري</string>
<string name="server_filter_manual_review">مراجعة يدوية</string>
@@ -359,6 +428,7 @@
<string name="server_filter_region_oceania">أوقيانوسيا</string>
<string name="not_accepting_new_members">لا يقبل استقبال أعضاء جدد</string>
<string name="category_special_interests">المصالح الخاصة</string>
<string name="signup_passwords_dont_match">كلمات المرور غير متطابقة</string>
<string name="pick_server_for_me">اختر لي</string>
<string name="profile_add_row">إضافة صف</string>
<string name="profile_setup">إعداد الملف الشخصي</string>
@@ -367,21 +437,265 @@
<string name="popular_on_mastodon">مشهور على ماستدون</string>
<string name="follow_all">اتبع الكل</string>
<string name="server_rules_disagree">لا أوافق</string>
<string name="privacy_policy_explanation">بالمختصر: نحن لا نجمع أو نعالج أي شيء.</string>
<!-- %s is server domain -->
<string name="server_policy_disagree">لا أوافق %s</string>
<string name="profile_bio">نبذة عنك</string>
<!-- Shown in a progress dialog when you tap "follow all" -->
<string name="sending_follows">متابعة المستخدمين…</string>
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="signup_email_domain_blocked">%1$s لا يسمح بالاشتراكات من %2$s. جرب خادما واحدا أو &lt;a&gt;اختر خادما مختلفا&lt;/a&gt;.</string>
<string name="spoiler_show">إظهاره على أي حال</string>
<string name="spoiler_hide">إعادة الإخفاء</string>
<string name="poll_multiple_choice">اختر واحدا أو أكثر</string>
<string name="save_changes">حفظ التغييرات</string>
<string name="profile_featured">المميزة</string>
<string name="profile_timeline">الخيط</string>
<string name="view_all">عرض الكل</string>
<string name="profile_endorsed_accounts">الحسابات</string>
<string name="verified_link">رابط متحقق منه</string>
<string name="show">إظهار</string>
<string name="hide">إخفاء</string>
<string name="join_default_server">الانضمام إلى %s</string>
<string name="pick_server">اختر خادما آخر</string>
<string name="signup_or_login">أو</string>
<string name="learn_more">تعلم المزيد</string>
<string name="welcome_to_mastodon">أهلًا بك على ماستدون</string>
<string name="welcome_paragraph1">ماستدون شبكة اجتماعية لامركزية، بمعنى أنه ليس هناك شركة واحدة تتحكم فيها. وهي تتألف من العديد من الخوادم التي تدار بشكل مستقل، وجميعها متصلة معا.</string>
<string name="what_are_servers">ما هي الخوادم؟</string>
<string name="welcome_paragraph2">تتم استضافة كل حساب ماستدون على خادم - ولكل خادم قيمه وقواعده ومسؤوليه الخاصين. مهما اخترت أي خادم، يمكنك متابعة الأشخاص والتفاعل معهم على أي خادم آخر.</string>
<string name="opening_link">رابط الافتتاح…</string>
<string name="link_not_supported">هذا الرابط غير مدعوم في التطبيق</string>
<string name="log_out_all_accounts">تسجيل الخروج من جميع الحسابات</string>
<string name="confirm_log_out_all_accounts">أتريد تسجيل الخروج من جميع الحسابات؟</string>
<string name="retry">حاول مجددًا</string>
<string name="post_failed">أخفق في الإرسال</string>
<!-- %s is formatted file size ("467 KB image") -->
<string name="attachment_description_image">صورة %s</string>
<string name="attachment_description_video">فيديو %s</string>
<string name="attachment_description_audio">مقطع صوتي %s</string>
<string name="attachment_description_unknown">ملف %s</string>
<string name="attachment_type_image">صورة</string>
<string name="attachment_type_video">فيديو</string>
<string name="attachment_type_audio">مقطع صوتي</string>
<string name="attachment_type_gif">GIF</string>
<string name="attachment_type_unknown">ملف</string>
<string name="attachment_x_percent_uploaded">%d%% تم الرفع</string>
<string name="add_poll_option">إضافة خيار للاستطلاع</string>
<string name="poll_length">مدة الاستطلاع</string>
<string name="poll_style">النوع</string>
<string name="compose_poll_single_choice">اختر واحدا</string>
<string name="compose_poll_multiple_choice">خيارات متعددة</string>
<string name="delete_poll_option">حذف خيار من الاستطلاع</string>
<string name="poll_style_title">نمط الاستطلاع</string>
<string name="alt_text">نص بديل</string>
<string name="help">المساعدة</string>
<string name="what_is_alt_text">ما هو النص البديل؟</string>
<string name="alt_text_help">يوفر النص البديل أوصافا للصور للأشخاص الذين يعانون من إعاقات بصرية أو اتصالات ذات نطاق ترددي منخفض أو أولئك الذين يبحثون عن سياق إضافي.\n\nيمكنك تحسين إمكانية الوصول والفهم للجميع من خلال كتابة نص بديل واضح وموجز وموضوعي.\n\n التقاط العناصر المهمة\n<ul><li>تلخيص النص في الصور</li>\n<li>استخدام بنية الجملة العادية</li>\n<li>تجنب المعلومات الزائدة</li>\n<li>التركيز على الاتجاهات والنتائج الرئيسية في العناصر المرئية المعقدة (مثل الرسوم البيانية أو الخرائط)</li><li></li></ul></string>
<string name="edit_post">تعديل المنشور</string>
<string name="no_verified_link">لم يتم التحقق من الرابط</string>
<string name="compose_autocomplete_emoji_empty">تصفح الرموز التعبيرية</string>
<string name="compose_autocomplete_users_empty">العثور على الأشخاص الذين تبحث عنهم</string>
<string name="no_search_results">تعذر العثور على أي نتائج لمصطلحات البحث هذه</string>
<string name="language">اللغة</string>
<string name="language_default">الافتراضية</string>
<string name="language_system">النظام</string>
<string name="language_detecting">اكتشاف اللغة</string>
<string name="language_cant_detect">تعذر اكتشاف اللغة</string>
<string name="language_detected">الكشف عن</string>
<string name="media_hidden">وسائط مخفية</string>
<string name="post_hidden">منشور مخفي</string>
<string name="report_title_post">الإبلاغ عن المنشور</string>
<string name="forward_report_explanation">الحساب من خادم آخر. هل تودّ إرسال نسخة مجهولة المصدر من هذا التقرير هناك أيضا؟</string>
<!-- %s is the server domain -->
<string name="forward_report_to_server">تحويله إلى %s</string>
<!-- Shown on the "stamp" on the screen that appears after you report a post/user. Please keep the translation short, preferably a single word -->
<string name="reported">تم الإبلاغ عنه</string>
<string name="report_unfollow_explanation">لعدم رؤية مشاركاتهم في خلاصة ملخصك بعد الآن، ألغِ متابعتهم.</string>
<string name="muted_user">كتم %s</string>
<string name="report_sent_already_blocked">لقد حظرت هذا المستخدم من قبل، لذلك لا يوجد شيء آخر عليك القيام به خلال مراجعة بلاغك.</string>
<string name="report_personal_already_blocked">لقد قمت بالفعل بحظر هذا المستخدم، لذلك لا يوجد شيء آخر عليك القيام به.\n\nشكرا للمساعدة في الحفاظ على ماستدون مكانا آمنا للجميع!</string>
<string name="blocked_user">حظر %s</string>
<string name="mark_all_notifications_read">اعتبار الكل كمقروء</string>
<string name="settings_display">الشاشة</string>
<string name="settings_filters">عوامل التصفية</string>
<string name="settings_server_explanation">نظرة عامة وقواعد ومشرفين</string>
<!-- %s is the app name (Mastodon, key app_name). I made it a placeholder so everything Just Works™ with forks -->
<string name="about_app">عن %s</string>
<string name="default_post_language">اللغة الافتراضية للمنشور</string>
<string name="settings_alt_text_reminders">إضافة تذكير بالنصوص البديلة</string>
<string name="settings_confirm_unfollow">السؤال قبل إلغاء متابعة شخص ما</string>
<string name="settings_confirm_boost">اسأل قبل إعادة النشر</string>
<string name="settings_confirm_delete_post">السؤال قبل حذف المشاركات</string>
<string name="pause_all_notifications">إيقاف الكل</string>
<string name="pause_notifications_off">إيقاف</string>
<string name="notifications_policy_anyone">أيا كان</string>
<string name="notifications_policy_followed">الأشخاص الذين تتابعهم</string>
<string name="notifications_policy_follower">الأشخاص الذين تتابعهم</string>
<string name="notifications_policy_no_one">لا أحد</string>
<string name="settings_notifications_policy">تلقي الإشعارات من</string>
<string name="notification_type_mentions_and_replies">الإشارات والردود</string>
<string name="pause_all_notifications_title">إيقاف جميع الإشعارات مؤقتًا</string>
<plurals name="x_weeks">
<item quantity="zero">%d أسبوع</item>
<item quantity="one">أسبوع واحد</item>
<item quantity="two">أسبوعان</item>
<item quantity="few">%d أسابيع</item>
<item quantity="many">%d أسبوعًا</item>
<item quantity="other">%d أسابيع</item>
</plurals>
<!-- %1$s is the date (may be relative, e.g. "today" or "yesterday"), %2$s is the time. You can reorder these placeholders if that works better for your language -->
<string name="date_at_time">%1$s في %2$s</string>
<string name="today">اليوم</string>
<string name="yesterday">أمس</string>
<string name="tomorrow">غدًا</string>
<!-- %s is the timestamp ("tomorrow at 12:34") -->
<string name="pause_notifications_ends">ينتهي %s</string>
<!-- %s is the timestamp ("tomorrow at 12:34") -->
<string name="pause_notifications_banner">سيتم استئناف الإشعارات %s.</string>
<string name="resume_notifications_now">استأنف الآن</string>
<string name="open_system_notification_settings">الانتقال إلى إعدادات الإشعارات</string>
<string name="about_server">عن</string>
<string name="server_rules">القواعد</string>
<string name="server_administrator">المدير</string>
<string name="send_email_to_server_admin">للاتصال بالمدير</string>
<string name="notifications_disabled_in_system">شغل الإشعارات من إعدادات جهازك لرؤية التحديثات من أي مكان.</string>
<string name="settings_even_more">المزيد من الإعدادات</string>
<string name="settings_show_cws">إظهار تحذيرات المحتوى</string>
<string name="settings_hide_sensitive_media">فلرتة الوسائط التي تم وضع علامة عليها على أنها حساسة</string>
<string name="settings_show_interaction_counts">عدد التفاعل مع المنشورات</string>
<string name="settings_show_emoji_in_names">رموز تعبيرية مخصصة في أسماء العرض</string>
<plurals name="in_x_seconds">
<item quantity="zero">في %d ثانية</item>
<item quantity="one">في ثانية واحدة</item>
<item quantity="two">في ثانيتين</item>
<item quantity="few">في %d ثوانٍ</item>
<item quantity="many">في %d ثانية</item>
<item quantity="other">في %d ثوان</item>
</plurals>
<plurals name="in_x_minutes">
<item quantity="zero">في %d دقيقة</item>
<item quantity="one">في دقيقة واحدة</item>
<item quantity="two">في دقيقتين</item>
<item quantity="few">في %d دقائق</item>
<item quantity="many">في %d دقيقة</item>
<item quantity="other">في %d دقائق</item>
</plurals>
<plurals name="in_x_hours">
<item quantity="zero">في %d ساعة</item>
<item quantity="one">خلال ساعة واحدة</item>
<item quantity="two">خلال ساعتان</item>
<item quantity="few">خلال %d ساعات</item>
<item quantity="many">خلال %d ساعة</item>
<item quantity="other">خلال %d ساعات</item>
</plurals>
<plurals name="x_hours_ago">
<item quantity="zero">منذ %d ساعات</item>
<item quantity="one">منذ ساعة واحدة</item>
<item quantity="two">منذ ساعتان</item>
<item quantity="few">منذ %d ساعات</item>
<item quantity="many">منذ %d ساعة</item>
<item quantity="other">منذ %d ساعات</item>
</plurals>
<string name="alt_text_reminder_title">تفتقد الوسائط إلى نص بديل</string>
<plurals name="alt_text_reminder_x_images">
<item quantity="zero">%s من صورك يفتقر إلى نص بديل. أتردد النشر على أي حال؟</item>
<item quantity="one">%s من صورك يفتقر إلى نص بديل. أتردد النشر على أي حال؟</item>
<item quantity="two">%s من صورك يفتقر إلى نص بديل. أتردد النشر على أي حال؟</item>
<item quantity="few">%s من صورك يفتقر إلى نص بديل. أتردد النشر على أي حال؟</item>
<item quantity="many">%s من صورك يفتقر إلى نص بديل. أتردد النشر على أي حال؟</item>
<item quantity="other">%s من صورك يفتقر إلى نص بديل. أتردد النشر على أي حال؟</item>
</plurals>
<plurals name="alt_text_reminder_x_attachments">
<item quantity="zero">%s من مرفقات الوسائط الخاصة بك يفتقر لنص بديل. نشر على أي حال؟</item>
<item quantity="one">%s من مرفقات الوسائط الخاصة بك يفتقر لنص بديل. نشر على أي حال؟</item>
<item quantity="two">%s من مرفقات الوسائط الخاصة بك يفتقر لنص بديل. نشر على أي حال؟</item>
<item quantity="few">%s من مرفقات الوسائط الخاصة بك يفتقر لنص بديل. نشر على أي حال؟</item>
<item quantity="many">%s من مرفقات الوسائط الخاصة بك يفتقر لنص بديل. نشر على أي حال؟</item>
<item quantity="other">%s من مرفقات الوسائط الخاصة بك يفتقر لنص بديل. نشر على أي حال؟</item>
</plurals>
<string name="count_one">واحد</string>
<string name="count_two">اثنان</string>
<string name="count_three">ثلاثة</string>
<string name="count_four">أربعة</string>
<string name="alt_text_reminder_post_anyway">مَنشور</string>
<!-- %s is the username -->
<string name="unfollow_confirmation">أتريد إلغاء متابعة %s؟</string>
<string name="filter_active">نشِط</string>
<string name="filter_inactive">خامل</string>
<string name="settings_add_filter">إضافة عامل تصفية</string>
<string name="settings_edit_filter">تعديل عامل التصفية</string>
<string name="settings_filter_duration">المدة</string>
<string name="settings_filter_muted_words">الكلمات المحظورة</string>
<string name="settings_filter_context">كتم الصوت من</string>
<string name="settings_filter_show_cw">عرض مع تحذير المحتوى</string>
<string name="settings_filter_show_cw_explanation">الاستمرار في عرض المشاركات التي تطابق هذا الفلتر، ولكن خلف تحذير حول المحتوى</string>
<string name="settings_delete_filter">حذف عامل التصفية</string>
<string name="filter_duration_forever">إلى الأبد</string>
<!-- %s is the timestamp ("tomorrow at 12:34") -->
<string name="settings_filter_ends">ينتهي %s</string>
<plurals name="settings_x_muted_words">
<item quantity="zero">%d كلمة أو عبارة مكتومة</item>
<item quantity="one">%d كلمة أو عبارة مكتومة</item>
<item quantity="two">%d كلمتان أو عبارتان مكتومتان</item>
<item quantity="few">%d كلمة أو عبارة مكتومة</item>
<item quantity="many">%d كلمة أو عبارة مكتومة</item>
<item quantity="other">%d كلمة أو عبارة مكتومة</item>
</plurals>
<string name="selection_2_options">%1$s و %2$s</string>
<string name="selection_3_options">%1$s و %2$s و %3$s</string>
<string name="selection_4_or_more">%1$s, %2$s, و %3$d والمزيد</string>
<string name="filter_context_home_lists">الخيط الزمني الرئيسي والقوائم</string>
<string name="filter_context_notifications">الإشعارات</string>
<string name="filter_context_public_timelines">الخيوط الزمنية العامة</string>
<string name="filter_context_threads_replies">سلاسل المحادثات والردود</string>
<string name="filter_context_profiles">الصفحات التعريفية</string>
<string name="settings_filter_title">العنوان</string>
<string name="settings_delete_filter_title">حذف عامل التصفية \"%s\"؟</string>
<string name="settings_delete_filter_confirmation">سيتم حذف هذا الفلتر من حسابك على جميع الأجهزة.</string>
<string name="add_muted_word">إضافة كلمة مكتومة</string>
<string name="edit_muted_word">تحرير كلمة مكتومة</string>
<string name="add">إضافة</string>
<string name="filter_word_or_phrase">كلمة أو عبارة</string>
<string name="filter_add_word_help">الكلمات غير حساسة لحالة الأحرف وتتطابق مع الكلمات الكاملة فقط.\n\nإذا قمت بتصفية الكلمة الرئيسية \"Apple\" ، فستخفي المشاركات التي تحتوي على \"apple\" أو \"aPpLe\" ولكن ليس \"pineapple.\"</string>
<string name="settings_delete_filter_word">حذف الكلمة \"%s\"؟</string>
<string name="enter_selection_mode">اختر</string>
<string name="select_all">اختيار الكل</string>
<string name="settings_filter_duration_title">مدة التصفية</string>
<string name="filter_duration_custom">مخصص</string>
<plurals name="settings_delete_x_filter_words">
<item quantity="zero">حذف %d كلمات؟</item>
<item quantity="one">حذف كلمة واحدة؟</item>
<item quantity="two">حذف كلمتان؟</item>
<item quantity="few">حذف %d كلمات؟</item>
<item quantity="many">حذف %d كلمة؟</item>
<item quantity="other">حذف %d كلمات؟</item>
</plurals>
<plurals name="x_items_selected">
<item quantity="zero">تم تحديد %d</item>
<item quantity="one">تم تحديد %d</item>
<item quantity="two">%d تم تحديدها</item>
<item quantity="few">%d تم تحديدها</item>
<item quantity="many">%d تم تحديدها</item>
<item quantity="other">%d تم تحديدها</item>
</plurals>
<string name="required_form_field_blank">لا يمكن أن يكون فارغاً</string>
<string name="filter_word_already_in_list">موجود بالفعل في القائمة</string>
<string name="app_update_ready">تحديث التطبيق جاهز</string>
<string name="app_update_version">الإصدار %s</string>
<string name="downloading_update">جارٍ التنزيل (%d%%)</string>
<!-- Shown like a content warning, %s is the name of the filter -->
<string name="post_matches_filter_x">تطابق عامل التصفية \"%s\"</string>
<string name="search_mastodon">البحث في ماستدون</string>
<string name="clear_all">امسح الكل</string>
<string name="search_open_url">فتح الرابط التشعبي في ماستدون</string>
<string name="posts_matching_hashtag">منشورات تحتوي على “%s”</string>
<string name="search_go_to_account">الانتقال إلى %s</string>
<string name="posts_matching_string">منشورات تحتوي على “%s”</string>
<string name="accounts_matching_string">أشخاص لديهم \"%s\"</string>
<!-- Shown in the post header. Please keep it short -->
<string name="time_seconds_ago_short">مُنذُ %dثا</string>
<string name="time_minutes_ago_short">مُنذُ %dد</string>
<string name="time_hours_ago_short">مُنذُ %dسا</string>
<string name="time_days_ago_short">مُنذُ %d أيام</string>
</resources>

View File

@@ -404,7 +404,6 @@
<string name="welcome_to_mastodon">Вітаем у Mastodon</string>
<string name="welcome_paragraph1">Mastodon - гэта дэцэнтралізаваная сацыяльная сетка, што азначае, што ні адна кампанія не кантралюе яе. Яна складаецца з мноства незалежна працуючых сервераў, злучаных разам.</string>
<string name="what_are_servers">Што такое серверы?</string>
<string name="welcome_paragraph2"><![CDATA[Кожны акаўнт Mastodon размяшчаецца на серверы - кожны са сваімі каштоўнасцямі, правіламі і адміністратарамі. Незалежна ад таго, які сервер вы вылучыце, вы можаце сачыць і ўзаемадзейнічаць з людзьмі на любым серверы.]]></string>
<string name="retry">Паўтарыць</string>
<!-- %s is formatted file size ("467 KB image") -->
<string name="attachment_type_video">Відэа</string>

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