Compare commits

...

528 Commits

Author SHA1 Message Date
sk
94d2406fa4 bump version 2022-11-12 20:32:30 +01:00
sk
8e8d24c828 Merge branch 'feature/delete-redraft' into fork 2022-11-12 20:31:57 +01:00
sk
91bd600f93 don't lock visibility for redrafted posts
fixes #40
2022-11-12 20:31:30 +01:00
sk
11067b9818 update readme 2022-11-12 20:24:00 +01:00
sk
7bfd841769 Merge branch 'feature/lists' into fork 2022-11-12 20:23:16 +01:00
sk
5c593a1025 improve adding/removing users from lists 2022-11-12 20:20:45 +01:00
sk
625134605b implement adding/removing users from lists 2022-11-12 16:14:45 +01:00
sk
1e0ae6e570 Merge branch 'main' into list-timeline-views 2022-11-12 12:35:05 +01:00
sk
f3efd1e3bc Merge branch 'feature/follow-requests' into fork 2022-11-12 03:22:27 +01:00
sk
dba8bd1862 clean up, revert unnecessary changes 2022-11-12 03:21:40 +01:00
sk
d78e3a738d bump version 2022-11-12 03:06:40 +01:00
sk
3898ad0c1c update readme 2022-11-12 03:06:11 +01:00
sk
5632016220 Merge branch 'feature/follow-requests' into fork 2022-11-12 03:05:02 +01:00
sk
12fdd70ad0 implement accepting/rejecting follow requests
closes #13
2022-11-12 03:03:52 +01:00
sk
7fd6a6f83e Merge branch 'feature/follow-requests' into fork 2022-11-12 03:03:14 +01:00
sk
515592e8ea implement accepting/rejecting follow requests
closes #13
2022-11-12 03:01:59 +01:00
sk
5fa81e6c8a add accept/decline buttons 2022-11-12 01:18:07 +01:00
sk
c22a139a5d bump version 2022-11-11 21:09:31 +01:00
sk
bed95e54d2 update readme 2022-11-11 21:07:36 +01:00
sk
f568415d8c Merge branch 'feature/favs-list' into fork 2022-11-11 21:06:53 +01:00
sk
c1137cf7b7 implement favorited posts list
closes #4
2022-11-11 21:05:06 +01:00
sk
7f678945be add title for github pages, update readme 2022-11-11 20:44:14 +01:00
sk
bb72d43270 update readme layout 2022-11-11 20:29:01 +01:00
sk
67524f6e53 update readme 2022-11-11 20:23:11 +01:00
sk
b2a7b62902 Rename “Community“ to “Local” 2022-11-11 19:57:19 +01:00
sk
a356bdeee3 change github and website url 2022-11-11 19:33:26 +01:00
sk
1438e65c10 change gh pages jekyll theme 2022-11-11 19:31:51 +01:00
sk
76043e87ad Merge branch 'fix-edit-polls' into fork 2022-11-11 19:20:16 +01:00
sk
3b28bd6ce9 get poll duration from edited status
fixes mastodon#343
2022-11-11 19:10:45 +01:00
sk
1236b16a3a Merge branch 'feature/delete-redraft' into fork 2022-11-11 18:00:09 +01:00
sk
5ec4e5339b fix null pointer exception 2022-11-11 17:59:53 +01:00
sk
0643f72a0b Merge branch 'feature/delete-redraft' into fork 2022-11-11 17:57:27 +01:00
sk
443b985b06 navigate to re-drafted status when opened 2022-11-11 17:53:04 +01:00
sk
aecaba2a92 show reply icon as workaround for mastodon#341 2022-11-11 17:18:21 +01:00
sk
977f3d0635 fix re-drafting replies 2022-11-11 17:17:52 +01:00
sk
5742493185 re-implement redraft from upstream edit function 2022-11-11 16:12:26 +01:00
sk
8b2d06c548 Merge branch 'main' into feature/delete-redraft 2022-11-11 15:36:33 +01:00
sk
56b76080ec Revert "implement deleting and re-drafting"
This reverts commit 3a4d13b1c6.
2022-11-11 15:35:30 +01:00
sk
1a12659a23 Revert "preserve visibility when re-drafting"
This reverts commit e8b43c7179.
2022-11-11 15:35:26 +01:00
sk
d711ed986c set hiding interactions counts as default 2022-11-11 14:58:39 +01:00
sk
53cb809996 bump version, update readme 2022-11-11 13:26:48 +01:00
sk
ff0a77d6b5 Merge branch 'settings/hide-interaction-numbers' into fork 2022-11-11 13:25:35 +01:00
sk
596ec3230f add option to hide interaction counts 2022-11-11 13:23:53 +01:00
sk
fa75c8eb6f Merge branch 'compact-extended-footer' into fork 2022-11-11 13:03:51 +01:00
sk
6c7a17fb81 change edit history icon 2022-11-11 13:03:39 +01:00
sk
0ad8f926cc set reblogs and favs always in one line 2022-11-11 12:54:17 +01:00
sk
764ab60fea bump version 2022-11-11 03:15:52 +01:00
sk
9b13bdb06f update readme 2022-11-11 03:15:32 +01:00
sk
9283ca8878 Merge branch 'list-timeline-views' into fork 2022-11-11 03:06:55 +01:00
sk
679ede1124 update strings 2022-11-11 03:02:17 +01:00
sk
f4b1bde8c5 improve list item styling 2022-11-11 02:42:58 +01:00
sk
eff6cc3e17 Merge branch 'compact-extended-footer' into fork 2022-11-11 02:11:52 +01:00
sk
03044b86b1 add clickable app name button 2022-11-11 02:11:37 +01:00
sk
90ff88f02b Merge branch 'compact-extended-footer' into fork 2022-11-11 01:35:13 +01:00
sk
e1f378977a set fixed visibility icon size 2022-11-11 01:35:01 +01:00
sk
a0f3d2862c Merge branch 'compact-extended-footer' into fork 2022-11-11 01:27:46 +01:00
sk
0c64145368 implement new old extended footer design
fixes #34
2022-11-11 01:27:17 +01:00
sk
a1474fb461 bump version 2022-11-09 23:29:30 +01:00
sk
965ebc8669 Merge branch 'feature/post-notifications-toggle' into fork 2022-11-09 23:28:59 +01:00
sk
3514152439 fix issues with post notification button 2022-11-09 23:28:48 +01:00
sk
1b1dc7085e update readme 2022-11-09 18:42:49 +01:00
sk
94d004724f Merge branch 'feature/post-notifications-toggle' into fork 2022-11-09 18:41:14 +01:00
sk
e9f5d235cb Implement post notifications button
Closes #10, mastodon#81
2022-11-09 18:39:28 +01:00
sk
9692afb323 bump version 2022-11-09 18:38:58 +01:00
sk
3ae37a700b Merge branch 'feature/post-notifications-toggle' into fork 2022-11-09 18:38:20 +01:00
sk
b166ca705e Implement post notifications button 2022-11-09 18:38:10 +01:00
sk
e6a67543f4 Bump version, update README 2022-11-09 15:54:33 +01:00
sk
7d38f031f1 Merge branch 'feature/follow_hashtags' into fork
Fixes #31, mastodon#233
2022-11-09 15:49:29 +01:00
sk
cf864e6f49 Implement following hashtags 2022-11-09 15:48:01 +01:00
sk
f4b1629a1d Merge branch 'feature/filter-home-timeline' into fork 2022-11-09 10:34:47 +01:00
sk
1fbb97021e align code 2022-11-09 10:34:36 +01:00
sk
229c815a59 move option to home timeline section 2022-11-09 10:32:12 +01:00
sk
ee8d78637d Merge branch 'settings/load-new-posts' into fork 2022-11-09 10:31:30 +01:00
sk
4ede842171 add option to disable automatically loading new posts 2022-11-09 10:30:25 +01:00
sk
8306e78ce3 Merge branch 'feature/filter-home-timeline' into fork 2022-11-09 10:12:16 +01:00
sk
65d093ee9d fix new posts not filtering 2022-11-09 10:11:55 +01:00
sk
aa48233a75 Merge branch 'main' into fork 2022-11-09 09:48:03 +01:00
Grishka
ae50e618c0 Fix #316 2022-11-08 09:41:07 +03:00
Grishka
defa8b014e Fix #315 2022-11-08 09:38:26 +03:00
Grishka
159d91f390 Crash fixes 2022-11-08 09:36:56 +03:00
sk
430d4ec93b Merge branch 'spoiler-height-independent' into fork 2022-11-08 00:55:10 +01:00
sk
ae64784daf revert increased vertical padding
i changed my mind
2022-11-08 00:54:48 +01:00
sk
18df7c32a4 Merge branch 'feature/filter-home-timeline' into fork 2022-11-08 00:27:31 +01:00
sk
4615612a65 use home_timeline string 2022-11-08 00:27:19 +01:00
sk
6b226cdcad bump version 2022-11-08 00:21:33 +01:00
sk
1776709b38 update readme 2022-11-08 00:21:17 +01:00
sk
75a35cd680 Merge branch 'feature/filter-home-timeline' into fork 2022-11-08 00:18:19 +01:00
sk
18d4f2fa36 add timeline filters for replies/boosts
fixes #32, mastodon#210
2022-11-08 00:13:06 +01:00
sk
bca936f6a2 move contribute to the spicy zone
i've been planning to do this for a while :D
2022-11-07 23:37:10 +01:00
sk
0b59379c4e update readme 2022-11-07 23:35:31 +01:00
sk
58abdad62a bump version 2022-11-07 21:18:41 +01:00
sk
c693414cc3 update readme 2022-11-07 21:18:37 +01:00
sk
ee158e1aba Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-07 21:04:44 +01:00
sk
40d478aaec tweak styles 2022-11-07 21:04:28 +01:00
sk
e7fb96b3ff Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-07 20:47:15 +01:00
sk
ef75427b45 fix option not being there when editing 2022-11-07 20:46:49 +01:00
sk
2ff4f00774 Merge branch 'feature/mark-media-as-sensitive' into fork 2022-11-07 20:17:39 +01:00
sk
534fd8c119 implement "mark media as sensitive" button
fixes #21
2022-11-07 20:15:16 +01:00
sk
2d2cd89454 Merge branch 'spoiler-height-independent' into fork 2022-11-07 17:32:55 +01:00
sk
9f3f5ca7c1 make spoiler height independent of content
fixes #12, fixes mastodon/mastodon-android#166
2022-11-07 17:32:37 +01:00
Samuel Kaiser
c0e6f17c83 Merge pull request #28 from br4yd/patch-1
Add German translation for the federated timeline
2022-11-07 16:57:21 +01:00
sk
677f1cb42d Merge remote-tracking branch 'origin/upstream' into fork 2022-11-07 16:56:27 +01:00
Gregory K
1ac6a04a46 Merge pull request #309 from julroy67/perapp-language
Add Android 13 per-app language preferences
2022-11-07 06:49:58 +03:00
Julien Humbert
21927d2e25 Add missing line in Android Manifest 2022-11-07 12:32:20 +09:00
Julien Humbert
c35441f5f7 Add per-app language preferences 2022-11-07 12:27:26 +09:00
br4yd
3718fe6601 Add German translation for the federated timeline
I added the translation strings for the federation timeline tab to the German translation file and also translated the values.
2022-11-06 22:21:53 +01:00
sk
095f234bd5 bump version 2022-11-06 11:16:31 +01:00
sk
f10c0e06db Merge branch 'fork' of github.com:sk22/mastodon-android-fork into fork 2022-11-06 11:13:42 +01:00
sk
ebbd5d1fa3 Merge branch 'upstream' into fork 2022-11-06 11:13:26 +01:00
Grishka
c8abf26040 Fix #301 2022-11-06 05:43:17 +03:00
Grishka
bc733af147 Actually no, this makes more sense 2022-11-06 05:39:22 +03:00
Grishka
77a2fd2a60 Fix #298 2022-11-06 05:36:43 +03:00
Samuel Kaiser
5cfc5eb08a Update README.md 2022-11-05 11:27:02 +01:00
Samuel Kaiser
b832d2df26 Update README.md 2022-11-04 18:02:33 +01:00
Samuel Kaiser
db34dc40ba Merge branch 'mastodon:master' into fork 2022-11-04 14:32:13 +01:00
Gregory K
bfa48c2d3e Merge pull request #289 from sk22/fix-editing-cw
Fix spoiler not being published when editing
2022-11-04 07:02:40 +03:00
Gregory K
b5e229a84d Merge pull request #288 from sk22/fix-editing-visibility
Fix wrong visibility displayed when editing
2022-11-04 07:02:19 +03:00
sk
ef207f885b increase update check interval 2022-11-04 03:37:32 +01:00
sk
c4eee28335 Merge branch 'feature/check-for-update-button' into fork 2022-11-04 03:34:06 +01:00
sk
6fe466779e add toash message 2022-11-04 03:33:41 +01:00
sk
e71db1b883 custom app name in strings 2022-11-04 03:11:10 +01:00
sk
b6efafe99d Merge branch 'feature/check-for-update-button' into fork 2022-11-04 03:09:39 +01:00
sk
688d466f8e implement manual update check settings item 2022-11-04 03:09:19 +01:00
Samuel Kaiser
3c5797932e add download badge 2022-11-03 17:42:31 +01:00
sk
48a5e262ce bump version 2022-11-03 17:18:57 +01:00
sk
63b0365208 remove reverted default visibility 2022-11-03 17:18:42 +01:00
sk
972c05d60b Revert "set unlisted as default visibility"
This reverts commit d34653750e.
2022-11-03 17:13:10 +01:00
sk
acb5778e0b bump version, again 2022-11-03 16:36:31 +01:00
sk
7694c50358 Merge branch 'fix-editing-cw' into fork 2022-11-03 16:35:31 +01:00
sk
7ffa368d10 fix spoiler not being published when editing 2022-11-03 16:33:12 +01:00
Y32Gcnte8z
53f8f41d88 remain visibility when editing 2022-11-03 16:15:17 +01:00
sk
93d57d847e fix newly published posts appearing twice
see https://github.com/mastodon/mastodon-android/issues/283
2022-11-03 16:14:12 +01:00
sk
83a4f5eec2 re-add missing onStatusUpdate event listener 2022-11-03 16:11:44 +01:00
obstsalatschuessel
21a526dda9 Add list timelines view
* get accounts list timelines from API
* display available lists in discover view tab
* display list timeline
2022-11-03 09:03:34 +01:00
Samuel Kaiser
836c2dba8d Merge branch 'mastodon:master' into fork 2022-11-02 22:32:57 +01:00
Samuel Kaiser
13ae8d2ebe Update README.md 2022-11-02 22:30:16 +01:00
Gregory K
98dafb4e49 Merge pull request #280 from sk22/fix-notifications-crash
Check whether title status item is null to fix null pointer exception
2022-11-03 00:27:07 +03:00
sk
85b8bae42e bump version 2022-11-02 22:15:26 +01:00
Samuel Kaiser
64416ef9ee Merge pull request #26 from Y32Gcnte8z/fork
Should we also change the client name in the footer of status?
2022-11-02 21:55:20 +01:00
Samuel Kaiser
6274ab3384 Merge pull request #27 from Y32Gcnte8z/visibility
Retain visibility of status when editing
2022-11-02 21:53:34 +01:00
sk
4ea5d94dc6 Merge branch 'fix-notifications-crash' into fork 2022-11-02 21:41:30 +01:00
sk
db4dd436b7 more concise null check 2022-11-02 21:36:14 +01:00
sk
6906ac6c8f remove unnecessary import 2022-11-02 21:34:05 +01:00
sk
bc0b56a90e avoid null in notification display items
fixes bug where type = status, but status = null,
causing buildDisplayItems to return a null title status item and an account card
2022-11-02 21:31:44 +01:00
sk
349d95b8ee fix syntax error 2022-11-02 20:25:42 +01:00
sk
8c8eb395de Merge remote-tracking branch 'origin/upstream' into fork 2022-11-02 20:22:33 +01:00
Grishka
50c844aa25 It was the wrong icon this whole time 2022-11-02 11:06:00 +03:00
Grishka
85b232330b Merge branch 'l10n_master' 2022-11-02 11:02:18 +03:00
Grishka
b4c305d094 Add privacy policy step to the signup flow 2022-11-02 11:01:52 +03:00
Grishka
be2ee3a029 Fix #277 2022-11-02 09:07:48 +03:00
sk
596799bf2f enable github update check 2022-11-01 23:28:55 +01:00
sk
10a405ef13 change self updater api url 2022-11-01 21:38:30 +01:00
Eugen Rochko
118c5d4b44 New translations strings.xml (French) 2022-11-01 21:31:10 +01:00
sk
a4cb05080a Merge branch 'upstream' into fork 2022-11-01 21:26:44 +01:00
Eugen Rochko
699ececc42 New translations strings.xml (French) 2022-11-01 20:17:30 +01:00
Gregory K
4f8f698911 Merge pull request #275 from davidmhewitt/fix-duplicate-emoji-crash
Fix crash on duplicate custom emojis
2022-11-01 21:51:24 +03:00
Eugen Rochko
30083837a5 New translations full_description.txt (Vietnamese) 2022-11-01 18:21:16 +01:00
Eugen Rochko
ea7aa6c52f New translations strings.xml (Vietnamese) 2022-11-01 18:21:15 +01:00
David Hewitt
244f2ed911 Fix crash on duplicate custom emojis
Fixes #274

Add a merge function to `Collectors.toMap` to discard any duplicate custom emojis that may be returned if a user uses the same custom emoji in both their name and profile.
2022-11-01 14:47:25 +00:00
Eugen Rochko
99a71c67e4 New translations full_description.txt (German) 2022-11-01 15:23:49 +01:00
Eugen Rochko
b3c0550a86 New translations strings.xml (German) 2022-11-01 15:23:47 +01:00
Eugen Rochko
39a0c6d08a New translations strings.xml (Vietnamese) 2022-11-01 11:03:52 +01:00
Gregory K
0ee494bcfc Merge pull request #273 from davidmhewitt/load-post-visibility-preference
Load post privacy preference
2022-11-01 00:30:01 +03:00
Eugen Rochko
4233c743e2 New translations strings.xml (Italian) 2022-10-31 22:06:56 +01:00
David Hewitt
eea00b0d53 Load post privacy preference
This queries the user's post visibility preference when opening the composer, and sets it on the composer.

