Compare commits

...

52 Commits

Author SHA1 Message Date
sk
36699e0ab1 change change log 2023-11-15 16:58:49 +01:00
sk22
1a382606d7 Translated using Weblate (German)
Currently translated at 100.0% (417 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-11-15 15:50:50 +00:00
sk22
2522c9c428 Translated using Weblate (German)
Currently translated at 97.6% (407 of 417 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-11-15 15:46:34 +00:00
gallegonovato
d1ee12f248 Translated using Weblate (Spanish)
Currently translated at 100.0% (412 of 412 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-11-15 15:46:34 +00:00
SomeTr
d5f9c19fd3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (19 of 19 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/uk/
2023-11-15 15:46:34 +00:00
SomeTr
dc700d9e37 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (412 of 412 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-11-15 15:46:34 +00:00
qbane
6eb87149e2 Translated using Weblate (Chinese (Traditional))
Currently translated at 24.0% (96 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-11-15 15:46:34 +00:00
qbane
79a0f8a891 Translated using Weblate (Chinese (Traditional))
Currently translated at 17.7% (71 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hant/
2023-11-15 15:46:34 +00:00
alextecplayz
26d41b006f Translated using Weblate (Romanian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ro/
2023-11-15 15:46:34 +00:00
EndermanCo
60d9dc22d2 Translated using Weblate (Persian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-11-15 15:46:34 +00:00
SomeTr
c0bf78fa7c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-11-15 15:46:34 +00:00
ling0412
f0c2bd0fe6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/zh_Hans/
2023-11-15 15:46:34 +00:00
Arkxv
235cfd1e80 Translated using Weblate (Japanese)
Currently translated at 93.9% (375 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/ja/
2023-11-15 15:46:34 +00:00
SomeTr
c96931ba72 Translated using Weblate (Croatian)
Currently translated at 64.4% (257 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/hr/
2023-11-15 15:46:34 +00:00
butterflyoffire
2c654a28d6 Translated using Weblate (French)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-11-15 15:46:33 +00:00
Andrewblasco
72d3fc84f5 Translated using Weblate (Spanish)
Currently translated at 100.0% (399 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-11-15 15:46:33 +00:00
ling0412
3b77604ae6 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (398 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-11-15 15:46:33 +00:00
poesty
4e5f49b37d Translated using Weblate (Chinese (Simplified))
Currently translated at 99.7% (398 of 399 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/zh_Hans/
2023-11-15 15:46:33 +00:00
sk22
a89c075082 Added translation using Weblate (Esperanto) 2023-11-15 15:46:33 +00:00
sk
99e881bb95 change string name 2023-11-15 16:46:24 +01:00
sk
4c585fc5d0 change confirm string 2023-11-15 16:45:29 +01:00
sk
ae934b5167 add changelog 2023-11-15 16:42:22 +01:00
Jacoco
6b234209c6 Previewing posts on Akkoma (#933)
* Previewing posts on Akkoma

* move preview property into status

* clean up code

* revert imm-related changes

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-15 15:17:29 +01:00
Jacoco
786ce78b08 Translations for Akkoma (#934)
* Translations for Akkoma

* simplify akkoma translation code

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-15 14:32:56 +01:00
sk
cde332684e bump version 2023-11-15 14:07:00 +01:00
sk
d51e06b61f save latest crash log
closes sk22#932
closes sk22#419
2023-11-15 12:06:54 +01:00
sk
0c376d57e7 fix timeline editor messing with non-hashtag tls 2023-11-13 21:31:54 +01:00
sk
ee82772fee weather icons :) 2023-11-13 21:30:06 +01:00
sk
d5b6750abe fix hashtag options showing up for all timelines 2023-11-13 21:17:58 +01:00
sk
70328217c6 add bookmarks and favorites as timelines
closes sk22#908
2023-11-13 21:10:33 +01:00
sk
733fb3f53a re-add pre-releases setting
closes sk22#906
2023-11-13 21:02:06 +01:00
sk
f82eb96a35 increase translate timeout to 1 minute
closes sk22#910
2023-11-13 20:50:43 +01:00
sk
148952b96c update cache on poll vote
closes sk22#924
2023-11-13 20:48:16 +01:00
sk
52928e1577 increase default read timeout
closes sk22#923
2023-11-13 20:43:34 +01:00
sk
cd13777c06 fix report view, allow opening posts in list
closes sk22#928
closes sk22#441
2023-11-13 20:39:27 +01:00
sk
b21e2acdb6 fix metadata items being cut off
closes sk22#921
closes sk22#648
2023-11-13 19:11:04 +01:00
sk
2b65aeb8b4 fix edit note item visible in context menu 2023-11-13 18:48:24 +01:00
Jacoco
a8dcb11094 feature: Display post that's being quoted on Akkoma (#927)
* Displaying Akkoma quote status

* Dummy display items for quote posts

* Only remove quote-inline with RE:

* fix null reference (reply-to instead of quote status)

* fix text bottom padding in quote

* Postprocess status quote

* fix rounded bottom for quoted media

closes sk22#929

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-13 18:46:15 +01:00
sk
aa42873274 use verified/error text colors for diff 2023-11-12 23:01:46 +01:00
FineFindus
8a5b36db96 feat(profile): add note (#918)
* feat(profile): add note

* simplify note code

* adjust spacing

* size and hitbox adjustments, progress

* profile menu item to add note

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-12 22:37:31 +01:00
FineFindus
c85af5502d feat: translate media attachments and poll options (#916)
* feat(status): translate media attachments

* feat(status): translate poll options

* fix(status/translation): do not require all fields

* feat(status/translation): support translating spoiler
2023-11-10 20:44:06 +01:00
FineFindus
06698d3c52 feat: display edit history diff (#922)
* build: add google's diff-match-patch

Copied from 62f2e689f4/java/src/name/fraser/neil/plaintext/diff_match_patch.java

* feat(status/edit-history): display diff for text

Closes https://github.com/sk22/megalodon/issues/789

* fix(status/edit-history): add fake poll id

* code style adjustments

* don't diff if only formatting changed

---------

Co-authored-by: sk <sk22@mailbox.org>
2023-11-10 20:40:34 +01:00
S1m
2818672cda Fix NullPointerException when receiving push when killed (#914) 2023-11-10 20:15:45 +01:00
S1m
24794f28aa Fix: registering multi account for UnifiedPush (#907)
* Register all account when enabling UnifiedPush
* Register new account to UnifiedPush if enabled
2023-11-10 20:12:27 +01:00
FineFindus
50d1523210 feat: re-add 12 hour option to polls (#917) 2023-11-10 20:10:50 +01:00
sk
613cd2a1ea remove unused playRelease build type 2023-11-10 20:06:18 +01:00
sk
295cb74287 don't show translate for empty decoding 2023-11-10 16:30:46 +01:00
sk
95dd3ff068 fix translation language name 2023-11-10 16:13:43 +01:00
sk
d6aeb753fc allow exclamation/question marks in pronouns 2023-11-10 16:13:14 +01:00
sk
a085744038 fix missing parens on start of pronouns 2023-11-10 15:37:31 +01:00
sk
26391a6f14 hopefully fix string index out of bounds
https://paste.crdroid.net/3FNRe7
2023-11-10 15:37:03 +01:00
sk
4cf734ce9a avoid IllegalArgumentException 2023-11-10 15:23:42 +01:00
86 changed files with 4162 additions and 628 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 108
versionName "2.1.6+fork.108"
versionCode 109
versionName "2.1.6+fork.109"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
}
@@ -33,7 +33,6 @@ android {
applicationIdSuffix '.debug'
}
githubRelease { initWith release }
playRelease { initWith release }
fdroidRelease { initWith release }
}
compileOptions {

View File

@@ -257,5 +257,9 @@ public class UiUtilsTest {
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
makeField("pronouns", "-- * (asterisk) --")
)).orElseThrow());
assertEquals("they/(she?)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
makeField("pronouns", "they/(she?)...")
)).orElseThrow());
}
}

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because it is too large Load Diff

View File

@@ -31,18 +31,44 @@ import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Instant;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
private static final String TAG="MainActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
AccountSession session=getCurrentSession();
UiUtils.setUserPreferredTheme(this, session);
super.onCreate(savedInstanceState);
Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e)->{
File file=new File(MastodonApp.context.getFilesDir(), "crash.log");
try(FileOutputStream out=new FileOutputStream(file)){
PrintWriter writer=new PrintWriter(out);
writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")");
writer.println(Instant.now().toString());
writer.println();
e.printStackTrace(writer);
writer.flush();
}catch(IOException x){
Log.e(TAG, "Error writing crash.log", x);
}finally{
defaultHandler.uncaughtException(t, e);
}
});
if(savedInstanceState==null){
restartHomeFragment();
}

View File

@@ -53,7 +53,9 @@ public class MastodonAPIController{
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
.create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
private static OkHttpClient httpClient=new OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.build();
private AccountSession session;
private static List<String> badDomains = new ArrayList<>();

View File

@@ -0,0 +1,19 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetPrivateNote extends MastodonAPIRequest<Relationship>{
public SetPrivateNote(String id, String comment){
super(MastodonAPIRequest.HttpMethod.POST, "/accounts/"+id+"/note", Relationship.class);
Request req = new Request(comment);
setRequestBody(req);
}
private static class Request{
public String comment;
public Request(String comment){
this.comment=comment;
}
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.AkkomaTranslation;
public class AkkomaTranslateStatus extends MastodonAPIRequest<AkkomaTranslation>{
public AkkomaTranslateStatus(String id, String lang){
super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toUpperCase(), AkkomaTranslation.class);
}
}

View File

@@ -48,6 +48,8 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public String quoteId;
public ContentType contentType;
public boolean preview;
public static class Poll{
public ArrayList<String> options=new ArrayList<>();
public int expiresIn;

View File

@@ -26,6 +26,8 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
s.visibility=StatusPrivacy.PUBLIC;
s.mentions=Collections.emptyList();
s.tags=Collections.emptyList();
if (s.poll != null)
s.poll.id="fakeID"+i;
i++;
}
super.validateAndPostprocessResponse(respObj, httpResponse);

View File

@@ -260,11 +260,13 @@ public class AccountSession{
}
private boolean isFilteredType(Status s){
AccountLocalPreferences localPreferences = getLocalPreferences();
return (!localPreferences.showReplies && s.inReplyToId != null)
|| (!localPreferences.showBoosts && s.reblog != null);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
AccountLocalPreferences localPreferences = getLocalPreferences();
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
Status s=extractor.apply(obj);
if(s!=null && s.filtered!=null){
@@ -307,7 +309,7 @@ public class AccountSession{
if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC))
return true;
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
if(localPreferences.serverSideFiltersSupported){
if(getLocalPreferences().serverSideFiltersSupported){
for(FilterResult filter : s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true;

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.api.session;
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
@@ -34,6 +36,7 @@ import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.unifiedpush.android.connector.UnifiedPush;
import java.io.File;
import java.io.FileInputStream;
@@ -101,6 +104,7 @@ public class AccountSessionManager{
}
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
Context context = MastodonApp.context;
instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
@@ -113,7 +117,14 @@ public class AccountSessionManager{
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
if (!UnifiedPush.getDistributor(context).isEmpty()) {
UnifiedPush.registerApp(
context,
session.getID(),
new ArrayList<>(),
context.getPackageName()
);
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
maybeUpdateShortcuts();

View File

@@ -19,12 +19,15 @@ import android.widget.Toolbar;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AkkomaTranslation;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship;
@@ -47,6 +50,7 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
@@ -59,6 +63,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -358,12 +363,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
});
list.addItemDecoration(new StatusListItemDecoration());
list.addItemDecoration(new InsetStatusItemDecoration(this));
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
if(list!=view.getParent()) return;
boolean hasDescendant=false, hasAncestor=false, isWarning=false;
int lastIndex=-1, firstIndex=-1;
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
list.getDecoratedBoundsWithMargins(view, outRect);
}else{
@@ -493,7 +500,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
spoilerFooterIndex=spoilerItem.contentItems.indexOf(pollItems.get(pollItems.size()-1));
}
pollItems.clear();
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems);
StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems);
if(spoilerItem!=null){
spoilerItem.contentItems.subList(spoilerFirstOptionIndex, spoilerFooterIndex+1).clear();
spoilerItem.contentItems.addAll(spoilerFirstOptionIndex, pollItems);
@@ -564,7 +571,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
toggleSpoiler(status, holder.getItemID());
boolean isForQuote=holder.getItem().isForQuote;
toggleSpoiler(status, isForQuote, holder.getItemID());
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
@@ -586,15 +594,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
else notifyItemChangedBefore(holder.getItem(), HeaderStatusDisplayItem.class);
}
protected void toggleSpoiler(Status status, String itemID){
protected void toggleSpoiler(Status status, boolean isForQuote, String itemID){
status.spoilerRevealed=!status.spoilerRevealed;
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
status.sensitiveRevealed = false;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
List<SpoilerStatusDisplayItem.Holder> spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0);
if(spoiler!=null) spoiler.rebind();
else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class);
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem());
int index=displayItems.indexOf(spoilerItem);
if(status.spoilerRevealed){
@@ -850,45 +859,53 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
status.translationState=Status.TranslationState.SHOWN;
}else{
status.translationState=Status.TranslationState.LOADING;
new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage())
.setCallback(new Callback<>(){
Consumer<Translation> successCallback=(result)->{
status.translation=result;
status.translationState=Status.TranslationState.SHOWN;
updateTranslation(itemID);
};
MastodonAPIRequest<?> req=isInstanceAkkoma()
? new AkkomaTranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
@Override
public void onSuccess(AkkomaTranslation result){
if(getActivity()!=null) successCallback.accept(result.toTranslation());
}
@Override
public void onError(ErrorResponse error){
if(getActivity()!=null) translationCallbackError(status, itemID);
}
})
: new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
@Override
public void onSuccess(Translation result){
if(getActivity()==null)
return;
status.translation=result;
status.translationState=Status.TranslationState.SHOWN;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
}else{
notifyItemChanged(itemID, TextStatusDisplayItem.class);
}
if(getActivity()!=null) successCallback.accept(result);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
status.translationState=Status.TranslationState.HIDDEN;
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
}else{
notifyItemChanged(itemID, TextStatusDisplayItem.class);
}
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.translation_failed)
.setPositiveButton(R.string.ok, null)
.show();
if(getActivity()!=null) translationCallbackError(status, itemID);
}
})
.exec(accountID);
});
// 1 minute
req.setTimeout(60000).exec(accountID);
}
}
}
updateTranslation(itemID);
}
private void translationCallbackError(Status status, String itemID) {
status.translationState=Status.TranslationState.HIDDEN;
updateTranslation(itemID);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.translation_failed)
.setPositiveButton(R.string.ok, null)
.show();
}
private void updateTranslation(String itemID) {
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null){
text.updateTranslation(true);
@@ -896,6 +913,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}else{
notifyItemChanged(itemID, TextStatusDisplayItem.class);
}
if(isInstanceAkkoma())
return;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null){
spoiler.rebind();
}
MediaGridStatusDisplayItem.Holder media=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
if (media!=null) {
media.rebind();
}
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
item.rebind();
}
}
}
public void rebuildAllDisplayItems(){

View File

@@ -736,7 +736,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
&& !status.spoilerText.startsWith("re: ") ? "re: " : "";
spoilerEdit.setText(prefix + replyTo.spoilerText);
spoilerEdit.setText(prefix + status.spoilerText);
spoilerBtn.setSelected(true);
}
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
@@ -807,25 +807,28 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
actionItem.setActionView(wrap);
actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
draftsBtn = wrap.findViewById(R.id.drafts_btn);
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
draftsBtn=wrap.findViewById(R.id.drafts_btn);
draftOptionsPopup=new PopupMenu(getContext(), draftsBtn);
draftOptionsPopup.inflate(R.menu.compose_more);
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
Menu draftOptionsMenu=draftOptionsPopup.getMenu();
draftMenuItem=draftOptionsMenu.findItem(R.id.draft);
undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft);
scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule);
unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule);
draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma());
draftOptionsPopup.setOnMenuItemClickListener(i->{
int id = i.getItemId();
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
else if (id == R.id.schedule) pickScheduledDateTime();
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
else navigateToUnsentPosts();
int id=i.getItemId();
if(id==R.id.draft) updateScheduledAt(getDraftInstant());
else if(id==R.id.schedule) pickScheduledDateTime();
else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null);
else if(id==R.id.drafts) navigateToUnsentPosts();
else if(id==R.id.preview) publish(true);
return true;
});
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
publishButton = wrap.findViewById(R.id.publish_btn);
languageButton = wrap.findViewById(R.id.language_btn);
publishButton=wrap.findViewById(R.id.publish_btn);
languageButton=wrap.findViewById(R.id.language_btn);
languageButton.setOnClickListener(v->showLanguageAlert());
languageButton.setOnLongClickListener(v->{
if(!getLocalPrefs().bottomEncoding){
@@ -1051,6 +1054,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void publish(){
publish(false);
}
private void publish(boolean preview){
sendingOverlay=new View(getActivity());
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
@@ -1064,10 +1071,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
publishButton.setEnabled(false);
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
mediaViewController.saveAltTextsBeforePublishing(
()->actuallyPublish(preview),
this::handlePublishError);
}
private void actuallyPublish(){
private void actuallyPublish(boolean preview){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
if("bottom".equals(postLang.encoding)){
@@ -1085,6 +1094,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
req.sensitive=sensitive;
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
req.scheduledAt=scheduledAt;
req.preview=preview;
if(!mediaViewController.isEmpty()){
req.mediaIds=mediaViewController.getAttachmentIDs();
if(editingStatus != null){
@@ -1112,7 +1122,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Callback<Status> resCallback=new Callback<>(){
@Override
public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> {
if(preview){
openPreview(result);
return;
}
maybeDeleteScheduledPost(()->{
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null || redraftStatus){
@@ -1134,10 +1149,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
E.post(new StatusUpdatedEvent(editedStatus));
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()){
Nav.finish(ComposeFragment.this);
}
if (getArguments().getBoolean("navigateToStatus", false)) {
if(getArguments().getBoolean("navigateToStatus", false)){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
@@ -1153,11 +1168,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
};
if(editingStatus!=null && !redraftStatus){
if(editingStatus!=null && !redraftStatus && !preview){
new EditStatus(req, editingStatus.id)
.setCallback(resCallback)
.exec(accountID);
}else if(req.scheduledAt == null){
}else if(req.scheduledAt == null || preview){
new CreateStatus(req, uuid)
.setCallback(resCallback)
.exec(accountID);
@@ -1210,6 +1225,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
}
private void openPreview(Status result){
result.preview=true;
wm.removeView(sendingOverlay);
sendingOverlay=null;
publishButton.setEnabled(true);
V.setVisibilityAnimated(sendProgress, View.GONE);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null){
args.putParcelable("inReplyTo", Parcels.wrap(replyTo));
args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo.account));
}
Nav.go(getActivity(), ThreadFragment.class, args);
}
private void updateRecentLanguages() {
if (postLang == null || postLang.language == null) return;
String language = postLang.language.getLanguage();
@@ -1528,6 +1562,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
contentTypePopup.setOnMenuItemClickListener(i->{
uuid=null;
int index=i.getItemId();
contentType=ContentType.values()[index];
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());

View File

@@ -60,191 +60,191 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
private String accountID;
private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper;
private Menu optionsMenu;
private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
private final List<ListTimeline> listTimelines = new ArrayList<>();
private final List<Hashtag> hashtags = new ArrayList<>();
private MenuItem addHashtagItem;
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
private String accountID;
private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper;
private Menu optionsMenu;
private boolean updated;
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
private final List<ListTimeline> listTimelines=new ArrayList<>();
private final List<Hashtag> hashtags=new ArrayList<>();
private MenuItem addHashtagItem;
public EditTimelinesFragment() {
super(10);
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
}
public EditTimelinesFragment(){
super(10);
ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback();
itemTouchHelper=new ItemTouchHelper(itemTouchCallback);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
setTitle(R.string.sk_timelines);
accountID = getArguments().getString("account");
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
setTitle(R.string.sk_timelines);
accountID=getArguments().getString("account");
new GetLists().setCallback(new Callback<>() {
@Override
public void onSuccess(List<ListTimeline> result) {
listTimelines.addAll(result);
updateOptionsMenu();
}
new GetLists().setCallback(new Callback<>(){
@Override
public void onSuccess(List<ListTimeline> result){
listTimelines.addAll(result);
updateOptionsMenu();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID);
@Override
public void onError(ErrorResponse error){
error.showToast(getContext());
}
}).exec(accountID);
new GetFollowedHashtags().setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result) {
hashtags.addAll(result);
updateOptionsMenu();
}
new GetFollowedHashtags().setCallback(new Callback<>(){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result){
hashtags.addAll(result);
updateOptionsMenu();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getContext());
}
}).exec(accountID);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
}
@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);
itemTouchHelper.attachToRecyclerView(list);
refreshLayout.setEnabled(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
itemTouchHelper.attachToRecyclerView(list);
refreshLayout.setEnabled(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
this.optionsMenu = menu;
updateOptionsMenu();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
this.optionsMenu=menu;
updateOptionsMenu();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_back) {
updateOptionsMenu();
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
return true;
}
TimelineDefinition tl = timelineByMenuItem.get(item);
if (tl != null) {
addTimeline(tl);
} else if (item == addHashtagItem) {
makeTimelineEditor(null, (hashtag) -> {
if (hashtag != null) addTimeline(hashtag);
}, null);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.menu_back){
updateOptionsMenu();
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
return true;
}
TimelineDefinition tl=timelineByMenuItem.get(item);
if(tl!=null){
addTimeline(tl);
}else if(item==addHashtagItem){
makeTimelineEditor(null, (hashtag)->{
if(hashtag!=null) addTimeline(hashtag);
}, null);
}
return true;
}
private void addTimeline(TimelineDefinition tl) {
data.add(tl.copy());
adapter.notifyItemInserted(data.size());
saveTimelines();
updateOptionsMenu();
}
private void addTimeline(TimelineDefinition tl){
data.add(tl.copy());
adapter.notifyItemInserted(data.size());
saveTimelines();
updateOptionsMenu();
}
private void addTimelineToOptions(TimelineDefinition tl, Menu menu) {
if (data.contains(tl)) return;
MenuItem item = addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes);
timelineByMenuItem.put(item, tl);
}
private void addTimelineToOptions(TimelineDefinition tl, Menu menu){
if(data.contains(tl)) return;
MenuItem item=addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes);
timelineByMenuItem.put(item, tl);
}
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
item.setIcon(icon);
return item;
}
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
item.setIcon(icon);
return item;
}
private void updateOptionsMenu() {
if(getActivity()==null) return;
optionsMenu.clear();
timelineByMenuItem.clear();
private void updateOptionsMenu(){
if(getActivity()==null) return;
optionsMenu.clear();
timelineByMenuItem.clear();
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
SubMenu menu=optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
makeBackItem(timelinesMenu);
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
makeBackItem(timelinesMenu);
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
listsMenu.getItem().setVisible(listsMenu.size() > 0);
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0);
timelinesMenu.getItem().setVisible(timelinesMenu.size()>0);
listsMenu.getItem().setVisible(listsMenu.size()>0);
hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0);
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
}
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
}
private void saveTimelines() {
updated=true;
private void saveTimelines(){
updated=true;
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
prefs.timelines=data;
prefs.save();
}
private void removeTimeline(int position) {
data.remove(position);
adapter.notifyItemRemoved(position);
saveTimelines();
updateOptionsMenu();
}
private void removeTimeline(int position){
data.remove(position);
adapter.notifyItemRemoved(position);
saveTimelines();
updateOptionsMenu();
}
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
updateOptionsMenu();
}
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
updateOptionsMenu();
}
@Override
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
return adapter = new TimelinesAdapter();
}
@Override
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter(){
return adapter=new TimelinesAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);
}
@Override
public void onDestroy() {
super.onDestroy();
if (updated) UiUtils.restartApp();
}
@Override
public void onDestroy(){
super.onDestroy();
if(updated) UiUtils.restartApp();
}
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
if (tags == null || tags.isEmpty()) return false;
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags){
if(tags==null || tags.isEmpty()) return false;
editText.setText(tags);
editText.chipifyAllUnterminatedTokens();
return true;
}
return true;
}
private NachoTextView prepareChipTextView(NachoTextView nacho) {
private NachoTextView prepareChipTextView(NachoTextView nacho){
//Ill Be Back
nacho.setChipTerminators(
Map.of(
@@ -254,223 +254,228 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
';', BEHAVIOR_CHIPIFY_ALL
)
);
nacho.enableEditChipOnTouch(true, true);
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
return nacho;
}
nacho.enableEditChipOnTouch(true, true);
nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens());
return nacho;
}
@SuppressLint("ClickableViewAccessibility")
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) {
Context ctx = getContext();
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
@SuppressLint("ClickableViewAccessibility")
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove){
Context ctx=getContext();
View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
View divider = view.findViewById(R.id.divider);
Button advancedBtn = view.findViewById(R.id.advanced);
EditText editText = view.findViewById(R.id.input);
if (item != null) editText.setText(item.getCustomTitle());
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
View divider=view.findViewById(R.id.divider);
Button advancedBtn=view.findViewById(R.id.advanced);
EditText editText=view.findViewById(R.id.input);
if(item!=null) editText.setText(item.getCustomTitle());
editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
advancedBtn.setOnClickListener(l -> {
advancedBtn.setSelected(!advancedBtn.isSelected());
LinearLayout tagWrap=view.findViewById(R.id.tag_wrap);
boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG;
advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE);
advancedBtn.setOnClickListener(l->{
advancedBtn.setSelected(!advancedBtn.isSelected());
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
UiUtils.beginLayoutTransition((ViewGroup) view);
});
});
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
view.findViewById(R.id.local_only)
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
Switch localOnlySwitch=view.findViewById(R.id.local_only_switch);
view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
EditText tagMain = view.findViewById(R.id.tag_main);
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any));
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all));
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
if (item != null) {
tagMain.setText(item.getHashtagName());
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
if (item.isHashtagLocalOnly()) {
localOnlySwitch.setChecked(true);
hasAdvanced = true;
}
if (hasAdvanced) {
advancedBtn.setSelected(true);
advancedBtn.setText(R.string.sk_advanced_options_hide);
EditText tagMain=view.findViewById(R.id.tag_main);
NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any));
NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all));
NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none));
if(item!=null && hashtagOptionsAvailable){
tagMain.setText(item.getHashtagName());
boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
if(item.isHashtagLocalOnly()){
localOnlySwitch.setChecked(true);
hasAdvanced=true;
}
if(hasAdvanced){
advancedBtn.setSelected(true);
advancedBtn.setText(R.string.sk_advanced_options_hide);
tagWrap.setVisibility(View.VISIBLE);
divider.setVisibility(View.VISIBLE);
}
}
}
}
ImageButton btn = view.findViewById(R.id.button);
PopupMenu popup = new PopupMenu(ctx, btn);
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
btn.setImageResource(currentIcon.iconRes);
btn.setTag(currentIcon.ordinal());
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
btn.setOnTouchListener(popup.getDragToOpenListener());
btn.setOnClickListener(l -> popup.show());
ImageButton btn=view.findViewById(R.id.button);
PopupMenu popup=new PopupMenu(ctx, btn);
TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
btn.setImageResource(currentIcon.iconRes);
btn.setTag(currentIcon.ordinal());
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
btn.setOnTouchListener(popup.getDragToOpenListener());
btn.setOnClickListener(l->popup.show());
Menu menu = popup.getMenu();
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
if (!currentIcon.equals(defaultIcon)) {
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
}
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue;
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
}
UiUtils.enablePopupMenuIcons(ctx, popup);
Menu menu=popup.getMenu();
TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
if(!currentIcon.equals(defaultIcon)){
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
}
for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){
if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue;
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
}
UiUtils.enablePopupMenuIcons(ctx, popup);
popup.setOnMenuItemClickListener(menuItem -> {
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
btn.setImageResource(icon.iconRes);
btn.setTag(menuItem.getItemId());
btn.setContentDescription(ctx.getString(icon.nameRes));
return true;
});
popup.setOnMenuItemClickListener(menuItem->{
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()];
btn.setImageResource(icon.iconRes);
btn.setTag(menuItem.getItemId());
btn.setContentDescription(ctx.getString(icon.nameRes));
return true;
});
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx)
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
.setView(view)
.setPositiveButton(R.string.save, (d, which) -> {
tagsAny.chipifyAllUnterminatedTokens();
tagsAll.chipifyAllUnterminatedTokens();
tagsNone.chipifyAllUnterminatedTokens();
String name = editText.getText().toString().trim();
String mainHashtag = tagMain.getText().toString().trim();
if (TextUtils.isEmpty(mainHashtag)) {
mainHashtag = name;
name = null;
}
if (TextUtils.isEmpty(mainHashtag) && (item != null && item.getType() == TimelineDefinition.TimelineType.HASHTAG)) {
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
onSave.accept(null);
return;
}
AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx)
.setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
.setView(view)
.setPositiveButton(R.string.save, (d, which)->{
String name=editText.getText().toString().trim();
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name);
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()];
tl.setIcon(icon);
tl.setTitle(name);
tl.setTagOptions(
mainHashtag,
tagsAny.getChipValues(),
tagsAll.getChipValues(),
tagsNone.getChipValues(),
localOnlySwitch.isChecked()
);
onSave.accept(tl);
})
.setNegativeButton(R.string.cancel, (d, which) -> {});
String mainHashtag=tagMain.getText().toString().trim();
if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){
tagsAny.chipifyAllUnterminatedTokens();
tagsAll.chipifyAllUnterminatedTokens();
tagsNone.chipifyAllUnterminatedTokens();
if(TextUtils.isEmpty(mainHashtag)){
mainHashtag=name;
name=null;
}
if(TextUtils.isEmpty(mainHashtag) && (item!=null && item.getType()==TimelineDefinition.TimelineType.HASHTAG)){
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
onSave.accept(null);
return;
}
}
if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run());
TimelineDefinition tl=item!=null ? item : TimelineDefinition.ofHashtag(name);
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[(int) btn.getTag()];
tl.setIcon(icon);
tl.setTitle(name);
if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){
tl.setTagOptions(
mainHashtag,
tagsAny.getChipValues(),
tagsAll.getChipValues(),
tagsNone.getChipValues(),
localOnlySwitch.isChecked()
);
}
onSave.accept(tl);
})
.setNegativeButton(R.string.cancel, (d, which)->{});
builder.show();
btn.requestFocus();
}
if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run());
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
@NonNull
@Override
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new TimelineViewHolder();
}
builder.show();
btn.requestFocus();
}
@Override
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
holder.bind(data.get(position));
}
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
@NonNull
@Override
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new TimelineViewHolder();
}
@Override
public int getItemCount() {
return data.size();
}
}
@Override
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){
holder.bind(data.get(position));
}
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
private final TextView title;
private final ImageView dragger;
@Override
public int getItemCount(){
return data.size();
}
}
public TimelineViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
dragger=findViewById(R.id.dragger_thingy);
}
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
private final TextView title;
private final ImageView dragger;
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBind(TimelineDefinition item) {
title.setText(item.getTitle(getContext()));
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
dragger.setVisibility(View.VISIBLE);
dragger.setOnTouchListener((View v, MotionEvent event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
itemTouchHelper.startDrag(this);
return true;
}
return false;
});
}
public TimelineViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
dragger=findViewById(R.id.dragger_thingy);
}
private void onSave(TimelineDefinition tl) {
saveTimelines();
rebind();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBind(TimelineDefinition item){
title.setText(item.getTitle(getContext()));
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
dragger.setVisibility(View.VISIBLE);
dragger.setOnTouchListener((View v, MotionEvent event)->{
if(event.getAction()==MotionEvent.ACTION_DOWN){
itemTouchHelper.startDrag(this);
return true;
}
return false;
});
}
private void onRemove() {
removeTimeline(getAbsoluteAdapterPosition());
}
private void onSave(TimelineDefinition tl){
saveTimelines();
rebind();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onClick() {
makeTimelineEditor(item, this::onSave, this::onRemove);
}
}
private void onRemove(){
removeTimeline(getAbsoluteAdapterPosition());
}
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
public ItemTouchHelperCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onClick(){
makeTimelineEditor(item, this::onSave, this::onRemove);
}
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
int toPosition = target.getAbsoluteAdapterPosition();
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
return false;
} else {
Collections.swap(data, fromPosition, toPosition);
adapter.notifyItemMoved(fromPosition, toPosition);
saveTimelines();
return true;
}
}
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{
public ItemTouchHelperCallback(){
super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
}
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
viewHolder.itemView.animate().alpha(0.65f);
}
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
int toPosition=target.getAbsoluteAdapterPosition();
if(Math.max(fromPosition, toPosition)>=data.size() || Math.min(fromPosition, toPosition)<0){
return false;
}else{
Collections.swap(data, fromPosition, toPosition);
adapter.notifyItemMoved(fromPosition, toPosition);
saveTimelines();
return true;
}
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().alpha(1f);
}
@Override
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){
viewHolder.itemView.animate().alpha(0.65f);
}
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAbsoluteAdapterPosition();
removeTimeline(position);
}
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().alpha(1f);
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
int position=viewHolder.getAbsoluteAdapterPosition();
removeTimeline(position);
}
}
}