In the case of composing a reply, the user's preference is only respected if it is "more private" than the privacy of the post being replied to, as this appears to be the behaviour in the web interface (and is what I'd expect)
2022-10-31 20:51:28 +00:00
Eugen Rochko
7cef4d282c New translations strings.xml (German) 2022-10-31 19:11:31 +01:00
Eugen Rochko
df6e085c97 New translations strings.xml (German) 2022-10-31 18:14:37 +01:00
Eugen Rochko
70cf48355e New translations strings.xml (German) 2022-10-31 10:54:54 +01:00
Eugen Rochko
da06c798d7 New translations strings.xml (German) 2022-10-31 09:23:34 +01:00
Eugen Rochko
9055c094e8 New translations strings.xml (Kabyle) 2022-10-31 07:46:18 +01:00
Eugen Rochko
5ab3ae3d59 New translations strings.xml (Chinese Traditional) 2022-10-31 07:46:17 +01:00
Eugen Rochko
92ebb2f339 New translations strings.xml (German) 2022-10-31 07:46:16 +01:00
Eugen Rochko
d8c034dfa3 New translations strings.xml (Arabic) 2022-10-31 07:46:15 +01:00
Eugen Rochko
247516849e New translations strings.xml (French) 2022-10-31 07:46:14 +01:00
Eugen Rochko
aad9996b6f New translations strings.xml (Spanish) 2022-10-31 07:46:13 +01:00
Eugen Rochko
76c1ad07a3 New translations strings.xml (Catalan) 2022-10-31 07:46:12 +01:00
Eugen Rochko
49599f48bb New translations strings.xml (Czech) 2022-10-31 07:46:11 +01:00
Eugen Rochko
158d128316 New translations strings.xml (Greek) 2022-10-31 07:46:09 +01:00
Eugen Rochko
00897e7388 New translations strings.xml (Basque) 2022-10-31 07:46:08 +01:00
Eugen Rochko
1867f294c8 New translations strings.xml (Finnish) 2022-10-31 07:46:07 +01:00
Eugen Rochko
eaa189d6b0 New translations strings.xml (Hebrew) 2022-10-31 07:46:06 +01:00
Eugen Rochko
87d11dcb0a New translations strings.xml (Armenian) 2022-10-31 07:46:05 +01:00
Eugen Rochko
ba99394890 New translations strings.xml (Italian) 2022-10-31 07:46:04 +01:00
Eugen Rochko
6b8236021e New translations strings.xml (Polish) 2022-10-31 07:46:03 +01:00
Eugen Rochko
68b12a579c New translations strings.xml (Japanese) 2022-10-31 07:46:01 +01:00
Eugen Rochko
3f0ae887b8 New translations strings.xml (Portuguese) 2022-10-31 07:46:00 +01:00
Eugen Rochko
187f9a2246 New translations strings.xml (Russian) 2022-10-31 07:45:59 +01:00
Eugen Rochko
e8566bc24b New translations strings.xml (Swedish) 2022-10-31 07:45:58 +01:00
Eugen Rochko
e375fc7d4d New translations strings.xml (Turkish) 2022-10-31 07:45:57 +01:00
Eugen Rochko
89157efce8 New translations strings.xml (Ukrainian) 2022-10-31 07:45:55 +01:00
Eugen Rochko
5e95291016 New translations strings.xml (Chinese Simplified) 2022-10-31 07:45:54 +01:00
Eugen Rochko
cbd6e668dc New translations strings.xml (Vietnamese) 2022-10-31 07:45:53 +01:00
Eugen Rochko
898e62490b New translations strings.xml (Galician) 2022-10-31 07:45:52 +01:00
Eugen Rochko
326ab4edf1 New translations strings.xml (Thai) 2022-10-31 07:45:51 +01:00
Eugen Rochko
f25a965478 New translations strings.xml (Croatian) 2022-10-31 07:45:50 +01:00
Eugen Rochko
f3f3bea7b3 New translations strings.xml (Bosnian) 2022-10-31 07:45:48 +01:00
Eugen Rochko
583b0788c6 New translations strings.xml (Occitan) 2022-10-31 07:45:47 +01:00
Eugen Rochko
ca29cee586 New translations strings.xml (Korean) 2022-10-31 07:45:46 +01:00
Eugen Rochko
ec905448b0 New translations strings.xml (Portuguese, Brazilian) 2022-10-31 07:45:45 +01:00
Gregory K
e8fa82d0de Merge pull request #269 from davidmhewitt/fix-image-keyboard
Fix receiving images from keyboards
2022-10-31 09:26:29 +03:00
Grishka
e381de812c Add self-updater for github builds 2022-10-31 09:26:17 +03:00
Eugen Rochko
1a9752d53b New translations strings.xml (German) 2022-10-30 23:05:59 +01:00
Eugen Rochko
09cc5c5fd2 New translations strings.xml (German) 2022-10-30 22:00:42 +01:00
Eugen Rochko
4e8d510d38 New translations strings.xml (German) 2022-10-30 20:57:57 +01:00
Eugen Rochko
7f7f6bae80 New translations strings.xml (German) 2022-10-30 17:30:49 +01:00
Eugen Rochko
358ff12fba New translations strings.xml (German) 2022-10-30 16:07:19 +01:00
David Hewitt
8ff3ecb4d4 Fix receiving images from keyboards
The call to `super.onCreateInputConnection` was overwriting the mimes in the `outAttrs`, so we can call that first and then modify the mimes.

This fixes the ability to insert GIFs with the default GBoard GIF menu for me.
2022-10-30 14:56:47 +00:00
Eugen Rochko
47df35f0fd New translations full_description.txt (Chinese Traditional) 2022-10-30 01:23:19 +02:00
Eugen Rochko
e226851e03 New translations strings.xml (Arabic) 2022-10-29 14:15:24 +02:00
Eugen Rochko
7228907682 New translations strings.xml (Arabic) 2022-10-29 13:13:13 +02:00
Eugen Rochko
1dbabed716 New translations strings.xml (German) 2022-10-28 16:21:33 +02:00
Eugen Rochko
4224cd037d New translations strings.xml (German) 2022-10-28 15:11:58 +02:00
Eugen Rochko
9aa6d3f531 New translations strings.xml (Chinese Traditional) 2022-10-28 05:30:56 +02:00
Eugen Rochko
c9823ae9d0 New translations strings.xml (Chinese Traditional) 2022-10-28 04:30:39 +02:00
Grishka
1fa8a9e858 Always show domain for own account 2022-10-26 18:28:57 +03:00
Grishka
212e8893b9 Fix editing 2022-10-26 03:01:39 +03:00
Grishka
367057421b Declare and request notifications permission (should fix #262) 2022-10-26 02:46:23 +03:00
Eugen Rochko
c821480842 New translations strings.xml (Polish) 2022-10-24 00:07:29 +02:00
Eugen Rochko
3ef7c11e3b New translations strings.xml (Portuguese, Brazilian) 2022-10-17 16:18:05 +02:00
Eugen Rochko
1112756bc2 New translations strings.xml (Galician) 2022-10-15 17:57:46 +02:00
Eugen Rochko
878ed43135 New translations strings.xml (Thai) 2022-10-15 16:57:55 +02:00
Eugen Rochko
01b746d30a New translations strings.xml (Thai) 2022-10-15 13:55:51 +02:00
Eugen Rochko
a900351729 New translations strings.xml (Arabic) 2022-10-08 17:35:18 +02:00
Eugen Rochko
4114a6e5d8 New translations strings.xml (Turkish) 2022-10-07 18:41:40 +02:00
Eugen Rochko
04dd232e38 New translations strings.xml (Italian) 2022-10-05 21:07:13 +02:00
Eugen Rochko
bc26dfe856 New translations strings.xml (Chinese Simplified) 2022-10-05 07:12:25 +02:00
Eugen Rochko
0322f845af New translations strings.xml (Chinese Simplified) 2022-10-05 06:12:27 +02:00
Eugen Rochko
7daa2d63e6 New translations strings.xml (Vietnamese) 2022-10-04 18:08:26 +02:00
Eugen Rochko
3a8a41f568 New translations strings.xml (Kabyle) 2022-10-04 06:41:10 +02:00
Eugen Rochko
65cd7d076b New translations strings.xml (French) 2022-10-04 06:41:09 +02:00
Eugen Rochko
a1f71091fe New translations strings.xml (Spanish) 2022-10-04 06:41:08 +02:00
Eugen Rochko
131834af86 New translations strings.xml (Arabic) 2022-10-04 06:41:07 +02:00
Eugen Rochko
c0789cbdb9 New translations strings.xml (Catalan) 2022-10-04 06:41:06 +02:00
Eugen Rochko
7d12d866ab New translations strings.xml (Czech) 2022-10-04 06:41:05 +02:00
Eugen Rochko
7c2589c35b New translations strings.xml (German) 2022-10-04 06:41:04 +02:00
Eugen Rochko
fd2031ccf5 New translations strings.xml (Basque) 2022-10-04 06:41:02 +02:00
Eugen Rochko
6a165ec9f4 New translations strings.xml (Italian) 2022-10-04 06:40:59 +02:00
Eugen Rochko
fd8dad487a New translations strings.xml (Japanese) 2022-10-04 06:40:58 +02:00
Eugen Rochko
e9443b841a New translations strings.xml (Thai) 2022-10-04 06:40:57 +02:00
Eugen Rochko
89ec6acac9 New translations strings.xml (Korean) 2022-10-04 06:40:56 +02:00
Eugen Rochko
9b389b346f New translations strings.xml (Portuguese) 2022-10-04 06:40:55 +02:00
Eugen Rochko
4a522bffc0 New translations strings.xml (Russian) 2022-10-04 06:40:54 +02:00
Eugen Rochko
9b62f8b7a2 New translations strings.xml (Turkish) 2022-10-04 06:40:52 +02:00
Eugen Rochko
96342d67e3 New translations strings.xml (Chinese Traditional) 2022-10-04 06:40:50 +02:00
Eugen Rochko
95fc2bef9e New translations strings.xml (Vietnamese) 2022-10-04 06:40:49 +02:00
Eugen Rochko
4f36fbe3b9 New translations strings.xml (Galician) 2022-10-04 06:40:48 +02:00
Eugen Rochko
a501d8b995 New translations strings.xml (Portuguese, Brazilian) 2022-10-04 06:40:47 +02:00
Eugen Rochko
74691650b3 New translations strings.xml (Croatian) 2022-10-04 06:40:46 +02:00
Eugen Rochko
9325781cbc New translations strings.xml (Bosnian) 2022-10-04 06:40:45 +02:00
Eugen Rochko
b383dd6419 New translations strings.xml (Polish) 2022-10-04 06:40:43 +02:00
Eugen Rochko
fe98ecf0cc New translations strings.xml (Chinese Simplified) 2022-10-04 06:40:42 +02:00
Grishka
01970ab69b Compose media attachment redesign 2022-10-04 07:35:31 +03:00
Grishka
3aa252f681 Fix editing 2022-10-01 01:12:31 +03:00
Grishka
18633291e6 Add monochrome icon 2022-09-29 01:23:00 +03:00
Eugen Rochko
b79619b6e4 New translations strings.xml (Chinese Simplified) 2022-09-24 19:11:57 +02:00
Eugen Rochko
dbb03ee688 New translations strings.xml (Thai) 2022-09-23 21:06:49 +02:00
Eugen Rochko
00894b41d2 New translations strings.xml (Thai) 2022-09-23 20:09:36 +02:00
Eugen Rochko
8868ace90d New translations full_description.txt (German) 2022-09-19 19:52:12 +02:00
Eugen Rochko
a7586eeba8 New translations strings.xml (German) 2022-09-19 19:52:10 +02:00
Eugen Rochko
19b89c606a New translations strings.xml (German) 2022-09-19 18:43:32 +02:00
Eugen Rochko
b6c1e7d11e New translations strings.xml (Italian) 2022-09-18 02:43:45 +02:00
Eugen Rochko
ca90c89b2a New translations strings.xml (Italian) 2022-09-18 01:24:36 +02:00
Eugen Rochko
170a758f5f New translations strings.xml (Polish) 2022-09-16 12:51:10 +02:00
Eugen Rochko
43dcd6e7f4 New translations strings.xml (Polish) 2022-09-16 11:20:42 +02:00
Eugen Rochko
063542b2f5 New translations strings.xml (Spanish) 2022-09-16 09:46:29 +02:00
Eugen Rochko
70e4ae4fb4 New translations short_description.txt (Czech) 2022-09-15 21:09:48 +02:00
Eugen Rochko
8f1a4c60df New translations full_description.txt (Czech) 2022-09-15 21:09:47 +02:00
Eugen Rochko
07212dba96 New translations strings.xml (Czech) 2022-09-15 21:09:46 +02:00
Eugen Rochko
fb95caadbe New translations full_description.txt (Czech) 2022-09-15 20:01:47 +02:00
Eugen Rochko
51e86b686d New translations strings.xml (Czech) 2022-09-15 20:01:46 +02:00
Eugen Rochko
6ec335087d New translations strings.xml (Chinese Simplified) 2022-09-15 17:03:34 +02:00
Y32Gcnte8z
96040e15fd remain visibility when editing 2022-09-15 22:38:44 +08:00
Y32Gcnte8z
12a5670441 change client name in footer of status 2022-09-15 21:21:50 +08:00
Eugen Rochko
b756fe2cdb New translations strings.xml (Italian) 2022-09-12 22:55:33 +02:00
Eugen Rochko
3732a4c844 New translations full_description.txt (German) 2022-09-10 20:18:22 +02:00
Eugen Rochko
5bd59bd999 New translations strings.xml (German) 2022-09-10 20:18:21 +02:00
Eugen Rochko
90ba5551d2 New translations strings.xml (German) 2022-09-10 18:59:59 +02:00
Eugen Rochko
484b6891fd New translations strings.xml (Chinese Simplified) 2022-09-10 16:02:25 +02:00
Eugen Rochko
0435d5f0c7 New translations strings.xml (Spanish) 2022-09-10 12:46:24 +02:00
Eugen Rochko
864c8de8de New translations strings.xml (Spanish) 2022-09-10 11:45:40 +02:00
Eugen Rochko
8bcea5bfb8 New translations strings.xml (Spanish) 2022-09-10 10:49:12 +02:00
Eugen Rochko
beded04579 New translations strings.xml (Vietnamese) 2022-09-09 19:55:30 +02:00
Eugen Rochko
5dcd41170c New translations strings.xml (Vietnamese) 2022-09-09 16:55:37 +02:00
sk
8aeda56fc8 merge upstream changes 2022-09-08 14:56:43 +02:00
Eugen Rochko
3bb921a859 New translations strings.xml (Chinese Traditional) 2022-09-03 08:09:40 +02:00
Eugen Rochko
38edbde645 New translations strings.xml (Chinese Traditional) 2022-09-03 07:11:51 +02:00
Grishka
f531a90b41 Better editing 2022-09-02 11:21:28 +03:00
Grishka
ff52c37868 Editing 2022-09-02 05:47:20 +03:00
Grishka
8fb2b454dd Post edit history + extended footer redesign 2022-09-02 02:00:25 +03:00
Grishka
265b2ad32c Fix #218 2022-08-29 21:21:08 +03:00
Grishka
ba3219d9fc Fix #209, fix #198 2022-08-29 00:57:09 +03:00
Eugen Rochko
bcac7401ee New translations strings.xml (Chinese Simplified) 2022-08-28 23:21:49 +02:00
Grishka
b44e3424e3 Fix #249 2022-08-29 00:12:15 +03:00
Eugen Rochko
c15f6519c0 New translations strings.xml (Vietnamese) 2022-08-01 08:15:42 +02:00
Eugen Rochko
2809b27be0 New translations strings.xml (Turkish) 2022-07-31 20:49:45 +02:00
Eugen Rochko
1b6e096bf9 New translations strings.xml (Kabyle) 2022-07-30 08:41:42 +02:00
Eugen Rochko
85db37f6a7 New translations full_description.txt (Czech) 2022-07-24 19:10:28 +02:00
Eugen Rochko
0cee490466 New translations strings.xml (Czech) 2022-07-24 19:10:27 +02:00
Eugen Rochko
a148c92da2 New translations full_description.txt (Czech) 2022-07-24 17:58:57 +02:00
sk
f073eba538 Merge branch 'feature/pin-posts' into fork 2022-07-22 12:18:42 +02:00
sk
7f78431eff minor code style fix 2022-07-22 12:18:16 +02:00
sk
24c1ac042c Merge branch 'master' into feature/pin-posts 2022-07-22 11:57:02 +02:00
sk
105fe68438 update app name, close #15 2022-07-22 11:51:10 +02:00
sk
46057af093 update readme 2022-07-22 11:50:04 +02:00
sk
750fa4c112 Merge branch 'master' into fork 2022-07-22 11:30:17 +02:00
Eugen Rochko
6272797834 New translations strings.xml (Czech) 2022-07-20 17:22:53 +02:00
Eugen Rochko
c048134ef2 New translations strings.xml (Czech) 2022-07-20 15:30:06 +02:00
Eugen Rochko
6f57cd9ffe New translations strings.xml (Czech) 2022-07-19 12:39:11 +02:00
Grishka
8b40643e63 Update more colors 2022-07-15 00:45:17 +03:00
Eugen Rochko
76815f8194 New translations strings.xml (Japanese) 2022-07-13 15:46:09 +02:00
Grishka
5968dcd05b Merge branch 'l10n_master' 2022-07-13 01:28:36 +03:00
Eugen Rochko
0c382bdbf6 New translations full_description.txt (Thai) 2022-07-08 15:39:13 +02:00
Grishka
74f03026cf Everything is purple now 2022-07-02 02:03:07 +03:00
Eugen Rochko
8ad6bd52ef New translations strings.xml (Russian) 2022-07-01 14:00:14 +02:00
Eugen Rochko
04da21edf3 New translations strings.xml (Catalan) 2022-06-15 20:48:39 +02:00
Eugen Rochko
ee709b6db7 New translations strings.xml (Spanish) 2022-06-15 20:48:38 +02:00
Eugen Rochko
b7432fe422 New translations full_description.txt (Spanish) 2022-06-15 19:26:46 +02:00
Eugen Rochko
8a0991d533 New translations strings.xml (Spanish) 2022-06-15 19:26:45 +02:00
Eugen Rochko
6fffe778d3 New translations strings.xml (Portuguese, Brazilian) 2022-06-13 06:36:58 +02:00
Eugen Rochko
b67b61dfe4 New translations strings.xml (Portuguese, Brazilian) 2022-06-13 05:36:15 +02:00
Eugen Rochko
65d86d9238 New translations strings.xml (Galician) 2022-06-10 07:15:17 +02:00
sk
c1e67c4f73 bump version 2022-06-08 21:44:39 +02:00
sk
e0e48f87eb Merge branch 'master' into fork 2022-06-08 21:42:42 +02:00
Grishka
b2db64022f Add pre-upload avatar and header resizing 2022-06-06 16:45:56 +03:00
Eugen Rochko
8336bfdf5c New translations strings.xml (Galician) 2022-06-06 11:07:30 +02:00
Samuel Kaiser
0ec14fe8fa Merge pull request #20 from Y32Gcnte8z/fork
fix simplified Chinese strings
2022-06-05 12:03:08 +02:00
Y32Gcnte8z
01a2f1d95c fix simplified Chinese strings 2022-06-04 13:20:56 +08:00
Eugen Rochko
b38bf5e431 New translations strings.xml (Chinese Simplified) 2022-06-03 15:20:59 +02:00
Samuel Kaiser
67b3e85837 Merge pull request #19 from Y32Gcnte8z/fork
New translations strings.xml (Chinese Simplified)
2022-06-03 11:31:19 +02:00
Eugen Rochko
310fb7db42 New translations strings.xml (Chinese Simplified) 2022-06-03 08:32:51 +02:00
Y32Gcnte8z
9f4d330ab1 New translations strings.xml (Chinese Simplified) 2022-06-03 13:55:49 +08:00
sk
25092fbcfb add icon to readme 2022-05-31 17:04:58 +02:00
sk
705e98729d initial pink branding 2022-05-31 16:51:05 +02:00
Eugen Rochko
2f24977996 New translations strings.xml (Chinese Simplified) 2022-05-29 10:45:49 +02:00
Eugen Rochko
6c336ba89e New translations strings.xml (Chinese Simplified) 2022-05-29 09:32:03 +02:00
sk
108d16a157 bump version 2022-05-26 22:44:31 +02:00
sk
e55ca6cc05 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:42:02 +02:00
sk
b8be1f184d hide redraft button when not applicable 2022-05-26 22:38:56 +02:00
sk
aa96ec54a3 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:09:39 +02:00
sk
e8b43c7179 preserve visibility when re-drafting 2022-05-26 22:09:02 +02:00
sk
b51b4a10ee Merge branch 'feature/delete-redraft' into fork 2022-05-26 21:44:07 +02:00
sk
f2b9ede27c Add proguard rules for parceler
according to http://parceler.org/
fixes issue where parceler can't find parcelable class
2022-05-26 21:43:39 +02:00
sk
a8c7d891f1 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:45:52 +02:00
sk
195c4d7b6d remove unused imports 2022-05-26 19:45:10 +02:00
sk
d280dc31e8 bump version 2022-05-26 19:35:36 +02:00
sk
eb0925c524 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:30:52 +02:00
sk
968de3664d fix german strings 2022-05-26 19:30:23 +02:00
sk
12f7336392 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:28:09 +02:00
sk
3a4d13b1c6 implement deleting and re-drafting 2022-05-26 19:19:42 +02:00
sk
273c841d9a Merge branch 'master' into feature/delete-redraft 2022-05-26 15:35:12 +02:00
Eugen Rochko
d1b1cb2082 New translations strings.xml (Korean) 2022-05-25 08:21:34 +02:00
Eugen Rochko
5bbe99be51 New translations full_description.txt (Portuguese) 2022-05-24 00:19:57 +02:00
Eugen Rochko
556d1e7433 New translations full_description.txt (Portuguese) 2022-05-23 23:03:11 +02:00
Eugen Rochko
293d7032ce New translations strings.xml (Russian) 2022-05-22 12:23:45 +02:00
sk
0186b7f8da Merge remote-tracking branch 'origin/fork' into fork 2022-05-22 02:14:06 +02:00
sk
d33654c793 bump version 2022-05-22 02:08:01 +02:00
Samuel Kaiser
86d2312615 Update README.md 2022-05-22 02:07:07 +02:00
sk
d1083c331b Merge branch 'feature/bookmarks' into fork 2022-05-22 02:04:08 +02:00
sk
ed7242217a add missing icon 2022-05-22 02:03:54 +02:00
sk
8fddaa8c82 implement bookmarks list!! 2022-05-22 02:03:29 +02:00
sk
00affe6e3e update readme 2022-05-21 23:38:56 +02:00
sk
f21b647ee0 bump version 2022-05-21 23:35:10 +02:00
sk
2a628a3791 Merge branch 'feature/back-returns-home' into fork 2022-05-21 23:34:18 +02:00
sk
ecd568503d make back button return home before exiting 2022-05-21 23:33:53 +02:00
sk
f9d0632a85 add missing files 2022-05-21 19:42:29 +02:00
sk
11905513b7 add missing icons 2022-05-21 19:40:49 +02:00
sk
9c89abf1c4 implement bookmark button 2022-05-21 19:27:44 +02:00
sk
4d950e43ac update readme 2022-05-21 18:59:13 +02:00
sk
99405f307d bump version 2022-05-21 18:49:59 +02:00
sk
f1bfe05263 Merge branch 'feature/always-preserve-cw' into fork 2022-05-21 18:49:07 +02:00
sk
0f223159c0 always preserve cw when replying 2022-05-21 18:48:48 +02:00
sk
ad9518e87c re-add deleted strings 2022-05-21 18:08:14 +02:00
sk
1c16cfb09e bump version 2022-05-21 18:05:01 +02:00
sk
d4a4b10017 Merge branch 'feature/compose-image-description-full-image' into fork 2022-05-21 18:03:29 +02:00
sk
74ae5bd04e change app name 2022-05-21 18:03:15 +02:00
sk
9638cf079f set image view height to wrap_content 2022-05-21 17:48:53 +02:00
sk
a6d161c1b4 minor code style change
for grishka's code style must prevail
2022-05-21 17:42:40 +02:00
sk
1136e40eb4 obey image max width 2022-05-21 17:39:28 +02:00
sk
98de3a2984 don't crop image when composing alt text 2022-05-21 17:27:31 +02:00
Eugen Rochko
48e7071450 New translations strings.xml (Arabic) 2022-05-19 22:57:49 +02:00
Eugen Rochko
bcc8d55c7b New translations strings.xml (Arabic) 2022-05-19 21:55:19 +02:00
Eugen Rochko
8d477efc28 New translations strings.xml (Czech) 2022-05-19 15:32:16 +02:00
Eugen Rochko
4ae21862a5 New translations strings.xml (Czech) 2022-05-19 14:30:33 +02:00
Eugen Rochko
6e6aebef35 New translations strings.xml (Czech) 2022-05-19 13:34:29 +02:00
Eugen Rochko
cd138032da New translations strings.xml (Czech) 2022-05-18 18:19:40 +02:00
Eugen Rochko
864e6fb9d0 New translations strings.xml (Czech) 2022-05-18 16:41:35 +02:00
Eugen Rochko
9d356b0635 New translations strings.xml (Czech) 2022-05-18 15:25:33 +02:00
Eugen Rochko
6c5d720a40 New translations strings.xml (Czech) 2022-05-18 14:28:15 +02:00
Eugen Rochko
d733d76ccf New translations strings.xml (Arabic) 2022-05-18 13:29:20 +02:00
Eugen Rochko
3483d8c3c0 New translations strings.xml (Portuguese, Brazilian) 2022-05-18 09:01:46 +02:00
Eugen Rochko
0f326c1362 New translations strings.xml (Kabyle) 2022-05-17 23:04:46 +02:00
Eugen Rochko
c6eda38400 New translations strings.xml (Kabyle) 2022-05-17 22:08:14 +02:00
Eugen Rochko
3c59c8cc0f New translations strings.xml (Arabic) 2022-05-17 22:08:13 +02:00
Eugen Rochko
8f1b9ec092 New translations strings.xml (French) 2022-05-17 22:08:12 +02:00
Eugen Rochko
5a42136395 New translations strings.xml (Czech) 2022-05-17 21:03:40 +02:00
Eugen Rochko
e9b347d130 New translations strings.xml (Thai) 2022-05-17 21:03:39 +02:00
Eugen Rochko
d86e203127 New translations strings.xml (Italian) 2022-05-17 18:03:57 +02:00
Eugen Rochko
c80417e671 New translations strings.xml (Ukrainian) 2022-05-17 18:03:56 +02:00
Eugen Rochko
55a55fbb1c New translations strings.xml (Croatian) 2022-05-17 18:03:55 +02:00
Eugen Rochko
b42236999b New translations strings.xml (Occitan) 2022-05-17 18:03:53 +02:00
Eugen Rochko
8ca8bd765b New translations strings.xml (Bosnian) 2022-05-17 18:03:51 +02:00
Eugen Rochko
7f4cf77283 New translations strings.xml (Portuguese, Brazilian) 2022-05-17 18:03:50 +02:00
Eugen Rochko
950d413bd1 New translations strings.xml (Chinese Traditional) 2022-05-17 18:03:49 +02:00
Eugen Rochko
475827b1c1 New translations strings.xml (Chinese Simplified) 2022-05-17 18:03:48 +02:00
Eugen Rochko
1c9164e559 New translations strings.xml (Galician) 2022-05-17 18:03:47 +02:00
Eugen Rochko
7311a394d8 New translations strings.xml (Turkish) 2022-05-17 18:03:46 +02:00
Eugen Rochko
aa09bc7ab2 New translations strings.xml (Russian) 2022-05-17 18:03:45 +02:00
Eugen Rochko
267a6a75ef New translations strings.xml (Portuguese) 2022-05-17 18:03:43 +02:00
Eugen Rochko
37660b4c73 New translations strings.xml (Polish) 2022-05-17 18:03:42 +02:00
Eugen Rochko
15b4d46ea1 New translations strings.xml (Korean) 2022-05-17 18:03:41 +02:00
Eugen Rochko
0b99e76b25 New translations strings.xml (Japanese) 2022-05-17 18:03:40 +02:00
Eugen Rochko
c851f666b3 New translations strings.xml (Basque) 2022-05-17 18:03:38 +02:00
Eugen Rochko
7662c81754 New translations strings.xml (German) 2022-05-17 18:03:36 +02:00
Eugen Rochko
5c1b583448 New translations strings.xml (Catalan) 2022-05-17 18:03:35 +02:00
Eugen Rochko
4071c1552d New translations strings.xml (Swedish) 2022-05-17 18:03:34 +02:00
Eugen Rochko
89856a81a3 New translations strings.xml (Spanish) 2022-05-17 18:03:33 +02:00
Eugen Rochko
005ddfb651 New translations strings.xml (Vietnamese) 2022-05-17 18:03:32 +02:00
Eugen Rochko
92d10d59c6 New translations strings.xml (Armenian) 2022-05-17 18:03:31 +02:00
Eugen Rochko
150f70edd8 New translations strings.xml (Arabic) 2022-05-17 18:03:22 +02:00
Eugen Rochko
73019eaade New translations strings.xml (Thai) 2022-05-17 18:03:17 +02:00
Eugen Rochko
46bac59ff5 New translations strings.xml (Finnish) 2022-05-17 18:03:15 +02:00
Eugen Rochko
9b162cb63b New translations strings.xml (Kabyle) 2022-05-17 18:03:13 +02:00
Eugen Rochko
7d216314c9 New translations strings.xml (French) 2022-05-17 18:03:11 +02:00
Grishka
080a320e12 Make the app name non-translatable 2022-05-17 18:47:11 +03:00
Eugen Rochko
28787b4068 New translations strings.xml (Vietnamese) 2022-05-17 15:22:20 +02:00
Eugen Rochko
a73ea62a9c New translations strings.xml (Galician) 2022-05-17 13:38:15 +02:00
Eugen Rochko
69b399e397 New translations strings.xml (Japanese) 2022-05-17 06:27:43 +02:00
Eugen Rochko
fc2c033e93 New translations strings.xml (Chinese Simplified) 2022-05-17 05:29:56 +02:00
Eugen Rochko
1d81abca5b New translations title.txt (Czech) 2022-05-17 04:16:52 +02:00
Eugen Rochko
0f3cd5d8d0 New translations short_description.txt (Czech) 2022-05-17 04:16:51 +02:00
Eugen Rochko
f0476f3187 New translations full_description.txt (Czech) 2022-05-17 04:16:50 +02:00
Eugen Rochko
b4677d14e5 New translations strings.xml (Czech) 2022-05-17 04:16:50 +02:00
Eugen Rochko
a8837bd4f8 New translations strings.xml (Basque) 2022-05-16 23:18:13 +02:00
Eugen Rochko
c6991a7067 New translations strings.xml (Chinese Simplified) 2022-05-16 19:37:18 +02:00
Eugen Rochko
0723e942f0 New translations strings.xml (Chinese Simplified) 2022-05-16 18:11:12 +02:00
sk
b08415ca8f bump version 2022-05-15 20:35:10 +02:00
sk
3639c69d36 Merge branch 'master' into fork 2022-05-15 20:34:14 +02:00
Eugen Rochko
52fd300d1e New translations strings.xml (German) 2022-05-15 20:26:29 +02:00
Grishka
37cefcaf6d Fix #164 2022-05-15 21:13:36 +03:00
Eugen Rochko
68c9f7a861 New translations full_description.txt (German) 2022-05-15 19:31:06 +02:00
Eugen Rochko
8eb0b12a09 New translations strings.xml (German) 2022-05-15 19:31:05 +02:00
Grishka
558adc6936 Add compose shortcut
closes #131
2022-05-15 19:14:24 +03:00
Eugen Rochko
68ecd7bc28 New translations strings.xml (German) 2022-05-14 18:55:50 +02:00
Eugen Rochko
5c7d4e389f New translations strings.xml (Polish) 2022-05-14 16:56:43 +02:00
sk
31e3a8592f bump version 2022-05-14 13:49:49 +02:00
sk
39655d5278 Merge branch 'master' into fork 2022-05-14 13:48:03 +02:00
Eugen Rochko
55fd74c227 New translations strings.xml (Italian) 2022-05-14 11:30:44 +02:00
Eugen Rochko
b65b7c53bc New translations strings.xml (Chinese Traditional) 2022-05-14 05:38:51 +02:00
Grishka
68d0862008 Close #122 2022-05-13 20:54:22 +03:00
Grishka
c9e13eefa5 Close #146 2022-05-13 20:49:35 +03:00
Grishka
349fbce5af Fix #128 2022-05-13 20:42:54 +03:00
Eugen Rochko
7a23c9b348 New translations strings.xml (Italian) 2022-05-13 19:23:19 +02:00
Grishka
95c66654aa Fix #149 2022-05-13 19:20:40 +03:00
Grishka
a8407571a4 Fix #151 2022-05-13 19:18:29 +03:00
Grishka
75538deb9b Fix #156 2022-05-13 19:10:27 +03:00
Grishka
601eec4607 Fix #157 2022-05-13 19:01:29 +03:00
Grishka
9b87d0bece Fix #153 2022-05-13 18:14:52 +03:00
Grishka
cb25632691 Delete statuses from cache and fix auto-refresh when posting 2022-05-13 17:57:41 +03:00
Grishka
63957250c5 Fix #141 + crash fixes 2022-05-13 17:51:28 +03:00
sk
d844a77e65 add ui items for redraft 2022-05-11 17:25:00 +02:00
Eugen Rochko
32cc760272 New translations full_description.txt (Polish) 2022-05-11 00:53:22 +02:00
Eugen Rochko
e105764aa8 New translations strings.xml (Polish) 2022-05-11 00:53:21 +02:00
Eugen Rochko
5a9a352e56 New translations full_description.txt (Polish) 2022-05-10 23:44:44 +02:00
Eugen Rochko
7deb2d452e New translations short_description.txt (Swedish) 2022-05-10 18:01:59 +02:00
Eugen Rochko
c3d2df88e8 New translations full_description.txt (Swedish) 2022-05-10 18:01:58 +02:00
Eugen Rochko
9943d19c31 New translations short_description.txt (Finnish) 2022-05-10 18:01:56 +02:00
Eugen Rochko
51c1e115c5 New translations full_description.txt (Finnish) 2022-05-10 18:01:55 +02:00
Eugen Rochko
f83ff93c68 New translations strings.xml (Finnish) 2022-05-10 18:01:54 +02:00
Eugen Rochko
7deb5d44c2 New translations strings.xml (Swedish) 2022-05-10 18:01:53 +02:00
Eugen Rochko
3140ae8046 New translations strings.xml (Finnish) 2022-05-10 17:00:35 +02:00
Eugen Rochko
772f79219b New translations strings.xml (Arabic) 2022-05-09 01:41:42 +02:00
Eugen Rochko
8830d67af0 New translations full_description.txt (Polish) 2022-05-09 00:34:14 +02:00
Eugen Rochko
89c02be41c New translations strings.xml (Polish) 2022-05-09 00:34:13 +02:00
Eugen Rochko
1d092c660b New translations strings.xml (Polish) 2022-05-08 23:32:42 +02:00
Eugen Rochko
a34084da5a New translations strings.xml (Vietnamese) 2022-05-07 17:18:03 +02:00
Eugen Rochko
212f5a9beb New translations full_description.txt (Korean) 2022-05-07 17:18:02 +02:00
Eugen Rochko
f6333de4e6 New translations strings.xml (Korean) 2022-05-07 17:17:59 +02:00
Eugen Rochko
5af22f1bab New translations strings.xml (Korean) 2022-05-07 16:19:40 +02:00
Eugen Rochko
02d866d7d6 New translations strings.xml (Turkish) 2022-05-06 23:36:32 +02:00
Eugen Rochko
fa7aa6240b New translations strings.xml (Turkish) 2022-05-06 22:37:34 +02:00
sk
bde2e398a8 bump version and change app name 2022-05-06 22:10:45 +02:00
sk
8d443b2051 Merge branch 'feature/pin-posts' into fork 2022-05-06 21:50:08 +02:00
sk
33d4b678ed update posts' pinned states 2022-05-06 21:49:33 +02:00
sk
3becad1468 fix created posts being added to pinned 2022-05-06 21:18:48 +02:00
sk
fad3ba3eae bump version 2022-05-06 19:49:36 +02:00
sk
cb16f95878 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:45:33 +02:00
sk
4e833490ff fix about section not being displayed 2022-05-06 19:45:09 +02:00
Samuel Kaiser
04a973f7b0 Update README.md 2022-05-06 19:28:43 +02:00
sk
0318169b74 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:24:58 +02:00
sk
972fb1e241 translate "pinned" strings to german 2022-05-06 19:21:39 +02:00
sk
9beb04b01d implement pinning and unpinning posts 2022-05-06 19:07:51 +02:00
sk
a3bea6ad24 add profile tab for pinned toots 2022-05-06 18:09:00 +02:00
Eugen Rochko
cb38e0d367 New translations strings.xml (Thai) 2022-05-06 17:20:31 +02:00
Eugen Rochko
a133a1d01f New translations strings.xml (Thai) 2022-05-06 16:22:05 +02:00
sk
7996e4ee4a change client name, versioning 2022-05-06 15:39:57 +02:00
Eugen Rochko
79bfc43431 New translations strings.xml (Thai) 2022-05-06 15:22:42 +02:00
sk
69c4bf4213 Merge branch 'feature/display-alt-text' into fork 2022-05-06 01:00:34 +02:00
sk
7cd5ca77f5 Merge remote-tracking branch 'origin/feature/display-alt-text' into feature/display-alt-text 2022-05-06 01:00:01 +02:00
sk
7e736d3cd3 clean up code 2022-05-06 00:59:43 +02:00
sk
13c2adba56 update version and readme 2022-05-06 00:34:08 +02:00
sk
010095a50e Merge branch 'master' into fork 2022-05-06 00:30:23 +02:00
Samuel Kaiser
f0cef2103f Merge branch 'mastodon:master' into feature/display-alt-text 2022-05-06 00:29:05 +02:00
sk
8ed731a48b edit application id for fork 2022-05-06 00:27:59 +02:00
sk
8660d43cb1 bump version 2022-05-06 00:21:08 +02:00
sk
0f495f620a Merge branch 'feature/display-alt-text' into fork 2022-05-06 00:17:07 +02:00
sk
ac81f10ea8 make button disappear when no description 2022-05-06 00:16:25 +02:00
sk
9aa95413e6 make text selectable 2022-05-06 00:15:56 +02:00
Eugen Rochko
72f3a51af7 New translations strings.xml (Italian) 2022-05-06 00:01:49 +02:00
sk
a0a28a0cb7 implement scroll-to-close 2022-05-05 23:39:24 +02:00
sk
11d88aed27 implement alt text as bottom sheet 2022-05-05 23:26:48 +02:00
Eugen Rochko
ee73b487ae New translations strings.xml (Italian) 2022-05-05 23:05:54 +02:00
Eugen Rochko
e580d2e890 New translations strings.xml (Thai) 2022-05-05 22:07:42 +02:00
Grishka
88504531d4 Crash fixes 2022-05-05 22:05:18 +03:00
Eugen Rochko
4f6f53061f New translations strings.xml (Thai) 2022-05-05 19:32:57 +02:00
sk
899c9cdf21 implement alt text as toast messages 2022-05-05 19:28:27 +02:00
Eugen Rochko
be23ec4176 New translations strings.xml (Thai) 2022-05-05 18:32:43 +02:00
Eugen Rochko
186636c2ef New translations strings.xml (French) 2022-05-05 18:32:42 +02:00
Grishka
4ad9fa030b Fix #127 2022-05-05 18:30:40 +03:00
Eugen Rochko
f0396ff418 New translations strings.xml (Polish) 2022-05-05 16:53:36 +02:00
Eugen Rochko
67e793b56a New translations strings.xml (Japanese) 2022-05-05 15:31:36 +02:00
Eugen Rochko
d84f011d27 New translations strings.xml (French) 2022-05-05 15:31:35 +02:00
Eugen Rochko
f685d9ccdd New translations strings.xml (French) 2022-05-05 14:36:22 +02:00
Eugen Rochko
380c742f54 New translations full_description.txt (Korean) 2022-05-05 06:07:07 +02:00
Eugen Rochko
ed84ea6162 New translations strings.xml (Korean) 2022-05-05 06:07:06 +02:00
Eugen Rochko
e99ffc0d4c New translations strings.xml (Korean) 2022-05-05 05:05:13 +02:00
Eugen Rochko
ca015db188 New translations strings.xml (Arabic) 2022-05-05 00:05:19 +02:00
Eugen Rochko
af3a3761f2 New translations strings.xml (French) 2022-05-04 19:48:30 +02:00
Eugen Rochko
95085b6306 New translations strings.xml (Italian) 2022-05-04 18:51:31 +02:00
233 changed files with 6996 additions and 1049 deletions

100
README.md
View File

@@ -1,16 +1,96 @@
# Forked Mastodon for Android
[![Crowdin](https://badges.crowdin.net/mastodon-for-android/localized.svg)](https://crowdin.com/project/mastodon-for-android)
![Pink version of the Mastodon for Android launcher icon](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
This is the repository for an officially forked Android app for Mastodon.
# Mastodos
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly wont ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer.
## Changes
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=d92aad&label=download%20apk&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fsk22%2Fmastodon-android-fork%2Freleases%2Flatest&style=for-the-badge)](https://github.com/sk22/mastodos/releases/latest/download/mastodos.apk)
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline)
---
## Key features
### **Unlisted posting**
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Local”, “Community” and “Posts”).**
When posting with Unlisted visibility, your posts will be publicly accessible through your profile and shown in peoples Home timelines only if they follow you or someone they follow reposted/replied to your post.
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
### **Federated timeline**
**This allows you to chronologically see all Public posts from people on all other Fediverse instances your home instance is connected to.**
Despite being one of the main features of federated social media, the Federated timeline wasnt included in the official Mastodon app supposedly, because this conflicts with Googles safety requirements for apps on the Play Store.
Thats one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
### **Image description viewer**
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
This is important to **ensure the content youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
### **Pinning posts**
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in peoples profiles shows all the posts they pinned.**
On the Fediverse, its quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
### **Bookmarks**
**They allow for quickly saving posts and viewing them through the Bookmarks button on the top right of your profile.**
To bookmark a post, press the button between the Favorite and Share buttons on the bottom of the post. Bookmarks are saved privately, so the post authors wont know you saved their post the list of bookmarked posts is only visible to you.
## Installation
**Press the download button above to download the APK. Open the downloaded file on your Android device to install it. Mastodos will automatically notify you about new updates inside the app.**
To install this app on your Android device, download the [latest release from GitHub](https://github.com/sk22/mastodos/releases/latest/download/mastodos.apk) and open it. You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/mastodos/releases) page.
Mastodos makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)s automatic update checker. Mastodos will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
---
## Detailed changes
### Features
* [Add “Unlisted” as a post visibility option](https://github.com/sk22/mastodos/commits/feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
* [Add “Federation” tab and change Discover tab order](https://github.com/sk22/mastodos/commits/feature/add-federated-timeline) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/8))
* [Add image description button and viewer](https://github.com/sk22/mastodos/commits/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodos/commits/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Implement deleting and re-drafting](https://github.com/sk22/mastodos/commits/feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
* [Implement a bookmark button and list](https://github.com/sk22/mastodos/commits/feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Add “Check for update” button in addition to integrated update checker](https://github.com/sk22/mastodos/commits/feature/check-for-update-button)
* [Add “Mark media as sensitive” option](https://github.com/sk22/mastodos/commits/feature/mark-media-as-sensitive)
* [Add settings to hide replies and reposts from the timeline](https://github.com/sk22/mastodos/commits/feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Follow and unfollow hashtags](https://github.com/sk22/mastodos/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
* [Notification bell for posts](https://github.com/sk22/mastodos/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
* [Viewing lists and adding/removing users from lists](https://github.com/sk22/mastodos/commits/list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
* [List favorited posts](https://github.com/sk22/mastodos/commits/feature/favs-list)
* [Accept/reject follow requests](https://github.com/sk22/mastodos/commits/feature/follow-requests)
### Behavior
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodos/commits/feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
* [Always preserve content warnings when replying](https://github.com/sk22/mastodos/commits/feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
* [Display full image when adding image description](https://github.com/sk22/mastodos/commits/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [Set spoiler height independently to content height](https://github.com/sk22/mastodos/commits/spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
* [Custom extended footer redesign](https://github.com/sk22/mastodos/commits/compact-extended-footer)
* [Option to hide interaction numbers](https://github.com/sk22/mastodos/commits/settings/hide-interaction-numbers)
### Branding
* App name “Mastodos”
* Pink primary color
* Custom icon: Modulate upstream icon using ImageMagick
```bash
mogrify -modulate 90,100,140 mastodon/src/main/res/mipmap-*/ic_launcher*.png
```
## Building
@@ -22,4 +102,4 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
## License
This project is released under the [GPL-3 License](./LICENSE).
This project is released under the [GPL-3 License](./LICENSE).

2
_config.yml Normal file
View File

@@ -0,0 +1,2 @@
title: Mastodos
theme: minima

View File

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

View File

@@ -0,0 +1 @@
Decentralizovaná sociální síť

View File

@@ -0,0 +1 @@
Mastodon

View File

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

View File

@@ -1,6 +1,6 @@
Mastodon es la red social descentralizada más grande de internet. En lugar de ser una sola web, es una red de millones de usuarios en comunidades independientes que pueden interactuar entre ellas de forma transparente. No importa qué es lo que hagas, podrás encontrar gente apasionada escribiendo sobre ello en Mastodon!
Únete a una comunidad y crea tu perfil. Encuentra y sigue a gente fascinante y lee sus publicaciones sin anuncios y de forma cronológica. Exprésate con emoticonos personalizados, imágenes, GIFs, vídeos y audio en publicaciónes de 500 caracteres. Responde a hilos e impulsa publicaciones de cualquiera para compartir contenido genial. Encuentra nuevas cuentas para seguir y los hashtags de actualidad para expandir tu red.
Únete a una comunidad y crea tu perfil. Encuentra y sigue a gente fascinante y lee sus publicaciones sin anuncios y de forma cronológica. Exprésate con emoticonos personalizados, imágenes, GIFs, vídeos y audio en publicaciónes de 500 caracteres. Responde a hilos e rebloguea publicaciones de cualquiera para compartir contenido genial. Encuentra nuevas cuentas para seguir y los hashtags de actualidad para expandir tu red.
Mastodon está construída con un enfoque en la privacidad y la seguridad. Decide si tus publicaciones se comparten con tus seguidores, solo a la gente que menciones, o a todo el mundo. Las advertencias de contenido te permiten esconder publicaciones con contenido sensible o limitarlas de tu visión hasta que estés listo para interactuar con ellas. Cada comunidad tiene sus propias reglas y moderadores para mantener a salvo a sus miembros, además de herramientas robustas para bloquear y reportar contenido para prevenir el abuso.
@@ -9,7 +9,7 @@ Más características:
• Modo oscuro: Lee las publicaciones en modo claro, oscuro o negro real
• Encuestas: Pide opinión a tus seguidores y cuenta los votos
• Explora: Hashtags y cuentas en tendencia a un solo toque
• Notificaciones: Recibe notificaciones sobre nuevos seguidores, respuestas e impulsos
• Notificaciones: Recibe notificaciones sobre nuevos seguidores, respuestas y reblogueos
• Compartir: Publica directamente a Mastodon desde cualquier hoja de acción en cualquier aplicación
• Preciosidad: Nuestra mascota es un elefante adorable, y verás que aparece de vez en cuando

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Mastodon on internetin suurin hajautettu sosiaalinen verkosto. Yhden verkkopalvelun sijaan, se on miljoonien itsenäisissä yhteisöissä olevien käyttäjien verkosto, jotka voivat olla vuorovaikutuksessa toistensa kanssa saumattomasti. Riippumatta siitä, mistä olet kiinnostunut, voit tavata intohimoisia ihmisiä, jotka julkaisevat aiheesta Mastodonissa!
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Liity yhteisöön ja luo itsellesi tili. Löydä ja seuraa kiehtovia ihmisiä ja lue heidän julkaisunsa ilman mainoksia, kronologisella aikajanalla. Ilmaise itseäsi mukautetuilla emojeilla, kuvilla, videoilla ja audiolla 500 merkin pituisissa julkaisuissa. Vastaa viestiketjuihin ja edelleen jaa julkaisuja keneltä tahansa, jakaaksesi hienoja juttuja. Löydä uusia tilejä seurattavaksi ja trendaavia hashtageja laajentaaksesi verkostoasi.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Mastodon on rakennettu keskittyen yksityisyyteen ja turvallisuuteen. Päätä, jaetaanko julkaisusi seuraajille, vain mainitsemillesi ihmisille vai koko maailmalle. Sisältövaroitusten avulla, voit piilottaa julkaisut, jotka sisältävät arkaluontoista tai laukaisevaa materiaalia, kunnes olet valmis käsittelemään niitä. Jokaisella yhteisöllä on omat ohjeistonsa ja valvojansa, jotka pitävät jäsenensä turvassa, ja tehokkaat esto- ja ilmiantotyökalut auttavat torjumaan väärinkäytöksiä.
More features:
Lisää ominaisuuksia:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
Explore: Trending hashtags and accounts are a tap away
Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Tumma tila: Lue julkaisut vaaleassa, tummassa tai mustan tummassa tilassa
Kyselyt: Kysy seuraajilta heidän mielipidettään ja laske äänet
Tutustu: Trendaavat hashtagit ja tilit ovat vain napsautuksen päässä
Ilmoitukset: Saat ilmoituksen uusista seuraajista, vastauksista ja edelleen jaoista
Jakaminen: Julkaise suoraan Mastodoniin minkä tahansa sovelluksen jakovalikon kautta
Suloisuus: Maskottimme on ihastuttava mastodontti ja näet sen ajoittain
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Mastodon on rekisteröity voittoa tavoittelematon organisaatio ja kehitystä tuetaan suoraan lahjoituksillasi. Ei mainontaa, kaupallistamista eikä riskipääomaa, ja aiomme pitää sen sellaisena.

View File

@@ -1 +1 @@
Decentralized social network
Hajautettu sosiaalinen verkosto

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
마스토돈은 인터넷에서 가장 큰 분산 소셜 네트워크입니다. 단 하나의 통일된 웹사이트 대신, 수백만 명의 사용자들이, 서로 경계 없이 소통할 수 있는 독립적인 커뮤니티의 네트워크입니다. 당신이 어디에 속하든간에, 마스토돈에 열정적으로 게시물을 남기는 사람들을 만날 수 있습니다!
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
커뮤니티에 가입하고 프로필을 생성하세요. 매력적인 사람들을 찾아 팔로우하고 그들의 글을 광고가 없고, 시간순으로 정렬된 타임라인에서 읽으세요. 커스텀 에모지, 그림, 움짤, 동영상, 소리와 함께 500자의 글로 당신을 표현하세요. 아무에게나 글타래에 답장을 하고 게시물을 리블로그하여 멋진 것들을 공유하세요. 새로 팔로우 할 계정이나 유행 중인 해시태그를 찾아 당신의 인맥을 넓히세요.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
마스토돈은 개인정보 보호와 안전에 중점을 두고 만들어졌습니다. 당신의 게시물을 팔로워들에게만 공개할지, 언급한 사람들에게만 공유할지, 아니면 전세계에 공유할 지 선택하세요. 열람주의는 민감하거나 남들에게 껄끄러울 수 있는 게시물을 마음의 준비가 된 사람들만 열람하도록 숨길 수 있도록 해줍니다. 각각의 커뮤니티는 구성원들을 안전하게 지키기 위한 각자의 규정과 중재자들을 가지고 있으며, 남용을 방지하기 위한 강력한 차단 도구와 신고 도구를 가지고 있습니다.
더 많은 기능:
• 다크모드: 게시물을 밝음, 어두움, 진정한 검정 모드에서 읽으세요
Polls: Ask followers for their opinion and tally the votes
Explore: Trending hashtags and accounts are a tap away
Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
투표: 팔로워들에게 의견을 물어보고 투표를 집계하세요
탐색: 유행 중인 해시태그와 계정을 탭 한 번에 볼 수 있습니다
알림: 새로운 팔로우, 답글, 리블로그에 대한 알림을 받으세요
공유: 어떤 앱에서든 공유 기능으로 바로 마스토돈에 게시하세요
귀여움: 우리의 마스코트는 귀여운 코끼리입니다, 때때로 이들을 볼 수 있습니다
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
마스토돈은 등록된 상호이며 여러분들의 직접적인 기부를 통해 개발되고 있습니다. 광고도, 유료화도, 벤처 캐피탈도 없습니다, 그리고 계속 그렇게 할 생각입니다.

View File

@@ -1,16 +1,16 @@
Mastodon to największa zdecentralizowana sieć społecznościowa w Internecie. Zamiast jednej strony internetowej, jest to sieć milionów użytkowników w niezależnych społecznościach, które mogą ze sobą wchodzić w interakcje. Niezależnie od swoich zainteresowań, momżesz poznać interesujących ludzi piszących o nich na Mastodonie!
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Find new accounts to follow and trending hashtags to expand your network.
Dołącz do społeczności i utwórz swój profil. Poznaj i obserwuj fascynujących ludzi i czytaj ich wpisy w chronologicznym osi czasu. Wyrażaj siebie za pomocą niestandardowych emoji, obrazów, GIFów, filmów i audio w 500-znakowych wpisach. Odpowiadaj na wątki i podawaj dalej posty od każdego, aby dzielić się wspaniałymi rzeczami. Odnajduj nowe konta do śledzenia i zyskujące popularność hashtagi, by poszerzać swoją sieć.
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Mastodon został zaprojektowany z myślą o prywatności i bezpieczeństwie. Decyduj, czy Twoje wpisy są udostępniane osobom śledzącym Cię, tylko wzmiankowanym, czy całemu światu. Ostrzeżenia dotyczące zawartości pozwalają ukrywać posty zawierające wrażliwe lub wyzywające materiały, dopóki nie będziesz gotów się z nimi pogodzić. Każda społeczność ma własne wytyczne i moderatorów, aby zapewniać swoim członkom bezpieczeństwo, a także solidne narzędzia blokowania i raportowania pomagające zapobiegać nadużyciom.
Więcej funkcji:
• Tryb ciemny: Czytaj wpisy w jasnym, ciemnym lub czarnym trybie
• Ankiety: Poproś obserwujących o ich opinię i poznaj ich głosy
Explore: Trending hashtags and accounts are a tap away
Odkrywaj: Najpopularniejsze hashtagi i konta są dostępne za jednym dotknięciem
• Powiadomienia: Otrzymuj powiadomienia o nowych obserwacjach, odpowiedziach i udostępnieniach
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Udostępnianie: Publikuj bezpośrednio na Mastodonie z menu udostępniania w dowolnej aplikacji
Słodycz: Nasza maskotka to uroczy słoń i zobaczysz go pojawiającego się od czasu do czasu
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Mastodon to zarejestrowana organizacja non-profit i rozwój jest wspierany bezpośrednio przez darowizny. Nie ma reklam, monetyzacji, ani kapitału inwestycyjnego i planujemy, by tak pozostało.

View File

@@ -1,8 +1,8 @@
O Mastodon é a maior rede social descentralizada da Internet. Em vez de ser um único site, é uma rede de milhões de utilizadores em comunidades independentes que podem facilmente interagir uns com os outros. No matter what youre into, you can meet passionate people posting about it on Mastodon!
O Mastodon é a maior rede social descentralizada da Internet. Em vez de ser um único site, é uma rede de milhões de utilizadores em comunidades independentes que podem facilmente interagir uns com os outros. Independemente dos teus gostos, consegues encontrar pessoas que os partilhem no Mastodon!
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Junta-te a uma comunidade e cria o teu perfil. Encontra, segue gente fascinante e lê as suas publicações numa cronologia sem anúncios. Expressa-te com emojis personalizados imagens, GIFs, vídeos e áudio em publicações com até 500 caracteres. Responde a tópicos e promove publicações de qualquer pessoa para partilhares ótimas coisas. Encontra novas contas e tendências a seguir para expandires a tua rede.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
O Mastodon é construído com foco na privacidade e segurança. Decide se as tuas publicações são partilhadas com os teus seguidores, apenas com as pessoas mencionadas, ou com o mundo inteiro. Avisos de conteúdo permitem-te esconder publicações que contenham material sensível ou provocatório até estares pronto(a) para o veres. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Mastodon är det största decentraliserade sociala nätverket på internet. I stället för en enda webbplats är det ett nätverk av miljontals användare på oberoende servrar som alla kan interagera med varandra, sömlöst. Oavsett vad du är intresserad av kan du träffa passionerade personer som diskuterar ämnet på Mastodon!
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Gå med på en server och skapa din profil. Hitta och följ fascinerande människor och läsa deras inlägg i en annonsfri, kronologisk tidslinje. Uttryck dig med anpassade emoji, bilder, GIF:ar, videor och ljud i 500-teckensinlägg. Svara på trådar och ompostningar från vem som helst för att dela bra saker. Hitta nya konton att följa och trendande hashtaggar för att utöka ditt nätverk.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Mastodon är byggt med fokus på integritet och trygghet. Bestäm om dina inlägg delas med dina följare, bara personer du omnämner, eller hela världen. Innehållsvarningar låter dig dölja inlägg som innehåller känsligt eller triggande material tills du är redo att interagera med dem. Varje server har sina egna riktlinjer och moderatorer för att hålla sina medlemmar trygga, och robusta blockerings- och rapporteringsverktyg för att förhindra missbruk.
More features:
Fler funktioner:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mörkt läge: Läs inlägg i ljust, mörkt eller helsvart läge
Omröstningar: Fråga följare om deras åsikt och sammanställ deras röster
Utforska: Trendande hashtaggar och konton är ett tryck bort
• Notiser: Bli meddelad om nya följare, svar och ompostningar
Delning: Posta direkt till Mastodon från delningsbladet i alla appar
Gullighet: Vår maskot är en bedårande elefant, och du kommer att se dem dyka upp då och då
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Mastodon är en registrerad ideell förening och utvecklingen stöds direkt av dina donationer. Det finns ingen reklam, ingen monetarisering, och inget riskkapital, och vi planerar att behålla det på det sättet.

View File

@@ -1 +1 @@
Decentralized social network
Decentraliserat socialt nätverk

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Mastodon เป็นเครือข่ายสังคมแบบกระจายศูนย์ที่ใหญ่ที่สุดบนอินเทอร์เน็ต ซึ่งไม่ได้เป็นเว็บไซต์เดียว แต่เป็นเครือข่ายของผู้ใช้หลายล้านคนในชุมชนอิสระที่ทุกคนสามารถโต้ตอบซึ่งกันและกันได้แบบไร้รอยต่อ ไม่ว่าคุณจะชอบอะไร คุณก็พบคนที่ชื่นชอบเหมือนกันโพสต์เกี่ยวกับสิ่งที่คุณชอบได้บน Mastodon!
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในไทม์ไลน์ที่ไร้โฆษณาและเรียงตามลำดับเวลาล้วน ๆ แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้ควบคุมเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
More features:
คุณสมบัติอื่น ๆ:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
Explore: Trending hashtags and accounts are a tap away
Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
โหมดมืด: อ่านโพสต์ในโหมดสว่าง มืด หรือโหมดมืดดำสนิท
การสำรวจความคิดเห็น: สำรวจความคิดเห็นของผู้ติดตามและนับจำนวนการลงคะแนน
สำรวจ: แตะปุ่มเดียวเพื่อดูแฮชแท็กและบัญชีที่เป็นที่นิยม
การแจ้งเตือน: รับการแจ้งเตือนเกี่ยวกับผู้ติดตามใหม่ การตอบกลับ และการดันโพสต์
การแชร์: โพสต์ลง Mastodon ได้โดยตรงจากแอปอื่น ๆ ที่อยู่ในเครื่อง
ความน่ารัก: มาสคอตของเราเป็นช้างน่ารัก และคุณจะเห็นมันโผล่ออกมาเป็นระยะ ๆ
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Mastodon เป็นองค์กรไม่แสวงหาผลกำไรที่จดทะเบียนแล้ว และการพัฒนาได้รับการสนับสนุนจากเงินบริจาคของคุณโดยตรง ดังนั้นจึงไม่มีโฆษณา ไม่มีการทำกำไร และไม่มีการร่วมลงทุน และเรามีแผนจะทำให้เป็นอย่างนี้ต่อไป

View File

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

View File

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

View File

@@ -4,14 +4,18 @@ plugins {
}
android {
compileSdk 31
compileSdk 33
defaultConfig {
applicationId "org.joinmastodon.android"
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 31
versionCode 4
versionName '1.1.0-dev+fork.1.0'
targetSdk 33
versionCode 39
versionName "1.1.4+fork.39"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
"ja-rJP", "kab", "ko-rKR", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ru-rRU",
"sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -33,6 +37,9 @@ android {
initWith release
versionNameSuffix "-beta"
}
githubRelease{
initWith release
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -46,6 +53,16 @@ android {
appcenterPublicBeta{
setRoot "src/appcenter"
}
githubRelease{
setRoot "src/github"
}
debug {
setRoot "src/github"
}
}
lintOptions{
checkReleaseBuilds false
abortOnError false
}
}

View File

@@ -46,4 +46,9 @@
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable
-keepattributes LineNumberTable
# Parceler library
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.joinmastodon.android">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application>
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>-->
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<provider
android:authorities="${applicationId}.self_update_provider"
android:name=".updater.SelfUpdateContentProvider"
android:grantUriPermissions="true"
android:exported="false"/>
</application>
</manifest>

View File

@@ -0,0 +1,356 @@
package org.joinmastodon.android.updater;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInstaller;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import java.io.File;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.Keep;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@Keep
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
private static final long CHECK_PERIOD=6*3600*1000L;
private static final String TAG="GithubSelfUpdater";
private UpdateState state=UpdateState.NO_UPDATE;
private UpdateInfo info;
private long downloadID;
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent){
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
MastodonApp.context.unregisterReceiver(this);
setState(UpdateState.DOWNLOADED);
}
}
};
public GithubSelfUpdaterImpl(){
SharedPreferences prefs=getPrefs();
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
info=new UpdateInfo();
info.version=prefs.getString("version", null);
info.size=prefs.getLong("apkSize", 0);
downloadID=prefs.getLong("downloadID", 0);
if(downloadID==0 || !getUpdateApkFile().exists()){
state=UpdateState.UPDATE_AVAILABLE;
}else{
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
if(state==UpdateState.DOWNLOADING){
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
// We are in a new version, running for the first time after update. Gotta clean things up.
long id=getPrefs().getLong("downloadID", 0);
if(id!=0){
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
}
getUpdateApkFile().delete();
getPrefs().edit()
.remove("apkSize")
.remove("version")
.remove("apkURL")
.remove("checkedByBuild")
.remove("downloadID")
.apply();
}
}
private SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
}
@Override
public void maybeCheckForUpdates(){
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
return;
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
if(timeSinceLastCheck>=CHECK_PERIOD){
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
}
@Override
public void checkForUpdates() {
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
private void actuallyCheckForUpdates(){
Request req=new Request.Builder()
.url("https://api.github.com/repos/sk22/mastodos/releases/latest")
.build();
Call call=MastodonAPIController.getHttpClient().newCall(req);
try(Response resp=call.execute()){
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
String tag=obj.get("tag_name").getAsString();
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
return;
}
int newMajor=Integer.parseInt(matcher.group(1)),
newMinor=Integer.parseInt(matcher.group(2)),
newRevision=Integer.parseInt(matcher.group(3)),
newForkNumber=Integer.parseInt(matcher.group(4));
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
return;
}
int curMajor=Integer.parseInt(matcher.group(1)),
curMinor=Integer.parseInt(matcher.group(2)),
curRevision=Integer.parseInt(matcher.group(3)),
curForkNumber=Integer.parseInt(matcher.group(4));
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
if(newVersion>curVersion || newForkNumber>curForkNumber || BuildConfig.DEBUG){
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
if("application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
long size=asset.get("size").getAsLong();
String url=asset.get("browser_download_url").getAsString();
UpdateInfo info=new UpdateInfo();
info.size=size;
info.version=version;
this.info=info;
getPrefs().edit()
.putLong("apkSize", size)
.putString("version", version)
.putString("apkURL", url)
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
.remove("downloadID")
.apply();
break;
}
}
}
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
}catch(Exception x){
Log.w(TAG, "actuallyCheckForUpdates", x);
}finally{
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
}
}
private void setState(UpdateState state){
this.state=state;
E.post(new SelfUpdateStateChangedEvent(state));
}
@Override
public UpdateState getState(){
return state;
}
@Override
public UpdateInfo getUpdateInfo(){
return info;
}
public File getUpdateApkFile(){
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
}
@Override
public void downloadUpdate(){
if(state==UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
downloadID=dm.enqueue(
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
);
getPrefs().edit().putLong("downloadID", downloadID).apply();
setState(UpdateState.DOWNLOADING);
}
@Override
public void installUpdate(Activity activity){
if(state!=UpdateState.DOWNLOADED)
throw new IllegalStateException();
Uri uri;
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}else{
uri=Uri.fromFile(getUpdateApkFile());
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
activity.startActivity(intent);
// TODO figure out how to restart the app when updating via this new API
/*
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
try{
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
@Override
public void onCreated(int i){
}
@Override
public void onBadgingChanged(int i){
}
@Override
public void onActiveChanged(int i, boolean b){
}
@Override
public void onProgressChanged(int id, float progress){
}
@Override
public void onFinished(int id, boolean success){
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
});
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
PackageInstaller.Session session=installer.openSession(sid);
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
byte[] buffer=new byte[16384];
int read;
while((read=in.read(buffer))>0){
out.write(buffer, 0, read);
}
}
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
session.commit(intent.getIntentSender());
}catch(IOException x){
Log.w(TAG, "installUpdate", x);
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
}
*/
}
@Override
public float getDownloadProgress(){
if(state!=UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
if(cursor.moveToFirst()){
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
return total>0 ? (float)loaded/total : 0f;
}
}
return 0;
}
@Override
public void cancelDownload(){
if(state!=UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
dm.remove(downloadID);
downloadID=0;
getPrefs().edit().remove("downloadID").apply();
setState(UpdateState.UPDATE_AVAILABLE);
}
@Override
public void handleIntentFromInstaller(Intent intent, Activity activity){
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
activity.startActivity(confirmIntent);
}else if(status!=PackageInstaller.STATUS_SUCCESS){
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
}
}
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}else if(status!=PackageInstaller.STATUS_SUCCESS){
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
}
}
}
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
Intent restartIntent=new Intent(context, MainActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setPackage(context.getPackageName());
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
context.startActivity(restartIntent);
}else{
// Bypass activity starting restrictions by starting it from a notification
NotificationManager nm=context.getSystemService(NotificationManager.class);
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
nm.createNotificationChannel(chan);
Notification n=new Notification.Builder(context, "selfUpdateRestart")
.setContentTitle(context.getString(R.string.update_installed))
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
.setSmallIcon(R.drawable.ic_ntf_logo)
.build();
nm.notify(1, n);
}
}
}
}*/
}

View File

@@ -0,0 +1,62 @@
package org.joinmastodon.android.updater;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.FileNotFoundException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SelfUpdateContentProvider extends ContentProvider{
@Override
public boolean onCreate(){
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri){
if(isCorrectUri(uri))
return "application/vnd.android.package-archive";
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
return 0;
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
if(isCorrectUri(uri)){
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException();
}
private boolean isCorrectUri(Uri uri){
return "/update.apk".equals(uri.getPath());
}
}

View File

@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
@@ -15,6 +16,7 @@
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Mastodon.AutoLightDark"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -7,6 +7,10 @@ public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
public static boolean trueBlackTheme;
public static boolean showReplies;
public static boolean showBoosts;
public static boolean loadNewPosts;
public static boolean showInteractionCounts;
public static ThemePreference theme;
private static SharedPreferences getPrefs(){
@@ -18,6 +22,10 @@ public class GlobalUserPreferences{
playGifs=prefs.getBoolean("playGifs", true);
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
}
@@ -25,7 +33,11 @@ public class GlobalUserPreferences{
getPrefs().edit()
.putBoolean("playGifs", playGifs)
.putBoolean("useCustomTabs", useCustomTabs)
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)
.putBoolean("loadNewPosts", loadNewPosts)
.putBoolean("trueBlackTheme", trueBlackTheme)
.putBoolean("showInteractionCounts", showInteractionCounts)
.putInt("theme", theme.ordinal())
.apply();
}

View File

@@ -1,14 +1,19 @@
package org.joinmastodon.android;
import android.Manifest;
import android.app.Application;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.SplashFragment;
@@ -16,6 +21,7 @@ import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.lang.reflect.InvocationTargetException;
@@ -56,6 +62,10 @@ public class MainActivity extends FragmentStackActivity{
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else{
maybeRequestNotificationsPermission();
}
}
}
@@ -65,6 +75,8 @@ public class MainActivity extends FragmentStackActivity{
try{
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
}else if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
}
}
@@ -91,7 +103,11 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
}
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
}*/
}
private void showFragmentForNotification(Notification notification, String accountID){
@@ -115,4 +131,21 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragment(fragment);
}
private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
return;
ComposeFragment compose=new ComposeFragment();
Bundle composeArgs=new Bundle();
composeArgs.putString("account", session.getID());
compose.setArguments(composeArgs);
showFragment(compose);
}
private void maybeRequestNotificationsPermission(){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
}
}
}