View File

@@ -174,7 +174,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
private Rect tmpRect=new Rect();

View File

@@ -21,8 +21,11 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
@@ -56,6 +59,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.SetPrivateNote;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -145,7 +149,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private SwipeRefreshLayout refreshLayout;
private View followersBtn, followingBtn;
private EditText nameEdit, bioEdit;
private ProgressBar actionProgress, notifyProgress;
private ProgressBar actionProgress, notifyProgress, noteSaveProgress;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private TextView followsYouView;
@@ -186,6 +190,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private ListImageLoaderWrapper imgLoader;
// profile note
private FrameLayout noteWrap;
private ImageButton noteSaveBtn;
private EditText noteEdit;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -257,6 +266,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
actionProgress=content.findViewById(R.id.action_progress);
notifyProgress=content.findViewById(R.id.notify_progress);
noteSaveProgress=content.findViewById(R.id.note_save_progress);
fab=content.findViewById(R.id.fab);
followsYouView=content.findViewById(R.id.follows_you);
countersLayout=content.findViewById(R.id.profile_counters);
@@ -271,6 +281,51 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
noteEdit=content.findViewById(R.id.note_edit);
noteWrap=content.findViewById(R.id.note_edit_wrap);
noteSaveBtn=content.findViewById(R.id.note_save_btn);
noteSaveBtn.setOnClickListener((v->{
savePrivateNote(noteEdit.getText().toString());
InputMethodManager imm=(InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
noteEdit.clearFocus();
noteSaveBtn.clearFocus();
}));
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus){
hideFab();
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
}else if(!noteSaveBtn.hasFocus()){
showFab();
hideNoteSaveBtnIfNotDirty();
}
});
noteEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
if(relationship!=null && noteSaveBtn.getVisibility()!=View.VISIBLE && !s.toString().equals(relationship.note))
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
}
@Override
public void afterTextChanged(Editable s){}
});
noteSaveBtn.setOnFocusChangeListener((v, hasFocus)->{
if(!hasFocus && !noteEdit.hasFocus()){
showFab();
hideNoteSaveBtnIfNotDirty();
}
});
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
@@ -435,6 +490,46 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return sizeWrapper;
}
private void hideNoteSaveBtnIfNotDirty(){
if(noteEdit.getText().toString().equals(relationship.note)){
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
}
}
private void showPrivateNote(){
noteWrap.setVisibility(View.VISIBLE);
noteEdit.setText(relationship.note);
}
private void hidePrivateNote(){
noteWrap.setVisibility(View.GONE);
noteEdit.setText(null);
}
private void savePrivateNote(String note){
if(note!=null && note.equals(relationship.note)){
updateRelationship();
invalidateOptionsMenu();
return;
}
V.setVisibilityAnimated(noteSaveProgress, View.VISIBLE);
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship result) {
updateRelationship(result);
invalidateOptionsMenu();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
}
}).exec(accountID);
}
private void onAccountLoaded(Account result) {
account=result;
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
@@ -793,6 +888,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else{
blockDomain.setVisible(false);
}
menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty())
? R.string.sk_add_note : R.string.sk_delete_note);
}
@Override
@@ -874,6 +971,26 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else if(id==R.id.save){
if(isInEditMode)
saveAndExitEditMode();
}else if(id==R.id.edit_note){
if(noteWrap.getVisibility()==View.GONE){
showPrivateNote();
UiUtils.beginLayoutTransition(scrollableContent);
noteEdit.requestFocus();
noteEdit.postDelayed(()->{
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.showSoftInput(noteEdit, 0);
}, 100);
}else if(relationship.note.isEmpty()){
hidePrivateNote();
UiUtils.beginLayoutTransition(scrollableContent);
}else{
new M3AlertDialogBuilder(getActivity())
.setMessage(getContext().getString(R.string.sk_private_note_confirm_delete, account.getDisplayUsername()))
.setPositiveButton(R.string.delete, (dlg, btn)->savePrivateNote(null))
.setNegativeButton(R.string.cancel, null)
.show();
}
invalidateOptionsMenu();
}
return true;
}
@@ -899,15 +1016,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void updateRelationship(){
if(getActivity()==null) return;
if(relationship.note!=null && !relationship.note.isEmpty()) showPrivateNote();
else hidePrivateNote();
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
noteSaveProgress.setIndeterminateTintList(noteEdit.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying);
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
UiUtils.beginLayoutTransition(scrollableContent);
}
public ImageButton getFab() {
@@ -1477,7 +1601,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
title.setText(item.parsedName);
value.setText(item.parsedValue);
if(item.verifiedAt!=null){
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
int textColor=UiUtils.getThemeColor(getContext(), R.attr.colorM3Success);
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate();

View File

@@ -12,18 +12,22 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
import name.fraser.neil.plaintext.diff_match_patch;
public class StatusEditHistoryFragment extends StatusListFragment{
private String id, url;
@@ -58,7 +62,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS);
List<StatusDisplayItem> items=new ArrayList<>();
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -83,8 +87,11 @@ public class StatusEditHistoryFragment extends StatusListFragment{
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
Status prev=data.get(idx+1);
if(!Objects.equals(s.content, prev.content)){
// if only formatting was changed, don't even try to create a diff text
if(!Objects.equals(HtmlParser.text(s.content), HtmlParser.text(prev.content))){
changes.add(StatusEditChangeType.TEXT_CHANGED);
//update status content to display a diffs
s.content=createDiffText(prev.content, s.content);
}
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
if(s.spoilerText==null){
@@ -147,15 +154,10 @@ public class StatusEditHistoryFragment extends StatusListFragment{
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null, s));
items.add(1, new DummyStatusDisplayItem(s.id, this));
}
items.addAll(StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER|StatusDisplayItem.FLAG_INSET|StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS));
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;
@@ -170,4 +172,28 @@ public class StatusEditHistoryFragment extends StatusListFragment{
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(url);
}
private String createDiffText(String original, String modified) {
diff_match_patch dmp=new diff_match_patch();
LinkedList<diff_match_patch.Diff> diffs=dmp.diff_main(original, modified);
dmp.diff_cleanupSemantic(diffs);
StringBuilder stringBuilder=new StringBuilder();
for(diff_match_patch.Diff diff : diffs){
switch(diff.operation){
case DELETE->{
stringBuilder.append("<edit-diff-delete>");
stringBuilder.append(diff.text);
stringBuilder.append("</edit-diff-delete>");
}
case INSERT->{
stringBuilder.append("<edit-diff-insert>");
stringBuilder.append(diff.text);
stringBuilder.append("</edit-diff-insert>");
}
default->stringBuilder.append(diff.text);
}
}
return stringBuilder.toString();
}
}

View File

@@ -84,8 +84,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
@Override
public void onItemClick(String id){
Status status=getContentStatusByID(id);
if(status==null)
return;
if(status==null || status.preview) return;
status.filterRevealed=true;
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -353,6 +352,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
Status contentStatus=status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(status.id, contentStatus, ev.poll);
AccountSessionManager.get(accountID).getCacheController().updateStatus(contentStatus);
}
}
}