View File

@@ -0,0 +1,39 @@
package org.joinmastodon.android.api;
import android.graphics.Rect;
import android.net.Uri;
import java.io.IOException;
public class AvatarResizedImageRequestBody extends ResizedImageRequestBody{
public AvatarResizedImageRequestBody(Uri uri, ProgressListener progressListener) throws IOException{
super(uri, 0, progressListener);
}
@Override
protected int[] getTargetSize(int srcWidth, int srcHeight){
float factor=400f/Math.min(srcWidth, srcHeight);
return new int[]{Math.round(srcWidth*factor), Math.round(srcHeight*factor)};
}
@Override
protected boolean needResize(int srcWidth, int srcHeight){
return srcHeight>400 || srcWidth!=srcHeight;
}
@Override
protected boolean needCrop(int srcWidth, int srcHeight){
return srcWidth!=srcHeight;
}
@Override
protected Rect getCropBounds(int srcWidth, int srcHeight){
Rect rect=new Rect();
if(srcWidth>srcHeight){
rect.set(srcWidth/2-srcHeight/2, 0, srcWidth/2-srcHeight/2+srcHeight, srcHeight);
}else{
rect.set(0, srcHeight/2-srcWidth/2, srcWidth, srcHeight/2-srcWidth/2+srcWidth);
}
return rect;
}
}

View File

@@ -102,7 +102,7 @@ public class CacheController{
.exec(accountID);
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
}finally{
closeDelayed();
}
@@ -184,7 +184,7 @@ public class CacheController{
.exec(accountID);
}catch(SQLiteException x){
Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500, x)));
}finally{
closeDelayed();
}
@@ -233,6 +233,12 @@ public class CacheController{
});
}
public void deleteStatus(String id){
runOnDbThread((db)->{
db.delete("home_timeline", "`id`=?", new String[]{id});
});
}
public void clearRecentSearches(){
runOnDbThread((db)->db.delete("recent_searches", null, null));
}

View File

@@ -96,11 +96,11 @@ public class MastodonAPIController{
if(call.isCanceled())
return;
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed: "+e);
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
synchronized(req){
req.okhttpCall=null;
}
req.onError(e.getLocalizedMessage(), 0);
req.onError(e.getLocalizedMessage(), 0, e);
}
@Override
@@ -133,7 +133,7 @@ public class MastodonAPIController{
}catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code());
req.onError(x.getLocalizedMessage(), response.code(), x);
return;
}
@@ -142,7 +142,7 @@ public class MastodonAPIController{
}catch(IOException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
req.onError(x.getLocalizedMessage(), response.code());
req.onError(x.getLocalizedMessage(), response.code(), x);
return;
}
@@ -155,7 +155,7 @@ public class MastodonAPIController{
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
if(error.has("details")){
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code());
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
JsonObject errorDetails=error.getAsJsonObject("details");
for(String key:errorDetails.keySet()){
@@ -172,12 +172,12 @@ public class MastodonAPIController{
err.detailedErrors=details;
req.onError(err);
}else{
req.onError(error.get("error").getAsString(), response.code());
req.onError(error.get("error").getAsString(), response.code(), null);
}
}catch(JsonIOException|JsonSyntaxException x){
req.onError(response.code()+" "+response.message(), response.code());
req.onError(response.code()+" "+response.message(), response.code(), x);
}catch(Exception x){
req.onError("Error parsing an API error", response.code());
req.onError("Error parsing an API error", response.code(), x);
}
}
}catch(Exception x){
@@ -189,7 +189,7 @@ public class MastodonAPIController{
}catch(Exception x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
req.onError(x.getLocalizedMessage(), 0);
req.onError(x.getLocalizedMessage(), 0, x);
}
}, 0);
}
@@ -197,4 +197,8 @@ public class MastodonAPIController{
public static void runInBackground(Runnable action){
thread.postRunnable(action, 0);
}
public static OkHttpClient getHttpClient(){
return httpClient;
}
}