View File

@@ -50,21 +50,25 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus, updatedStatus;
protected Status mainStatus, updatedStatus, replyTo;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
private StatusContext result;
protected boolean contextInitiallyRendered, transitionFinished;
protected boolean contextInitiallyRendered, transitionFinished, preview;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
refreshing=contextInitiallyRendered=getArguments().getBoolean("refresh", false);
if(inReplyToAccount!=null)
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis));
preview=mainStatus.preview;
if(preview) setRefreshEnabled(false);
setTitle(preview ? getString(R.string.sk_post_preview) : HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis));
transitionFinished = getArguments().getBoolean("noTransition", false);
}
@@ -129,11 +133,21 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override
protected void doLoadData(int offset, int count){
if (refreshing) loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id)
if(preview && replyTo==null){
result=new StatusContext();
result.descendants=Collections.emptyList();
result.ancestors=Collections.emptyList();
return;
}
if(refreshing && !preview) loadMainStatus();
currentRequest=new GetStatusContext(preview ? replyTo.id : mainStatus.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if(preview){
result.descendants=Collections.emptyList();
result.ancestors.add(replyTo);
}
ThreadFragment.this.result = result;
maybeApplyContext();
}

View File

@@ -1,8 +1,6 @@
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
@@ -25,6 +23,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -97,8 +96,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
.exec(accountID);
}
@Override
public void onItemClick(String id){
public void onToggleItem(String id){
if(selectedIDs.contains(id))
selectedIDs.remove(id);
else
@@ -121,13 +119,20 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder)
return;
outRect.left=V.dp(40);
boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
if(isRTL) outRect.right=V.dp(40);
else outRect.left=V.dp(40);
if(holder instanceof AudioStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
outRect.bottom=V.dp(8);
if(isRTL){
outRect.right+=V.dp(16);
outRect.left=V.dp(16);
}else{
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
}
}
}
});
@@ -155,9 +160,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
return adapter;
}
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
}
private void onButtonClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -201,7 +203,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
items.add(new DummyStatusDisplayItem(s.getID(), this));
return items;
}
@Override

View File

@@ -204,14 +204,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
float off=paint.getStrokeWidth()/2f;
c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), 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<?>){
outRect.left=outRect.right=V.dp(16);
}
}
});
}
}

View File

@@ -2,19 +2,38 @@ package org.joinmastodon.android.fragments.settings;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
@@ -24,30 +43,46 @@ import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
private ListItem<Void> mediaCacheItem;
private static final String TAG="SettingsAboutAppFragment";
private ListItem<Void> mediaCacheItem, copyCrashLogItem;
private CheckableListItem<Void> enablePreReleasesItem;
private AccountSession session;
private boolean timelineCacheCleared=false;
private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log");
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(getString(R.string.about_app, getString(R.string.sk_app_name)));
session=AccountSessionManager.get(accountID);
onDataLoaded(List.of(
String lastModified=crashLogFile.exists()
? DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT).withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(crashLogFile.lastModified()))
: getString(R.string.sk_settings_crash_log_unavailable);
List<ListItem<Void>> items=new ArrayList<>(List.of(
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, i->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag))),
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))),
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick),
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick)
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick),
copyCrashLogItem=new ListItem<>(getString(R.string.sk_settings_copy_crash_log), lastModified, 0, this::onCopyCrashLog)
));
if(GithubSelfUpdater.needSelfUpdating()){
items.add(enablePreReleasesItem=new CheckableListItem<>(R.string.sk_updater_enable_pre_releases, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enablePreReleases, i->toggleCheckableItem(enablePreReleasesItem)));
}
copyCrashLogItem.isEnabled=crashLogFile.exists();
onDataLoaded(items);
updateMediaCacheItem();
}
@Override
protected void onHidden(){
super.onHidden();
GlobalUserPreferences.enablePreReleases=enablePreReleasesItem.checked;
GlobalUserPreferences.save();
if(timelineCacheCleared) getActivity().recreate();
}
@@ -94,4 +129,17 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
mediaCacheItem.isEnabled=size>0;
rebindItem(mediaCacheItem);
}
private void onCopyCrashLog(ListItem<?> item){
if(!crashLogFile.exists()) return;
try(InputStream is=new FileInputStream(crashLogFile)){
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
StringBuilder sb=new StringBuilder();
String line;
while ((line=reader.readLine())!=null) sb.append(line).append("\n");
UiUtils.copyText(list, sb.toString());
} catch(IOException e){
Log.e(TAG, "Error reading crash log", e);
}
}
}

View File

@@ -334,13 +334,15 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
return;
}
UnifiedPush.unregisterApp(
getContext(),
accountID
);
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()) {
UnifiedPush.unregisterApp(
getContext(),
accountSession.getID()
);
//re-register to fcm
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
//re-register to fcm
accountSession.getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
}
unifiedPushItem.toggle();
rebindItem(unifiedPushItem);
}
@@ -350,12 +352,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
(dialog, which)->{
String userDistrib = distributors.get(which);
UnifiedPush.saveDistributor(getContext(), userDistrib);
UnifiedPush.registerApp(
getContext(),
accountID,
new ArrayList<>(),
getContext().getPackageName()
);
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
UnifiedPush.registerApp(
getContext(),
accountSession.getID(),
new ArrayList<>(),
getContext().getPackageName()
);
}
unifiedPushItem.toggle();
rebindItem(unifiedPushItem);
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.model;
public class AkkomaTranslation extends BaseModel{
public String text;
public String detectedLanguage;
public Translation toTranslation() {
Translation translation=new Translation();
translation.content=text;
translation.detectedSourceLanguage=detectedLanguage;
translation.provider="Akkoma";
return translation;
}
}

View File

@@ -161,11 +161,15 @@ public class Instance extends BaseModel{
case BUBBLE_TIMELINE -> pleromaFeatures
.map(f -> f.contains("bubble_timeline"))
.orElse(false);
case MACHINE_TRANSLATION -> pleromaFeatures
.map(f -> f.contains("akkoma:machine_translation"))
.orElse(false);
};
}
public enum Feature {
BUBBLE_TIMELINE
BUBBLE_TIMELINE,
MACHINE_TRANSLATION
}
@Parcel

View File

@@ -4,6 +4,7 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
import static org.joinmastodon.android.api.MastodonAPIController.gsonWithoutDeserializer;
import android.text.TextUtils;
import android.util.Pair;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
@@ -96,6 +97,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public transient TranslationState translationState=TranslationState.HIDDEN;
public transient Translation translation;
public transient boolean fromStatusCreated;
public transient boolean preview;
public Status(){}
@@ -123,6 +125,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
if(filtered!=null)
for(FilterResult fr:filtered)
fr.postprocess();
if(quote!=null)
quote.postprocess();
spoilerRevealed=!hasSpoiler();
if(!spoilerRevealed) sensitive=true;
@@ -217,16 +221,17 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48");
public boolean isEligibleForTranslation(AccountSession session){
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
boolean translateEnabled = instanceInfo != null &&
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
instanceInfo.v2.configuration.translation.enabled;
Instance instanceInfo=AccountSessionManager.getInstance().getInstanceInfo(session.domain);
boolean translateEnabled=instanceInfo!=null && (
(instanceInfo.v2!=null && instanceInfo.v2.configuration.translation!=null && instanceInfo.v2.configuration.translation.enabled) ||
(instanceInfo.isAkkoma() && instanceInfo.hasFeature(Instance.Feature.MACHINE_TRANSLATION))
);
try {
String bottomText = BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find()
Pair<String, List<String>> decoded=BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find()
? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN)
: null;
if(bottomText==null || bottomText.length()==0 || bottomText.equals("\u0005")) bottomText=null;
String bottomText=decoded==null || decoded.second.stream().allMatch(s->s.trim().isEmpty()) ? null : decoded.first;
if(bottomText!=null){
translation=new Translation();
translation.content=bottomText;
@@ -263,7 +268,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
s.visibility=StatusPrivacy.PUBLIC;
s.reactions=List.of();
s.mentions=List.of();
s.tags =List.of();
s.tags=List.of();
s.emojis=List.of();
s.filtered=List.of();
return s;

View File

@@ -12,6 +12,8 @@ import androidx.annotation.StringRes;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BookmarkedStatusListFragment;
import org.joinmastodon.android.fragments.FavoritedStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
@@ -138,6 +140,8 @@ public class TimelineDefinition {
case LIST -> listTitle;
case HASHTAG -> hashtagName;
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
case BOOKMARKS -> ctx.getString(R.string.bookmarks);
case FAVORITES -> ctx.getString(R.string.your_favorites);
};
}
@@ -150,6 +154,8 @@ public class TimelineDefinition {
case LIST -> listIsExclusive ? Icon.EXCLUSIVE_LIST : Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
case BUBBLE -> Icon.BUBBLE;
case BOOKMARKS -> Icon.BOOKMARKS;
case FAVORITES -> Icon.FAVORITES;
};
}
@@ -162,6 +168,8 @@ public class TimelineDefinition {
case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment();
case BUBBLE -> new BubbleTimelineFragment();
case BOOKMARKS -> new BookmarkedStatusListFragment();
case FAVORITES -> new FavoritedStatusListFragment();
};
}
@@ -228,7 +236,19 @@ public class TimelineDefinition {
return args;
}
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, BUBBLE }
public enum TimelineType {
HOME,
LOCAL,
FEDERATED,
POST_NOTIFICATIONS,
LIST,
HASHTAG,
BUBBLE,
// not really timelines, but some people want it, so,,
BOOKMARKS,
FAVORITES
}
public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
@@ -293,6 +313,13 @@ public class TimelineDefinition {
DOCTOR(R.drawable.ic_fluent_doctor_24_regular, R.string.sk_icon_doctor),
DIAMOND(R.drawable.ic_fluent_premium_24_regular, R.string.sk_icon_diamond),
UMBRELLA(R.drawable.ic_fluent_umbrella_24_regular, R.string.sk_icon_umbrella),
WATER(R.drawable.ic_fluent_water_24_regular, R.string.sk_icon_water),
SUN(R.drawable.ic_fluent_weather_sunny_24_regular, R.string.sk_icon_sun),
SUNSET(R.drawable.ic_fluent_weather_sunny_low_24_regular, R.string.sk_icon_sunset),
CLOUD(R.drawable.ic_fluent_cloud_24_regular, R.string.sk_icon_cloud),
THUNDERSTORM(R.drawable.ic_fluent_weather_thunderstorm_24_regular, R.string.sk_icon_thunderstorm),
RAIN(R.drawable.ic_fluent_weather_rain_24_regular, R.string.sk_icon_rain),
SNOWFLAKE(R.drawable.ic_fluent_weather_snowflake_24_regular, R.string.sk_icon_snowflake),
HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true),
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
@@ -301,7 +328,9 @@ public class TimelineDefinition {
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
EXCLUSIVE_LIST(R.drawable.ic_fluent_rss_24_regular, R.string.sk_exclusive_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true),
BOOKMARKS(R.drawable.ic_fluent_bookmark_multiple_24_regular, R.string.bookmarks, true),
FAVORITES(R.drawable.ic_fluent_star_24_regular, R.string.your_favorites, true);
public final int iconRes, nameRes;
public final boolean hidden;
@@ -321,6 +350,8 @@ public class TimelineDefinition {
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final TimelineDefinition BOOKMARKS_TIMELINE = new TimelineDefinition(TimelineType.BOOKMARKS);
public static final TimelineDefinition FAVORITES_TIMELINE = new TimelineDefinition(TimelineType.FAVORITES);
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
@Override
public boolean isCompatible(AccountSession session) {
@@ -365,6 +396,8 @@ public class TimelineDefinition {
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POSTS_TIMELINE,
BUBBLE_TIMELINE
BUBBLE_TIMELINE,
BOOKMARKS_TIMELINE,
FAVORITES_TIMELINE
);
}

View File

@@ -1,10 +1,30 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.AllFieldsAreRequired;
@AllFieldsAreRequired
import org.joinmastodon.android.api.RequiredField;
public class Translation extends BaseModel{
@RequiredField
public String content;
@RequiredField
public String detectedSourceLanguage;
@RequiredField
public String provider;
public String spoilerText;
public MediaAttachment[] mediaAttachments;
public PollTranslation poll;
public static class MediaAttachment {
public String id;
public String description;
}
public static class PollTranslation {
public String id;
public PollOption[] options;
}
public static class PollOption {
public String title;
}
}

View File

@@ -32,7 +32,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class AudioStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final Attachment attachment;
private final ImageLoaderRequest imageRequest;

View File

@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.CheckBox;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.report.ReportAddPostsChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
@@ -34,8 +34,16 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_header_checkable, parent);
checkbox=findViewById(R.id.checkbox);
view=(CheckableRelativeLayout) itemView;
view=findViewById(R.id.checkbox_wrap);
checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
view.setOnClickListener(this::onToggle);
view.setAccessibilityDelegate(new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(CheckBox.class.getName());
}
});
}
@Override
@@ -46,6 +54,12 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
}
}
private void onToggle(View v){
if(item.parentFragment instanceof ReportAddPostsChoiceFragment reportFragment){
reportFragment.onToggleItem(item.parentID);
}
}
public void setIsChecked(Predicate<Holder> isChecked){
this.isChecked=isChecked;
}