View File

@@ -82,7 +82,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
account.getApiController().submitRequest(this);
}catch(Exception x){
Log.e(TAG, "exec: this shouldn't happen, but it still did", x);
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1));
invokeErrorCallback(new MastodonErrorResponse(x.getLocalizedMessage(), -1, x));
}
return this;
}
@@ -194,8 +194,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
invokeErrorCallback(err);
}
void onError(String msg, int httpStatus){
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
void onError(String msg, int httpStatus, Throwable exception){
invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus, exception));
}
void onSuccess(T resp){

View File

@@ -7,8 +7,8 @@ import java.util.Map;
public class MastodonDetailedErrorResponse extends MastodonErrorResponse{
public Map<String, List<FieldError>> detailedErrors;
public MastodonDetailedErrorResponse(String error, int httpStatus){
super(error, httpStatus);
public MastodonDetailedErrorResponse(String error, int httpStatus, Throwable exception){
super(error, httpStatus, exception);
}
public static class FieldError{

View File

@@ -12,10 +12,12 @@ import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{
public final String error;
public final int httpStatus;
public final Throwable underlyingException;
public MastodonErrorResponse(String error, int httpStatus){
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
this.error=error;
this.httpStatus=httpStatus;
this.underlyingException=exception;
}
@Override
@@ -26,6 +28,8 @@ public class MastodonErrorResponse extends ErrorResponse{
@Override
public void showToast(Context context){
if(context==null)
return;
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -12,8 +12,10 @@ import android.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileOutputStream;
@@ -30,62 +32,107 @@ public class ResizedImageRequestBody extends CountingRequestBody{
private File tempFile;
private Uri uri;
private String contentType;
private int maxSize;
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
super(progressListener);
this.uri=uri;
contentType=MastodonApp.context.getContentResolver().getType(uri);
this.maxSize=maxSize;
BitmapFactory.Options opts=new BitmapFactory.Options();
opts.inJustDecodeBounds=true;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
if("file".equals(uri.getScheme())){
BitmapFactory.decodeFile(uri.getPath(), opts);
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
}else{
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
}
contentType=MastodonApp.context.getContentResolver().getType(uri);
}
if(opts.outWidth*opts.outHeight>maxSize){
if(TextUtils.isEmpty(contentType))
contentType="image/jpeg";
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
Bitmap bitmap;
if(Build.VERSION.SDK_INT>=29){
bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight())));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth())));
if(Build.VERSION.SDK_INT>=28){
ImageDecoder.Source source;
if("file".equals(uri.getScheme())){
source=ImageDecoder.createSource(new File(uri.getPath()));
}else{
source=ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri);
}
bitmap=ImageDecoder.decodeBitmap(source, (decoder, info, _source)->{
int[] size=getTargetSize(info.getSize().getWidth(), info.getSize().getHeight());
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
decoder.setTargetSize(targetWidth, targetHeight);
decoder.setTargetSize(size[0], size[1]);
// Breaks images in mysterious ways
// if(needCrop(size[0], size[1]))
// decoder.setCrop(getCropBounds(size[0], size[1]));
});
if(needCrop(bitmap.getWidth(), bitmap.getHeight())){
Rect crop=getCropBounds(bitmap.getWidth(), bitmap.getHeight());
bitmap=Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height());
}
}else{
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight)));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth)));
int[] size=getTargetSize(opts.outWidth, opts.outHeight);
int targetWidth=size[0];
int targetHeight=size[1];
float factor=opts.outWidth/(float)targetWidth;
opts=new BitmapFactory.Options();
opts.inSampleSize=(int)factor;
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
bitmap=BitmapFactory.decodeStream(in, null, opts);
if("file".equals(uri.getScheme())){
bitmap=BitmapFactory.decodeFile(uri.getPath(), opts);
}else{
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
bitmap=BitmapFactory.decodeStream(in, null, opts);
}
}
if(factor%1f!=0f){
Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, targetWidth, targetHeight), new Paint(Paint.FILTER_BITMAP_FLAG));
boolean needCrop=needCrop(targetWidth, targetHeight);
if(factor%1f!=0f || needCrop){
Rect srcBounds=null;
Rect dstBounds;
if(needCrop){
Rect crop=getCropBounds(targetWidth, targetHeight);
dstBounds=new Rect(0, 0, crop.width(), crop.height());
srcBounds=new Rect(
Math.round(crop.left/(float)targetWidth*bitmap.getWidth()),
Math.round(crop.top/(float)targetHeight*bitmap.getHeight()),
Math.round(crop.right/(float)targetWidth*bitmap.getWidth()),
Math.round(crop.bottom/(float)targetHeight*bitmap.getHeight())
);
}else{
dstBounds=new Rect(0, 0, targetWidth, targetHeight);
}
Bitmap scaled=Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888);
new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG));
bitmap=scaled;
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
int rotation;
int orientation=0;
if("file".equals(uri.getScheme())){
ExifInterface exif=new ExifInterface(uri.getPath());
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
ExifInterface exif=new ExifInterface(in);
int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
rotation=switch(orientation){
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
default -> 0;
};
}
if(rotation!=0){
Matrix matrix=new Matrix();
matrix.setRotate(rotation);
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
}
}
int rotation=switch(orientation){
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
default -> 0;
};
if(rotation!=0){
Matrix matrix=new Matrix();
matrix.setRotate(rotation);
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
}
}
tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image");
boolean isPNG="image/png".equals(contentType);
tempFile=File.createTempFile("mastodon_tmp_resized", null);
try(FileOutputStream out=new FileOutputStream(tempFile)){
if("image/png".equals(contentType)){
if(isPNG){
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
}else{
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
@@ -94,9 +141,13 @@ public class ResizedImageRequestBody extends CountingRequestBody{
}
length=tempFile.length();
}else{
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst();
length=cursor.getInt(0);
if("file".equals(uri.getScheme())){
length=new File(uri.getPath()).length();
}else{
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst();
length=cursor.getInt(0);
}
}
}
}
@@ -125,4 +176,22 @@ public class ResizedImageRequestBody extends CountingRequestBody{
}
}
}
protected int[] getTargetSize(int srcWidth, int srcHeight){
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)srcWidth/srcHeight)));
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)srcHeight/srcWidth)));
return new int[]{targetWidth, targetHeight};
}
protected boolean needResize(int srcWidth, int srcHeight){
return srcWidth*srcHeight>maxSize;
}
protected boolean needCrop(int srcWidth, int srcHeight){
return false;
}
protected Rect getCropBounds(int srcWidth, int srcHeight){
return null;
}
}

View File

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

View File

@@ -21,18 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true");
addQueryParameter("exclude_reblogs", "true");
}
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
}
}
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
PINNED,
MEDIA,
NO_REBLOGS
NO_REBLOGS,
OWN_POSTS_AND_REPLIES
}
}

View File

@@ -0,0 +1,52 @@
package org.joinmastodon.android.api.requests.accounts;
import androidx.annotation.NonNull;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okhttp3.Response;
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
private String maxId;
public GetBookmarks(String maxID, String minID, int limit){
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
super.validateAndPostprocessResponse(respObj, httpResponse);
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
String link=httpResponse.header("link");
// parsing link header by hand; using a library would be cleaner
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
if(link==null) return;
String maxIdEq="max_id=";
for(String s : link.split(",")) {
if(s.contains("rel=\"next\"")) {
int start=s.indexOf(maxIdEq)+maxIdEq.length();
int end=s.indexOf('>');
if(start<0 || start>end) return;
this.maxId=s.substring(start, end);
}
}
}
public String getMaxId() {
return maxId;
}
}

View File

@@ -0,0 +1,49 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.io.IOException;
import java.util.List;
import okhttp3.Response;
public class GetFavourites extends MastodonAPIRequest<List<Status>>{
private String maxId;
public GetFavourites(String maxID, String minID, int limit){
super(HttpMethod.GET, "/favourites", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
super.validateAndPostprocessResponse(respObj, httpResponse);
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
String link=httpResponse.header("link");
// parsing link header by hand; using a library would be cleaner
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
if(link==null) return;
String maxIdEq="max_id=";
for(String s : link.split(",")) {
if(s.contains("rel=\"next\"")) {
int start=s.indexOf(maxIdEq)+maxIdEq.length();
int end=s.indexOf('>');
if(start<0 || start>end) return;
this.maxId=s.substring(start, end);
}
}
}
public String getMaxId() {
return maxId;
}
}

View File

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

View File

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

View File

@@ -2,13 +2,16 @@ package org.joinmastodon.android.api.requests.accounts;
import android.net.Uri;
import org.joinmastodon.android.api.AvatarResizedImageRequestBody;
import org.joinmastodon.android.api.ContentUriRequestBody;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.ResizedImageRequestBody;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
import okhttp3.MultipartBody;
@@ -39,21 +42,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
}
@Override
public RequestBody getRequestBody(){
public RequestBody getRequestBody() throws IOException{
MultipartBody.Builder bldr=new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("display_name", displayName)
.addFormDataPart("note", bio);
if(avatar!=null){
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null));
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new AvatarResizedImageRequestBody(avatar, null));
}else if(avatarFile!=null){
bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile));
bldr.addFormDataPart("avatar", avatarFile.getName(), new AvatarResizedImageRequestBody(Uri.fromFile(avatarFile), null));
}
if(cover!=null){
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null));
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ResizedImageRequestBody(cover, 1500*500, null));
}else if(coverFile!=null){
bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile));
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
}
if(fields.isEmpty()){
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.follow_requests;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class AuthorizeFollowRequest extends MastodonAPIRequest<Relationship>{
public AuthorizeFollowRequest(String id){
super(HttpMethod.POST, "/follow_requests/"+id+"/authorize", Relationship.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.follow_requests;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class RejectFollowRequest extends MastodonAPIRequest<Relationship>{
public RejectFollowRequest(String id){
super(HttpMethod.POST, "/follow_requests/"+id+"/reject", Relationship.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.util.List;
public class AddAccountsToList extends MastodonAPIRequest<Object> {
public AddAccountsToList(String listId, List<String> accountIds){
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
Request req = new Request();
req.accountIds = accountIds;
setRequestBody(req);
}
public static class Request{
public List<String> accountIds;
}
}

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
import java.util.List;
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
public GetLists() {
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
}
public GetLists(String accountID) {
super(HttpMethod.GET, "/accounts/"+accountID+"/lists", new TypeToken<>(){});
}
}

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.util.List;
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
public RemoveAccountsFromList(String listId, List<String> accountIds){
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
Request req = new Request();
req.accountIds = accountIds;
setRequestBody(req);
}
public static class Request{
public List<String> accountIds;
}
}

View File

@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
}
private static class Request{
public String clientName="Mastodon for Android";
public String clientName="Mastodos";
public String redirectUris=AccountSessionManager.REDIRECT_URI;
public String scopes=AccountSessionManager.SCOPE;
public String website="https://app.joinmastodon.org/android";
public String website="https://sk22.github.io/mastodos";
}
}

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 EditStatus extends MastodonAPIRequest<Status>{
public EditStatus(CreateStatus.Request req, String id){
super(HttpMethod.PUT, "/statuses/"+id, Status.class);
setRequestBody(req);
}
}

View File

@@ -0,0 +1,21 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Attachment;
import java.io.IOException;
import okhttp3.Response;
public class GetAttachmentByID extends MastodonAPIRequest<Attachment>{
public GetAttachmentByID(String id){
super(HttpMethod.GET, "/media/"+id, Attachment.class);
}
@Override
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
if(httpResponse.code()==206)
respObj.url="";
super.validateAndPostprocessResponse(respObj, httpResponse);
}
}

View File

@@ -0,0 +1,33 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import okhttp3.Response;
public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
public GetStatusEditHistory(String id){
super(HttpMethod.GET, "/statuses/"+id+"/history", new TypeToken<>(){});
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException{
int i=0;
for(Status s:respObj){
s.uri="";
s.id="fakeID"+i;
s.visibility=StatusPrivacy.PUBLIC;
s.mentions=Collections.emptyList();
s.tags=Collections.emptyList();
i++;
}
super.validateAndPostprocessResponse(respObj, httpResponse);
}
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.BaseModel;
public class GetStatusSourceText extends MastodonAPIRequest<GetStatusSourceText.Response>{
public GetStatusSourceText(String id){
super(HttpMethod.GET, "/statuses/"+id+"/source", Response.class);
}
@AllFieldsAreRequired
public static class Response extends BaseModel{
public String id;
public String text;
public String spoilerText;
}
}

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import java.io.IOException;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
private Uri uri;
@@ -40,6 +41,18 @@ public class UploadAttachment extends MastodonAPIRequest<Attachment>{
return this;
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
@Override
public void validateAndPostprocessResponse(Attachment respObj, Response httpResponse) throws IOException{
if(respObj.url==null)
respObj.url="";
super.validateAndPostprocessResponse(respObj, httpResponse);
}
@Override
public RequestBody getRequestBody() throws IOException{
MultipartBody.Builder builder=new MultipartBody.Builder()

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.tags;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
public class GetHashtag extends MastodonAPIRequest<Hashtag> {
public GetHashtag(String name){
super(HttpMethod.GET, "/tags/"+name, Hashtag.class);
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.tags;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
public class SetHashtagFollowed extends MastodonAPIRequest<Hashtag>{
public SetHashtagFollowed(String name, boolean followed){
super(HttpMethod.POST, "/tags/"+name+"/"+(followed ? "follow" : "unfollow"), Hashtag.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) {
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
}
}

View File

@@ -2,15 +2,22 @@ package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
@@ -85,11 +92,12 @@ public class AccountSessionManager{
domains.add(session.domain.toLowerCase());
sessions.put(session.getID(), session);
}
}catch(IOException|JsonParseException x){
}catch(Exception x){
Log.e(TAG, "Error loading accounts", x);
}
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
@@ -102,6 +110,7 @@ public class AccountSessionManager{
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
maybeUpdateShortcuts();
}
public synchronized void writeAccountsFile(){
@@ -181,6 +190,7 @@ public class AccountSessionManager{
NotificationManager nm=MastodonApp.context.getSystemService(NotificationManager.class);
nm.deleteNotificationChannelGroup(id);
}
maybeUpdateShortcuts();
}
@NonNull
@@ -358,7 +368,7 @@ public class AccountSessionManager{
customEmojis.put(domain, groupCustomEmojis(emojis));
instances.put(domain, emojis.instance);
instancesLastUpdated.put(domain, emojis.lastUpdated);
}catch(IOException|JsonParseException x){
}catch(Exception x){
Log.w(TAG, "Error reading instance info file for "+domain, x);
}
}
@@ -395,6 +405,29 @@ public class AccountSessionManager{
writeAccountsFile();
}
private void maybeUpdateShortcuts(){
if(Build.VERSION.SDK_INT<26)
return;
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
// There are no shortcuts, but there are accounts. Add a compose shortcut.
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
.setAction(Intent.ACTION_MAIN)
.putExtra("compose", true))
.build();
sm.setDynamicShortcuts(Collections.singletonList(info));
}else if(sessions.isEmpty()){
// There are shortcuts, but no accounts. Disable existing shortcuts.
sm.disableShortcuts(Collections.singletonList("compose"), MastodonApp.context.getString(R.string.err_not_logged_in));
}else{
sm.enableShortcuts(Collections.singletonList("compose"));
}
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.events;
public class NotificationDeletedEvent{
public final String id;
public NotificationDeletedEvent(String id){
this.id=id;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.updater.GithubSelfUpdater;
public class SelfUpdateStateChangedEvent{
public final GithubSelfUpdater.UpdateState state;
public SelfUpdateStateChangedEvent(GithubSelfUpdater.UpdateState state){
this.state=state;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.Status;
public class StatusUpdatedEvent{
public Status status;
public StatusUpdatedEvent(Status status){
this.status=status;
}
}

View File

@@ -8,8 +8,10 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.parceler.Parcels;
import java.util.Collections;
@@ -76,6 +78,7 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
return;
if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){
// Keep replies to self, discard all other replies
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
@@ -86,4 +89,24 @@ public class AccountTimelineFragment extends StatusListFragment{
}
prependItems(Collections.singletonList(ev.status), true);
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
return;
Status status=getStatusByID(ev.id);
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
}

View File

@@ -457,13 +457,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
status.spoilerRevealed=true;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null)
header.rebind();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(true);
}
updateImagesSpoilerState(status, itemID);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
@@ -472,12 +470,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()+getMainAdapterOffset());
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
}
holder.rebind();
for(ImageStatusDisplayItem.Holder<?> photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(holder.getItemID(), ImageStatusDisplayItem.Holder.class)){
updateImagesSpoilerState(status, holder.getItemID());
}
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
photo.setRevealed(status.spoilerRevealed);
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
}
}
@@ -568,6 +579,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return true;
}
public ArrayList<StatusDisplayItem> getDisplayItems(){
return displayItems;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){

View File

@@ -0,0 +1,53 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class BookmarksListFragment extends StatusListFragment{
private String accountID;
private Account self;
private String lastMaxId=null;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
setTitle(R.string.bookmarks);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count) {
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
currentRequest=b.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, b.getMaxId()!=null);
lastMaxId=b.getMaxId();
}
})
.exec(accountID);
}
}

View File

@@ -0,0 +1,46 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFavourites;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class FavoritesListFragment extends StatusListFragment{
private String accountID;
private String lastMaxId=null;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
setTitle(R.string.favorited_posts);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count) {
GetFavourites b=new GetFavourites(offset>0 ? lastMaxId : null, null, count);
currentRequest=b.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, b.getMaxId()!=null);
lastMaxId=b.getMaxId();
}
})
.exec(accountID);
}
}

View File

@@ -2,23 +2,34 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetHashtag;
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class HashtagTimelineFragment extends StatusListFragment{
private String hashtag;
private boolean following;
private ImageButton fab;
private MenuItem followButton;
public HashtagTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -27,10 +38,61 @@ public class HashtagTimelineFragment extends StatusListFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
hashtag=getArguments().getString("hashtag");
updateTitle(getArguments().getString("hashtag"));
following=getArguments().getBoolean("following", false);
setHasOptionsMenu(true);
}
private void updateTitle(String hashtagName) {
hashtag = hashtagName;
setTitle('#'+hashtag);
}
private void updateFollowingState(boolean newFollowing) {
this.following = newFollowing;
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.hashtag_timeline, menu);
followButton = menu.findItem(R.id.follow_hashtag);
updateFollowingState(following);
followButton.setOnMenuItemClickListener(i -> {
updateFollowingState(!following);
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag i) {
updateFollowingState(i.following);
Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
updateFollowingState(!following);
}
}).exec(accountID);
return true;
});
new GetHashtag(hashtag).setCallback(new Callback<>() {
@Override
public void onSuccess(Hashtag hashtag) {
updateTitle(hashtag.name);
updateFollowingState(hashtag.following);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)

View File

@@ -2,13 +2,9 @@ package org.joinmastodon.android.fragments;
import android.app.Fragment;
import android.app.NotificationManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -18,20 +14,14 @@ import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.PushNotificationReceiver;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.discover.SearchFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.parceler.Parcels;
@@ -41,15 +31,12 @@ import java.util.ArrayList;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
@@ -141,7 +128,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
});
}
}else{
}
return content;
@@ -255,9 +241,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public boolean onBackPressed(){
if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed();
if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed();
if (searchFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home);
return true;
}
return false;
}

View File

@@ -23,9 +23,12 @@ import android.widget.Toolbar;
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.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
@@ -33,6 +36,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.Collections;
@@ -70,6 +74,13 @@ public class HomeTimelineFragment extends StatusListFragment{
loadData();
}
private List<Status> filterPosts(List<Status> items) {
return items.stream().filter(i ->
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || i.reblog == null)
).collect(Collectors.toList());
}
@Override
protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance()
@@ -79,7 +90,8 @@ public class HomeTimelineFragment extends StatusListFragment{
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
if(getActivity()==null)
return;
onDataLoaded(result.items, !result.items.isEmpty());
List<Status> filteredItems = filterPosts(result.items);
onDataLoaded(filteredItems, !result.items.isEmpty());
maxID=result.maxID;
if(result.isFromCache())
loadNewPosts();
@@ -101,6 +113,11 @@ public class HomeTimelineFragment extends StatusListFragment{
}
}
});
if(GithubSelfUpdater.needSelfUpdating()){
E.register(this);
updateUpdateState(GithubSelfUpdater.getInstance().getState());
}
}
@Override
@@ -134,7 +151,6 @@ public class HomeTimelineFragment extends StatusListFragment{
}
}
@Subscribe
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
}
@@ -146,6 +162,7 @@ public class HomeTimelineFragment extends StatusListFragment{
}
private void loadNewPosts(){
if (!GlobalUserPreferences.loadNewPosts) return;
dataLoading=true;
// The idea here is that we request the timeline such that if there are fewer than `limit` posts,
// we'll get the currently topmost post as last in the response. This way we know there's no gap
@@ -157,6 +174,7 @@ public class HomeTimelineFragment extends StatusListFragment{
public void onSuccess(List<Status> result){
currentRequest=null;
dataLoading=false;
result = filterPosts(result);
if(result.isEmpty() || getActivity()==null)
return;
Status last=result.get(result.size()-1);
@@ -397,4 +415,22 @@ public class HomeTimelineFragment extends StatusListFragment{
scrollToTop();
}
}
@Override
public void onDestroyView(){
super.onDestroyView();
if(GithubSelfUpdater.needSelfUpdating()){
E.unregister(this);
}
}
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateState(ev.state);
}
}

View File

@@ -0,0 +1,84 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.media.MediaRouter;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ListTimelineFragment extends StatusListFragment {
private String listID;
private String listTitle;
private ImageButton fab;
public ListTimelineFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
listID=getArguments().getString("listID");
listTitle=getArguments().getString("listTitle");
setTitle(listTitle);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
// inflater.inflate(R.menu.list, menu);
}
@Override
protected void doLoadData(int offset, int count) {
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<Status> result) {
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
protected void onShown() {
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
}
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", listID+' ');
Nav.go(getActivity(), ComposeFragment.class, args);
}
@Override
protected void onSetFabBottomInset(int inset) {
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
}
}

View File

@@ -0,0 +1,188 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
private String accountId;
private String profileAccountId;
private String profileDisplayUsername;
private HashMap<String, Boolean> userInListBefore = new HashMap<>();
private HashMap<String, Boolean> userInList = new HashMap<>();
private int inProgress = 0;
public ListTimelinesFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
profileDisplayUsername=args.getString("profileDisplayUsername");
setTitle(getString(R.string.lists_with_user, profileDisplayUsername));
// setHasOptionsMenu(true);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
// @Override
// public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// Button saveButton=new Button(getActivity());
// saveButton.setText(R.string.save);
// saveButton.setOnClickListener(this::onSaveClick);
// LinearLayout wrap=new LinearLayout(getActivity());
// wrap.setOrientation(LinearLayout.HORIZONTAL);
// wrap.addView(saveButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
// wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
// wrap.setClipToPadding(false);
// MenuItem item=menu.add(R.string.save);
// item.setActionView(wrap);
// item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// }
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(Object o) {}
}).exec(accountId);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
@Override
protected RecyclerView.Adapter getAdapter() {
return new ListsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_list_timeline, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
UiUtils.openListTimeline(getActivity(), accountId, item);
}
}
}

View File

@@ -1,10 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.view.View;
@@ -13,27 +9,25 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
@@ -84,9 +78,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else{
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
}else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account, n);
return Arrays.asList(titleItem, card);
}else{
return Collections.emptyList();
}
}
@@ -160,91 +156,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private int bgColor=UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground);
private int borderColor=UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted);
private RectF rect=new RectF();
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
int pos=0;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
pos=holder.getAbsoluteAdapterPosition();
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
if(inset){
if(rect.isEmpty()){
rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
}else{
rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight());
}
}else if(!rect.isEmpty()){
drawInsetBackground(c);
rect.setEmpty();
}
}
if(!rect.isEmpty()){
if(pos<displayItems.size()-1 && displayItems.get(pos+1).inset){
rect.bottom=parent.getHeight()+V.dp(10);
}
drawInsetBackground(c);
rect.setEmpty();
}
}
private void drawInsetBackground(Canvas c){
paint.setStyle(Paint.Style.FILL);
paint.setColor(bgColor);
rect.left=V.dp(12);
rect.right=list.getWidth()-V.dp(12);
rect.inset(V.dp(4), V.dp(4));
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
paint.setColor(borderColor);
rect.inset(paint.getStrokeWidth()/2f, paint.getStrokeWidth()/2f);
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
boolean inset=sdi.getItem().inset;
int pos=holder.getAbsoluteAdapterPosition();
if(inset){
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
int pad;
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
pad=V.dp(16);
else
pad=V.dp(12);
boolean insetLeft=true, insetRight=true;
if(holder instanceof ImageStatusDisplayItem.Holder<?> img){
PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile;
// only inset those items that are on the edges of the layout
insetLeft=tile.startCol==0;
insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length;
// inset all items in the bottom row
if(tile.startRow+tile.rowSpan==layout.rowSizes.length)
bottomSiblingInset=false;
}
if(insetLeft)
outRect.left=pad;
if(insetRight)
outRect.right=pad;
if(!topSiblingInset)
outRect.top=pad;
if(!bottomSiblingInset)
outRect.bottom=pad;
}
}
}
});
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
private Notification getNotificationByID(String id){
@@ -268,4 +180,29 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
}
@Subscribe
public void onNotificationDeleted(NotificationDeletedEvent ev) {
Notification notification = getNotificationByID(ev.id);
if(notification==null)
return;
data.remove(notification);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(ev.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
}

View File

@@ -98,10 +98,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
private ProgressBarButton actionButton;
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@@ -109,7 +109,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private float titleTransY;
private View postsBtn, followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress;
private ProgressBar actionProgress, notifyProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
@@ -128,6 +128,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View fab;
private WindowInsets childInsets;
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar);
@@ -180,6 +181,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsLabel=content.findViewById(R.id.posts_label);
postsBtn=content.findViewById(R.id.posts_btn);
actionButton=content.findViewById(R.id.profile_action_btn);
notifyButton=content.findViewById(R.id.notify_btn);
pager=content.findViewById(R.id.pager);
scrollView=content.findViewById(R.id.scroller);
tabbar=content.findViewById(R.id.tabbar);
@@ -187,6 +189,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit=content.findViewById(R.id.name_edit);
bioEdit=content.findViewById(R.id.bio_edit);
actionProgress=content.findViewById(R.id.action_progress);
notifyProgress=content.findViewById(R.id.notify_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
@@ -208,14 +211,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
};
tabViews=new FrameLayout[4];
tabViews=new FrameLayout[5];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_posts;
case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media;
case 3 -> R.id.profile_about;
case 2 -> R.id.profile_pinned_posts;
case 3 -> R.id.profile_media;
case 4 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -223,7 +227,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabViews[i]=tabView;
}
pager.setOffscreenPageLimit(4);
pager.setOffscreenPageLimit(5);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -239,8 +243,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media;
case 3 -> R.string.profile_about;
case 2 -> R.string.pinned_posts;
case 3 -> R.string.media;
case 4 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
@@ -255,6 +260,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
});
actionButton.setOnClickListener(this::onActionButtonClick);
notifyButton.setOnClickListener(this::onNotifyButtonClick);
avatar.setOnClickListener(this::onAvatarClick);
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
@@ -297,6 +303,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
@@ -321,6 +329,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
@@ -401,22 +410,30 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
pinnedPostsFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
}
}
private void bindHeaderView(){
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
setTitle(ssb);
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_fluent_lock_closed_20_filled, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
@@ -424,7 +441,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
username.setText(ssb);
}else{
username.setText('@'+account.acct);
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
@@ -436,13 +454,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, account.statusesCount)));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
UiUtils.loadCustomEmojiInTextView(name);
UiUtils.loadCustomEmojiInTextView(bio);
notifyButton.setVisibility(View.GONE);
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
actionButton.setText(R.string.edit_profile);
}else{
@@ -508,17 +527,28 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return;
}
if(relationship==null)
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.lists_with_user, account.getDisplayUsername()));
if(isOwnProfile){
for(int i=0;i<menu.size();i++){
MenuItem item=menu.getItem(i);
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks || item.getItemId()==R.id.manage_user_lists);
}
menu.findItem(R.id.favorites_list).setVisible(true);
return;
}
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
if(relationship.following)
if(relationship.following) {
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
else
}else {
menu.findItem(R.id.hide_boosts).setVisible(false);
menu.findItem(R.id.manage_user_lists).setVisible(false);
}
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
else
@@ -528,11 +558,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
if(id==R.id.share) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.bookmarks) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(getActivity(), BookmarksListFragment.class, args);
}else if(id==R.id.favorites_list) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(getActivity(), FavoritesListFragment.class, args);
}else if(id==R.id.mute){
confirmToggleMuted();
}else if(id==R.id.block){
@@ -550,7 +590,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
@@ -564,6 +604,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.manage_user_lists){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
}
return true;
}
@@ -595,9 +641,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void updateRelationship(){
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
UiUtils.setRelationshipToActionButton(relationship, notifyButton, true);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying);
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username));
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
@@ -636,8 +687,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
case 3 -> aboutFragment;
case 2 -> pinnedPostsFragment;
case 3 -> mediaFragment;
case 4 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@@ -663,18 +715,33 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
actionButton.setClickable(!visible);
}
private void setNotifyProgressVisible(boolean visible){
notifyButton.setTextVisible(!visible);
notifyProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
notifyButton.setClickable(!visible);
}
private void loadAccountInfoAndEnterEditMode(){
if(editModeLoading)
return;
editModeLoading=true;
setActionProgressVisible(true);
new GetOwnAccount()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
editModeLoading=false;
if(getActivity()==null)
return;
enterEditMode(result);
setActionProgressVisible(false);
}
@Override
public void onError(ErrorResponse error){
editModeLoading=false;
if(getActivity()==null)
return;
error.showToast(getActivity());
setActionProgressVisible(false);
}
@@ -689,9 +756,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.done);
pager.setCurrentItem(3);
pager.setCurrentItem(4);
ArrayList<Animator> animators=new ArrayList<>();
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false);
}
@@ -732,7 +799,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
}
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
@@ -750,7 +817,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
@@ -822,6 +889,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return Collections.singletonList(att);
}
private void onNotifyButtonClick(View v) {
UiUtils.performToggleAccountNotifications(getActivity(), account, accountID, relationship, actionButton, this::setNotifyProgressVisible, this::updateRelationship);
}
private void onAvatarClick(View v){
if(isInEditMode){
startImagePicker(AVATAR_RESULT);
@@ -927,7 +998,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public int getItemCount(){
return loaded ? 4 : 0;
return loaded ? tabViews.length : 0;
}
@Override

View File

@@ -13,6 +13,8 @@ public interface ScrollableToTop{
* @param list
*/
default void smoothScrollRecyclerViewToTop(RecyclerView list){
if(list==null) // TODO find out why this happens because it should not be possible
return;
if(list.getChildCount()>0 && list.getChildAdapterPosition(list.getChildAt(0))>10){
list.scrollToPosition(0);
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
@@ -14,15 +15,22 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
@@ -31,11 +39,13 @@ import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.ArrayList;
import java.util.function.Consumer;
@@ -47,7 +57,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
@@ -63,6 +72,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
private PushSubscription pushSubscription;
private ImageView themeTransitionWindowView;
private TextItem checkForUpdateItem;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -73,6 +83,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
GithubSelfUpdater.UpdateState state=updater.getState();
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
items.add(new UpdateItem());
}
}
items.add(new HeaderItem(R.string.settings_theme));
items.add(themeItem=new ThemeItem());
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
@@ -86,6 +104,24 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.useCustomTabs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
GlobalUserPreferences.showInteractionCounts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.home_timeline));
items.add(new SwitchItem(R.string.settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
GlobalUserPreferences.showBoosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
GlobalUserPreferences.loadNewPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
@@ -97,11 +133,15 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new HeaderItem(R.string.settings_boring));
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new RedHeaderItem(R.string.settings_spicy));
if (GithubSelfUpdater.needSelfUpdating()) {
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
@@ -131,7 +171,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
// Add 32dp gaps between sections
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
outRect.top=V.dp(32);
}
});
@@ -155,6 +195,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if(GithubSelfUpdater.needSelfUpdating())
E.register(this);
}
@Override
public void onDestroyView(){
super.onDestroyView();
if(GithubSelfUpdater.needSelfUpdating())
E.unregister(this);
}
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
GlobalUserPreferences.theme=theme;
GlobalUserPreferences.save();
@@ -294,6 +348,30 @@ public class SettingsFragment extends MastodonToolbarFragment{
});
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING;
if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind();
UpdateItem updateItem = null;
if(items.get(0) instanceof UpdateItem item0) {
updateItem = item0;
} else if (ev.state != GithubSelfUpdater.UpdateState.CHECKING
&& ev.state != GithubSelfUpdater.UpdateState.NO_UPDATE) {
updateItem = new UpdateItem();
items.add(0, updateItem);
list.setAdapter(new SettingsAdapter());
}
if(updateItem != null && list.findViewHolderForAdapterPosition(0) instanceof UpdateViewHolder uvh){
uvh.bind(updateItem);
}
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
}
}
private static abstract class Item{
public abstract int getViewType();
}
@@ -358,10 +436,16 @@ public class SettingsFragment extends MastodonToolbarFragment{
private class TextItem extends Item{
private String text;
private Runnable onClick;
private boolean loading;
public TextItem(@StringRes int text, Runnable onClick){
public TextItem(@StringRes int text, Runnable onClick) {
this(text, onClick, false);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
this.text=getString(text);
this.onClick=onClick;
this.loading=loading;
}
@Override
@@ -395,6 +479,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class UpdateItem extends Item{
@Override
public int getViewType(){
return 7;
}
}
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
@NonNull
@Override
@@ -408,6 +500,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
case 4 -> new TextViewHolder();
case 5 -> new HeaderViewHolder(true);
case 6 -> new FooterViewHolder();
case 7 -> new UpdateViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@@ -581,14 +674,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
private final ProgressBar progress;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text=(TextView) itemView;
text = itemView.findViewById(R.id.text);
progress = itemView.findViewById(R.id.progress);
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
progress.animate().alpha(item.loading ? 1 : 0);
}
@Override
@@ -609,4 +706,75 @@ public class SettingsFragment extends MastodonToolbarFragment{
text.setText(item.text);
}
}
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
private final TextView text;
private final Button button;
private final ImageButton cancelBtn;
private final ProgressBar progress;
private ObjectAnimator rotationAnimator;
private Runnable progressUpdater=this::updateProgress;
public UpdateViewHolder(){
super(getActivity(), R.layout.item_settings_update, list);
text=findViewById(R.id.text);
button=findViewById(R.id.button);
cancelBtn=findViewById(R.id.cancel_btn);
progress=findViewById(R.id.progress);
button.setOnClickListener(v->{
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
switch(updater.getState()){
case UPDATE_AVAILABLE -> updater.downloadUpdate();
case DOWNLOADED -> updater.installUpdate(getActivity());
}
});
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
rotationAnimator.setInterpolator(new LinearInterpolator());
rotationAnimator.setDuration(1500);
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
}
@Override
public void onBind(UpdateItem item){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
GithubSelfUpdater.UpdateState state=updater.getState();
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
text.setText(getString(R.string.update_available, info.version));
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
}else{
text.setText(getString(R.string.update_ready, info.version));
button.setText(R.string.install_update);
}
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
rotationAnimator.start();
button.setVisibility(View.INVISIBLE);
cancelBtn.setVisibility(View.VISIBLE);
progress.setVisibility(View.VISIBLE);
updateProgress();
}else{
rotationAnimator.cancel();
button.setVisibility(View.VISIBLE);
cancelBtn.setVisibility(View.GONE);
progress.setVisibility(View.GONE);
progress.removeCallbacks(progressUpdater);
}
}
private void updateProgress(){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
return;
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
if(Build.VERSION.SDK_INT>=24)
progress.setProgress(value, true);
else
progress.setProgress(value);
progress.postDelayed(progressUpdater, 1000);
}
}
}