View File

@@ -58,7 +58,6 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
public final Status status;
private final Drawable placeholder;
private final boolean hideEmpty, forAnnouncement, playGifs;
private final String accountID;

View File

@@ -36,7 +36,6 @@ import androidx.annotation.PluralsRes;
import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final String accountID;
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
@@ -130,6 +129,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
}
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
if(item.status.preview) return;
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
@@ -137,6 +137,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
}
private void startEditHistoryFragment(){
if(item.status.preview) return;
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putString("id", item.status.id);

View File

@@ -38,7 +38,6 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class FooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private final String accountID;
public boolean hideCounts;
@@ -159,6 +158,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onButtonTouch(View v, MotionEvent event){
if(item.status.preview) return false;
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
int action = event.getAction();
@@ -181,6 +181,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onReplyClick(View v){
if(item.status.preview) return;
UiUtils.opacityIn(v);
Bundle args=new Bundle();
args.putString("account", item.accountID);
@@ -189,6 +190,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onReplyLongClick(View v) {
if(item.status.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
Bundle args=new Bundle();
@@ -204,6 +206,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onBoostClick(View v){
if(item.status.preview) return;
if (GlobalUserPreferences.confirmBoost) {
UiUtils.opacityIn(v);
onBoostLongClick(v);
@@ -219,6 +222,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onBoostLongClick(View v){
if(item.status.preview) return false;
Context ctx = itemView.getContext();
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
@@ -301,6 +305,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onFavoriteClick(View v){
if(item.status.preview) return;
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
UiUtils.opacityIn(v);
@@ -309,6 +314,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onFavoriteLongClick(View v) {
if(item.status.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
@@ -323,6 +329,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onBookmarkClick(View v){
if(item.status.preview) return;
bookmark.setSelected(!item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
UiUtils.opacityIn(v);
@@ -330,6 +337,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onBookmarkLongClick(View v) {
if(item.status.preview) return false;
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
@@ -344,6 +352,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onShareClick(View v){
if(item.status.preview) return;
UiUtils.opacityIn(v);
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
@@ -352,6 +361,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onShareLongClick(View v){
if(item.status.preview) return false;
UiUtils.copyText(v, item.status.url);
return true;
}

View File

@@ -19,7 +19,6 @@ import me.grishka.appkit.utils.V;
public class GapStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
private final Status status;
public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){
super(parentID, parentFragment);

View File

@@ -76,7 +76,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private String accountID;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private SpannableStringBuilder parsedName;
public final Status status;
public boolean hasVisibilityToggle;
boolean needBottomPadding;
private CharSequence extraText;
@@ -446,6 +445,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void onMoreClick(View v){
if(item.status.preview) return;
updateOptionsMenu();
optionsMenu.show();
if(relationship==null && currentRelationshipRequest==null){

View File

@@ -24,7 +24,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
private final Status status;
private final UrlImageLoaderRequest imgRequest;
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){

View File

@@ -12,6 +12,7 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils;
import android.util.Pair;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
@@ -26,6 +27,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
@@ -38,7 +40,12 @@ import org.joinmastodon.android.ui.views.MediaGridLayout;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -52,8 +59,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments;
private final Map<String, Pair<String, String>> translatedAttachments = new HashMap<>();
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public String sensitiveTitle;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
@@ -189,6 +196,25 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
c.btnsWrap.setAlpha(1f);
}
controllers.add(c);
if (item.status.translation != null){
if(item.status.translationState==Status.TranslationState.SHOWN){
if(!item.translatedAttachments.containsKey(att.id)){
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();
translatedAttachment.ifPresent(mediaAttachment->{
item.translatedAttachments.put(mediaAttachment.id, new Pair<>(att.description, mediaAttachment.description));
att.description=mediaAttachment.description;
});
}else{
//SAFETY: must be non-null, as we check if the map contains the attachment before
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).second;
}
}else{
if (item.translatedAttachments.containsKey(att.id)) {
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).first;
}
}
}
c.bind(att, item.status);
i++;
}

View File

@@ -11,6 +11,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
@@ -23,6 +24,7 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
private CharSequence translatedText;
public final Poll.Option option;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private boolean showResults;
@@ -30,12 +32,15 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
private boolean isMostVoted;
private final int optionIndex;
public final Poll poll;
public final Status status;
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment){
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment, Status status){
super(parentID, parentFragment);
this.optionIndex=optionIndex;
option=poll.options.get(optionIndex);
this.poll=poll;
this.status=status;
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
emojiHelper.setText(text);
showResults=poll.isExpired() || poll.voted;
@@ -84,7 +89,14 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(PollOptionStatusDisplayItem item){
text.setText(item.text);
if (item.status.translation != null && item.status.translationState == Status.TranslationState.SHOWN) {
if(item.translatedText==null){
item.translatedText=item.status.translation.poll.options[item.optionIndex].title;
}
text.setText(item.translatedText);
} else {
text.setText(item.text);
}
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
itemView.setClickable(!item.showResults);
icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ?

View File

@@ -43,7 +43,6 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public boolean needBottomPadding;
ReblogOrReplyLineStatusDisplayItem extra;
CharSequence fullText;
Status status;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);

View File

@@ -24,9 +24,9 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
public class SpoilerStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
private final CharSequence parsedTitle;
private CharSequence translatedTitle;
private final CustomEmojiHelper emojiHelper;
private final Type type;
private final int attachmentCount;
@@ -90,7 +90,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(SpoilerStatusDisplayItem item){
title.setText(item.parsedTitle);
if(item.status.translationState==Status.TranslationState.SHOWN){
if(item.translatedTitle==null){
item.translatedTitle=item.status.translation.spoilerText;
}
title.setText(item.translatedTitle);
}else{
title.setText(item.parsedTitle);
}
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.sk_spoiler_show);
itemView.setPadding(
itemView.getPaddingLeft(),

View File

@@ -58,13 +58,15 @@ import me.grishka.appkit.views.UsableRecyclerView;
public abstract class StatusDisplayItem{
public final String parentID;
public final BaseStatusListFragment<?> parentFragment;
public Status status;
public boolean inset;
public int index;
public boolean
hasDescendantNeighbor=false,
hasAncestoringNeighbor=false,
isMainStatus=true,
isDirectDescendant=false;
isDirectDescendant=false,
isForQuote=false;
public static final int FLAG_INSET=1;
public static final int FLAG_NO_FOOTER=1 << 1;
@@ -73,6 +75,7 @@ public abstract class StatusDisplayItem{
public static final int FLAG_NO_HEADER=1 << 4;
public static final int FLAG_NO_TRANSLATE=1 << 5;
public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6;
public static final int FLAG_IS_FOR_QUOTE=1 << 7;
public void setAncestryInfo(
boolean hasDescendantNeighbor,
@@ -232,27 +235,28 @@ public abstract class StatusDisplayItem{
if(statusForContent.hasSpoiler()){
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
if((flags & FLAG_IS_FOR_QUOTE)!=0){
for(StatusDisplayItem item:spoilerItem.contentItems){
item.isForQuote=true;
}
}
items.add(spoilerItem);
contentItems=spoilerItem.contentItems;
}else{
contentItems=items;
}
if (statusForContent.quote != null) {
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
if (!hasQuoteInlineTag) {
String quoteUrl = statusForContent.quote.url;
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
if(statusForContent.quote!=null) {
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
if (quoteInlineIndex!=-1)
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
}
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
if(!TextUtils.isEmpty(statusForContent.content)){
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID);
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
contentItems.add(text);
}else if(!hasSpoiler && header!=null){
header.needBottomPadding=true;
@@ -270,9 +274,11 @@ public abstract class StatusDisplayItem{
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
statusForContent.sensitiveRevealed=false;
statusForContent.sensitive=true;
} else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
statusForContent.sensitiveRevealed=true;
contentItems.add(mediaGrid);
}
@@ -285,17 +291,23 @@ public abstract class StatusDisplayItem{
}
}
if(statusForContent.poll!=null){
buildPollItems(parentID, fragment, statusForContent.poll, contentItems);
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
}
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null){
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
}
if(statusForContent.quote!=null && !(parentObject instanceof Notification)){
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE));
}
if(contentItems!=items && statusForContent.spoilerRevealed){
items.addAll(contentItems);
}
AccountLocalPreferences lp=fragment.getLocalPrefs();
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && lp.emojiReactionsEnabled &&
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment)){
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
statusForContent.reactions!=null){
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
@@ -307,8 +319,9 @@ public abstract class StatusDisplayItem{
items.add(footer);
}
boolean inset=(flags & FLAG_INSET)!=0;
boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0;
// add inset dummy so last content item doesn't clip out of inset bounds
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0){
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){
items.add(new DummyStatusDisplayItem(parentID, fragment));
// in case we ever need the dummy to display a margin for the media grid again:
// (i forgot why we apparently don't need this anymore)
@@ -320,12 +333,22 @@ public abstract class StatusDisplayItem{
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
int i=1;
for(StatusDisplayItem item:items){
item.inset=inset;
if(inset)
item.inset=true;
if(isForQuote){
item.status=statusForContent;
item.isForQuote=true;
}
item.index=i++;
}
if(items!=contentItems && !statusForContent.spoilerRevealed){
for(StatusDisplayItem item:contentItems){
item.inset=inset;
if(inset)
item.inset=true;
if(isForQuote){
item.status=statusForContent;
item.isForQuote=true;
}
item.index=i++;
}
}
@@ -339,10 +362,10 @@ public abstract class StatusDisplayItem{
);
}
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> items){
int i=0;
for(Poll.Option opt:poll.options){
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment));
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment, status));
i++;
}
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
@@ -375,12 +398,15 @@ public abstract class StatusDisplayItem{
}
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
private Context context;
public Holder(View itemView){
super(itemView);
}
public Holder(Context context, int layout, ViewGroup parent){
super(context, layout, parent);
this.context=context;
}
public String getItemID(){
@@ -389,6 +415,16 @@ public abstract class StatusDisplayItem{
@Override
public void onClick(){
if(item.isForQuote){
item.status.filterRevealed=true;
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status.clone()));
args.putBoolean("refresh", true);
Nav.go((Activity) context, ThreadFragment.class, args);
return;
}
item.parentFragment.onItemClick(item.parentID);
}
@@ -420,13 +456,13 @@ public abstract class StatusDisplayItem{
public boolean isLastDisplayItemForStatus(){
return getNextVisibleDisplayItem()
.map(n->!n.parentID.equals(item.parentID))
.map(next->!next.parentID.equals(item.parentID) || item.inset && !next.inset)
.orElse(true);
}
@Override
public boolean isEnabled(){
return item.parentFragment.isItemEnabled(item.parentID);
return item.parentFragment.isItemEnabled(item.parentID) || item.isForQuote;
}
}
}

View File

@@ -39,7 +39,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public boolean textSelectable;
public boolean reduceTopPadding;
public boolean disableTranslate;
public final Status status;
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
super(parentID, parentFragment);
@@ -113,7 +112,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setText(item.text);
}
text.setTextIsSelectable(false);
if(item.textSelectable) itemView.post(() -> text.setTextIsSelectable(true));
if(item.textSelectable && !item.isForQuote) itemView.post(() -> text.setTextIsSelectable(true));
text.setInvalidateOnEveryFrame(false);
itemView.setClickable(false);
itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom());
@@ -124,8 +123,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
if(next!=null && !next.parentID.equals(item.parentID)) next=null;
int bottomPadding=next instanceof FooterStatusDisplayItem ? V.dp(6)
: item.inset ? V.dp(12)
int bottomPadding=item.inset ? V.dp(12)
: next instanceof FooterStatusDisplayItem ? V.dp(6)
: (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0
: V.dp(12);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
@@ -192,7 +191,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void updateTranslation(boolean updateText){
if(item.status==null)
return;
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession());
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()) && !item.isForQuote;
if(translationFooter==null && translateEnabled){
translationFooter=translationFooterStub.inflate();
translationInfo=findViewById(R.id.translation_info_text);
@@ -211,8 +210,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null;
String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language;
Locale locale=lang!=null ? Locale.forLanguageTag(lang) : null;
translationButton.setText(locale!=null
? item.parentFragment.getString(R.string.translate_post, locale.getDisplayLanguage())
String displayLang=locale==null || locale.getDisplayLanguage().isBlank() ? lang : locale.getDisplayLanguage();
translationButton.setText(displayLang!=null
? item.parentFragment.getString(R.string.translate_post, displayLang)
: item.parentFragment.getString(R.string.sk_translate_post));
translationButton.setClickable(true);
translationButton.animate().alpha(1).setDuration(100).start();

View File

@@ -14,7 +14,6 @@ import java.util.List;
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
public final Status status;
public List<StatusDisplayItem> filteredItems;
public LegacyFilter applyingFilter;

View File

@@ -0,0 +1,26 @@
package org.joinmastodon.android.ui.text;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
public class DiffRemovedSpan extends CharacterStyle {
private final String text;
private final int color;
public DiffRemovedSpan(String text, int color){
this.text=text;
this.color=color;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setStrikeThruText(true);
tp.setColor(color);
}
public String getText() {
return text;
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@@ -69,6 +70,10 @@ public class HtmlParser{
private HtmlParser(){}
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
return parse(source, emojis, mentions, tags, accountID, null);
}
/**
* Parse HTML and custom emoji into a spanned string for display.
* Supported tags: <ul>
@@ -81,7 +86,7 @@ public class HtmlParser{
* @param emojis Custom emojis that are present in source as <code>:code:</code>
* @return a spanned string
*/
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Context context){
class SpanInfo{
public Object span;
public int start;
@@ -106,6 +111,9 @@ public class HtmlParser{
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
final SpannableStringBuilder ssb=new SpannableStringBuilder();
int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success);
int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error);
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
@@ -171,6 +179,9 @@ public class HtmlParser{
}
case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el));
case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el));
// fake elements for the edit history diff view
case "edit-diff-insert" -> openSpans.add(new SpanInfo(new ForegroundColorSpan(colorInsert), ssb.length(), el));
case "edit-diff-delete" -> openSpans.add(new SpanInfo(new DiffRemovedSpan(el.text(), colorDelete), ssb.length(), el));
}
}
}

View File

@@ -1683,12 +1683,14 @@ public class UiUtils {
"pronouns.page/"
};
private static final Pattern trimPronouns=Pattern.compile("[^\\w*]*([\\w*].*[\\w*]|[\\w*])\\W*");
private static final String PRONOUN_CHARS="\\w*¿¡!?";
private static final Pattern trimPronouns=
Pattern.compile("[^"+PRONOUN_CHARS+"]*(["+PRONOUN_CHARS+"].*["+PRONOUN_CHARS+"]|["+PRONOUN_CHARS+"])\\W*");
private static String extractPronounsFromField(String localizedPronouns, AccountField field) {
if(!field.name.toLowerCase().contains(localizedPronouns) &&
!field.name.toLowerCase().contains("pronouns")) return null;
String text=HtmlParser.text(field.value);
if(field.value.toLowerCase().contains("https://")){
if(text.toLowerCase().contains("https://")){
for(String pronounUrl : pronounsUrls){
int index=text.indexOf(pronounUrl);
int beginPronouns=index+pronounUrl.length();
@@ -1707,13 +1709,20 @@ public class UiUtils {
Matcher matcher=trimPronouns.matcher(text);
if(!matcher.find()) return null;
String pronouns=matcher.group(1);
// crude fix to allow for pronouns like "it(/she)"
int missingClosingParens=0;
// crude fix to allow for pronouns like "it(/she)" or "(de) sie/ihr"
int missingParens=0, missingBrackets=0;
for(char c : pronouns.toCharArray()){
if(c=='(') missingClosingParens++;
if(c==')') missingClosingParens--;
if(c=='(') missingParens++;
else if(c=='[') missingBrackets++;
else if(c==')') missingParens--;
else if(c==']') missingBrackets--;
}
pronouns+=")".repeat(Math.max(0, missingClosingParens));
if(missingParens > 0) pronouns+=")".repeat(missingParens);
else if(missingParens < 0) pronouns="(".repeat(missingParens*-1)+pronouns;
if(missingBrackets > 0) pronouns+="]".repeat(missingBrackets);
else if(missingBrackets < 0) pronouns="[".repeat(missingBrackets*-1)+pronouns;
// if ends with an un-closed custom emoji
if(pronouns.matches("^.*\\s+:[a-zA-Z_]+$")) pronouns+=':';
return pronouns;

View File

@@ -42,6 +42,7 @@ public class ComposePollViewController{
30*60,
3600,
6*3600,
12*3600,
24*3600,
3*24*3600,
7*24*3600,

View File

@@ -217,6 +217,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.edit_note).setVisible(false);
menu.findItem(R.id.manage_user_lists).setTitle(fragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
MenuItem mute=menu.findItem(R.id.mute);
mute.setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));

View File

@@ -1,9 +1,9 @@
package org.joinmastodon.android.utils;
import android.text.TextUtils;
import org.joinmastodon.android.fragments.ComposeFragment;
import android.util.Pair;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
@@ -40,19 +40,22 @@ public class StatusTextEncoder {
}
// prettiest almost-exact replica of a pretty function
public String decode(String content, Pattern regex) {
Matcher m = regex.matcher(content);
StringBuilder decodedString = new StringBuilder();
int previousEnd = 0;
public Pair<String, List<String>> decode(String content, Pattern regex) {
Matcher m=regex.matcher(content);
StringBuilder decodedString=new StringBuilder();
List<String> decodedParts=new ArrayList<>();
int previousEnd=0;
while (m.find()) {
MatchResult res = m.toMatchResult();
MatchResult res=m.toMatchResult();
// everything before the match - do not decode
decodedString.append(content.substring(previousEnd, res.start()));
previousEnd = res.end();
previousEnd=res.end();
// the match - do decode
decodedString.append(fn.apply(res.group()));
String decoded=fn.apply(res.group());
decodedParts.add(decoded);
decodedString.append(decoded);
}
decodedString.append(content.substring(previousEnd));
return decodedString.toString();
return Pair.create(decodedString.toString(), decodedParts);
}
}

View File

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

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 5.5c-2.413 0-4.383 1.9-4.495 4.285-0.019 0.4-0.349 0.715-0.75 0.715H6.5c-1.657 0-3 1.343-3 3s1.343 3 3 3h11c1.657 0 3-1.343 3-3s-1.343-3-3-3h-0.256c-0.4 0-0.73-0.315-0.749-0.715C16.383 7.4 14.413 5.5 12 5.5zM6.08 9.02C6.548 6.171 9.02 4 12 4s5.452 2.172 5.92 5.02C20.208 9.23 22 11.155 22 13.5c0 2.485-2.015 4.5-4.5 4.5h-11C4.015 18 2 15.985 2 13.5c0-2.344 1.792-4.269 4.08-4.48z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M11 15c0-0.35 0.06-0.687 0.171-1H4.253c-1.242 0-2.25 1.007-2.25 2.25v0.577c0 0.892 0.32 1.756 0.9 2.435 1.565 1.834 3.951 2.74 7.097 2.74 0.398 0 0.783-0.015 1.157-0.044C11.055 21.658 11 21.335 11 21v-0.534c-0.322 0.023-0.655 0.035-1 0.035-2.739 0-4.705-0.745-5.958-2.213-0.348-0.407-0.54-0.926-0.54-1.461v-0.578c0-0.413 0.336-0.749 0.75-0.749H11V15zM10 2.005c2.762 0 5 2.239 5 5s-2.238 5-5 5c-2.761 0-5-2.239-5-5s2.239-5 5-5zm0 1.5c-1.933 0-3.5 1.567-3.5 3.5s1.567 3.5 3.5 3.5 3.5-1.567 3.5-3.5-1.567-3.5-3.5-3.5zM12 15c0-1.104 0.896-2 2-2h7c1.105 0 2 0.896 2 2v6c0 1.105-0.895 2-2 2h-7c-1.104 0-2-0.895-2-2v-6zm2.5 1c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6c0.277 0 0.5-0.224 0.5-0.5S20.777 16 20.5 16h-6zm0 3c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6c0.277 0 0.5-0.224 0.5-0.5S20.777 19 20.5 19h-6z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M16.088 6.412c-0.072-0.093-0.15-0.182-0.234-0.266-0.312-0.313-0.693-0.55-1.113-0.69L13.363 5.01c-0.106-0.037-0.198-0.107-0.263-0.198C13.035 4.719 13 4.609 13 4.497c0-0.113 0.035-0.223 0.1-0.315 0.065-0.091 0.157-0.16 0.263-0.198l1.378-0.448c0.414-0.143 0.789-0.379 1.096-0.69 0.299-0.304 0.525-0.67 0.663-1.072l0.011-0.034 0.448-1.377c0.038-0.106 0.107-0.198 0.2-0.263 0.091-0.065 0.2-0.1 0.313-0.1 0.113 0 0.223 0.035 0.315 0.1s0.161 0.157 0.199 0.263l0.447 1.377c0.14 0.418 0.375 0.798 0.687 1.11 0.312 0.312 0.693 0.547 1.111 0.686l1.378 0.448 0.028 0.007c0.106 0.037 0.198 0.107 0.263 0.198 0.065 0.092 0.1 0.202 0.1 0.314 0 0.113-0.035 0.223-0.1 0.315-0.065 0.091-0.157 0.16-0.263 0.198l-1.378 0.448c-0.419 0.139-0.8 0.374-1.112 0.686-0.312 0.311-0.547 0.692-0.686 1.11l-0.448 1.377L18 8.671c-0.04 0.092-0.104 0.171-0.186 0.23C17.722 8.964 17.613 9 17.5 9c-0.113 0-0.222-0.035-0.314-0.1s-0.162-0.157-0.2-0.263L16.54 7.26c-0.101-0.307-0.254-0.593-0.45-0.848zm7.695 3.801l-0.766-0.248c-0.232-0.078-0.444-0.208-0.617-0.381-0.173-0.174-0.304-0.385-0.381-0.617l-0.25-0.765c-0.02-0.06-0.059-0.11-0.11-0.146C21.61 8.018 21.547 8 21.485 8c-0.063 0-0.124 0.02-0.175 0.056-0.051 0.036-0.09 0.087-0.11 0.146l-0.25 0.764c-0.075 0.231-0.203 0.442-0.374 0.615-0.17 0.173-0.379 0.304-0.609 0.384l-0.765 0.248c-0.06 0.021-0.11 0.06-0.147 0.11C19.02 10.376 19 10.437 19 10.499c0 0.063 0.02 0.124 0.055 0.175 0.037 0.05 0.088 0.09 0.147 0.11l0.765 0.249c0.233 0.077 0.445 0.208 0.619 0.382 0.173 0.174 0.303 0.386 0.38 0.62l0.249 0.764c0.02 0.06 0.06 0.11 0.11 0.146C21.376 12.982 21.437 13 21.5 13c0.063 0 0.124-0.02 0.175-0.056 0.05-0.036 0.09-0.087 0.11-0.146l0.249-0.764c0.077-0.233 0.208-0.444 0.381-0.618 0.174-0.173 0.385-0.303 0.618-0.38l0.765-0.25c0.06-0.02 0.11-0.059 0.147-0.11C23.98 10.626 24 10.564 24 10.502c0-0.063-0.02-0.124-0.055-0.175-0.037-0.05-0.088-0.09-0.147-0.11l-0.015-0.004zM5.25 3h7.797c-0.306 0.11-0.573 0.308-0.767 0.569C12.098 3.834 12 4.148 12 4.469V4.5H5.25C4.836 4.5 4.5 4.836 4.5 5.25v12.5c0 0.966 0.784 1.75 1.75 1.75h9.25V7.327c0.034 0.072 0.065 0.146 0.09 0.222L16 8.999c0.106 0.31 0.305 0.579 0.57 0.77 0.133 0.092 0.278 0.162 0.43 0.21V14h4v3.75c0 1.795-1.455 3.25-3.25 3.25H6.25C4.455 21 3 19.545 3 17.75V5.25C3 4.007 4.007 3 5.25 3zM17 19.5h0.75c0.966 0 1.75-0.784 1.75-1.75V15.5H17v4zM7.25 7C6.836 7 6.5 7.336 6.5 7.75S6.836 8.5 7.25 8.5h5.5c0.414 0 0.75-0.336 0.75-0.75S13.164 7 12.75 7h-5.5zM6.5 11.75C6.5 11.336 6.836 11 7.25 11h5.5c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75h-5.5c-0.414 0-0.75-0.336-0.75-0.75zM7.25 15c-0.414 0-0.75 0.336-0.75 0.75s0.336 0.75 0.75 0.75h3c0.414 0 0.75-0.336 0.75-0.75S10.664 15 10.25 15h-3z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M18.38 4c0.31 0 0.588 0.191 0.7 0.482 0.56 1.462 1.609 2.015 2.17 2.015 0.414 0 0.75 0.335 0.75 0.75 0 0.414-0.336 0.75-0.75 0.75-1.002 0-2.087-0.611-2.873-1.687-0.814 1.093-1.971 1.687-3.187 1.687-1.217 0-2.376-0.595-3.19-1.69-0.814 1.095-1.973 1.69-3.19 1.69-1.215 0-2.373-0.593-3.187-1.686C4.838 7.388 3.753 8 2.75 8 2.336 8 2 7.664 2 7.25S2.336 6.5 2.75 6.5c0.56 0 1.61-0.553 2.17-2.018C5.031 4.192 5.31 4.001 5.62 4c0.311 0 0.59 0.192 0.701 0.482 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.596 2.489-2.015C11.41 4.192 11.69 4 12 4c0.31 0 0.59 0.192 0.7 0.482 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.597 2.489-2.015C17.79 4.192 18.069 4 18.379 4zm0 6c0.31 0 0.588 0.191 0.7 0.482 0.56 1.462 1.609 2.015 2.17 2.015 0.414 0 0.75 0.335 0.75 0.75 0 0.414-0.336 0.75-0.75 0.75-1.002 0-2.087-0.611-2.873-1.687-0.814 1.093-1.971 1.687-3.187 1.687-1.217 0-2.376-0.595-3.19-1.69-0.814 1.095-1.973 1.69-3.19 1.69-1.215 0-2.373-0.593-3.187-1.685C4.838 13.388 3.753 14 2.75 14 2.336 14 2 13.664 2 13.25s0.336-0.75 0.75-0.75c0.56 0 1.61-0.553 2.17-2.018 0.111-0.29 0.39-0.481 0.7-0.481 0.311 0 0.59 0.191 0.701 0.481 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.596 2.489-2.015 0.11-0.29 0.39-0.481 0.7-0.481 0.31 0 0.59 0.191 0.7 0.481 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.597 2.489-2.015 0.111-0.29 0.39-0.482 0.7-0.482zm0.7 6.482C18.969 16.192 18.69 16 18.38 16c-0.311 0-0.59 0.192-0.701 0.482-0.544 1.418-1.57 2.015-2.49 2.015-0.92 0-1.945-0.596-2.489-2.015C12.59 16.192 12.31 16 12 16c-0.31 0-0.59 0.192-0.7 0.482-0.544 1.419-1.57 2.015-2.49 2.015-0.92 0-1.945-0.596-2.489-2.015C6.21 16.192 5.931 16 5.621 16c-0.311 0-0.59 0.192-0.7 0.482C4.358 17.947 3.31 18.5 2.75 18.5 2.336 18.5 2 18.836 2 19.25S2.336 20 2.75 20c1.003 0 2.088-0.612 2.873-1.689 0.814 1.093 1.972 1.686 3.187 1.686 1.217 0 2.376-0.595 3.19-1.69 0.814 1.096 1.973 1.69 3.19 1.69 1.216 0 2.373-0.594 3.187-1.687 0.786 1.076 1.87 1.687 2.873 1.687 0.414 0 0.75-0.336 0.75-0.75 0-0.415-0.336-0.75-0.75-0.75-0.561 0-1.61-0.553-2.17-2.015z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 4.001c3.168 0 4.966 2.097 5.227 4.63h0.08c2.04 0 3.692 1.649 3.692 3.683 0 2.033-1.653 3.682-3.692 3.682h-0.582l-1.582 2.635c-0.207 0.358-0.666 0.481-1.025 0.274-0.329-0.19-0.46-0.591-0.32-0.933l0.046-0.091 1.149-1.885h-2.136l-1.582 2.635c-0.207 0.358-0.666 0.481-1.025 0.274-0.329-0.19-0.46-0.591-0.32-0.933l0.046-0.091 1.148-1.885H8.988l-1.582 2.635c-0.207 0.358-0.665 0.481-1.024 0.274-0.329-0.19-0.46-0.591-0.32-0.933l0.045-0.091 1.148-1.885H6.693C4.653 15.996 3 14.347 3 12.314 3 10.28 4.654 8.63 6.693 8.63h0.08C7.035 6.081 8.83 4.001 12 4.001zm0 1.498c-2.071 0-3.877 1.633-3.877 3.889 0 0.357-0.319 0.638-0.684 0.638H6.75c-1.261 0-2.284 1.001-2.284 2.236 0 1.235 1.022 2.237 2.284 2.237h10.5c1.261 0 2.284-1.002 2.284-2.237s-1.023-2.236-2.284-2.236h-0.69c-0.365 0-0.684-0.28-0.684-0.638 0-2.285-1.806-3.89-3.877-3.89z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M11.75 2c0.414 0 0.75 0.336 0.75 0.75v3.155l2.133-2.134c0.293-0.292 0.768-0.292 1.061 0 0.293 0.293 0.293 0.768 0 1.061L12.5 8.026V11h2.974l3.194-3.194c0.293-0.293 0.768-0.293 1.06 0 0.293 0.293 0.293 0.768 0 1.06L17.596 11h3.155c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75h-3.155l2.134 2.133c0.293 0.293 0.293 0.768 0 1.061-0.293 0.293-0.768 0.293-1.061 0L15.474 12.5H12.5v2.974l3.194 3.194c0.293 0.293 0.293 0.768 0 1.06-0.293 0.293-0.768 0.293-1.06 0L12.5 17.596v3.155c0 0.414-0.336 0.75-0.75 0.75S11 21.164 11 20.75v-3.155L8.867 19.73c-0.293 0.293-0.768 0.293-1.061 0-0.293-0.293-0.293-0.768 0-1.061L11 15.474V12.5H8.026l-3.194 3.194c-0.293 0.293-0.768 0.293-1.06 0-0.293-0.293-0.293-0.768 0-1.06L5.904 12.5H2.75C2.336 12.5 2 12.164 2 11.75S2.336 11 2.75 11h3.155L3.77 8.867c-0.292-0.293-0.292-0.768 0-1.061 0.293-0.293 0.768-0.293 1.061 0L8.026 11H11V8.026L7.806 4.832c-0.293-0.293-0.293-0.768 0-1.06 0.293-0.293 0.768-0.293 1.06 0L11 5.904V2.75C11 2.336 11.336 2 11.75 2z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 2c0.414 0 0.75 0.336 0.75 0.75v1.5C12.75 4.664 12.414 5 12 5s-0.75-0.336-0.75-0.75v-1.5C11.25 2.336 11.586 2 12 2zm0 15c2.761 0 5-2.239 5-5s-2.239-5-5-5-5 2.239-5 5 2.239 5 5 5zm0-1.5c-1.933 0-3.5-1.567-3.5-3.5s1.567-3.5 3.5-3.5 3.5 1.567 3.5 3.5-1.567 3.5-3.5 3.5zm9.25-2.75c0.414 0 0.75-0.336 0.75-0.75s-0.336-0.75-0.75-0.75h-1.5C19.336 11.25 19 11.586 19 12s0.336 0.75 0.75 0.75h1.5zM12 19c0.414 0 0.75 0.336 0.75 0.75v1.5c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75v-1.5c0-0.414 0.336-0.75 0.75-0.75zm-7.75-6.25C4.664 12.75 5 12.414 5 12s-0.336-0.75-0.75-0.75h-1.5C2.336 11.25 2 11.586 2 12s0.336 0.75 0.75 0.75h1.5zM4.22 4.22c0.293-0.293 0.767-0.293 1.06 0l1.5 1.5c0.293 0.293 0.293 0.768 0 1.06-0.293 0.294-0.767 0.294-1.06 0l-1.5-1.5c-0.293-0.292-0.293-0.767 0-1.06zm1.06 15.56c-0.293 0.294-0.767 0.294-1.06 0-0.293-0.292-0.293-0.767 0-1.06l1.5-1.5c0.293-0.293 0.767-0.293 1.06 0 0.293 0.293 0.293 0.768 0 1.06l-1.5 1.5zm14.5-15.56c-0.293-0.293-0.767-0.293-1.06 0l-1.5 1.5c-0.293 0.293-0.293 0.768 0 1.06 0.293 0.294 0.767 0.294 1.06 0l1.5-1.5c0.293-0.292 0.293-0.767 0-1.06zm-1.06 15.56c0.293 0.294 0.767 0.294 1.06 0 0.293-0.292 0.293-0.767 0-1.06l-1.5-1.5c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.768 0 1.06l1.5 1.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12.75 2.75C12.75 2.336 12.414 2 12 2s-0.75 0.336-0.75 0.75v1.5C11.25 4.664 11.586 5 12 5s0.75-0.336 0.75-0.75v-1.5zm6.28 2.22c0.293 0.293 0.293 0.767 0 1.06l-1.06 1.061c-0.293 0.293-0.768 0.293-1.061 0-0.293-0.293-0.293-0.768 0-1.06l1.06-1.061c0.294-0.293 0.768-0.293 1.061 0zM17.41 13c0.059-0.324 0.09-0.659 0.09-1 0-3.038-2.462-5.5-5.5-5.5S6.5 8.962 6.5 12c0 0.341 0.031 0.676 0.09 1H2.75C2.336 13 2 13.336 2 13.75s0.336 0.75 0.75 0.75h18.5c0.414 0 0.75-0.336 0.75-0.75S21.664 13 21.25 13h-3.84zM12 8c2.21 0 4 1.79 4 4 0 0.345-0.044 0.68-0.126 1H8.126C8.044 12.68 8 12.345 8 12c0-2.21 1.79-4 4-4zm-6 8.75C6 16.336 6.336 16 6.75 16h10.5c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75H6.75C6.336 17.5 6 17.164 6 16.75zm4 3c0-0.414 0.336-0.75 0.75-0.75h2.5c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75h-2.5c-0.414 0-0.75-0.336-0.75-0.75zM4.97 4.97c0.293-0.293 0.768-0.293 1.06 0l1.061 1.06c0.293 0.293 0.293 0.768 0 1.061-0.293 0.293-0.768 0.293-1.06 0L4.97 6.031c-0.293-0.294-0.293-0.768 0-1.061z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M10.464 15.748L12.7 13.26c0.277-0.308 0.751-0.333 1.06-0.056 0.28 0.252 0.326 0.666 0.124 0.971l-0.068 0.088-1.111 1.236h2.276c0.594 0 0.938 0.648 0.645 1.134l-0.058 0.084-3.212 4.031c-0.258 0.324-0.73 0.378-1.054 0.12-0.294-0.235-0.365-0.646-0.182-0.963l0.063-0.091 2.242-2.815h-2.403c-0.613 0-0.953-0.685-0.623-1.168l0.065-0.083L12.7 13.26l-2.236 2.488zm2.538-10.74c3.168 0 4.966 2.098 5.227 4.631h0.08c2.04 0 3.692 1.649 3.692 3.683 0 2.033-1.653 3.682-3.692 3.682l-1.788 0.001c0.144-0.213 0.228-0.471 0.228-0.748 0-0.279-0.085-0.537-0.23-0.751h1.734c1.261 0 2.283-1 2.283-2.236 0-1.235-1.022-2.236-2.283-2.236h-0.69c-0.366 0-0.685-0.28-0.685-0.638 0-2.285-1.805-3.89-3.876-3.89-2.072 0-3.877 1.634-3.877 3.89 0 0.357-0.319 0.638-0.684 0.638H7.75c-1.262 0-2.284 1-2.284 2.236 0 1.235 1.022 2.237 2.283 2.237l1.762-0.001c-0.146 0.214-0.23 0.472-0.23 0.75s0.084 0.535 0.228 0.75l-1.816-0.002c-2.039 0-3.692-1.649-3.692-3.682 0-2.034 1.653-3.683 3.692-3.683h0.08c0.263-2.55 2.06-4.63 5.228-4.63zM10 2c1.617 0 3.05 0.815 3.9 2.062-0.29-0.035-0.59-0.053-0.898-0.053-0.395 0-0.775 0.029-1.139 0.085C11.335 3.72 10.69 3.5 10 3.5c-1.567 0-2.902 1.13-3.17 2.656L6.759 6.57c-0.084 0.478-0.5 0.827-0.985 0.827h-0.49C4.297 7.397 3.5 8.195 3.5 9.18c0 0.49 0.198 0.934 0.52 1.257-0.316 0.4-0.566 0.855-0.736 1.347C2.504 11.184 2 10.24 2 9.18 2 7.43 3.37 6 5.096 5.903l0.257-0.006C5.743 3.677 7.682 2 10 2z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -1,40 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CheckableRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingLeft="16dp"
android:clipToPadding="false">
<FrameLayout
<org.joinmastodon.android.ui.views.CheckableRelativeLayout
android:id="@+id/checkbox_wrap"
android:layout_width="wrap_content"
android:layout_height="46sp"
android:duplicateParentState="true">
android:layout_width="56dp"
android:layout_height="match_parent"
android:paddingTop="16dp">
<View
android:id="@+id/checkbox"
android:layout_gravity="center_vertical"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="-4dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="12dp"
android:duplicateParentState="true"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="46sp"
android:duplicateParentState="true">
</FrameLayout>
<View
android:id="@+id/checkbox"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:duplicateParentState="true"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-16dp"
android:layout_marginStart="-16dp"
android:layout_marginHorizontal="-16dp"
android:layout_toEndOf="@id/checkbox_wrap">
<include layout="@layout/display_item_header" />
</FrameLayout>
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>
</RelativeLayout>

View File

@@ -13,7 +13,7 @@
<org.joinmastodon.android.ui.views.NestedRecyclerScrollView
android:id="@+id/scroller"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="true">
<org.joinmastodon.android.ui.views.CustomDrawingOrderLinearLayout
@@ -50,7 +50,7 @@
android:text="@string/follows_you"
android:textAllCaps="true"
android:textColor="#fff"
android:textSize="14dp"
android:textSize="14sp"
android:visibility="gone"
tools:visibility="visible" />
@@ -80,11 +80,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/cover"
android:layout_alignParentEnd="true">
android:layout_alignParentEnd="true"
android:clipChildren="false">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp">
@@ -95,7 +97,8 @@
style="@style/Widget.Mastodon.M3.Button.Tonal"
android:background="@drawable/bg_button_m3_tonal_circle_selector"
android:paddingStart="12dp"
android:drawableStart="@drawable/ic_fluent_alert_24_selector" />
android:drawableStart="@drawable/ic_fluent_alert_24_selector"
tools:ignore="RtlSymmetry" />
<ProgressBar
android:id="@+id/notify_progress"
@@ -112,6 +115,7 @@
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_marginTop="16dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp">
@@ -213,6 +217,60 @@
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/username"
android:id="@+id/note_edit_wrap"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"
android:layout_marginHorizontal="16dp"
android:visibility="gone">
<EditText
android:id="@+id/note_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="52dp"
android:paddingVertical="15dp"
android:textColor="?colorM3OnSurface"
android:inputType="text|textMultiLine|textCapSentences"
android:singleLine="false"
android:background="@drawable/bg_note_edit"
android:paddingEnd="52dp"
android:paddingStart="20dp"
android:elevation="0dp"
android:hint="@string/sk_private_note_hint"
tools:ignore="RtlSymmetry" />
<FrameLayout
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="end">
<ImageButton
android:id="@+id/note_save_btn"
android:layout_width="52dp"
android:layout_height="52dp"
android:visibility="invisible"
android:background="@drawable/bg_button_m3_text_circle"
android:tooltipText="@string/sk_confirm_changes"
android:contentDescription="@string/sk_confirm_changes"
android:src="@drawable/ic_fluent_checkmark_24_regular"/>
<ProgressBar
android:id="@+id/note_save_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
style="?android:progressBarStyleSmall"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout>
</FrameLayout>
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/bio"
android:layout_width="match_parent"
@@ -284,7 +342,8 @@
<me.grishka.appkit.views.UsableRecyclerView
android:id="@+id/metadata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingTop="4dp" />
<LinearLayout

View File

@@ -54,7 +54,7 @@
android:id="@+id/forward_report"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginTop="8dp"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:background="?android:selectableItemBackground">

View File

@@ -5,4 +5,5 @@
<item android:id="@+id/draft" android:title="@string/sk_mark_as_draft" android:icon="@drawable/ic_fluent_drafts_24_regular" />
<item android:id="@+id/undraft" android:title="@string/sk_mark_as_draft" android:icon="@drawable/ic_fluent_drafts_24_filled" android:contentDescription="@string/sk_compose_no_draft" />
<item android:id="@+id/drafts" android:title="@string/sk_unsent_posts" android:icon="@drawable/ic_fluent_folder_open_24_regular" />
<item android:id="@+id/preview" android:title="@string/sk_open_post_preview" android:icon="@drawable/ic_fluent_receipt_sparkles_24_regular" android:visible="false" />
</menu>

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/menu_group1">
<item android:id="@+id/edit_note" android:title="@string/sk_add_note" android:icon="@drawable/ic_fluent_person_note_24_regular" />
</group>
<group android:id="@+id/menu_group2">
<item android:id="@+id/manage_user_lists" android:title="@string/sk_lists_with_user" android:icon="@drawable/ic_fluent_people_24_regular"/>
<item android:id="@+id/mute" android:title="@string/mute_user" android:icon="@drawable/ic_fluent_speaker_off_24_regular"/>
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user" android:icon="@drawable/ic_fluent_arrow_repeat_all_off_24_regular"/>
@@ -9,7 +12,7 @@
<item android:id="@+id/report" android:title="@string/report_user" android:icon="@drawable/ic_fluent_warning_24_regular"/>
<item android:id="@+id/block_domain" android:title="@string/block_domain" android:icon="@drawable/ic_fluent_shield_prohibited_24_regular"/>
</group>
<group android:id="@+id/menu_group2">
<group android:id="@+id/menu_group3">
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser" android:icon="@drawable/ic_fluent_globe_24_regular"/>
<item android:id="@+id/share" android:title="@string/share_user" android:icon="@drawable/ic_fluent_share_24_regular"/>
<item android:id="@+id/open_with_account" android:title="@string/sk_open_with_account" android:visible="false" android:icon="@drawable/ic_fluent_person_swap_24_regular">

View File

@@ -415,5 +415,23 @@
<string name="sk_settings_default_visibility">Standard-Sichtbarkeit für Posts</string>
<string name="sk_settings_lock_account">Neue Follower_innen manuell genehmigen</string>
<string name="sk_timeline_cache_cleared">Start-Timeline geleert</string>
<string name="sk_button_mutuals">Befreundet</string>
<string name="sk_button_mutuals">Befreundet</string>
<string name="sk_icon_snowflake">Schneeflocke</string>
<string name="sk_icon_cloud">Wolke</string>
<string name="sk_icon_sunset">Sonnenuntergang</string>
<string name="sk_private_note_hint">Private Notiz über dieses Profil hinzufügen</string>
<string name="sk_icon_water">Wasser</string>
<string name="sk_icon_sun">Sonne</string>
<string name="sk_icon_rain">Regen</string>
<string name="sk_icon_thunderstorm">Gewitter</string>
<string name="sk_private_note_update_failed">Notiz speichern fehlgeschlagen</string>
<string name="sk_delete_note">Private Notiz löschen</string>
<string name="sk_confirm_changes">Änderungen bestätigen</string>
<string name="sk_settings_crash_log_unavailable">Keines verfügbar… noch</string>
<string name="sk_crash_log_copied">Absturzprotokoll kopiert</string>
<string name="sk_add_note">Private Notiz hinzufügen</string>
<string name="sk_settings_copy_crash_log">Neuestes Absturzprotokoll kopieren</string>
<string name="sk_open_post_preview">Vorschau öffnen</string>
<string name="sk_post_preview">Vorschau</string>
<string name="sk_private_note_confirm_delete">Private Notiz über %s löschen\?</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -402,15 +402,28 @@
<string name="sk_trending_links_info_banner">Estas noticias están dando que hablar en todo el Fediverso.</string>
<string name="sk_blocked_accounts">Cuentas bloqueadas</string>
<string name="sk_muted_accounts">Cuentas silenciadas</string>
<string name="sk_settings_like_icon">Utilizar el corazón como icono favorito</string>
<string name="sk_settings_like_icon">Utilizar un corazón como icono de favorito</string>
<string name="sk_recently_used">Utilizado recientemente</string>
<string name="sk_set_as_default">Establecer por defecto</string>
<string name="sk_settings_color_palette_default">Por defecto (%s)</string>
<string name="sk_settings_underlined_links">Enlaces subrayados</string>
<string name="sk_settings_underlined_links">Subrayar enlaces</string>
<string name="sk_edit_alt_text">Editar el texto alternativo</string>
<string name="sk_settings_default_visibility">Visibilidad de la publicación predeterminada</string>
<string name="sk_settings_default_visibility">Visibilidad de publicación predeterminada</string>
<string name="sk_settings_lock_account">Aprobar nuevos seguidores manualmente</string>
<string name="sk_timeline_cache_cleared">Caché de la línea de tiempo de inicio borrada</string>
<string name="sk_settings_clear_timeline_cache">Borrar la caché de la línea de tiempo de inicio</string>
<string name="sk_timeline_cache_cleared">Caché de la cronología de inicio borrada</string>
<string name="sk_settings_clear_timeline_cache">Borrar la caché de la cronología de inicio</string>
<string name="sk_button_mutuals">Amigos</string>
<string name="sk_icon_snowflake">Copos de nieve</string>
<string name="sk_private_note_confirm">Confirmar los cambios en la nota</string>
<string name="sk_private_note_update_failed">No se pudo guardar la nota</string>
<string name="sk_icon_cloud">Nube</string>
<string name="sk_delete_note">Borrar la nota personal</string>
<string name="sk_icon_sunset">Puesta de sol</string>
<string name="sk_private_note_hint">Añadir una nota personal sobre este perfil</string>
<string name="sk_icon_water">Agua</string>
<string name="sk_icon_sun">Sol</string>
<string name="sk_add_note">Añadir una nota personal</string>
<string name="sk_icon_rain">Lluvia</string>
<string name="sk_icon_thunderstorm">Tormenta eléctrica</string>
<string name="sk_private_note_confirm_delete">¿Borrar la nota personal sobre %s\?</string>
</resources>

View File

@@ -403,4 +403,9 @@
<string name="sk_muted_accounts">حساب‌های خموش شده</string>
<string name="sk_recently_used">اخیرا مورد استفاده قرار گرفته</string>
<string name="sk_edit_alt_text">ویرایش متن جایگزین</string>
<string name="sk_settings_default_visibility">نمایانی فرسته پیش‌گزیده</string>
<string name="sk_settings_lock_account">تایید دستی پیگیران جدید</string>
<string name="sk_timeline_cache_cleared">کش خط زمانی خانه پاک شد</string>
<string name="sk_button_mutuals">متقابل</string>
<string name="sk_settings_clear_timeline_cache">پاک کردن کش خط زمانی خانه</string>
</resources>

View File

@@ -297,7 +297,7 @@
<string name="sk_settings_allow_remote_loading">Charger des informations à partir d\'instances distantes</string>
<string name="sk_no_remote_info_hint">informations distantes indisponibles</string>
<string name="sk_error_loading_profile">Échec du chargement du profil via %s</string>
<string name="sk_settings_allow_remote_loading_explanation">Essayez de récupérer des listes plus précises pour les abonnés, les likes et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
<string name="sk_settings_allow_remote_loading_explanation">Essaye de récupérer des listes plus précises pour les abonné·e·s, les favoris et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Révéler les CW identiques dans les réponses</string>
<string name="sk_settings_auto_reveal_nobody">Jamais</string>
<string name="sk_settings_auto_reveal_author">Réponses du même auteur</string>
@@ -412,7 +412,7 @@
<string name="sk_message_cache_cleared">Cache des messages vidé</string>
<string name="sk_settings_clear_timeline_cache">Effacer le cache du fil d\'accueil</string>
<string name="sk_settings_default_visibility">Visibilité de publication par défaut</string>
<string name="sk_settings_lock_account">Approuver manuellement les nouveaux abonnés</string>
<string name="sk_settings_lock_account">Approuver manuellement les nouveaux·elles abonné·e·s</string>
<string name="sk_timeline_cache_cleared">Cache du fil d\'accueil vidé</string>
<string name="sk_button_mutuals">Amis</string>
<string name="sk_button_mutuals">Suivi mutuel</string>
</resources>

View File

@@ -157,7 +157,7 @@
<string name="sk_visibility_unlisted">Nenavedeno</string>
<string name="sk_lists_with_user">Liste s %s</string>
<string name="sk_settings_show_federated_timeline">Prikaži objedinjenu vremensku traku</string>
<string name="sk_translated_using">"Prevedeno uporabom %s"</string>
<string name="sk_translated_using">Prevedeno uporabom %s</string>
<string name="sk_settings_translate_only_opened">Prevodi samo otvorene objave</string>
<string name="sk_loading_fediverse_resource_title">Pretraga na Fediversu</string>
<string name="sk_loading_resource_on_instance_title">Pretraga na %s</string>

View File

@@ -4,7 +4,7 @@
<string name="sk_delete_and_redraft">削除して再編集</string>
<string name="sk_confirm_delete_and_redraft_title">投稿を削除して再編集</string>
<string name="sk_confirm_delete_and_redraft">本当にこの投稿を削除して再編集しますか?</string>
<string name="sk_pin_post">プロファイルに固定</string>
<string name="sk_pin_post">プロフィールに固定</string>
<string name="sk_confirm_pin_post_title">投稿をプロフィールに固定</string>
<string name="sk_confirm_pin_post">この投稿をあなたのプロフィールに固定しますか?</string>
<string name="sk_pinning">投稿を固定しています…</string>
@@ -154,7 +154,7 @@
<string name="sk_scheduled_too_soon">投稿は10分以上後の予約である必要があります。</string>
<string name="sk_settings_unifiedpush">UnifiedPush を使用</string>
<string name="sk_settings_unifiedpush_choose">ディストリビューターを選択</string>
<string name="sk_list_replies_policy_followed">フォローされたユーザー</string>
<string name="sk_list_replies_policy_followed">フォロー中のユーザー</string>
<string name="sk_delete_list">リストを削除</string>
<string name="sk_delete_list_confirm">本当にリスト “%s” を削除しますか?</string>
<string name="sk_edit_list_title">リストを編集</string>
@@ -288,7 +288,7 @@
<string name="sk_settings_display_pronouns_in_user_listings">ユーザーリストに代名詞を表示</string>
<string name="sk_settings_unifiedpush_no_distributor_body">UnifiedPush による通知を動作させるにはディストリビューターをインストールする必要があります。詳しくは https://unifiedpush.org/ をご覧ください</string>
<string name="sk_list_replies_policy_list">リストのメンバー</string>
<string name="sk_settings_support_local_only">サーバーはローカルのみの投稿をサポートしていま</string>
<string name="sk_settings_support_local_only">ローカルのみの投稿をサポートするサーバー</string>
<string name="sk_spoiler_show">コンテンツを表示</string>
<string name="sk_advanced_options_hide">高度な設定を非表示にする</string>
<string name="sk_signed_up">登録済み</string>
@@ -302,7 +302,7 @@
</plurals>
<string name="sk_reported">報告済み</string>
<string name="sk_set_as_default">デフォルトに設定</string>
<string name="sk_settings_like_icon">お気に入りアイコンにハートを使用する</string>
<string name="sk_settings_like_icon">お気に入りアイコンにハートを使用</string>
<string name="sk_content_type_html">HTML</string>
<string name="sk_settings_color_palette_default">デフォルト (%s)</string>
<string name="sk_open_in_app">アプリで開く</string>
@@ -354,4 +354,34 @@
<string name="sk_show_thread">スレッドを表示</string>
<string name="sk_settings_clear_timeline_cache">ホームタイムラインキャッシュをクリア</string>
<string name="sk_add_timeline_tag_error_empty">ハッシュタグは空欄にできません</string>
<string name="sk_settings_allow_remote_loading_explanation">発信元のインスタンスから情報を読み込んで、フォロワー、いいね、ブーストのより正確なリストを取得しましょう。</string>
<string name="sk_disable_pill_shaped_active_indicator">錠剤型のアクティブタブインジケーターを無効にする</string>
<string name="sk_settings_show_emoji_reactions_only_opened">投稿を開いた時のみ</string>
<string name="sk_settings_glitch_instance">Glitchのローカル限定モード</string>
<string name="sk_settings_allow_remote_loading">リモートインスタンスから情報を読み込む</string>
<string name="sk_notify_posts_info_banner">投稿通知を有効にすると、その人の新しい投稿がここに表示されます。</string>
<string name="sk_settings_auto_reveal_anyone">全員の返信</string>
<string name="sk_settings_show_emoji_reactions_hide_empty">空の絵文字リアクションを隠す</string>
<string name="sk_settings_show_emoji_reactions">絵文字リアクションをタイムラインに表示</string>
<string name="sk_settings_default_visibility">投稿のデフォルト公開範囲</string>
<string name="sk_settings_underlined_links">下線付きリンク</string>
<string name="sk_list_replies_policy_none">なし</string>
<string name="sk_settings_show_alt_indicator">代替テキストがあることを示す表示</string>
<string name="sk_settings_auto_reveal_nobody">なし</string>
<string name="sk_settings_lock_account">新しいフォロワーを手動で承認</string>
<string name="sk_settings_glitch_mode_explanation">ホームインスタンスがGlitchで動作している場合、これを有効にしてください。HometownやAkkomaでは不要です。</string>
<string name="sk_settings_auto_reveal_equal_spoilers">返信内にある同一のCWを自動で展開</string>
<string name="sk_settings_show_no_alt_indicator">代替テキストがないことを示す表示</string>
<string name="sk_no_remote_info_hint">リモートの情報が利用できません</string>
<string name="sk_settings_auto_reveal_author">同じ作成者の返信</string>
<string name="sk_error_loading_profile">%s からのプロフィールの読み込みに失敗しました</string>
<string name="sk_trending_posts_info_banner">これらの投稿は現在Fediverseで人気を集めています。</string>
<string name="sk_in_reply">返信</string>
<string name="sk_settings_confirm_before_reblog">ブーストする前に確認</string>
<string name="sk_settings_emoji_reactions_explanation">投稿に対する絵文字リアクションを表示、追加することができます。様々なFediverseサーバーがこれに対応していますが、Mastodonは対応していません。</string>
<string name="sk_recently_used">最近使用</string>
<string name="sk_button_mutuals">相互フォロー中</string>
<string name="sk_notify_poll_results">投票結果</string>
<string name="sk_post_contains_media">メディアを含む投稿</string>
<string name="sk_edit_alt_text">代替テキストを編集</string>
</resources>

View File

@@ -409,4 +409,5 @@
<string name="sk_settings_lock_account">Aprobați manual urmăritori noi</string>
<string name="sk_timeline_cache_cleared">Memoria cache a cronologiei acasă a fost ștearsă</string>
<string name="sk_settings_clear_timeline_cache">Ștergeți memoria cache a cronologiei acasă</string>
<string name="sk_button_mutuals">Prieteni</string>
</resources>

View File

@@ -415,4 +415,18 @@
<string name="sk_settings_default_visibility">Усталена видимість дописів</string>
<string name="sk_settings_lock_account">Затверджувати нових підписників уручну</string>
<string name="sk_timeline_cache_cleared">Кеш домашньої стрічки очищено</string>
<string name="sk_button_mutuals">Друзі</string>
<string name="sk_icon_snowflake">Сніжинка</string>
<string name="sk_private_note_confirm">Підтвердити зміни</string>
<string name="sk_private_note_update_failed">Не вдалося зберегти нотатку</string>
<string name="sk_icon_cloud">Хмара</string>
<string name="sk_delete_note">Видалити нотатку</string>
<string name="sk_icon_sunset">Захід сонця</string>
<string name="sk_private_note_hint">Додати нотатку для себе про цей профіль</string>
<string name="sk_icon_water">Вода</string>
<string name="sk_icon_sun">Сонце</string>
<string name="sk_add_note">Додати нотатку</string>
<string name="sk_icon_rain">Дощ</string>
<string name="sk_icon_thunderstorm">Гроза</string>
<string name="sk_private_note_confirm_delete">Видалити нотатку про %s\?</string>
</resources>

View File

@@ -2,29 +2,29 @@
<resources>
<string name="sk_pinned_posts">置顶</string>
<string name="sk_delete_and_redraft">删除并重新编辑</string>
<string name="sk_confirm_delete_and_redraft_title">删除并重新编辑</string>
<string name="sk_confirm_delete_and_redraft">确定要删除并重新编辑此文吗?</string>
<string name="sk_confirm_delete_and_redraft_title">删除并重新编辑</string>
<string name="sk_confirm_delete_and_redraft">确定要删除并重新编辑此文吗?</string>
<string name="sk_pin_post">置顶</string>
<string name="sk_confirm_pin_post_title">置顶</string>
<string name="sk_confirm_pin_post">你确定要在资料页置顶此文吗?</string>
<string name="sk_pinning">正在置顶文…</string>
<string name="sk_confirm_pin_post_title">置顶</string>
<string name="sk_confirm_pin_post">你确定要在资料页置顶此文吗?</string>
<string name="sk_pinning">正在置顶文…</string>
<string name="sk_unpin_post">取消置顶</string>
<string name="sk_confirm_unpin_post_title">取消文置顶</string>
<string name="sk_confirm_unpin_post">你确定不再置顶此文吗?</string>
<string name="sk_confirm_unpin_post_title">取消文置顶</string>
<string name="sk_confirm_unpin_post">你确定不再置顶此文吗?</string>
<string name="sk_unpinning">正在取消置顶…</string>
<string name="sk_image_description">图片描述</string>
<string name="sk_visibility_unlisted">不公开</string>
<string name="sk_federated_timeline">联邦时间轴</string>
<string name="sk_federated_timeline_info_banner">这些是互联实例中最新发布的文。</string>
<string name="sk_federated_timeline_info_banner">这些是互联实例中最新发布的文。</string>
<string name="sk_app_name">Megalodon</string>
<string name="sk_settings_show_replies">显示回复</string>
<string name="sk_settings_show_boosts">显示转嘟</string>
<string name="sk_settings_load_new_posts">自动加载新</string>
<string name="sk_settings_load_new_posts">自动加载新</string>
<string name="sk_settings_show_interaction_counts">显示互动次数</string>
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
<string name="sk_mark_media_as_sensitive">标记为敏感媒体</string>
<string name="sk_user_post_notifications_on">启用 %s 的文通知</string>
<string name="sk_user_post_notifications_off">关闭 %s 的文通知</string>
<string name="sk_user_post_notifications_on">启用 %s 的文通知</string>
<string name="sk_user_post_notifications_off">关闭 %s 的文通知</string>
<string name="sk_update_available">Megalodon %s 已经可以下载了。</string>
<string name="sk_update_ready">Megalodon %s 已下载,准备安装。</string>
<string name="sk_check_for_update">检查更新</string>
@@ -45,8 +45,8 @@
<string name="sk_color_palette_blue"></string>
<string name="sk_color_palette_brown"></string>
<string name="sk_color_palette_yellow"></string>
<string name="sk_notification_type_status"></string>
<string name="sk_notification_type_posts">文通知</string>
<string name="sk_notification_type_status"></string>
<string name="sk_notification_type_posts">文通知</string>
<string name="sk_translate_post">翻译</string>
<string name="sk_translate_show_original">显示原文</string>
<string name="sk_poll_allow_multiple">允许多选</string>
@@ -84,8 +84,8 @@
<string name="sk_loading_fediverse_resource_title">在联邦宇宙上查找</string>
<string name="sk_undo_reblog">撤销转嘟</string>
<string name="sk_reblog_with_visibility">转嘟可见性</string>
<string name="sk_quote_post">引用此</string>
<string name="sk_copy_link_to_post">复制文链接</string>
<string name="sk_quote_post">引用此</string>
<string name="sk_copy_link_to_post">复制文链接</string>
<string name="sk_hashtags_you_follow">你关注的标签</string>
<string name="sk_loading_resource_on_instance_title">在 %s 上查找</string>
<string name="sk_resource_not_found">找不到资源</string>
@@ -101,27 +101,27 @@
<string name="sk_already_reblogged">已转嘟过</string>
<string name="sk_reply_as">用其他账号回复</string>
<string name="sk_settings_uniform_icon_for_notifications">所有通知的统一图标</string>
<string name="sk_unsent_posts">未发送的</string>
<string name="sk_unsent_posts">未发送的</string>
<string name="sk_confirm_delete_draft_title">删除草稿</string>
<string name="sk_draft">草稿</string>
<string name="sk_schedule">定时</string>
<string name="sk_confirm_delete_scheduled_post_title">删除定时</string>
<string name="sk_confirm_delete_scheduled_post">你确定要删除此定时文吗?</string>
<string name="sk_confirm_delete_scheduled_post_title">删除定时</string>
<string name="sk_confirm_delete_scheduled_post">你确定要删除此定时文吗?</string>
<string name="sk_draft_or_schedule">草稿或定时</string>
<string name="sk_compose_draft">文将保存为草稿。</string>
<string name="sk_compose_draft">文将保存为草稿。</string>
<string name="sk_compose_scheduled">定时于</string>
<string name="sk_draft_saved">草稿已保存</string>
<string name="sk_post_scheduled">文已定时</string>
<string name="sk_post_scheduled">文已定时</string>
<string name="sk_forward_report_to">转嘟给 %s</string>
<string name="sk_confirm_delete_draft">你确定要删除此文草稿吗?</string>
<string name="sk_confirm_delete_draft">你确定要删除此文草稿吗?</string>
<string name="sk_scheduled_too_soon_title">定时时间过早</string>
<string name="sk_scheduled_too_soon">文只能设置为 10 分钟或更晚发送。</string>
<string name="sk_scheduled_too_soon">文只能设置为 10 分钟或更晚发送。</string>
<string name="sk_save_draft">保存为草稿?</string>
<string name="sk_save_changes">保存更改?</string>
<string name="sk_confirm_save_draft">保存草稿?</string>
<string name="sk_confirm_save_changes">保存更改?</string>
<string name="sk_mark_as_draft">标记为草稿</string>
<string name="sk_schedule_post">定时</string>
<string name="sk_schedule_post">定时</string>
<string name="sk_compose_no_schedule">不要定时</string>
<string name="sk_compose_no_draft">不要标记为草稿</string>
<string name="sk_settings_reduce_motion">减少动画效果</string>
@@ -151,14 +151,14 @@
<string name="sk_timeline_local">本站</string>
<string name="sk_alt_text_missing">至少有一个附件不包含描述。</string>
<string name="sk_publish_anyway">仍然发布</string>
<string name="sk_notify_posts_info_banner">如果你为某些人启用了文通知,其新文将显示在此处。</string>
<string name="sk_notify_posts_info_banner">如果你为某些人启用了文通知,其新文将显示在此处。</string>
<string name="sk_timelines">时间线</string>
<string name="sk_edit_timelines">编辑时间线</string>
<string name="sk_alt_button">ALT</string>
<string name="sk_post_edited">编辑</string>
<string name="sk_notification_type_update">编辑</string>
<string name="sk_notification_type_update">编辑</string>
<string name="sk_alt_text_missing_title">缺少 ALT 文本</string>
<string name="sk_timeline_posts"></string>
<string name="sk_timeline_posts"></string>
<string name="sk_timelines_add">添加</string>
<string name="sk_timeline">时间线</string>
<string name="sk_list">列表</string>
@@ -223,7 +223,7 @@
<string name="sk_icon_headphones">耳机</string>
<string name="sk_icon_human">人类</string>
<string name="sk_icon_globe">地球</string>
<string name="sk_notify_update">编辑已转嘟</string>
<string name="sk_notify_update">编辑已转嘟</string>
<string name="sk_icon_pin">钉子</string>
<string name="sk_remove_follower_confirm">通过屏蔽并立即解除屏蔽以移除%s的关注者身份</string>
<string name="sk_icon_clapper_board">拍板</string>
@@ -248,7 +248,7 @@
<string name="sk_settings_glitch_mode_explanation">如果你的主实例运行 Glitch请启用此功能。Hometown 或 Akkoma 不需要启用。</string>
<string name="sk_sign_ups">用户注册</string>
<string name="sk_new_reports">新举报</string>
<string name="sk_settings_see_new_posts_button">“查看新文” 按钮</string>
<string name="sk_settings_see_new_posts_button">“查看新文” 按钮</string>
<string name="sk_settings_server_version">服务器版本: %s</string>
<string name="sk_notify_poll_results">投票结果</string>
<string name="sk_expand">展开</string>
@@ -256,8 +256,8 @@
<string name="sk_unfinished_attachments">正在上传附件</string>
<string name="sk_unfinished_attachments_message">部分附件尚未上传完毕。</string>
<string name="sk_filtered">已过滤:%s</string>
<string name="sk_settings_collapse_long_posts">折叠很长的</string>
<string name="sk_settings_prefix_reply_cw_with_re">回复时在 CW 前加上 “re:”</string>
<string name="sk_settings_collapse_long_posts">折叠很长的</string>
<string name="sk_settings_prefix_reply_cw_with_re">回复时在内容警告信息前加上 “re:”</string>
<string name="sk_spectator_mode">旁观模式</string>
<string name="sk_settings_hide_interaction">隐藏互动按钮</string>
<string name="sk_settings_reply_visibility_self">对我的回复</string>
@@ -287,7 +287,7 @@
<string name="sk_content_type_markdown">Markdown</string>
<string name="sk_content_type_bbcode">BBCode</string>
<string name="sk_content_type_mfm">MFM</string>
<string name="sk_settings_content_types">启用文格式</string>
<string name="sk_settings_content_types">启用文格式</string>
<string name="sk_settings_content_types_explanation">允许在创建文章时设置类似Markdown的内容类型。注意不是所有的实例都支持这个。</string>
<string name="sk_settings_default_content_type">默认的内容类型</string>
<string name="sk_timeline_bubble">Bubble</string>
@@ -307,7 +307,7 @@
<string name="sk_edit_timeline_tag_all">…并包含其中全部</string>
<string name="sk_edit_timeline_tags_explanation">请注意,服务器会处理这些操作。可能不支持合并这些操作。</string>
<string name="sk_settings_allow_remote_loading_explanation">尝试从原实例加载信息,以获取更准确的关注者、点赞和转发列表。</string>
<string name="sk_settings_auto_reveal_equal_spoilers">在回复中自动显示相同的 CWs</string>
<string name="sk_settings_auto_reveal_equal_spoilers">在回复中自动显示相同的内容警告</string>
<string name="sk_settings_forward_report_default">\"转发报告 \"开关默认值</string>
<string name="sk_list_exclusive_switch_explanation">排除列表的成员不会显示在你的主页时间线上--如果你的实例支持的话。</string>
<string name="sk_advanced_options_show">显示高级选项</string>
@@ -319,7 +319,7 @@
<string name="sk_disable_pill_shaped_active_indicator">禁用药丸状的活跃选项卡指示器</string>
<string name="sk_settings_true_black">全黑模式</string>
<string name="sk_settings_display_pronouns_in_timelines">在时间线上显示性别代词</string>
<string name="sk_settings_emoji_reactions_explanation">显示对文的表情回应,并允许你添加自己的表情回应。许多 Fediverse 服务器支持此功能,但 Mastodon 不支持。</string>
<string name="sk_settings_emoji_reactions_explanation">显示对文的表情回应,并允许你添加自己的表情回应。许多 Fediverse 服务器支持此功能,但 Mastodon 不支持。</string>
<string name="sk_settings_emoji_reactions_in_lists">在时间线中显示表情回应</string>
<string name="sk_settings_emoji_reactions_in_lists_explanation">是否在时间线中显示表情回应。如果此选项为关闭,则只有在查看对话时才会显示表情回应。</string>
<string name="sk_button_react">用表情回应</string>
@@ -349,12 +349,12 @@
<string name="sk_icon_doctor">医生</string>
<string name="sk_icon_diamond">钻石</string>
<string name="sk_icon_umbrella">雨伞</string>
<string name="sk_edit_timeline_tag_main">包含标签的文…</string>
<string name="sk_edit_timeline_tag_main">包含标签的文…</string>
<string name="sk_edit_timeline_tag_none">…但都不包含</string>
<string name="sk_edit_timeline_tag_any">…或包含其中任何一个</string>
<string name="sk_edit_timeline_tag_hint">输入标签…</string>
<string name="sk_edit_timeline_tags_hint">输入标签…</string>
<string name="sk_hashtag_timeline_local_only_switch">仅显示本地文?</string>
<string name="sk_hashtag_timeline_local_only_switch">仅显示本地文?</string>
<string name="sk_add_timeline_tag_error_empty">标签不可为空</string>
<string name="sk_gif_badge">GIF</string>
<string name="sk_settings_prefix_replies_always">回复至任何人</string>
@@ -370,29 +370,29 @@
<string name="sk_tab_profile">个人资料</string>
<string name="sk_settings_show_labels_in_navigation_bar">在导航栏中显示选项卡标签</string>
<string name="sk_reacted_with">%1$s 回应了 %2$s</string>
<string name="sk_bubble_timeline_info_banner">这些都是实例管理员从网络中最新精选出来的文。</string>
<string name="sk_bubble_timeline_info_banner">这些都是实例管理员从网络中最新精选出来的文。</string>
<string name="sk_search_fediverse">搜索联邦宇宙</string>
<string name="sk_time_seconds">%d 秒</string>
<string name="sk_settings_show_emoji_reactions_only_opened">仅当文被打开时</string>
<string name="sk_settings_show_emoji_reactions_only_opened">仅当文被打开时</string>
<string name="sk_time_hours">%d 时</string>
<string name="sk_settings_show_emoji_reactions_hide_empty">隐藏空的表情回应</string>
<string name="sk_settings_show_emoji_reactions">在时间线中显示表情回应</string>
<plurals name="sk_posts_count_label">
<item quantity="other"></item>
<item quantity="other"></item>
</plurals>
<string name="sk_suicide_search_terms">自杀</string>
<string name="sk_load_missing_posts_above">加载较新的</string>
<string name="sk_load_missing_posts_above">加载较新的</string>
<string name="sk_time_days">%d 天</string>
<string name="sk_settings_show_emoji_reactions_always">始终显示添加按钮</string>
<string name="sk_search_suicide_hotlines">查找求助热线</string>
<string name="sk_do_not_show_again">下次不再显示</string>
<string name="sk_trending_posts_info_banner">这些文正在联邦宇宙上引起关注。</string>
<string name="sk_load_missing_posts_below">加载较旧的</string>
<string name="sk_trending_posts_info_banner">这些文正在联邦宇宙上引起关注。</string>
<string name="sk_load_missing_posts_below">加载较旧的</string>
<string name="sk_search_suicide_title">以防你遇到困难…</string>
<string name="sk_time_minutes">%d 分</string>
<string name="sk_search_suicide_message">如果你正在寻找一个不自杀的迹象,这就是。如果你遇到困难,请考虑拨打当地的自杀热线。</string>
<string name="sk_trending_links_info_banner">这些新闻故事正在联邦宇宙上被讨论。</string>
<string name="sk_post_contains_media">文包含媒体</string>
<string name="sk_post_contains_media">文包含媒体</string>
<string name="sk_blocked_accounts">已屏蔽账号</string>
<string name="sk_muted_accounts">已静音账号</string>
<string name="sk_settings_like_icon">使用心形作为收藏图标</string>

View File

@@ -60,4 +60,41 @@
<string name="sk_color_palette_yellow">黃色</string>
<string name="sk_settings_translation_availability_note_unavailable">%s 似乎不支援翻譯.</string>
<string name="sk_lists_with_user">%s 所在的列表</string>
<string name="sk_settings_color_palette_default">預設 (%s)</string>
<plurals name="sk_posts_count_label">
<item quantity="other">嘟文</item>
</plurals>
<string name="sk_quoting_user">引用自 %s</string>
<string name="sk_example_domain">example.social</string>
<string name="sk_settings_reply_visibility_all">所有回覆</string>
<string name="sk_settings_reply_visibility">回覆能見度</string>
<string name="sk_list">列表</string>
<string name="sk_post_contains_media">包含媒體的嘟文</string>
<string name="sk_clear_all_notifications_confirm_action">全部刪除</string>
<string name="sk_copy_link_to_post">複製嘟文連結</string>
<string name="sk_mark_as_draft">標記為草稿</string>
<string name="sk_announcements">公告</string>
<string name="sk_settings_enable_delete_notifications">允許刪除通知</string>
<string name="sk_clear_all_notifications">清除所有通知</string>
<string name="sk_settings_see_new_posts_button">「顯示新嘟文」按鈕</string>
<string name="sk_edit_timelines">編輯時間軸</string>
<string name="sk_quote_post">在新嘟文中引述</string>
<string name="sk_blocked_accounts">已封鎖的帳號</string>
<string name="sk_your_lists">您的清單</string>
<string name="sk_settings_donate">捐款</string>
<string name="sk_timeline_home">首頁</string>
<string name="sk_settings_lock_account">手動審查新的跟隨者</string>
<string name="sk_schedule_post">排定時間發佈</string>
<string name="sk_timeline_federated">聯邦時間軸</string>
<string name="sk_settings_instance">站臺</string>
<string name="sk_timeline_local">此伺服器</string>
<string name="sk_hashtag_timeline_local_only_switch">只顯示此伺服器的嘟文?</string>
<string name="sk_settings_uniform_icon_for_notifications">所有通知使用一致的圖示</string>
<string name="sk_muted_accounts">已靜音的帳號</string>
<string name="sk_settings_single_notification">只顯示一則通知</string>
<string name="sk_unsent_posts">未送出的嘟文</string>
<string name="sk_attach_file">附加檔案</string>
<string name="sk_search_fediverse">在聯邦宇宙中搜尋</string>
<string name="sk_tab_notifications">通知</string>
<string name="sk_settings_unifiedpush">使用 UnifiedPush</string>
</resources>

View File

@@ -37,6 +37,7 @@
<attr name="colorM3DarkOnSurface" format="color" />
<attr name="colorTabBarAlpha" format="color" />
<attr name="colorFilledCardAlpha" format="color" />
<attr name="colorM3Success" format="color" />
<attr name="toolbarActionButtonStyle" format="reference" />
<attr name="colorPrimary25" format="color" />

View File

@@ -59,6 +59,7 @@
<item name="colorPoll">@color/bookmark_selected</item>
<item name="colorTabBarAlpha">#14000000</item>
<item name="colorFilledCardAlpha">#22000000</item>
<item name="colorM3Success">#FF5b8e63</item>
<item name="colorM3DisabledBackground">#1F1F1F1F</item>
<item name="colorM3Error">#B3261E</item>
@@ -143,6 +144,7 @@
<item name="colorM3OnErrorContainer">#F9DEDC</item>
<item name="colorWhite">#000</item>
<item name="colorSensitiveOverlay">#80000000</item>
<item name="colorM3Success">#FF89bb9c</item>
</style>
<style name="ColorPalette.Dark.TrueBlack">

View File

@@ -244,6 +244,13 @@
<string name="sk_icon_doctor">Doctor</string>
<string name="sk_icon_diamond">Diamond</string>
<string name="sk_icon_umbrella">Umbrella</string>
<string name="sk_icon_water">Water</string>
<string name="sk_icon_sun">Sun</string>
<string name="sk_icon_sunset">Sunset</string>
<string name="sk_icon_cloud">Cloud</string>
<string name="sk_icon_thunderstorm">Thunderstorm</string>
<string name="sk_icon_rain">Rain</string>
<string name="sk_icon_snowflake">Snowflake</string>
<string name="sk_edit_timeline">Edit timeline</string>
<string name="sk_add_timeline">Add timeline</string>
<string name="sk_edit_timelines">Edit timelines</string>
@@ -417,4 +424,15 @@
<string name="sk_settings_lock_account">Manually approve new followers</string>
<string name="sk_settings_default_visibility">Default posting visibility</string>
<string name="sk_button_mutuals">Mutuals</string>
<string name="sk_private_note_hint">Add a personal note about this profile</string>
<string name="sk_confirm_changes">Confirm changes</string>
<string name="sk_private_note_update_failed">Failed to save note</string>
<string name="sk_private_note_confirm_delete">Delete personal note about %s?</string>
<string name="sk_delete_note">Delete personal note</string>
<string name="sk_add_note">Add personal note</string>
<string name="sk_settings_copy_crash_log">Copy latest crash log</string>
<string name="sk_settings_crash_log_unavailable">None available… yet</string>
<string name="sk_crash_log_copied">Crash log copied</string>
<string name="sk_open_post_preview">Preview post</string>
<string name="sk_post_preview">Preview</string>
</resources>

View File

@@ -0,0 +1,6 @@
- The latest crash log is now kept and ready to be copied from the "About Megalodon" settings page
- Add and display personal notes on profiles
- Text modifications are now highlighted in the edit history
- Akkoma: Quoted posts are displayed in the timeline; translation; preview posts before publishing
- Added Bookmarks and Your favorites as pinnable timelines
- Various bugfixes

View File

@@ -0,0 +1,6 @@
- Нарешті: довгоочікуване оновлення інтерфейсу Material 3!
- Прогалини в домашній стрічці тепер дозволяють вибирати, завантажувати нові чи старі дані
- Для серверних оголошень і користувачів Akkoma: реакції емоджі!
- Нещодавно використані емоджі в інструменті вибору емоджі
- Займенники тепер відображаються поруч із іменами
- Багато виправлень та нових помилок, які ви можете виявити ✨

View File

@@ -1,4 +1,4 @@
Megalodon — це модифікована версія <a href="https://github.com/mastodon/mastodon-android">офіційного застосунку Mastodon для Android</a>, який додає важливі функції, яких немає в офіційному застосунку, наприклад федеративна стрічка, приховані дописи, а також переглядач опису зображення.
Megalodon — це модифікована версія <a href="https://github.com/mastodon/mastodon-android">офіційного застосунку Mastodon для Android</a>, яка додає важливі функції, яких немає в офіційному застосунку, наприклад федеративна стрічка, приховані дописи, а також переглядач опису зображення.
<b>Ключові функції</b>
@@ -6,11 +6,8 @@ Megalodon — це модифікована версія <a href="https://github
- <b>Федеративна стрічка</b>: Переглядайте всі загальнодоступні дописи від людей зі всього Федівсесвіту!
- <b>Власні стрічки</b>: Закріпіть будь-який список або хештег на домашній вкладці Megalodon, щоб просто переходити між улюбленими темами та людьми!
- <b>Чернетки й заплановані дописи</b>: Дозволяє підготувати допис і запланувати його автонадсилання у вказаний час.
- <b>Закріплення дописів</b>: Закріплюйте ваші найважливіші дописи у своєму профілі та переглядайте, що закріпили люди у вкладці «Закріплене».
- <b>Слідкуйте за хештегами</b>: Переглядайте нові дописи з певними хештегами безпосередньо у своїй домашній стрічці, підписавшись на них.
- <b>Відповідайте на запити на стеження</b>: Погоджуйте або відхиляйте запити на стеження зі сповіщення чи виділеного списку «Запити на стеження».
- <b>Видалити та переробити</b>: Улюблена функція, яка уможливила редагування дописів без фактичної функції редагування.
- <b>Вибір мови selector</b>: Зручний вибір мови для кожного допису для коректної роботи фільтрів і перекладу.
- <b>Переклад</b>: Легко перекладайте дописи прямо в Megalodon! Працює лише за умови, що ця функція також доступна у вашому Mastodon для браузера.
- <b>Покажчик видимості допису</b>: Під час відкриття або відповіді на допис показуватиметься зручна піктограма, яка вказує на видимість допису.
- <b>Колірні теми</b>: Якщо вам не подобається усталений рожевий колір (акулка мовчазно засуджує вас), кольорові теми Moshidon пропонують вам інші варіанти.
- <b>Краща сумісність</b>: Для тих, хто працює на серверах Glitch або Akkoma, Megalodon надає різні додаткові функції та покращення.

View File

@@ -0,0 +1,6 @@
- 改进的过滤器,包括“隐藏警告”兼容性
- 重新设计的个人资料页面,元数据直接位于个人简介下方
- 对于很长的帖子可以折叠或展开
- 自动在回复的内容警告信息前加上“re:”的选项
- 隐藏时间线中交互按钮的选项
- 各种错误修正、调整和改进

View File

@@ -0,0 +1,4 @@
- 长按 "关注 "按钮可关注其他账户中的配置文件
- 在其他账户中打开配置文件的选项
- 向下滚动时间轴时自动隐藏撰写按钮
- 修复打开服务器管理员配置文件时的崩溃问题

View File

@@ -0,0 +1,5 @@
- 直接从通知栏点赞、收藏和回复
- 时间线中转发和回复的标题更美观、更一致
- 通知点(尚未实际加载通知)
- 针对 Akkoma 用户: 回复可见性、主题回复排序、引用...
- 崩溃修复和细微调整

View File

@@ -0,0 +1,8 @@
- 改进后更清晰的主题视图
- 通过共享网站到 Megalodon 打开账户/帖子
- 从源实例加载追随者/收藏夹/......列表
- 改进非Mastodon服务器的兼容性
- 设置帖子内容类型的选项
- 支持从 Pixel 设备上的 "Recent" 应用程序复制 URL
- Auto-reveal equal CWs in threads
- 错误修复和用户界面改进