View File

@@ -0,0 +1,157 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEditHistoryFragment extends StatusListFragment{
private String id;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id=getArguments().getString("id");
loadData();
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.edit_history);
}
@Override
protected void doLoadData(int offset, int count){
new GetStatusEditHistory(id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
onDataLoaded(result, false);
}
})
.exec(accountID);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
String action="";
if(idx==data.size()-1){
action=getString(R.string.edit_original_post);
}else{
enum StatusEditChangeType{
TEXT_CHANGED,
SPOILER_ADDED,
SPOILER_REMOVED,
SPOILER_CHANGED,
POLL_ADDED,
POLL_REMOVED,
POLL_CHANGED,
MEDIA_ADDED,
MEDIA_REMOVED,
MEDIA_REORDERED,
MARKED_SENSITIVE,
MARKED_NOT_SENSITIVE
}
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
Status prev=data.get(idx+1);
if(!Objects.equals(s.content, prev.content)){
changes.add(StatusEditChangeType.TEXT_CHANGED);
}
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
if(s.spoilerText==null){
changes.add(StatusEditChangeType.SPOILER_REMOVED);
}else if(prev.spoilerText==null){
changes.add(StatusEditChangeType.SPOILER_ADDED);
}else{
changes.add(StatusEditChangeType.SPOILER_CHANGED);
}
}
if(s.poll!=null || prev.poll!=null){
if(s.poll==null){
changes.add(StatusEditChangeType.POLL_REMOVED);
}else if(prev.poll==null){
changes.add(StatusEditChangeType.POLL_ADDED);
}else if(!s.poll.id.equals(prev.poll.id)){
changes.add(StatusEditChangeType.POLL_CHANGED);
}
}
List<String> newAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
List<String> prevAttachmentIDs=s.mediaAttachments.stream().map(att->att.id).collect(Collectors.toList());
boolean addedOrRemoved=false;
if(!newAttachmentIDs.containsAll(prevAttachmentIDs)){
changes.add(StatusEditChangeType.MEDIA_REMOVED);
addedOrRemoved=true;
}
if(!prevAttachmentIDs.containsAll(newAttachmentIDs)){
changes.add(StatusEditChangeType.MEDIA_ADDED);
addedOrRemoved=true;
}
if(!addedOrRemoved && !newAttachmentIDs.equals(prevAttachmentIDs)){
changes.add(StatusEditChangeType.MEDIA_REORDERED);
}
if(s.sensitive && !prev.sensitive){
changes.add(StatusEditChangeType.MARKED_SENSITIVE);
}else if(prev.sensitive && !s.sensitive){
changes.add(StatusEditChangeType.MARKED_NOT_SENSITIVE);
}
if(changes.size()==1){
action=getString(switch(changes.iterator().next()){
case TEXT_CHANGED -> R.string.edit_text_edited;
case SPOILER_ADDED -> R.string.edit_spoiler_added;
case SPOILER_REMOVED -> R.string.edit_spoiler_removed;
case SPOILER_CHANGED -> R.string.edit_spoiler_edited;
case POLL_ADDED -> R.string.edit_poll_added;
case POLL_REMOVED -> R.string.edit_poll_removed;
case POLL_CHANGED -> R.string.edit_poll_edited;
case MEDIA_ADDED -> R.string.edit_media_added;
case MEDIA_REMOVED -> R.string.edit_media_removed;
case MEDIA_REORDERED -> R.string.edit_media_reordered;
case MARKED_SENSITIVE -> R.string.edit_marked_sensitive;
case MARKED_NOT_SENSITIVE -> R.string.edit_marked_not_sensitive;
});
}else{
action=getString(R.string.edit_multiple_changed);
}
}
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0));
}
return items;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
@Override
public boolean isItemEnabled(String id){
return false;
}
}

View File

@@ -9,13 +9,14 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
@@ -61,6 +62,59 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusUpdated(StatusUpdatedEvent ev){
ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
for(int i=0;i<data.size();i++){
Status s=data.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
s.reblog=ev.status;
statusesForDisplayItems.add(s);
}else if(s.id.equals(ev.status.id)){
data.set(i, ev.status);
statusesForDisplayItems.add(ev.status);
}
}
for(int i=0;i<preloadedData.size();i++){
Status s=preloadedData.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
s.reblog=ev.status;
}else if(s.id.equals(ev.status.id)){
preloadedData.set(i, ev.status);
}
}
if(statusesForDisplayItems.isEmpty())
return;
for(Status s:statusesForDisplayItems){
int i=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(s.id)){
int start=i;
for(;i<displayItems.size();i++){
if(!displayItems.get(i).parentID.equals(s.id))
break;
}
List<StatusDisplayItem> postItems=displayItems.subList(start, i);
postItems.clear();
postItems.addAll(buildDisplayItems(s));
int oldSize=i-start, newSize=postItems.size();
if(oldSize==newSize){
adapter.notifyItemRangeChanged(start, newSize);
}else if(oldSize<newSize){
adapter.notifyItemRangeChanged(start, oldSize);
adapter.notifyItemRangeInserted(start+oldSize, newSize-oldSize);
}else{
adapter.notifyItemRangeChanged(start, newSize);
adapter.notifyItemRangeRemoved(start+newSize, oldSize-newSize);
}
break;
}
i++;
}
}
}
protected Status getContentStatusByID(String id){
Status s=getStatusByID(id);
return s==null ? null : s.getContentStatus();
@@ -113,10 +167,15 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
return;
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(ev.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
@@ -131,6 +190,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
StatusListFragment.this.onStatusCreated(ev);
}
@Subscribe
public void onStatusUpdated(StatusUpdatedEvent ev){
StatusListFragment.this.onStatusUpdated(ev);
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))

View File

@@ -43,7 +43,7 @@ public class ThreadFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
if(s==mainStatus){
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;

View File

@@ -22,6 +22,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
@@ -234,7 +235,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null){
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
@@ -352,7 +353,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
bindRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@@ -12,7 +12,7 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, account.followersCount, account.followersCount));
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@Override

View File

@@ -12,7 +12,7 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setSubtitle(getResources().getQuantityString(R.plurals.x_following, account.followingCount, account.followingCount));
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
}
@Override

View File

@@ -11,7 +11,7 @@ public class StatusFavoritesListFragment extends StatusRelatedAccountListFragmen
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(getResources().getQuantityString(R.plurals.x_favorites, status.favouritesCount, status.favouritesCount));
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
}
@Override

View File

@@ -11,7 +11,7 @@ public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, status.reblogsCount, status.reblogsCount));
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
}
@Override

View File

@@ -220,9 +220,9 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, Math.min(999, 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.posts, (int)Math.min(999, item.account.statusesCount)));
relationship=relationships.get(item.account.id);
if(relationship==null){
actionWrap.setVisibility(View.GONE);

View File

@@ -19,6 +19,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
@@ -52,6 +53,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private SearchFragment searchFragment;
private LocalTimelineFragment localTimelineFragment;
private FederatedTimelineFragment federatedTimelineFragment;
private ListTimelinesFragment listTimelinesFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
@@ -73,7 +75,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[6];
tabViews=new FrameLayout[7];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
@@ -83,6 +85,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> R.id.discover_posts;
case 4 -> R.id.discover_news;
case 5 -> R.id.discover_users;
case 6 -> R.id.discover_lists;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -131,6 +134,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
federatedTimelineFragment=new FederatedTimelineFragment();
federatedTimelineFragment.setArguments(args);
listTimelinesFragment=new ListTimelinesFragment();
listTimelinesFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_local_timeline, localTimelineFragment)
@@ -138,6 +144,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
.add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment)
.add(R.id.discover_lists, listTimelinesFragment)
.commit();
}
@@ -151,12 +158,25 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> R.string.posts;
case 4 -> R.string.news;
case 5 -> R.string.for_you;
case 6 -> R.string.list_timelines;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
}
});
tabLayoutMediator.attach();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
@@ -267,6 +287,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> postsFragment;
case 4 -> newsFragment;
case 5 -> accountsFragment;
case 6 -> listTimelinesFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
};
}

View File

@@ -91,6 +91,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@Override
public void onItemClick(String id){
SearchResult res=getResultByID(id);
if(res==null)
return;
switch(res.type){
case ACCOUNT -> {
Bundle args=new Bundle();
@@ -98,7 +100,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
args.putParcelable("profileAccount", Parcels.wrap(res.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
case STATUS -> {
Status status=res.status.getContentStatus();
Bundle args=new Bundle();
@@ -117,6 +119,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
if(getActivity()==null)
return;
unfilteredResults=sr;
prevDisplayItems=new ArrayList<>(displayItems);
onDataLoaded(sr, false);
@@ -203,7 +207,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
}

View File

@@ -107,7 +107,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
@Override
public void onClick(){
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
}
}
}

View File

@@ -0,0 +1,231 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.parceler.Parcels;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class GoogleMadeMeAddThisFragment extends AppKitFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
private View buttonBar;
private Instance instance;
private ArrayList<Item> items=new ArrayList<>();
private Call currentRequest;
private ItemsAdapter itemsAdapter;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
loadServerPrivacyPolicy();
}
@Override
public void onDestroy(){
super.onDestroy();
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.privacy_policy_title);
subtitle.setText(R.string.privacy_policy_subtitle);
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
list.setAdapter(adapter);
list.setSelector(null);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildViewHolder(view) instanceof ItemViewHolder){
outRect.left=outRect.right=V.dp(18.5f);
outRect.top=V.dp(16);
}
}
});
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), SignupFragment.class, args);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private void loadServerPrivacyPolicy(){
Request req=new Request.Builder()
.url("https://"+instance.uri+"/terms")
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
.build();
currentRequest=MastodonAPIController.getHttpClient().newCall(req);
currentRequest.enqueue(new Callback(){
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e){
currentRequest=null;
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
currentRequest=null;
try(ResponseBody body=response.body()){
if(!response.isSuccessful())
return;
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
final Item item=new Item(doc.title(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
Activity activity=getActivity();
if(activity!=null){
activity.runOnUiThread(()->{
items.add(item);
itemsAdapter.notifyItemInserted(items.size()-1);
});
}
}
}
});
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ItemViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
}
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView domain, title;
private final ImageView favicon;
public ItemViewHolder(){
super(getActivity(), R.layout.item_privacy_policy_link, list);
domain=findViewById(R.id.domain);
title=findViewById(R.id.title);
favicon=findViewById(R.id.favicon);
itemView.setOutlineProvider(OutlineProviders.roundedRect(10));
itemView.setClipToOutline(true);
}
@Override
public void onBind(Item item){
domain.setText(item.domain);
title.setText(item.title);
ViewImageLoader.load(favicon, null, new UrlImageLoaderRequest(item.faviconUrl));
}
@Override
public void onClick(){
UiUtils.launchWebBrowser(getActivity(), item.url);
}
}
private static class Item{
public String title, domain, url, faviconUrl;
public Item(String title, String domain, String url, String faviconUrl){
this.title=title;
this.domain=domain;
this.url=url;
this.faviconUrl=faviconUrl;
}
}
}

View File

@@ -84,7 +84,7 @@ public class InstanceRulesFragment extends AppKitFragment{
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), SignupFragment.class, args);
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args);
}
@Override

View File

@@ -200,7 +200,6 @@ public class SignupFragment extends AppKitFragment{
@Override
public void onSuccess(Token result){
progressDialog.dismiss();
progressDialog=null;
Account fakeAccount=new Account();
fakeAccount.acct=fakeAccount.username=username;
fakeAccount.id="tmp"+System.currentTimeMillis();
@@ -238,7 +237,6 @@ public class SignupFragment extends AppKitFragment{
error.showToast(getActivity());
}
progressDialog.dismiss();
progressDialog=null;
}
})
.exec(instance.uri, apiToken);
@@ -255,9 +253,11 @@ public class SignupFragment extends AppKitFragment{
}
private void showProgressDialog(){
progressDialog=new ProgressDialog(getActivity());
progressDialog.setMessage(getString(R.string.loading));
progressDialog.setCancelable(false);
if(progressDialog==null){
progressDialog=new ProgressDialog(getActivity());
progressDialog.setMessage(getString(R.string.loading));
progressDialog.setCancelable(false);
}
progressDialog.show();
}
@@ -280,7 +280,6 @@ public class SignupFragment extends AppKitFragment{
if(submitAfterGettingToken){
submitAfterGettingToken=false;
progressDialog.dismiss();
progressDialog=null;
error.showToast(getActivity());
}
}
@@ -307,7 +306,6 @@ public class SignupFragment extends AppKitFragment{
if(submitAfterGettingToken){
submitAfterGettingToken=false;
progressDialog.dismiss();
progressDialog=null;
error.showToast(getActivity());
}
}

View File

@@ -85,7 +85,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.NO_REBLOGS)
currentRequest=new GetAccountStatuses(reportAccount.id, offset>0 ? getMaxID() : null, null, count, GetAccountStatuses.Filter.OWN_POSTS_AND_REPLIES)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
@@ -102,6 +102,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
else
selectedIDs.add(id);
list.invalidate();
btn.setEnabled(!selectedIDs.isEmpty());
}
@Override

View File

@@ -125,7 +125,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
}
private void onUnfollowClick(){
new SetAccountFollowed(reportAccount.id, false, false)
new SetAccountFollowed(reportAccount.id, false, false, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@@ -96,15 +96,15 @@ public class Account extends BaseModel{
/**
* How many statuses are attached to this account.
*/
public int statusesCount;
public long statusesCount;
/**
* The reported followers of this profile.
*/
public int followersCount;
public long followersCount;
/**
* The reported follows of this profile.
*/
public int followingCount;
public long followingCount;
// Optional attributes

View File

@@ -0,0 +1,12 @@
package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
public enum ExpandMedia {
@SerializedName("default")
DEFAULT,
@SerializedName("show_all")
SHOW_ALL,
@SerializedName("hide_all")
HIDE_ALL;
}

View File

@@ -11,6 +11,7 @@ public class Hashtag extends BaseModel{
public String name;
@RequiredField
public String url;
public boolean following;
public List<History> history;
@Override
@@ -18,6 +19,7 @@ public class Hashtag extends BaseModel{
return "Hashtag{"+
"name='"+name+'\''+
", url='"+url+'\''+
", following="+following+
", history="+history+
'}';
}

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@Parcel
public class ListTimeline extends BaseModel {
@RequiredField
public String id;
@RequiredField
public String title;
@RequiredField
public RepliesPolicy repliesPolicy;
@Override
public String toString() {
return "List{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", repliesPolicy=" + repliesPolicy +
'}';
}
public enum RepliesPolicy{
@SerializedName("followed")
FOLLOWED,
@SerializedName("list")
LIST,
@SerializedName("none")
NONE
}
}

View File

@@ -0,0 +1,38 @@
package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
/**
* Preferred common behaviors to be shared across clients.
*/
public class Preferences extends BaseModel {
/**
* Default visibility for new posts
*/
@SerializedName("posting:default:visibility")
public StatusPrivacy postingDefaultVisibility;
/**
* Default sensitivity flag for new posts
*/
@SerializedName("posting:default:sensitive")
public boolean postingDefaultSensitive;
/**
* Default language for new posts
*/
@SerializedName("posting:default:language")
public String postingDefaultLanguage;
/**
* Whether media attachments should be automatically displayed or blurred/hidden.
*/
@SerializedName("reading:expand:media")
public ExpandMedia readingExpandMedia;
/**
* Whether CWs should be expanded by default.
*/
@SerializedName("reading:expand:spoilers")
public boolean readingExpandSpoilers;
}

View File

@@ -34,9 +34,10 @@ public class Status extends BaseModel implements DisplayItemsParent{
public List<Hashtag> tags;
@RequiredField
public List<Emoji> emojis;
public int reblogsCount;
public int favouritesCount;
public int repliesCount;
public long reblogsCount;
public long favouritesCount;
public long repliesCount;
public Instant editedAt;
public String url;
public String inReplyToId;
@@ -126,6 +127,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
repliesCount=ev.replies;
favourited=ev.favorited;
reblogged=ev.reblogged;
pinned=ev.pinned;
}
public Status getContentStatus(){

View File

@@ -4,11 +4,25 @@ import com.google.gson.annotations.SerializedName;
public enum StatusPrivacy{
@SerializedName("public")
PUBLIC,
PUBLIC(0),
@SerializedName("unlisted")
UNLISTED,
UNLISTED(1),
@SerializedName("private")
PRIVATE,
PRIVATE(2),
@SerializedName("direct")
DIRECT;
DIRECT(3);
private int privacy;
StatusPrivacy(int privacy) {
this.privacy = privacy;
}
public boolean isLessVisibleThan(StatusPrivacy other) {
return privacy > other.getPrivacy();
}
public int getPrivacy() {
return privacy;
}
}

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