diff --git a/app/build.gradle b/app/build.gradle index 8aace30fe..2046c73dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { defaultConfig { minSdk 21 targetSdk 33 - versionCode 457 - versionName "3.13.3" + versionCode 462 + versionName "3.14.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } flavorDimensions "default" diff --git a/app/src/main/assets/release_notes/notes.json b/app/src/main/assets/release_notes/notes.json index 24c9070a8..c55a9703b 100644 --- a/app/src/main/assets/release_notes/notes.json +++ b/app/src/main/assets/release_notes/notes.json @@ -1,4 +1,29 @@ [ + { + "version": "3.14.0", + "code": "462", + "note": "Added:\n\n- Add Bubble timeline support in extra-features with filters\n- Allow to display public profiles by default to get all messages (Settings > Interface)\n- Glitch: Allow to post messages locally (Can be turned off in Settings)\n- Pixelfed: Custom layout to display Media fully (Also works for other software when there are media)\n- Allow to align left action buttons in messages\n\nChanged:\n- Full rework on links in messages (also mentions and tags)\n- Add pinned tag in \"any\" to avoid to lose it when renaming timeline\n\nFixed:\n- Links to messages not handled by the app\n- CW when editing a message\n- Fix push notifications with several accounts\n- New messages or edition notifications not pushed\n- Fix quotes with tags/mentions\n- Fix notifications\n- Fix sending multiple media\n- Fix crashes" + }, + { + "version": "3.13.7", + "code": "461", + "note": "Added:\n- Pixelfed: Custom layout to display Media fully \n*(Settings > Timelines > Pixelfed Presentation) - Also works for other softwares when there are media\n\nChanged:\n- Add pinned tag in \"any\" to avoid to lose it when renaming timeline\n\nFixed:\n- Fix push notifications with several accounts\n- Fix quotes with tags/mentions\n- Fix notifications\n- Fix sending multiple media\n- Some crashes" + }, + { + "version": "3.13.6", + "code": "460", + "note": "Fixed:\n- Cross-compose: Wrong instance emojis\n- Custom emojis not displayed in notifications\n- Fav/Boost markers with shared messages\n- Empty notifications\n- Fix cw removed when replying\n- Fix expand media with fit preview images when sensitive\n- Fix an issue with fetch more displayed too often (cache clear will help or wait new messages)" + }, + { + "version": "3.13.5", + "code": "459", + "note": "Added:\n- Glitch: Allow to post messages locally (Can be turned off in Settings)\n\nFixed:\n- Crashes" + }, + { + "version": "3.13.4", + "code": "458", + "note": "Added:\n- Add Bubble timeline support in extra-features with filters\n- Allow to display public profiles by default to get all messages (Settings > Interface)\n\nChanged:\n- Full rework on links in messages (also mentions and tags)\n\nFixed:\n- Spoiler text when editing\n- Fix watermarks" + }, { "version": "3.13.3", "code": "457", diff --git a/app/src/main/java/app/fedilab/android/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/BaseMainActivity.java index a09a6ec54..43b4ddb3b 100644 --- a/app/src/main/java/app/fedilab/android/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/BaseMainActivity.java @@ -165,6 +165,7 @@ import retrofit2.converter.gson.GsonConverterFactory; public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener, FragmentMastodonTimeline.UpdateCounters, FragmentNotificationContainer.UpdateCounters, FragmentMastodonConversation.UpdateCounters { + private static final int REQUEST_CODE = 5415; public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software; public static HashMap> emojis = new HashMap<>(); public static Account.API api; @@ -297,7 +298,6 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt }; private NetworkStateReceiver networkStateReceiver; private boolean headerMenuOpen; - private static final int REQUEST_CODE = 5415; @Override protected void onCreate(Bundle savedInstanceState) { @@ -683,6 +683,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt regex_local = sharedpreferences.getString(getString(R.string.SET_FILTER_REGEX_LOCAL) + currentUserID + currentInstance, null); regex_public = sharedpreferences.getString(getString(R.string.SET_FILTER_REGEX_PUBLIC) + currentUserID + currentInstance, null); show_art_nsfw = sharedpreferences.getBoolean(getString(R.string.SET_ART_WITH_NSFW) + currentUserID + currentInstance, false); + binding.profilePicture.setOnClickListener(v -> binding.drawerLayout.openDrawer(GravityCompat.START)); Helper.loadPP(BaseMainActivity.this, binding.profilePicture, currentAccount); headerMainBinding.accountAcc.setText(String.format("%s@%s", currentAccount.mastodon_account.username, currentAccount.instance)); @@ -785,7 +786,6 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt }); - binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE)); //For receiving data from other activities LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA)); @@ -1041,7 +1041,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt String title = ""; String description = ""; - if(titleEl != null) { + if (titleEl != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { title = Html.fromHtml(titleEl.attr("content"), Html.FROM_HTML_MODE_LEGACY).toString(); } else { @@ -1049,7 +1049,7 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } } - if(descriptionEl != null) { + if (descriptionEl != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { description = Html.fromHtml(descriptionEl.attr("content"), Html.FROM_HTML_MODE_LEGACY).toString(); } else { @@ -1058,13 +1058,13 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } String imageUrl = ""; - if(imageUrlEl != null) { + if (imageUrlEl != null) { imageUrl = imageUrlEl.attr("content"); } StringBuilder titleBuilder = new StringBuilder(); - if(!originalUrl.trim().equalsIgnoreCase(sharedText.trim())) { + if (!originalUrl.trim().equalsIgnoreCase(sharedText.trim())) { // If the shared text is not just the URL, add it to the top String toAppend = sharedText.replaceAll("\\s*" + Pattern.quote(originalUrl) + "\\s*", ""); titleBuilder.append(toAppend); @@ -1072,7 +1072,8 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt if (title.length() > 0) { // OG title fetched from source - if(titleBuilder.length() > 0) titleBuilder.append("\n\n"); + if (titleBuilder.length() > 0) + titleBuilder.append("\n\n"); titleBuilder.append(title); } @@ -1138,7 +1139,6 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt b.putSerializable(Helper.ARG_MEDIA_ATTACHMENTS, new ArrayList<>(attachments)); CrossActionHelper.doCrossShare(BaseMainActivity.this, b); }); - CrossActionHelper.doCrossShare(BaseMainActivity.this, b); } else { Toasty.warning(BaseMainActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show(); } diff --git a/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java b/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java index 03fda1b7f..384a76980 100644 --- a/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/AccountReportActivity.java @@ -320,6 +320,7 @@ public class AccountReportActivity extends BaseBarActivity { } } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { diff --git a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java index 5ff370bac..849b28a94 100644 --- a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java @@ -107,11 +107,6 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana private Status statusReply, statusMention, statusQuoted; private StatusDraft statusDraft; private ComposeAdapter composeAdapter; - private boolean promptSaveDraft; - private boolean restoredDraft; - private List sharedAttachments; - - private final BroadcastReceiver imageReceiver = new BroadcastReceiver() { @Override public void onReceive(android.content.Context context, Intent intent) { @@ -138,7 +133,9 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } } }; - + private boolean promptSaveDraft; + private boolean restoredDraft; + private List sharedAttachments; private ActivityPaginationBinding binding; private BaseAccount account; private String instance, token; @@ -537,10 +534,10 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana if (token == null) { token = account.token; } - if (emojis == null || !emojis.containsKey(currentInstance)) { + if (emojis == null || !emojis.containsKey(instance)) { new Thread(() -> { try { - emojis.put(currentInstance, new EmojiInstance(ComposeActivity.this).getEmojiList(currentInstance)); + emojis.put(instance, new EmojiInstance(ComposeActivity.this).getEmojiList(instance)); } catch (DBException e) { e.printStackTrace(); } @@ -637,6 +634,9 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } if (statusReply.spoiler_text != null) { statusDraftList.get(0).spoiler_text = statusReply.spoiler_text; + if (statusReply.spoiler_text.trim().length() > 0) { + statusDraftList.get(0).spoilerChecked = true; + } } if (statusReply.language != null && !statusReply.language.isEmpty()) { statusDraftList.get(0).language = statusReply.language; diff --git a/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java index 7bf49af50..af6b0c533 100644 --- a/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java @@ -221,6 +221,8 @@ public class HashTagActivity extends BaseActivity { tagTimeline.name = stripTag.trim(); tagTimeline.isNSFW = false; tagTimeline.isART = false; + tagTimeline.any = new ArrayList<>(); + tagTimeline.any.add(stripTag.trim()); pinnedTimeline.tagTimeline = tagTimeline; pinned.pinnedTimelines.add(pinnedTimeline); if (update) { diff --git a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java index 97b13d769..1ab57d6d8 100644 --- a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java @@ -117,8 +117,6 @@ public class ProfileActivity extends BaseActivity { private String mention_str; private WellKnownNodeinfo.NodeInfo nodeInfo; private boolean checkRemotely; - private boolean homeMuted; - private final BroadcastReceiver broadcast_data = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -133,6 +131,7 @@ public class ProfileActivity extends BaseActivity { } } }; + private boolean homeMuted; @Override protected void onCreate(Bundle savedInstanceState) { @@ -145,6 +144,7 @@ public class ProfileActivity extends BaseActivity { Bundle b = getIntent().getExtras(); binding.accountFollow.setEnabled(false); checkRemotely = false; + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(this); homeMuted = false; if (b != null) { account = (Account) b.getSerializable(Helper.ARG_ACCOUNT); @@ -152,6 +152,9 @@ public class ProfileActivity extends BaseActivity { mention_str = b.getString(Helper.ARG_MENTION, null); checkRemotely = b.getBoolean(Helper.ARG_CHECK_REMOTELY, false); } + if (!checkRemotely) { + checkRemotely = sharedpreferences.getBoolean(getString(R.string.SET_PROFILE_REMOTELY), false); + } ActivityCompat.postponeEnterTransition(ProfileActivity.this); //Remove title if (actionBar != null) { @@ -162,7 +165,7 @@ public class ProfileActivity extends BaseActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); } - SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(this); + float scale = sharedpreferences.getFloat(getString(R.string.SET_FONT_SCALE), 1.1f); binding.title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18 * 1.1f / scale); accountsVM = new ViewModelProvider(ProfileActivity.this).get(AccountsVM.class); diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonNotificationsService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonNotificationsService.java index d6f2f15a1..898359fb9 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonNotificationsService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonNotificationsService.java @@ -71,7 +71,12 @@ public interface MastodonNotificationsService { @Field("data[alerts][favourite]") boolean favourite, @Field("data[alerts][reblog]") boolean reblog, @Field("data[alerts][mention]") boolean mention, - @Field("data[alerts][poll]") boolean poll + @Field("data[alerts][poll]") boolean poll, + @Field("data[alerts][status]") boolean status, + @Field("data[alerts][update]") boolean update, + @Field("data[alerts][admin.sign_up]") boolean admin_sign_up, + @Field("data[alerts][admin.report]") boolean admin_report + ); @GET("push/subscription") diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java index 619bc92a7..a2c953dbb 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java @@ -53,6 +53,19 @@ public interface MastodonTimelinesService { @Query("limit") Integer limit ); + @GET("timelines/bubble") + Call> getBubble( + @Header("Authorization") String token, + @Query("only_media") Boolean only_media, + @Query("remote") Boolean remote, + @Query("with_muted") Boolean with_muted, + @Query("exclude_visibilities") List exclude_visibilities, + @Query("reply_visibility") String reply_visibility, + @Query("max_id") String max_id, + @Query("since_id") String since_id, + @Query("min_id") String min_id, + @Query("limit") Integer limit + ); @GET("trends/statuses") Call> getStatusTrends( diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Account.java b/app/src/main/java/app/fedilab/android/client/entities/api/Account.java index 9c423554c..36667aa8c 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Account.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Account.java @@ -87,7 +87,38 @@ public class Account implements Serializable { public Account moved; @SerializedName("role") public Role role; + public transient RelationShip relationShip; + public synchronized Spannable getSpanDisplayName(Context context, WeakReference viewWeakReference) { + if (display_name == null || display_name.isEmpty()) { + display_name = username; + } + return SpannableHelper.convert(context, display_name, null, this, null, viewWeakReference); + } + + public synchronized Spannable getSpanDisplayName(Activity activity, WeakReference viewWeakReference) { + if (display_name == null || display_name.isEmpty()) { + display_name = username; + } + return SpannableHelper.convertEmoji(activity, display_name, this, viewWeakReference); + } + + public synchronized Spannable getSpanDisplayNameTitle(Context context, WeakReference viewWeakReference, String title) { + return SpannableHelper.convert(context, title, null, this, null, viewWeakReference); + } + + public synchronized Spannable getSpanNote(Context context, WeakReference viewWeakReference) { + return SpannableHelper.convert(context, note, null, this, null, viewWeakReference); + } + + @Override + public boolean equals(@Nullable Object obj) { + boolean same = false; + if (obj instanceof Account) { + same = this.id.equals(((Account) obj).id); + } + return same; + } public static class Role implements Serializable { @SerializedName("id") @@ -108,31 +139,6 @@ public class Account implements Serializable { public Date updated_at; } - - public transient RelationShip relationShip; - - public synchronized Spannable getSpanDisplayName(Context context, WeakReference viewWeakReference) { - if (display_name == null || display_name.isEmpty()) { - display_name = username; - } - return SpannableHelper.convert(context, display_name, null, this, null, false, false, viewWeakReference); - } - - public synchronized Spannable getSpanDisplayName(Activity activity, WeakReference viewWeakReference) { - if (display_name == null || display_name.isEmpty()) { - display_name = username; - } - return SpannableHelper.convertEmoji(activity, display_name, this, viewWeakReference); - } - - public synchronized Spannable getSpanDisplayNameTitle(Context context, WeakReference viewWeakReference, String title) { - return SpannableHelper.convert(context, title, null, this, null, false, false, viewWeakReference); - } - - public synchronized Spannable getSpanNote(Context context, WeakReference viewWeakReference) { - return SpannableHelper.convert(context, note, null, this, null, true, true, viewWeakReference); - } - public static class AccountParams implements Serializable { @SerializedName("discoverable") public boolean discoverable; @@ -150,14 +156,4 @@ public class Account implements Serializable { public LinkedHashMap fields; } - - - @Override - public boolean equals(@Nullable Object obj) { - boolean same = false; - if (obj instanceof Account) { - same = this.id.equals(((Account) obj).id); - } - return same; - } } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java b/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java index 808ac6e5d..8bf18863b 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Announcement.java @@ -56,7 +56,7 @@ public class Announcement { public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference) { - return SpannableHelper.convert(context, content, null, null, this, true, false, viewWeakReference); + return SpannableHelper.convert(context, content, null, null, this, viewWeakReference); } } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Field.java b/app/src/main/java/app/fedilab/android/client/entities/api/Field.java index af574ee32..f038740ec 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Field.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Field.java @@ -47,7 +47,7 @@ public class Field implements Serializable { if (verified_at != null && value != null) { value_span = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.verified_text)); } - Spannable spannable = SpannableHelper.convert(context, value, null, account, null, true, true, viewWeakReference); + Spannable spannable = SpannableHelper.convert(context, value, null, account, null, viewWeakReference); if (value_span != null && spannable != null) { spannable.setSpan(value_span, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -57,7 +57,7 @@ public class Field implements Serializable { public synchronized Spannable getLabelSpan(Context context, Account account, WeakReference viewWeakReference) { - Spannable spannable = SpannableHelper.convert(context, name, null, account, null, true, true, viewWeakReference); + Spannable spannable = SpannableHelper.convert(context, name, null, account, null, viewWeakReference); if (name_span != null && spannable != null) { spannable.setSpan(name_span, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java b/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java index cf0d1d8b9..98b161a94 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Poll.java @@ -61,7 +61,7 @@ public class Poll implements Serializable { public transient Spannable span_title; public Spannable getSpanTitle(Context context, Status status, WeakReference viewWeakReference) { - span_title = SpannableHelper.convert(context, title, status, null, null, false, false, viewWeakReference); + span_title = SpannableHelper.convert(context, title, status, null, null, viewWeakReference); return span_title; } } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java index e6a030745..fb3919a5e 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Status.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Status.java @@ -102,13 +102,15 @@ public class Status implements Serializable, Cloneable { public List filtered; @SerializedName("pleroma") public Pleroma pleroma; + @SerializedName("local_only") + public boolean local_only = false; @SerializedName("cached") public boolean cached = false; public Attachment art_attachment; public boolean isExpended = false; public boolean isTruncated = true; - public boolean isFetchMore = false; - public PositionFetchMore positionFetchMore = PositionFetchMore.BOTTOM; + public transient boolean isFetchMore = false; + public transient PositionFetchMore positionFetchMore = PositionFetchMore.BOTTOM; public boolean isChecked = false; public String translationContent; public boolean translationShown; @@ -134,29 +136,25 @@ public class Status implements Serializable, Cloneable { public synchronized Spannable getSpanContent(Context context, WeakReference viewWeakReference, Callback callback) { if (contentSpan == null) { - contentSpan = SpannableHelper.convert(context, content, this, null, null, true, false, viewWeakReference, callback); + contentSpan = SpannableHelper.convert(context, content, this, null, null, viewWeakReference, callback); } return contentSpan; } public synchronized Spannable getSpanSpoiler(Context context, WeakReference viewWeakReference, Callback callback) { if (contentSpoilerSpan == null) { - contentSpoilerSpan = SpannableHelper.convert(context, spoiler_text, this, null, null, true, false, viewWeakReference, callback); + contentSpoilerSpan = SpannableHelper.convert(context, spoiler_text, this, null, null, viewWeakReference, callback); } return contentSpoilerSpan; } public synchronized Spannable getSpanTranslate(Context context, WeakReference viewWeakReference, Callback callback) { if (contentTranslateSpan == null) { - contentTranslateSpan = SpannableHelper.convert(context, translationContent, this, null, null, true, false, viewWeakReference, callback); + contentTranslateSpan = SpannableHelper.convert(context, translationContent, this, null, null, viewWeakReference, callback); } return contentTranslateSpan; } - public interface Callback { - void emojiFetched(); - } - @NonNull public Object clone() throws CloneNotSupportedException { return super.clone(); @@ -167,4 +165,8 @@ public class Status implements Serializable, Cloneable { BOTTOM } + public interface Callback { + void emojiFetched(); + } + } diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java index 26eb86a75..070f91e3b 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/admin/AdminAccount.java @@ -25,16 +25,6 @@ import app.fedilab.android.client.entities.api.Account; public class AdminAccount implements Serializable { - @SerializedName("id") - public String id; - @SerializedName("username") - public String username; - @SerializedName("domain") - public String domain; - @SerializedName("created_at") - public Date created_at; - @SerializedName("email") - public String email; public static LinkedHashMap permissions; static { @@ -61,6 +51,16 @@ public class AdminAccount implements Serializable { permissions.put(80000, "Delete User Data"); } + @SerializedName("id") + public String id; + @SerializedName("username") + public String username; + @SerializedName("domain") + public String domain; + @SerializedName("created_at") + public Date created_at; + @SerializedName("email") + public String email; @SerializedName("ip") public String ip; @SerializedName("role") diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/BubbleTimeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/BubbleTimeline.java new file mode 100644 index 000000000..b871af44c --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/app/BubbleTimeline.java @@ -0,0 +1,35 @@ +package app.fedilab.android.client.entities.app; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.List; + +public class BubbleTimeline implements Serializable { + @SerializedName("id") + public int id; + @SerializedName("only_media") + public boolean only_media = false; + @SerializedName("remote") + public boolean remote = false; + @SerializedName("with_muted") + public boolean with_muted; + @SerializedName("exclude_visibilities") + public List exclude_visibilities = null; + @SerializedName("reply_visibility") + public String reply_visibility = null; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java index 3bfa487b5..1158f816b 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/PinnedTimeline.java @@ -38,6 +38,8 @@ public class PinnedTimeline implements Serializable { public RemoteInstance remoteInstance; @SerializedName("tagTimeline") public TagTimeline tagTimeline; + @SerializedName("bubbleTimeline") + public BubbleTimeline bubbleTimeline; @SerializedName("mastodonList") public MastodonList mastodonList; @SerializedName("currentFilter") diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java b/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java index 626722580..d60a72ed1 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/StatusCache.java @@ -509,7 +509,7 @@ public class StatusCache { try { db.delete(Sqlite.TABLE_STATUS_CACHE, Sqlite.COL_USER_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =? AND " + Sqlite.COL_STATUS + " LIKE ?", - new String[]{userid, instance, "%\"id\":\"" + targetedUser + "\"%" }); + new String[]{userid, instance, "%\"id\":\"" + targetedUser + "\"%"}); } catch (Exception e) { e.printStackTrace(); } diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java index 109a8790f..e3c5fa665 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java @@ -364,6 +364,8 @@ public class Timeline { LOCAL("LOCAL"), @SerializedName("PUBLIC") PUBLIC("PUBLIC"), + @SerializedName("BUBBLE") + BUBBLE("BUBBLE"), @SerializedName("CONTEXT") CONTEXT("CONTEXT"), @SerializedName("TAG") diff --git a/app/src/main/java/app/fedilab/android/helper/ECDHFedilab.java b/app/src/main/java/app/fedilab/android/helper/ECDHFedilab.java new file mode 100644 index 000000000..de56a1921 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/ECDHFedilab.java @@ -0,0 +1,237 @@ +package app.fedilab.android.helper; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static app.fedilab.android.client.entities.app.StatusCache.restoreNotificationFromString; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; + +import androidx.preference.PreferenceManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import app.fedilab.android.client.entities.api.Notification; + + +public class ECDHFedilab { + + + public static final String kp_public = "kp_public"; + public static final String peer_public = "peer_public"; + + public static final String name = "prime256v1"; + private static final byte[] P256_HEAD = new byte[]{(byte) 0x30, (byte) 0x59, (byte) 0x30, (byte) 0x13, (byte) 0x06, (byte) 0x07, (byte) 0x2a, + (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06, (byte) 0x08, (byte) 0x2a, (byte) 0x86, + (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x03, (byte) 0x01, (byte) 0x07, (byte) 0x03, (byte) 0x42, (byte) 0x00}; + + static { + Security.addProvider(new org.spongycastle.jce.provider.BouncyCastleProvider()); + } + + private final KeyPairGenerator kpg; + private final PublicKey publicKey; + private final String encodedPublicKey; + private final byte[] authKey; + private final String slug; + private final String pushPublicKey; + private final String encodedAuthKey; + private final String pushAccountID; + private final String pushPrivateKey; + PrivateKey privateKey; + private String pushPrivateKe; + + public ECDHFedilab(Context context, String slug) throws Exception { + if (slug == null) { + throw new Exception("slug cannot be null"); + } + try { + kpg = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec spec = new ECGenParameterSpec("prime256v1"); + kpg.initialize(spec); + KeyPair keyPair = kpg.generateKeyPair(); + publicKey = keyPair.getPublic(); + privateKey = keyPair.getPrivate(); + encodedPublicKey = Base64.encodeToString(serializeRawPublicKey(publicKey), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + authKey = new byte[16]; + SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(authKey); + byte[] randomAccountID = new byte[16]; + secureRandom.nextBytes(randomAccountID); + pushPrivateKey = Base64.encodeToString(privateKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + pushPublicKey = Base64.encodeToString(publicKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + encodedAuthKey = Base64.encodeToString(authKey, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + pushAccountID = Base64.encodeToString(randomAccountID, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + SharedPreferences.Editor prefsEditor = PreferenceManager + .getDefaultSharedPreferences(context).edit(); + prefsEditor.putString("pushPrivateKey" + slug, pushPrivateKey); + prefsEditor.putString("pushPublicKey" + slug, pushPublicKey); + prefsEditor.putString("encodedAuthKey" + slug, encodedAuthKey); + prefsEditor.putString("pushAccountID" + slug, pushAccountID); + prefsEditor.apply(); + this.slug = slug; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static String getServerKey(Context context, String slug) { + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); + return sharedPreferences.getString("server_key" + slug, null); + } + + private static byte[] serializeRawPublicKey(PublicKey key) { + ECPoint point = ((ECPublicKey) key).getW(); + byte[] x = point.getAffineX().toByteArray(); + byte[] y = point.getAffineY().toByteArray(); + if (x.length > 32) + x = Arrays.copyOfRange(x, x.length - 32, x.length); + if (y.length > 32) + y = Arrays.copyOfRange(y, y.length - 32, y.length); + byte[] result = new byte[65]; + result[0] = 4; + System.arraycopy(x, 0, result, 1 + (32 - x.length), x.length); + System.arraycopy(y, 0, result, result.length - y.length, y.length); + return result; + } + + public static Notification decryptNotification(Context context, String slug, byte[] messageEncrypted) { + + + SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); + String pushPrivateKey = sharedPreferences.getString("pushPrivateKey" + slug, null); + String pushPublicKey = sharedPreferences.getString("pushPublicKey" + slug, null); + String encodedAuthKey = sharedPreferences.getString("encodedAuthKey" + slug, null); + sharedPreferences.getString("pushAccountID" + slug, null); + + + PublicKey serverKey = null; + serverKey = deserializeRawPublicKey(Base64.decode(getServerKey(context, slug), Base64.URL_SAFE)); + PrivateKey privateKey; + PublicKey publicKey; + byte[] authKey; + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.decode(pushPrivateKey, Base64.URL_SAFE))); + publicKey = kf.generatePublic(new X509EncodedKeySpec(Base64.decode(pushPublicKey, Base64.URL_SAFE))); + authKey = Base64.decode(encodedAuthKey, Base64.URL_SAFE); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + e.printStackTrace(); + return null; + } + byte[] sharedSecret; + try { + KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH"); + keyAgreement.init(privateKey); + keyAgreement.doPhase(serverKey, true); + sharedSecret = keyAgreement.generateSecret(); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + e.printStackTrace(); + return null; + } + byte[] secondSaltInfo = "Content-Encoding: auth\0".getBytes(StandardCharsets.UTF_8); + byte[] deriveKey; + try { + deriveKey = deriveKey(authKey, sharedSecret, secondSaltInfo, 32); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + e.printStackTrace(); + return null; + } + String decryptedStr; + try { + + SecretKeySpec aesKey = new SecretKeySpec(deriveKey, "AES"); + byte[] iv = Arrays.copyOfRange(messageEncrypted, 0, 12); + byte[] ciphertext = Arrays.copyOfRange(messageEncrypted, 12, messageEncrypted.length); // Separate ciphertext (the MAC is implicitly separated from the ciphertext) + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.DECRYPT_MODE, aesKey, gCMParameterSpec); + byte[] decrypted = cipher.doFinal(ciphertext); + decryptedStr = new String(decrypted, 2, decrypted.length - 2, StandardCharsets.UTF_8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { + e.printStackTrace(); + return null; + } + return restoreNotificationFromString(decryptedStr); + } + + protected static PublicKey deserializeRawPublicKey(byte[] rawBytes) { + if (rawBytes.length != 65 && rawBytes.length != 64) + return null; + try { + KeyFactory kf = KeyFactory.getInstance("EC"); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + os.write(P256_HEAD); + if (rawBytes.length == 64) + os.write(4); + os.write(rawBytes); + return kf.generatePublic(new X509EncodedKeySpec(os.toByteArray())); + } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) { + e.printStackTrace(); + } + return null; + } + + private static byte[] deriveKey(byte[] firstSalt, byte[] secondSalt, byte[] info, int length) throws NoSuchAlgorithmException, InvalidKeyException { + Mac hmacContext = Mac.getInstance("HmacSHA256"); + hmacContext.init(new SecretKeySpec(firstSalt, "HmacSHA256")); + byte[] hmac = hmacContext.doFinal(secondSalt); + hmacContext.init(new SecretKeySpec(hmac, "HmacSHA256")); + hmacContext.update(info); + byte[] result = hmacContext.doFinal(new byte[]{1}); + return result.length <= length ? result : Arrays.copyOfRange(result, 0, length); + } + + public String getPublicKey() { + return this.encodedPublicKey; + } + + public String getAuthKey() { + return this.encodedAuthKey; + } + +} diff --git a/app/src/main/java/app/fedilab/android/helper/Helper.java b/app/src/main/java/app/fedilab/android/helper/Helper.java index cb02fdb93..310edb0d4 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -257,6 +257,7 @@ public class Helper { public static final String ARG_SEARCH_KEYWORD_CACHE = "ARG_SEARCH_KEYWORD_CACHE"; public static final String ARG_VIEW_MODEL_KEY = "ARG_VIEW_MODEL_KEY"; public static final String ARG_TAG_TIMELINE = "ARG_TAG_TIMELINE"; + public static final String ARG_BUBBLE_TIMELINE = "ARG_BUBBLE_TIMELINE"; public static final String ARG_MEDIA_POSITION = "ARG_MEDIA_POSITION"; public static final String ARG_MEDIA_ATTACHMENT = "ARG_MEDIA_ATTACHMENT"; public static final String ARG_MEDIA_ATTACHMENTS = "ARG_MEDIA_ATTACHMENTS"; @@ -1172,19 +1173,17 @@ public class Helper { File files = new File(attachment.local_path); float textSize = 15; Paint paint = new Paint(); - float textWidht = paint.measureText(waterMark); - float width = Helper.convertDpToPixel(textWidht, context); + float width = paint.measureText(waterMark, 0, waterMark.length()); try { BitmapFactory.Options options = new BitmapFactory.Options(); Bitmap backgroundBitmap = BitmapFactory.decodeFile(files.getAbsolutePath(), options); - - int w = options.outWidth; - int h = options.outHeight; - float valx = (float) 1.0 - width / (float) w; + int w = backgroundBitmap.getWidth(); + int h = backgroundBitmap.getHeight(); + float valx = (float) 1.0 - ((Helper.convertDpToPixel(width, context) + 10)) / (float) w; if (valx < 0) valx = 0; - float valy = (h - Helper.convertDpToPixel(textSize, context) - 10) / (float) h; + float valy = (h - Helper.convertDpToPixel(textSize, context) - 0) / (float) h; WatermarkText watermarkText = new WatermarkText(waterMark) .setPositionX(valx) .setPositionY(valy) @@ -1964,6 +1963,20 @@ public class Helper { return R.style.AppTheme; } + public static void addMutedAccount(app.fedilab.android.client.entities.api.Account target) { + if (MainActivity.filteredAccounts == null) { + MainActivity.filteredAccounts = new ArrayList<>(); + } + if (!MainActivity.filteredAccounts.contains(target)) { + MainActivity.filteredAccounts.add(target); + } + } + + public static void removeMutedAccount(app.fedilab.android.client.entities.api.Account target) { + if (MainActivity.filteredAccounts != null) { + MainActivity.filteredAccounts.remove(target); + } + } //Enum that described actions to replace inside a toot content public enum PatternType { @@ -1995,19 +2008,4 @@ public class Helper { public interface OnFileCopied { void onFileCopied(File file); } - - public static void addMutedAccount(app.fedilab.android.client.entities.api.Account target) { - if (MainActivity.filteredAccounts == null) { - MainActivity.filteredAccounts = new ArrayList<>(); - } - if (!MainActivity.filteredAccounts.contains(target)) { - MainActivity.filteredAccounts.add(target); - } - } - - public static void removeMutedAccount(app.fedilab.android.client.entities.api.Account target) { - if (MainActivity.filteredAccounts != null) { - MainActivity.filteredAccounts.remove(target); - } - } } diff --git a/app/src/main/java/app/fedilab/android/helper/MediaHelper.java b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java index 352075b65..770436b5f 100644 --- a/app/src/main/java/app/fedilab/android/helper/MediaHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java @@ -409,15 +409,6 @@ public class MediaHelper { return maxHeight; } - //Listener for recording media - public interface ActionRecord { - void onRecorded(String file); - } - - public interface OnSchedule { - void scheduledAt(String scheduledDate); - } - public static void ResizedImageRequestBody(Context context, Uri uri, File targetedFile) { InputStream decodeBitmapInputStream = null; try { @@ -529,7 +520,6 @@ public class MediaHelper { } } - private static long getMaxSize(long maxSize) { if (MainActivity.instanceInfo != null && MainActivity.instanceInfo.configuration != null && MainActivity.instanceInfo.configuration.media_attachments != null) { maxSize = MainActivity.instanceInfo.configuration.media_attachments.image_size_limit; @@ -584,4 +574,14 @@ public class MediaHelper { return null; } + + //Listener for recording media + public interface ActionRecord { + void onRecorded(String file); + } + + public interface OnSchedule { + void scheduledAt(String scheduledDate); + } + } diff --git a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java index 18a81cfbd..4473d48f8 100644 --- a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java @@ -90,8 +90,13 @@ public class NotificationsHelper { boolean notif_share = prefs.getBoolean(context.getString(R.string.SET_NOTIF_SHARE), true); boolean notif_poll = prefs.getBoolean(context.getString(R.string.SET_NOTIF_POLL), true); boolean notif_fav = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FAVOURITE), true); + boolean notif_status = prefs.getBoolean(context.getString(R.string.SET_NOTIF_STATUS), true); + boolean notif_updates = prefs.getBoolean(context.getString(R.string.SET_NOTIF_UPDATE), true); + boolean notif_signup = prefs.getBoolean(context.getString(R.string.SET_NOTIF_ADMIN_SIGNUP), true); + boolean notif_report = prefs.getBoolean(context.getString(R.string.SET_NOTIF_ADMIN_REPORT), true); + //User disagree with all notifications - if (!notif_follow && !notif_fav && !notif_mention && !notif_share && !notif_poll) + if (!notif_follow && !notif_fav && !notif_mention && !notif_share && !notif_poll && !notif_status && !notif_updates && !notif_signup && !notif_report) return; //Nothing is done MastodonNotificationsService mastodonNotificationsService = init(context, slugArray[1]); diff --git a/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java index e39da3142..f95e42c38 100644 --- a/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java @@ -14,6 +14,7 @@ package app.fedilab.android.helper; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ + import static app.fedilab.android.BaseMainActivity.currentAccount; import static app.fedilab.android.BaseMainActivity.currentInstance; import static app.fedilab.android.BaseMainActivity.currentUserID; @@ -22,7 +23,6 @@ import static app.fedilab.android.BaseMainActivity.show_replies; import static app.fedilab.android.ui.pageadapter.FedilabPageAdapter.BOTTOM_TIMELINE_COUNT; import android.annotation.SuppressLint; -import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.text.Editable; @@ -57,8 +57,10 @@ import java.util.regex.Pattern; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.app.BottomMenu; +import app.fedilab.android.client.entities.app.BubbleTimeline; import app.fedilab.android.client.entities.app.Pinned; import app.fedilab.android.client.entities.app.PinnedTimeline; import app.fedilab.android.client.entities.app.RemoteInstance; @@ -66,6 +68,8 @@ import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.client.entities.app.TagTimeline; import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.ActivityMainBinding; +import app.fedilab.android.databinding.DialogBubbleExcludeVisibilityBinding; +import app.fedilab.android.databinding.DialogBubbleReplyVisibilityBinding; import app.fedilab.android.databinding.TabCustomDefaultViewBinding; import app.fedilab.android.databinding.TabCustomViewBinding; import app.fedilab.android.exception.DBException; @@ -94,60 +98,6 @@ public class PinnedTimelineHelper { } - /** - * Returns the slug of the first loaded fragment - * - * @param context - Context - * @param pinned - {@link Pinned} - * @param bottomMenu - {@link BottomMenu} - * @return String - slug - */ - public static String firstTimelineSlug(Context context, Pinned pinned, BottomMenu bottomMenu) { - String slug = Timeline.TimeLineEnum.HOME.getValue(); - SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); - boolean singleBar = sharedpreferences.getBoolean(context.getString(R.string.SET_USE_SINGLE_TOPBAR), false); - PinnedTimeline pinnedTimelineMin = null; - if (singleBar) { - if (pinned != null && pinned.pinnedTimelines != null) { - for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { - if (pinnedTimeline.displayed) { - if (pinnedTimelineMin == null) { - pinnedTimelineMin = pinnedTimeline; - } else if (pinnedTimelineMin.position > pinnedTimeline.position) { - pinnedTimelineMin = pinnedTimeline; - } - } - } - } - } else { - if (bottomMenu != null && bottomMenu.bottom_menu != null && bottomMenu.bottom_menu.size() > 0) { - BottomMenu.MenuItem menuItem = bottomMenu.bottom_menu.get(0); - return menuItem.item_menu_type.getValue(); - } - - } - String ident = null; - if (pinnedTimelineMin != null) { - if (pinnedTimelineMin.tagTimeline != null) { - ident = "@T@" + pinnedTimelineMin.tagTimeline.name; - if (pinnedTimelineMin.tagTimeline.isART) { - pinnedTimelineMin.type = Timeline.TimeLineEnum.ART; - } - } else if (pinnedTimelineMin.mastodonList != null) { - ident = "@l@" + pinnedTimelineMin.mastodonList.id; - } else if (pinnedTimelineMin.remoteInstance != null) { - if (pinnedTimelineMin.remoteInstance.type == RemoteInstance.InstanceType.NITTER) { - String remoteInstance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); - ident = "@R@" + remoteInstance; - } else { - ident = "@R@" + pinnedTimelineMin.remoteInstance.host; - } - } - slug = pinnedTimelineMin.type.getValue() + (ident != null ? "|" + ident : ""); - } - return slug; - } - public synchronized static void redrawTopBarPinned(BaseMainActivity activity, ActivityMainBinding activityMainBinding, Pinned pinned, BottomMenu bottomMenu, List mastodonLists) { //Values must be initialized if there is no records in db if (pinned == null) { @@ -159,8 +109,8 @@ public class PinnedTimelineHelper { pinned.pinnedTimelines = new ArrayList<>(); } //Set the slug of first visible fragment - String slugOfFirstFragment = PinnedTimelineHelper.firstTimelineSlug(activity, pinned, bottomMenu); - Helper.setSlugOfFirstFragment(activity, slugOfFirstFragment, currentUserID, currentInstance); + /*String slugOfFirstFragment = PinnedTimelineHelper.firstTimelineSlug(activity, pinned, bottomMenu); + Helper.setSlugOfFirstFragment(activity, slugOfFirstFragment, currentUserID, currentInstance);*/ SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); boolean singleBar = sharedpreferences.getBoolean(activity.getString(R.string.SET_USE_SINGLE_TOPBAR), false); @@ -187,6 +137,7 @@ public class PinnedTimelineHelper { activityMainBinding.viewPager.setLayoutParams(params); List pinnedTimelines = pinned.pinnedTimelines; + boolean extraFeatures = sharedpreferences.getBoolean(activity.getString(R.string.SET_EXTAND_EXTRA_FEATURES) + MainActivity.currentUserID + MainActivity.currentInstance, false); if (singleBar) { boolean createDefaultAtTop = true; @@ -222,15 +173,46 @@ public class PinnedTimelineHelper { pinnedTimelineConversations.type = Timeline.TimeLineEnum.DIRECT; pinnedTimelineConversations.position = 4; pinned.pinnedTimelines.add(pinnedTimelineConversations); - try { new Pinned(activity).updatePinned(pinned); } catch (DBException e) { e.printStackTrace(); } } - } + if (extraFeatures) { + try { + Pinned pinnedAll = new Pinned(activity).getAllPinned(currentAccount); + if (pinnedAll == null) { + pinnedAll = new Pinned(); + pinnedAll.user_id = currentUserID; + pinnedAll.instance = currentInstance; + pinnedAll.pinnedTimelines = new ArrayList<>(); + } + boolean createDefaultBubbleAtTop = true; + for (PinnedTimeline pinnedTimeline : pinnedAll.pinnedTimelines) { + if (pinnedTimeline.type == Timeline.TimeLineEnum.BUBBLE) { + createDefaultBubbleAtTop = false; + break; + } + } + if (createDefaultBubbleAtTop) { + PinnedTimeline pinnedTimelineBubble = new PinnedTimeline(); + pinnedTimelineBubble.type = Timeline.TimeLineEnum.BUBBLE; + pinnedTimelineBubble.position = pinnedAll.pinnedTimelines != null ? pinnedAll.pinnedTimelines.size() : 0; + pinned.pinnedTimelines.add(pinnedTimelineBubble); + boolean exist = new Pinned(activity).pinnedExist(pinned); + if (exist) { + new Pinned(activity).updatePinned(pinned); + } else { + new Pinned(activity).insertPinned(pinned); + } + } + } catch (DBException e) { + e.printStackTrace(); + } + } + sortPositionAsc(pinnedTimelines); //Check if changes occurred, if mastodonLists is null it does need, because it is the first call to draw pinned boolean needRedraw = mastodonLists == null; @@ -421,6 +403,9 @@ public class PinnedTimelineHelper { case DIRECT: tabCustomDefaultViewBinding.icon.setImageResource(R.drawable.ic_baseline_mail_24); break; + case BUBBLE: + tabCustomDefaultViewBinding.icon.setImageResource(R.drawable.ic_baseline_bubble_chart_24); + break; } tab.setCustomView(tabCustomDefaultViewBinding.getRoot()); } @@ -522,6 +507,9 @@ public class PinnedTimelineHelper { case TAG: tagClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); break; + case BUBBLE: + bubbleClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); + break; case REMOTE: if (pinnedTimelineVisibleList.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER) { instanceClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); @@ -1004,6 +992,244 @@ public class PinnedTimelineHelper { } + /** + * Manage long clicks on Bubble timelines + * + * @param activity - BaseMainActivity activity + * @param pinned - {@link Pinned} + * @param view - View + * @param position - int position of the tab + */ + public static void bubbleClick(BaseMainActivity activity, Pinned pinned, View view, ActivityMainBinding activityMainBinding, int position, String slug) { + int toRemove = itemToRemoveInBottomMenu(activity); + PopupMenu popup = new PopupMenu(activity, view); + int offSetPosition = position - (BOTTOM_TIMELINE_COUNT - toRemove); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean singleBar = sharedpreferences.getBoolean(activity.getString(R.string.SET_USE_SINGLE_TOPBAR), false); + if (singleBar) { + offSetPosition = position; + } + + if (pinned.pinnedTimelines.get(offSetPosition).bubbleTimeline == null) { + pinned.pinnedTimelines.get(offSetPosition).bubbleTimeline = new BubbleTimeline(); + } + BubbleTimeline bubbleTimeline = pinned.pinnedTimelines.get(offSetPosition).bubbleTimeline; + + popup.getMenuInflater() + .inflate(R.menu.option_bubble_timeline, popup.getMenu()); + Menu menu = popup.getMenu(); + + final MenuItem itemMediaOnly = menu.findItem(R.id.action_show_media_only); + final MenuItem itemRemote = menu.findItem(R.id.action_remote); + + + final boolean[] changes = {false}; + final boolean[] mediaOnly = {false}; + final boolean[] remote = {false}; + mediaOnly[0] = bubbleTimeline.only_media; + remote[0] = bubbleTimeline.remote; + itemMediaOnly.setChecked(mediaOnly[0]); + itemRemote.setChecked(remote[0]); + popup.setOnDismissListener(menu1 -> { + if (changes[0]) { + if (activityMainBinding.viewPager.getAdapter() != null) { + try { + new StatusCache(activity).deleteForSlug(slug); + } catch (DBException e) { + e.printStackTrace(); + } + + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(activity.getString(R.string.SET_INNER_MARKER) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance + slug, null); + editor.commit(); + Fragment fragmentMastodonTimeline = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition()); + if (fragmentMastodonTimeline instanceof FragmentMastodonTimeline && fragmentMastodonTimeline.isVisible()) { + FragmentTransaction fragTransaction = activity.getSupportFragmentManager().beginTransaction(); + fragTransaction.detach(fragmentMastodonTimeline).commit(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.BUBBLE); + bundle.putSerializable(Helper.ARG_BUBBLE_TIMELINE, bubbleTimeline); + bundle.putSerializable(Helper.ARG_INITIALIZE_VIEW, false); + fragmentMastodonTimeline.setArguments(bundle); + FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction(); + fragTransaction2.attach(fragmentMastodonTimeline); + fragTransaction2.commit(); + ((FragmentMastodonTimeline) fragmentMastodonTimeline).recreate(); + } + } + } + }); + + + int finalOffSetPosition = offSetPosition; + popup.setOnMenuItemClickListener(item -> { + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + item.setActionView(new View(activity)); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return false; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return false; + } + }); + changes[0] = true; + int itemId = item.getItemId(); + if (itemId == R.id.action_show_media_only) { + mediaOnly[0] = !mediaOnly[0]; + bubbleTimeline.only_media = mediaOnly[0]; + pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline; + itemMediaOnly.setChecked(mediaOnly[0]); + try { + new Pinned(activity).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + } else if (itemId == R.id.action_remote) { + remote[0] = !remote[0]; + bubbleTimeline.remote = remote[0]; + pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline; + itemRemote.setChecked(remote[0]); + try { + new Pinned(activity).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + } else if (itemId == R.id.action_exclude_visibility) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle()); + DialogBubbleExcludeVisibilityBinding dialogBinding = DialogBubbleExcludeVisibilityBinding.inflate(activity.getLayoutInflater()); + dialogBuilder.setView(dialogBinding.getRoot()); + dialogBuilder.setTitle(R.string.exclude_visibility); + if (bubbleTimeline.exclude_visibilities == null) { + bubbleTimeline.exclude_visibilities = new ArrayList<>(); + } + for (String value : bubbleTimeline.exclude_visibilities) { + if (value.equalsIgnoreCase("public")) { + dialogBinding.valuePublic.setChecked(true); + } + if (value.equalsIgnoreCase("local")) { + dialogBinding.valueLocal.setChecked(true); + } + if (value.equalsIgnoreCase("direct")) { + dialogBinding.valueDirect.setChecked(true); + } + if (value.equalsIgnoreCase("list")) { + dialogBinding.valueList.setChecked(true); + } + if (value.equalsIgnoreCase("private")) { + dialogBinding.valuePrivate.setChecked(true); + } + if (value.equalsIgnoreCase("unlisted")) { + dialogBinding.valueUnlisted.setChecked(true); + } + } + dialogBinding.valuePrivate.setOnCheckedChangeListener((compoundButton, checked) -> { + if (checked) { + if (!bubbleTimeline.exclude_visibilities.contains("private")) { + bubbleTimeline.exclude_visibilities.add("private"); + } + } else { + bubbleTimeline.exclude_visibilities.remove("private"); + } + }); + dialogBinding.valueDirect.setOnCheckedChangeListener((compoundButton, checked) -> { + if (checked) { + if (!bubbleTimeline.exclude_visibilities.contains("direct")) { + bubbleTimeline.exclude_visibilities.add("direct"); + } + } else { + bubbleTimeline.exclude_visibilities.remove("direct"); + } + }); + dialogBinding.valueList.setOnCheckedChangeListener((compoundButton, checked) -> { + if (checked) { + if (!bubbleTimeline.exclude_visibilities.contains("list")) { + bubbleTimeline.exclude_visibilities.add("list"); + } + } else { + bubbleTimeline.exclude_visibilities.remove("list"); + } + }); + dialogBinding.valueLocal.setOnCheckedChangeListener((compoundButton, checked) -> { + if (checked) { + if (!bubbleTimeline.exclude_visibilities.contains("local")) { + bubbleTimeline.exclude_visibilities.add("local"); + } + } else { + bubbleTimeline.exclude_visibilities.remove("local"); + } + }); + dialogBinding.valuePublic.setOnCheckedChangeListener((compoundButton, checked) -> { + if (checked) { + if (!bubbleTimeline.exclude_visibilities.contains("public")) { + bubbleTimeline.exclude_visibilities.add("public"); + } + } else { + bubbleTimeline.exclude_visibilities.remove("public"); + } + }); + dialogBinding.valueUnlisted.setOnCheckedChangeListener((compoundButton, checked) -> { + if (checked) { + if (!bubbleTimeline.exclude_visibilities.contains("unlisted")) { + bubbleTimeline.exclude_visibilities.add("unlisted"); + } + } else { + bubbleTimeline.exclude_visibilities.remove("unlisted"); + } + }); + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline; + try { + new Pinned(activity).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.show(); + } else if (itemId == R.id.action_reply_visibility) { + AlertDialog.Builder dialogBuilder; + AlertDialog alertDialog; + dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle()); + DialogBubbleReplyVisibilityBinding dialogBinding = DialogBubbleReplyVisibilityBinding.inflate(activity.getLayoutInflater()); + dialogBuilder.setView(dialogBinding.getRoot()); + dialogBuilder.setTitle(R.string.reply_visibility); + int checkedId = R.id.all; + if (bubbleTimeline.reply_visibility != null && bubbleTimeline.reply_visibility.equalsIgnoreCase("following")) { + checkedId = R.id.following; + } else if (bubbleTimeline.reply_visibility != null && bubbleTimeline.reply_visibility.equalsIgnoreCase("self")) { + checkedId = R.id.self; + } + dialogBinding.replyVisibility.check(checkedId); + dialogBinding.replyVisibility.setOnCheckedChangeListener((radioGroup, checkedElement) -> { + if (checkedElement == R.id.all) { + bubbleTimeline.reply_visibility = null; + } else if (checkedElement == R.id.following) { + bubbleTimeline.reply_visibility = "following"; + } else if (checkedElement == R.id.self) { + bubbleTimeline.reply_visibility = "self"; + } + }); + dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { + pinned.pinnedTimelines.get(finalOffSetPosition).bubbleTimeline = bubbleTimeline; + try { + new Pinned(activity).updatePinned(pinned); + } catch (DBException e) { + e.printStackTrace(); + } + }); + alertDialog = dialogBuilder.create(); + alertDialog.show(); + } + return false; + }); + popup.show(); + } + + /** * Manage long clicks on followed instances * diff --git a/app/src/main/java/app/fedilab/android/helper/PushNotifications.java b/app/src/main/java/app/fedilab/android/helper/PushNotifications.java index 0b1d561c4..eda242596 100644 --- a/app/src/main/java/app/fedilab/android/helper/PushNotifications.java +++ b/app/src/main/java/app/fedilab/android/helper/PushNotifications.java @@ -15,9 +15,6 @@ package app.fedilab.android.helper; * see . */ -import static app.fedilab.android.helper.ECDH.kp_private; -import static app.fedilab.android.helper.ECDH.kp_public; - import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; @@ -26,10 +23,8 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; -import java.util.Random; import java.util.concurrent.TimeUnit; -import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.client.endpoints.MastodonNotificationsService; import app.fedilab.android.client.entities.api.PushSubscription; @@ -50,24 +45,18 @@ public class PushNotifications { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); - String strPub = prefs.getString(kp_public + slug, ""); - String strPriv = prefs.getString(kp_private + slug, ""); - ECDH ecdh = null; + ECDHFedilab ecdh = null; try { - ecdh = ECDH.getInstance(slug); + ecdh = new ECDHFedilab(context, slug); } catch (Exception e) { e.printStackTrace(); } if (ecdh == null) { return; } - if (strPub.trim().isEmpty() || strPriv.trim().isEmpty()) { - ecdh.newPair(context); - } - String pubKey = ecdh.getPublicKey(context); - byte[] randBytes = new byte[16]; - new Random().nextBytes(randBytes); - String auth = ECDH.base64Encode(randBytes); + + String pubKey = ecdh.getPublicKey(); + String auth = ecdh.getAuthKey(); boolean notif_follow = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FOLLOW), true); @@ -75,8 +64,10 @@ public class PushNotifications { boolean notif_share = prefs.getBoolean(context.getString(R.string.SET_NOTIF_SHARE), true); boolean notif_poll = prefs.getBoolean(context.getString(R.string.SET_NOTIF_POLL), true); boolean notif_fav = prefs.getBoolean(context.getString(R.string.SET_NOTIF_FAVOURITE), true); - MastodonNotificationsService mastodonNotificationsService = init(context, BaseMainActivity.currentInstance); - ECDH finalEcdh = ecdh; + boolean notif_status = prefs.getBoolean(context.getString(R.string.SET_NOTIF_STATUS), true); + boolean notif_updates = prefs.getBoolean(context.getString(R.string.SET_NOTIF_UPDATE), true); + boolean notif_signup = prefs.getBoolean(context.getString(R.string.SET_NOTIF_ADMIN_SIGNUP), true); + boolean notif_report = prefs.getBoolean(context.getString(R.string.SET_NOTIF_ADMIN_REPORT), true); new Thread(() -> { String[] slugArray = slug.split("@"); BaseAccount accountDb = null; @@ -85,9 +76,11 @@ public class PushNotifications { } catch (DBException e) { e.printStackTrace(); } + if (accountDb == null) { return; } + MastodonNotificationsService mastodonNotificationsService = init(context, accountDb.instance); PushSubscription pushSubscription; Call pushSubscriptionCall = mastodonNotificationsService.pushSubscription( accountDb.token, @@ -98,14 +91,23 @@ public class PushNotifications { notif_fav, notif_share, notif_mention, - notif_poll); + notif_poll, + notif_status, + notif_updates, + notif_signup, + notif_report); if (pushSubscriptionCall != null) { try { Response pushSubscriptionResponse = pushSubscriptionCall.execute(); if (pushSubscriptionResponse.isSuccessful()) { pushSubscription = pushSubscriptionResponse.body(); if (pushSubscription != null) { - finalEcdh.saveServerKey(context, pushSubscription.server_key); + pushSubscription.server_key = pushSubscription.server_key.replace('/', '_'); + pushSubscription.server_key = pushSubscription.server_key.replace('+', '-'); + SharedPreferences.Editor prefsEditor = PreferenceManager + .getDefaultSharedPreferences(context).edit(); + prefsEditor.putString("server_key" + slug, pushSubscription.server_key); + prefsEditor.apply(); } } } catch (Exception e) { @@ -122,6 +124,7 @@ public class PushNotifications { } + public static String getToken(Context context, String slug) { return context.getSharedPreferences("unifiedpush.connector", Context.MODE_PRIVATE).getString( slug + "/unifiedpush.connector", null); diff --git a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java index 26cb125d9..f53c1172a 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -41,6 +41,7 @@ import android.text.style.URLSpan; import android.util.Patterns; import android.view.LayoutInflater; import android.view.View; +import android.webkit.URLUtil; import android.widget.Toast; import androidx.annotation.NonNull; @@ -52,12 +53,6 @@ import androidx.lifecycle.ViewModelStoreOwner; import androidx.preference.PreferenceManager; import com.bumptech.glide.Glide; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; import java.io.IOException; import java.lang.ref.WeakReference; @@ -94,20 +89,15 @@ import es.dmoral.toasty.Toasty; public class SpannableHelper { public static final String CLICKABLE_SPAN = "CLICKABLE_SPAN"; - - public static Spannable convert(Context context, String text, - Status status, Account account, Announcement announcement, - boolean convertHtml, boolean forceMentions, WeakReference viewWeakReference) { - return convert(context, text, status, account, announcement, convertHtml, forceMentions, viewWeakReference, null); - } - - private static int linkColor; + public static Spannable convert(Context context, String text, + Status status, Account account, Announcement announcement, WeakReference viewWeakReference) { + return convert(context, text, status, account, announcement, viewWeakReference, null); + } + public static Spannable convert(Context context, String text, Status status, Account account, Announcement announcement, - boolean convertHtml, - boolean forceMentions, WeakReference viewWeakReference, Status.Callback callback) { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -131,24 +121,126 @@ public class SpannableHelper { if (linkColor == 0) { linkColor = -1; } - SpannableString initialContent; - if (text == null) { - return null; + List mentions = new ArrayList<>(); + if (status != null && status.mentions != null) { + mentions.addAll(status.mentions); } - Document htmlContent = Jsoup.parse(text); - Elements mentionElements = htmlContent.select("a.mention"); - //We keep a reference to mentions - HashMap mentionsMap = new HashMap<>(); - if (mentionElements.size() > 0) { - for (int i = 0; i < mentionElements.size(); i++) { - Element mentionElement = mentionElements.get(i); - String href = mentionElement.attr("href"); - String mention = mentionElement.text(); - mentionsMap.put(mention, href); + text = text.replaceAll("((<\\s?p\\s?>|<\\s?br\\s?/?>)>(((?!(<\\s?br\\s?/?>|<\\s?/s?p\\s?>)).)*))", "$2
$3
"); + text = text.trim().replaceAll("\\s{3}", "   "); + text = text.trim().replaceAll("\\s{2}", "  "); + SpannableString initialContent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); + else + initialContent = new SpannableString(Html.fromHtml(text)); + + //Get all links + SpannableStringBuilder content = new SpannableStringBuilder(initialContent); + URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); + //Loop through links + for (URLSpan span : urls) { + String url = span.getURL(); + int start = content.getSpanStart(span); + int end = content.getSpanEnd(span); + if (start < 0 || end > content.length()) { + continue; } + content.removeSpan(span); + //Get the matching word associated to the URL + String word = content.subSequence(start, end).toString(); + if (word.startsWith("@") || word.startsWith("#")) { + content.setSpan(new LongClickableSpan() { + @Override + public void onLongClick(View textView) { + textView.setTag(CLICKABLE_SPAN); + if (word.startsWith("#") && BaseMainActivity.filterFetched && MainActivity.mainFilters != null) { + String tag = word.trim(); + if (!tag.startsWith("#")) { + tag = "#" + tag; + } + Filter fedilabFilter = null; + for (Filter filter : MainActivity.mainFilters) { + if (filter.title.equals(Helper.FEDILAB_MUTED_HASHTAGS)) { + fedilabFilter = filter; + break; + } + } + //Filter for Fedilab doesn't exist we have to create it + if (fedilabFilter == null) { + Filter.FilterParams filterParams = new Filter.FilterParams(); + filterParams.title = Helper.FEDILAB_MUTED_HASHTAGS; + filterParams.filter_action = "hide"; + filterParams.context = new ArrayList<>(); + filterParams.context.add("home"); + filterParams.context.add("public"); + filterParams.context.add("thread"); + filterParams.context.add("account"); + String finalTag = tag; + FiltersVM filtersVM = new ViewModelProvider((ViewModelStoreOwner) context).get(FiltersVM.class); + filtersVM.addFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filterParams) + .observe((LifecycleOwner) context, filter -> { + if (filter != null) { + MainActivity.mainFilters.add(filter); + addTagToFilter(context, finalTag, status, filter); + } + }); + } else { + addTagToFilter(context, tag, status, fedilabFilter); + } + } + } + + @Override + public void onClick(@NonNull View textView) { + textView.setTag(CLICKABLE_SPAN); + Intent intent; + Bundle b; + if (word.startsWith("#")) { + intent = new Intent(context, HashTagActivity.class); + b = new Bundle(); + b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim()); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else if (word.startsWith("@")) { + intent = new Intent(context, ProfileActivity.class); + b = new Bundle(); + Mention targetedMention = null; + + for (Mention mention : mentions) { + if (word.compareToIgnoreCase("@" + mention.username) == 0) { + targetedMention = mention; + break; + } + } + if (targetedMention != null) { + b.putString(Helper.ARG_USER_ID, targetedMention.id); + } else { + b.putString(Helper.ARG_MENTION, word); + } + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + if (linkColor != -1) { + ds.setColor(linkColor); + } + } + + }, start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } else { + makeLinks(context, content, url, start, end); + } + replaceQuoteSpans(context, content); + emails(context, content); } - text = text.replaceAll("((<\\s?p\\s?>|<\\s?br\\s?\\/?>)>(((?!([<])).)*))", "$2
$3
"); Pattern imgPattern = Pattern.compile("]*src=\"([^\"]+)\"[^>]*>"); Matcher matcherImg = imgPattern.matcher(text); HashMap imagesToReplace = new LinkedHashMap<>(); @@ -160,57 +252,15 @@ public class SpannableHelper { text = text.replaceAll(Pattern.quote(matcherImg.group()), replacement); } - SpannableStringBuilder content; View view = viewWeakReference.get(); - List mentionList = null; List emojiList = null; if (status != null) { - mentionList = status.mentions; emojiList = status.emojis; } else if (account != null) { emojiList = account.emojis; } else if (announcement != null) { emojiList = announcement.emojis; } - //UrlDetails will contain links having a text different from the url - HashMap urlDetails = new HashMap<>(); - if (convertHtml) { - Matcher matcherALink = Helper.aLink.matcher(text); - - //We stock details - while (matcherALink.find()) { - String urlText = matcherALink.group(3); - String url = matcherALink.group(2); - if (urlText != null && urlText.startsWith(">")) { - urlText = urlText.substring(1); - } - if (url != null && urlText != null && !url.equalsIgnoreCase(urlText) && !urlText.contains("= Build.VERSION_CODES.N) - initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)); - else - initialContent = new SpannableString(Html.fromHtml(text)); - - content = new SpannableStringBuilder(initialContent); - URLSpan[] urls = content.getSpans(0, (content.length() - 1), URLSpan.class); - for (URLSpan span : urls) { - content.removeSpan(span); - } - //Make tags, mentions, groups - interaction(context, content, status, mentionList, forceMentions, mentionsMap); - //Make all links - linkify(context, content, urlDetails); - linkifyURL(context, content, urlDetails); - emails(context, content); - gemini(context, content); - replaceQuoteSpans(context, content); - } else { - content = new SpannableStringBuilder(text); - } boolean animate = !sharedpreferences.getBoolean(context.getString(R.string.SET_DISABLE_ANIMATED_EMOJI), false); CustomEmoji customEmoji = new CustomEmoji(new WeakReference<>(view)); content = customEmoji.makeEmoji(content, emojiList, animate, callback); @@ -234,542 +284,251 @@ public class SpannableHelper { return trimSpannable(new SpannableStringBuilder(content)); } - private static void linkify(Context context, SpannableStringBuilder content, HashMap urlDetails) { - //--- URLs ---- - Matcher matcherLink = Patterns.WEB_URL.matcher(content); - int offSetTruncate = 0; + private static void makeLinks(Context context, SpannableStringBuilder content, String url, int start, int end) { + String newUrl = url; + boolean validUrl = URLUtil.isValidUrl(url) && url.length() == (end - start); + if (validUrl) { + newUrl = Helper.transformURL(context, url); + } - while (matcherLink.find()) { - int matchStart = matcherLink.start() - offSetTruncate; - int matchEnd = matchStart + matcherLink.group().length(); - if (matchEnd > content.toString().length()) { - matchEnd = content.toString().length(); - } + //If URL has been transformed + if (validUrl && newUrl.compareTo(url) != 0) { + content.replace(start, end, newUrl); + end = start + newUrl.length(); + url = newUrl; + } + if (url.length() > 30 && (validUrl || url.startsWith("gimini://"))) { + newUrl = url.substring(0, 30); + newUrl += "…"; + content.replace(start, end, newUrl); + } + int matchEnd = validUrl ? start + newUrl.length() : end; - if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) { - continue; - } + String finalUrl = url; + if (content.length() < matchEnd) { + matchEnd = content.length(); + } + content.setSpan(new LongClickableSpan() { + @Override + public void onLongClick(View view) { + Context mContext = view.getContext(); + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext); + PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context)); + dialogBuilder.setView(popupLinksBinding.getRoot()); + AlertDialog alertDialog = dialogBuilder.create(); + alertDialog.show(); + popupLinksBinding.displayFullLink.setOnClickListener(v -> { + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setMessage(finalUrl); + builder.setTitle(context.getString(R.string.display_full_link)); + builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) + .show(); + alertDialog.dismiss(); + }); + popupLinksBinding.shareLink.setOnClickListener(v -> { + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); + sendIntent.putExtra(Intent.EXTRA_TEXT, finalUrl); + sendIntent.setType("text/plain"); + sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with)); + intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intentChooser); + alertDialog.dismiss(); + }); + popupLinksBinding.openOtherApp.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(finalUrl)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(intent); + } catch (Exception e) { + Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); + } + alertDialog.dismiss(); + }); - final String url = content.toString().substring(matchStart, matchEnd); - if (urlDetails.containsKey(url)) { - continue; - } + popupLinksBinding.copyLink.setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, finalUrl); + if (clipboard != null) { + clipboard.setPrimaryClip(clip); + Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); + } + alertDialog.dismiss(); + }); - ClickableSpan[] clickableSpans = content.getSpans(matchStart, matchEnd, ClickableSpan.class); - if (clickableSpans != null) { - for (ClickableSpan clickableSpan : clickableSpans) { - content.removeSpan(clickableSpan); - } - } - content.removeSpan(clickableSpans); - String newURL = Helper.transformURL(context, url); - //If URL has been transformed - if (newURL.compareTo(url) != 0) { - content.replace(matchStart, matchEnd, newURL); - offSetTruncate -= (newURL.length() - url.length()); - matchEnd = matchStart + newURL.length(); - } + popupLinksBinding.checkRedirect.setOnClickListener(v -> { + try { - //Truncate URL if needed - //TODO: add an option to disable truncated URLs - String urlText = newURL; - if (newURL.length() > 30 && !urlDetails.containsKey(urlText) && !urlText.startsWith("gemini")) { - urlText = urlText.substring(0, 30); - urlText += "…"; - content.replace(matchStart, matchEnd, urlText); - matchEnd = matchStart + 31; - offSetTruncate += (newURL.length() - urlText.length()); - } - - - if (matchEnd <= content.length() && matchEnd >= matchStart) { - content.setSpan(new LongClickableSpan() { - @Override - public void onLongClick(View view) { - Context mContext = view.getContext(); - MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(mContext); - PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context)); - materialAlertDialogBuilder.setView(popupLinksBinding.getRoot()); - AlertDialog alertDialog = materialAlertDialogBuilder.create(); - alertDialog.show(); - String finalURl = newURL; - String uniqueUrl = newURL.endsWith("…") ? newURL : newURL + "…"; - if (urlDetails.containsValue(uniqueUrl)) { - finalURl = Helper.getKeyByValue(urlDetails, uniqueUrl); - } - if (finalURl == null) { - return; - } - if (finalURl.startsWith("http://")) { - finalURl = finalURl.replace("http://", "https://"); - } - String finalURl1 = finalURl; - popupLinksBinding.displayFullLink.setOnClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setMessage(finalURl1); - builder.setTitle(context.getString(R.string.display_full_link)); - builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) - .show(); - alertDialog.dismiss(); - }); - popupLinksBinding.shareLink.setOnClickListener(v -> { - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); - sendIntent.putExtra(Intent.EXTRA_TEXT, finalURl1); - sendIntent.setType("text/plain"); - sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with)); - intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intentChooser); - alertDialog.dismiss(); - }); - - popupLinksBinding.openOtherApp.setOnClickListener(v -> { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(finalURl1)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + URL finalUrlCheck = new URL(finalUrl); + new Thread(() -> { try { - context.startActivity(intent); - } catch (Exception e) { - Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); - } - alertDialog.dismiss(); - }); - - popupLinksBinding.copyLink.setOnClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, finalURl1); - if (clipboard != null) { - clipboard.setPrimaryClip(clip); - Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); - } - alertDialog.dismiss(); - }); - - popupLinksBinding.checkRedirect.setOnClickListener(v -> { - try { - - URL finalUrlCheck = new URL(finalURl1); - new Thread(() -> { - try { - String redirect = null; - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection(); - httpsURLConnection.setConnectTimeout(10 * 1000); - httpsURLConnection.setRequestProperty("http.keepAlive", "false"); - // httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT); - httpsURLConnection.setRequestMethod("HEAD"); - httpsURLConnection.setInstanceFollowRedirects(false); - if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) { - Map> map = httpsURLConnection.getHeaderFields(); - for (Map.Entry> entry : map.entrySet()) { - if (entry.toString().toLowerCase().startsWith("location")) { - Matcher matcher = Patterns.WEB_URL.matcher(entry.toString()); - if (matcher.find()) { - redirect = matcher.group(1); - } - } + String redirect = null; + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection(); + httpsURLConnection.setConnectTimeout(10 * 1000); + httpsURLConnection.setRequestProperty("http.keepAlive", "false"); + //httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT); + httpsURLConnection.setRequestMethod("HEAD"); + httpsURLConnection.setInstanceFollowRedirects(false); + if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) { + Map> map = httpsURLConnection.getHeaderFields(); + for (Map.Entry> entry : map.entrySet()) { + if (entry.toString().toLowerCase().startsWith("location")) { + Matcher matcher = Patterns.WEB_URL.matcher(entry.toString()); + if (matcher.find()) { + redirect = matcher.group(1); } } - httpsURLConnection.getInputStream().close(); - if (redirect != null && redirect.compareTo(finalURl1) != 0) { - URL redirectURL = new URL(redirect); - String host = redirectURL.getHost(); - String protocol = redirectURL.getProtocol(); - if (protocol == null || host == null) { - redirect = null; - } - } - Handler mainHandler = new Handler(context.getMainLooper()); - String finalRedirect = redirect; - Runnable myRunnable = () -> { - AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext()); - if (finalRedirect != null) { - builder1.setMessage(context.getString(R.string.redirect_detected, finalURl1, finalRedirect)); - builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> { - ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect); - if (clipboard1 != null) { - clipboard1.setPrimaryClip(clip1); - Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); - } - dialog.dismiss(); - }); - builder1.setNeutralButton(R.string.share_link, (dialog, which) -> { - Intent sendIntent1 = new Intent(Intent.ACTION_SEND); - sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); - sendIntent1.putExtra(Intent.EXTRA_TEXT, finalURl1); - sendIntent1.setType("text/plain"); - context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with))); - dialog.dismiss(); - }); - } else { - builder1.setMessage(R.string.no_redirect); - } - builder1.setTitle(context.getString(R.string.check_redirect)); - builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) - .show(); - - }; - mainHandler.post(myRunnable); - } catch (IOException e) { - e.printStackTrace(); } + } + httpsURLConnection.getInputStream().close(); + if (redirect != null && redirect.compareTo(finalUrl) != 0) { + URL redirectURL = new URL(redirect); + String host = redirectURL.getHost(); + String protocol = redirectURL.getProtocol(); + if (protocol == null || host == null) { + redirect = null; + } + } + Handler mainHandler = new Handler(context.getMainLooper()); + String finalRedirect = redirect; + Runnable myRunnable = () -> { + AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext()); + if (finalRedirect != null) { + builder1.setMessage(context.getString(R.string.redirect_detected, finalUrl, finalRedirect)); + builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> { + ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect); + if (clipboard1 != null) { + clipboard1.setPrimaryClip(clip1); + Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); + } + dialog.dismiss(); + }); + builder1.setNeutralButton(R.string.share_link, (dialog, which) -> { + Intent sendIntent1 = new Intent(Intent.ACTION_SEND); + sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); + sendIntent1.putExtra(Intent.EXTRA_TEXT, finalUrl); + sendIntent1.setType("text/plain"); + context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with))); + dialog.dismiss(); + }); + } else { + builder1.setMessage(R.string.no_redirect); + } + builder1.setTitle(context.getString(R.string.check_redirect)); + builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) + .show(); - }).start(); - } catch (MalformedURLException e) { + }; + mainHandler.post(myRunnable); + } catch (IOException e) { e.printStackTrace(); } - alertDialog.dismiss(); - }); - + }).start(); + } catch (MalformedURLException e) { + e.printStackTrace(); } - @Override - public void onClick(@NonNull View textView) { - String finalURl = newURL; - String finalURl2 = url; - String uniqueNewURL = newURL.endsWith("…") ? newURL : newURL + "…"; - if (urlDetails.containsValue(uniqueNewURL)) { - finalURl = Helper.getKeyByValue(urlDetails, uniqueNewURL); - } - String uniqueUrl = url.endsWith("…") ? url : url + "…"; - if (urlDetails.containsValue(uniqueUrl)) { - finalURl2 = Helper.getKeyByValue(urlDetails, uniqueUrl); - } - textView.setTag(CLICKABLE_SPAN); - Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$"); - Matcher matcherLink = null; - if (finalURl2 != null) { - matcherLink = link.matcher(finalURl2); - } - if (finalURl2 != null && matcherLink.find() && !finalURl2.contains("medium.com")) { - if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot - CrossActionHelper.fetchRemoteStatus(context, currentAccount, finalURl2, new CrossActionHelper.Callback() { - @Override - public void federatedStatus(Status status) { - Intent intent = new Intent(context, ContextActivity.class); - intent.putExtra(Helper.ARG_STATUS, status); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } + alertDialog.dismiss(); + }); - @Override - public void federatedAccount(Account account) { - } - }); - } else {//It's an account - CrossActionHelper.fetchRemoteAccount(context, currentAccount, matcherLink.group(2) + "@" + matcherLink.group(1), new CrossActionHelper.Callback() { - @Override - public void federatedStatus(Status status) { - } - - @Override - public void federatedAccount(Account account) { - Intent intent = new Intent(context, ProfileActivity.class); - Bundle b = new Bundle(); - b.putSerializable(Helper.ARG_ACCOUNT, account); - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - }); - } - } else { - Helper.openBrowser(context, finalURl); - } - - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - if (linkColor != -1) { - ds.setColor(linkColor); - } - } - - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } - } - } - private static void linkifyURL(Context context, SpannableStringBuilder content, HashMap urlDetails) { + @Override + public void onClick(@NonNull View textView) { - for (Map.Entry entry : urlDetails.entrySet()) { - String value = entry.getValue(); - if (value.startsWith("@") || value.startsWith("#")) { - continue; - } - SpannableString contentUrl; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - contentUrl = new SpannableString(Html.fromHtml(value, Html.FROM_HTML_MODE_LEGACY)); - else - contentUrl = new SpannableString(Html.fromHtml(value)); - if (contentUrl.toString().trim().isEmpty()) { - continue; - } - Pattern word = Pattern.compile(Pattern.quote(contentUrl.toString())); - Matcher matcherLink = word.matcher(content); - while (matcherLink.find()) { - String url = entry.getKey(); - int matchStart = matcherLink.start(); - int matchEnd = matchStart + matcherLink.group().length(); - if (matchEnd > content.toString().length()) { - matchEnd = content.toString().length(); - } - - if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) { - continue; - } - - ClickableSpan[] clickableSpans = content.getSpans(matchStart, matchEnd, ClickableSpan.class); - if (clickableSpans != null) { - for (ClickableSpan clickableSpan : clickableSpans) { - content.removeSpan(clickableSpan); - } - } - content.removeSpan(clickableSpans); - - if (matchEnd <= content.length()) { - content.setSpan(new LongClickableSpan() { - @Override - public void onLongClick(View view) { - Context mContext = view.getContext(); - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext); - PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context)); - dialogBuilder.setView(popupLinksBinding.getRoot()); - AlertDialog alertDialog = dialogBuilder.create(); - alertDialog.show(); - String finalURl = url; - if (urlDetails.containsValue(url)) { - finalURl = Helper.getKeyByValue(urlDetails, url); - } - String finalURl1 = finalURl; - popupLinksBinding.displayFullLink.setOnClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setMessage(finalURl1); - builder.setTitle(context.getString(R.string.display_full_link)); - builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) - .show(); - alertDialog.dismiss(); - }); - popupLinksBinding.shareLink.setOnClickListener(v -> { - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); - sendIntent.putExtra(Intent.EXTRA_TEXT, finalURl1); - sendIntent.setType("text/plain"); - sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Intent intentChooser = Intent.createChooser(sendIntent, context.getString(R.string.share_with)); - intentChooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intentChooser); - alertDialog.dismiss(); - }); - - popupLinksBinding.openOtherApp.setOnClickListener(v -> { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(finalURl1)); + textView.setTag(CLICKABLE_SPAN); + Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$"); + Matcher matcherLink = link.matcher(finalUrl); + Pattern linkLong = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w_.-]+@[a-zA-Z0-9][a-zA-Z0-9.-]{1,61}[a-zA-Z0-9](?:\\.[a-zA-Z]{2,})+)(/[0-9]+)?$"); + Matcher matcherLinkLong = linkLong.matcher(finalUrl); + if (matcherLink.find() && !finalUrl.contains("medium.com")) { + if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot + CrossActionHelper.fetchRemoteStatus(context, currentAccount, finalUrl, new CrossActionHelper.Callback() { + @Override + public void federatedStatus(Status status) { + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, status); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - context.startActivity(intent); - } catch (Exception e) { - Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); - } - alertDialog.dismiss(); - }); - - popupLinksBinding.copyLink.setOnClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(Helper.CLIP_BOARD, finalURl1); - if (clipboard != null) { - clipboard.setPrimaryClip(clip); - Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); - } - alertDialog.dismiss(); - }); - - popupLinksBinding.checkRedirect.setOnClickListener(v -> { - try { - - URL finalUrlCheck = new URL(finalURl1); - new Thread(() -> { - try { - String redirect = null; - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) finalUrlCheck.openConnection(); - httpsURLConnection.setConnectTimeout(10 * 1000); - httpsURLConnection.setRequestProperty("http.keepAlive", "false"); - //httpsURLConnection.setRequestProperty("User-Agent", USER_AGENT); - httpsURLConnection.setRequestMethod("HEAD"); - httpsURLConnection.setInstanceFollowRedirects(false); - if (httpsURLConnection.getResponseCode() == 301 || httpsURLConnection.getResponseCode() == 302) { - Map> map = httpsURLConnection.getHeaderFields(); - for (Map.Entry> entry : map.entrySet()) { - if (entry.toString().toLowerCase().startsWith("location")) { - Matcher matcher = Patterns.WEB_URL.matcher(entry.toString()); - if (matcher.find()) { - redirect = matcher.group(1); - } - } - } - } - httpsURLConnection.getInputStream().close(); - if (redirect != null && finalURl1 != null && redirect.compareTo(finalURl1) != 0) { - URL redirectURL = new URL(redirect); - String host = redirectURL.getHost(); - String protocol = redirectURL.getProtocol(); - if (protocol == null || host == null) { - redirect = null; - } - } - Handler mainHandler = new Handler(context.getMainLooper()); - String finalRedirect = redirect; - Runnable myRunnable = () -> { - AlertDialog.Builder builder1 = new AlertDialog.Builder(view.getContext()); - if (finalRedirect != null) { - builder1.setMessage(context.getString(R.string.redirect_detected, finalURl1, finalRedirect)); - builder1.setNegativeButton(R.string.copy_link, (dialog, which) -> { - ClipboardManager clipboard1 = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip1 = ClipData.newPlainText(Helper.CLIP_BOARD, finalRedirect); - if (clipboard1 != null) { - clipboard1.setPrimaryClip(clip1); - Toasty.info(context, context.getString(R.string.clipboard_url), Toast.LENGTH_LONG).show(); - } - dialog.dismiss(); - }); - builder1.setNeutralButton(R.string.share_link, (dialog, which) -> { - Intent sendIntent1 = new Intent(Intent.ACTION_SEND); - sendIntent1.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.shared_via)); - sendIntent1.putExtra(Intent.EXTRA_TEXT, finalURl1); - sendIntent1.setType("text/plain"); - context.startActivity(Intent.createChooser(sendIntent1, context.getString(R.string.share_with))); - dialog.dismiss(); - }); - } else { - builder1.setMessage(R.string.no_redirect); - } - builder1.setTitle(context.getString(R.string.check_redirect)); - builder1.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) - .show(); - - }; - mainHandler.post(myRunnable); - } catch (IOException e) { - e.printStackTrace(); - } - - }).start(); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - - alertDialog.dismiss(); - }); - - } - - @Override - public void onClick(@NonNull View textView) { - String finalURl = url; - if (urlDetails.containsValue(url)) { - finalURl = Helper.getKeyByValue(urlDetails, url); + context.startActivity(intent); } - textView.setTag(CLICKABLE_SPAN); - Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$"); - Matcher matcherLink = null; - if (finalURl != null) { - matcherLink = link.matcher(finalURl); + @Override + public void federatedAccount(Account account) { } - if (finalURl != null && matcherLink.find() && !finalURl.contains("medium.com")) { - if (matcherLink.group(3) != null && Objects.requireNonNull(matcherLink.group(3)).length() > 0) { //It's a toot - CrossActionHelper.fetchRemoteStatus(context, currentAccount, finalURl, new CrossActionHelper.Callback() { - @Override - public void federatedStatus(Status status) { - Intent intent = new Intent(context, ContextActivity.class); - intent.putExtra(Helper.ARG_STATUS, status); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - @Override - public void federatedAccount(Account account) { - } - }); - } else {//It's an account - CrossActionHelper.fetchRemoteAccount(context, currentAccount, matcherLink.group(2) + "@" + matcherLink.group(1), new CrossActionHelper.Callback() { - @Override - public void federatedStatus(Status status) { - } - - @Override - public void federatedAccount(Account account) { - Intent intent = new Intent(context, ProfileActivity.class); - Bundle b = new Bundle(); - b.putSerializable(Helper.ARG_ACCOUNT, account); - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - }); - } - } else { - Helper.openBrowser(context, finalURl); + }); + } else {//It's an account + CrossActionHelper.fetchRemoteAccount(context, currentAccount, matcherLink.group(2) + "@" + matcherLink.group(1), new CrossActionHelper.Callback() { + @Override + public void federatedStatus(Status status) { } - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - if (linkColor != -1) { - ds.setColor(linkColor); + @Override + public void federatedAccount(Account account) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); } - } - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + }); + } + } else if (matcherLinkLong.find() && !finalUrl.contains("medium.com")) { + if (matcherLinkLong.group(3) != null && Objects.requireNonNull(matcherLinkLong.group(3)).length() > 0) { //It's a toot + CrossActionHelper.fetchRemoteStatus(context, currentAccount, finalUrl, new CrossActionHelper.Callback() { + @Override + public void federatedStatus(Status status) { + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, status); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + @Override + public void federatedAccount(Account account) { + } + }); + } else if (matcherLinkLong.group(2) != null) {//It's an account + CrossActionHelper.fetchRemoteAccount(context, currentAccount, matcherLinkLong.group(2), new CrossActionHelper.Callback() { + @Override + public void federatedStatus(Status status) { + } + + @Override + public void federatedAccount(Account account) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, account); + intent.putExtras(b); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + } + } else { + Helper.openBrowser(context, finalUrl); } } - } - } - private static void gemini(Context context, Spannable content) { - // --- For all patterns defined in Helper class --- - Pattern pattern = Helper.geminiPattern; - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - int matchStart = matcher.start(); - int matchEnd = matcher.end(); - String geminiLink = content.toString().substring(matchStart, matchEnd); - if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { - ClickableSpan[] clickableSpans = content.getSpans(matchStart, matchEnd, ClickableSpan.class); - if (clickableSpans != null) { - for (ClickableSpan clickableSpan : clickableSpans) { - content.removeSpan(clickableSpan); - } + @Override + public void updateDrawState(@NonNull TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + if (linkColor != -1) { + ds.setColor(linkColor); } - content.removeSpan(clickableSpans); - content.setSpan(new ClickableSpan() { - @Override - public void onClick(@NonNull View textView) { - Helper.openBrowser(context, geminiLink); - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - if (linkColor != -1) { - ds.setColor(linkColor); - } - } - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } - } + }, start, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } + private static void emails(Context context, Spannable content) { // --- For all patterns defined in Helper class --- Pattern pattern = Helper.emailPattern; @@ -809,167 +568,6 @@ public class SpannableHelper { } } - private static void interaction(Context context, Spannable content, Status status, List mentions, boolean forceMentions, HashMap mentionsMap) { - // --- For all patterns defined in Helper class --- - for (Map.Entry entry : Helper.patternHashMap.entrySet()) { - Helper.PatternType patternType = entry.getKey(); - Pattern pattern = entry.getValue(); - Matcher matcher = pattern.matcher(content); - if (pattern == Helper.mentionPattern && mentions == null && !forceMentions) { - continue; - } else if (pattern == Helper.mentionLongPattern && mentions == null && !forceMentions) { - continue; - } - - while (matcher.find()) { - int matchStart = matcher.start(); - int matchEnd = matcher.end(); - String word = content.toString().substring(matchStart, matchEnd); - if (matchStart >= 0 && matchEnd <= content.toString().length() && matchEnd >= matchStart) { - URLSpan[] span = content.getSpans(matchStart, matchEnd, URLSpan.class); - content.removeSpan(span); - content.setSpan(new LongClickableSpan() { - @Override - public void onLongClick(View textView) { - textView.setTag(CLICKABLE_SPAN); - if (patternType == Helper.PatternType.TAG && BaseMainActivity.filterFetched && MainActivity.mainFilters != null) { - String tag = word.trim(); - if (!tag.startsWith("#")) { - tag = "#" + tag; - } - Filter fedilabFilter = null; - for (Filter filter : MainActivity.mainFilters) { - if (filter.title.equals(Helper.FEDILAB_MUTED_HASHTAGS)) { - fedilabFilter = filter; - break; - } - } - //Filter for Fedilab doesn't exist we have to create it - if (fedilabFilter == null) { - Filter.FilterParams filterParams = new Filter.FilterParams(); - filterParams.title = Helper.FEDILAB_MUTED_HASHTAGS; - filterParams.filter_action = "hide"; - filterParams.context = new ArrayList<>(); - filterParams.context.add("home"); - filterParams.context.add("public"); - filterParams.context.add("thread"); - filterParams.context.add("account"); - String finalTag = tag; - FiltersVM filtersVM = new ViewModelProvider((ViewModelStoreOwner) context).get(FiltersVM.class); - filtersVM.addFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filterParams) - .observe((LifecycleOwner) context, filter -> { - if (filter != null) { - MainActivity.mainFilters.add(filter); - addTagToFilter(context, finalTag, status, filter); - } - }); - } else { - addTagToFilter(context, tag, status, fedilabFilter); - } - } - } - - @Override - public void onClick(@NonNull View textView) { - textView.setTag(CLICKABLE_SPAN); - switch (patternType) { - case TAG: - Intent intent = new Intent(context, HashTagActivity.class); - Bundle b = new Bundle(); - b.putString(Helper.ARG_SEARCH_KEYWORD, word.trim()); - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - case GROUP: - break; - case MENTION: - intent = new Intent(context, ProfileActivity.class); - b = new Bundle(); - Mention targetedMention = null; - String acct = null; - HashMap countUsername = new HashMap<>(); - //Mentions is retrieved with associated Mentions array - if (mentions != null) { - for (Mention mention : mentions) { - Integer count = countUsername.get(mention.username); - if (count == null) { - count = 0; - } - if (countUsername.containsKey(mention.username)) { - countUsername.put(mention.username, count + 1); - } else { - countUsername.put(mention.username, 1); - } - } - for (Mention mention : mentions) { - Integer count = countUsername.get(mention.username); - if (count == null) { - count = 0; - } - if (word.trim().compareToIgnoreCase("@" + mention.username) == 0 && count == 1) { - targetedMention = mention; - break; - } - } - } else if (mentionsMap.containsKey(word.trim())) {//Mentions will be find through its URL - URL url; - try { - url = new URL(mentionsMap.get(word.trim())); - acct = word.trim() + "@" + url.getHost(); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - } - if (targetedMention != null) { - b.putString(Helper.ARG_USER_ID, targetedMention.id); - } else { - b.putString(Helper.ARG_MENTION, acct != null ? acct : word.trim()); - } - - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - case MENTION_LONG: - intent = new Intent(context, ProfileActivity.class); - b = new Bundle(); - targetedMention = null; - if (mentions != null) { - for (Mention mention : mentions) { - if (word.trim().substring(1).compareToIgnoreCase("@" + mention.acct) == 0) { - targetedMention = mention; - break; - } - } - } - if (targetedMention != null) { - b.putString(Helper.ARG_USER_ID, targetedMention.id); - } else { - b.putString(Helper.ARG_MENTION, word.trim()); - } - intent.putExtras(b); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - break; - } - } - - @Override - public void updateDrawState(@NonNull TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - if (linkColor != -1) { - ds.setColor(linkColor); - } - } - - }, matchStart, matchEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - } - } - } - } - public static void addTagToFilter(Context context, String tag, Status status, Filter filter) { for (Filter.KeywordsAttributes keywords : filter.keywords) { if (keywords.keyword.equalsIgnoreCase(tag)) { @@ -1006,7 +604,6 @@ public class SpannableHelper { * * @param status {@link Status} - Status concerned by the spannable transformation * @param content String - text to convert, it can be content, spoiler, poll items, etc. - * @return Spannable string */ private static void convertOuich(@NonNull Status status, SpannableStringBuilder content) { @@ -1058,7 +655,6 @@ public class SpannableHelper { } - /** * Remove extra carriage returns at the bottom due to

tags in toots * diff --git a/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java b/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java index 15fd4b083..46223b1a1 100644 --- a/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/ThemeHelper.java @@ -70,7 +70,6 @@ public class ThemeHelper { } - /** * Animate two views, the current view will be hidden to left * @@ -230,7 +229,6 @@ public class ThemeHelper { } - /** * Allow to set colors for having description on media * diff --git a/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java b/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java index f0d5ea6ba..6433e0158 100644 --- a/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java +++ b/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java @@ -71,6 +71,8 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList private final ConstraintSet mConstraintSet = new ConstraintSet(); PhotoEditor mPhotoEditor; String path; + CropImageContractOptions cropImageContractOptions; + ActivityResultLauncher cropImageContractOptionsActivityResultLauncher; private PropertiesBSFragment mPropertiesBSFragment; private ShapeBSFragment mShapeBSFragment; private ShapeBuilder mShapeBuilder; @@ -79,8 +81,6 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList private Uri uri; private boolean exit; private ActivityEditImageBinding binding; - CropImageContractOptions cropImageContractOptions; - ActivityResultLauncher cropImageContractOptionsActivityResultLauncher; private static int exifToDegrees(int exifOrientation) { if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) { diff --git a/app/src/main/java/app/fedilab/android/jobs/ComposeWorker.java b/app/src/main/java/app/fedilab/android/jobs/ComposeWorker.java index 05ed092b9..3e9f97be6 100644 --- a/app/src/main/java/app/fedilab/android/jobs/ComposeWorker.java +++ b/app/src/main/java/app/fedilab/android/jobs/ComposeWorker.java @@ -218,6 +218,9 @@ public class ComposeWorker extends Worker { return; } String language = sharedPreferences.getString(context.getString(R.string.SET_COMPOSE_LANGUAGE) + dataPost.userId + dataPost.instance, null); + if (statuses.get(i).local_only) { + statuses.get(i).text += " \uD83D\uDC41"; + } if (dataPost.scheduledDate == null) { if (dataPost.statusEditId == null) { statusCall = mastodonStatusesService.createStatus(null, dataPost.token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in, @@ -267,7 +270,11 @@ public class ComposeWorker extends Worker { b.putBoolean(Helper.RECEIVE_COMPOSE_ERROR_MESSAGE, true); Intent intentBD = new Intent(Helper.INTENT_COMPOSE_ERROR_MESSAGE); b.putSerializable(Helper.ARG_STATUS_DRAFT, dataPost.statusDraft); - b.putSerializable(Helper.RECEIVE_ERROR_MESSAGE, statusResponse.errorBody().string()); + String err = statusResponse.errorBody().string(); + if (err.contains("{\"error\":\"")) { + err = err.replaceAll("\\{\"error\":\"(.*)\"\\}", "$1"); + } + b.putSerializable(Helper.RECEIVE_ERROR_MESSAGE, err); intentBD.putExtras(b); LocalBroadcastManager.getInstance(context).sendBroadcast(intentBD); return; diff --git a/app/src/main/java/app/fedilab/android/services/CustomReceiver.java b/app/src/main/java/app/fedilab/android/services/CustomReceiver.java index f87ded11c..a6eb6f77a 100644 --- a/app/src/main/java/app/fedilab/android/services/CustomReceiver.java +++ b/app/src/main/java/app/fedilab/android/services/CustomReceiver.java @@ -40,11 +40,12 @@ public class CustomReceiver extends MessagingReceiver { // Called when a new message is received. The message contains the full POST body of the push message new Thread(() -> { try { - /* ECDH ecdh = ECDH.getInstance(slug); - if (ecdh == null) { - return; - }*/ - //String decrypted = ecdh.uncryptMessage(context, String.valueOf(message)); + /*Notification notification = ECDHFedilab.decryptNotification(context, slug, message); + Log.v(Helper.TAG,"notification: " + notification); + if(notification != null) { + Log.v(Helper.TAG,"id: " + notification.id); + } + */ NotificationsHelper.task(context, slug); } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java index 3ccea89d7..9faa16bc1 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountAdapter.java @@ -56,8 +56,8 @@ import es.dmoral.toasty.Toasty; public class AccountAdapter extends RecyclerView.Adapter { private final List accountList; - private Context context; private final boolean home_mute; + private Context context; public AccountAdapter(List accountList, boolean home_mute) { this.accountList = accountList; diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java index 0a6b23bac..b09f4b08c 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java @@ -68,7 +68,6 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; @@ -150,6 +149,30 @@ public class ComposeAdapter extends RecyclerView.Adapter statusList; + private final int TYPE_NORMAL = 0; + private final BaseAccount account; + private final String visibility; + private final app.fedilab.android.client.entities.api.Account mentionedAccount; + private final String editMessageId; + public ManageDrafts manageDrafts; + public promptDraftListener promptDraftListener; + private int statusCount; + private Context context; + private AlertDialog alertDialogEmoji; + private List emojisList = new ArrayList<>(); + private boolean unlisted_changed = false; + + public ComposeAdapter(List statusList, int statusCount, BaseAccount account, app.fedilab.android.client.entities.api.Account mentionedAccount, String visibility, String editMessageId) { + this.statusList = statusList; + this.statusCount = statusCount; + this.account = account; + this.mentionedAccount = mentionedAccount; + this.visibility = visibility; + this.editMessageId = editMessageId; + + } public static int countMorseChar(String content) { int count_char = 0; @@ -180,81 +203,6 @@ public class ComposeAdapter extends RecyclerView.Adapter statusList; - private final int TYPE_NORMAL = 0; - private final BaseAccount account; - private final String visibility; - private final app.fedilab.android.client.entities.api.Account mentionedAccount; - private final String editMessageId; - public ManageDrafts manageDrafts; - private int statusCount; - private Context context; - private AlertDialog alertDialogEmoji; - private List emojisList = new ArrayList<>(); - public promptDraftListener promptDraftListener; - private boolean unlisted_changed = false; - public static int currentCursorPosition; - - public ComposeAdapter(List statusList, int statusCount, BaseAccount account, app.fedilab.android.client.entities.api.Account mentionedAccount, String visibility, String editMessageId) { - this.statusList = statusList; - this.statusCount = statusCount; - this.account = account; - this.mentionedAccount = mentionedAccount; - this.visibility = visibility; - this.editMessageId = editMessageId; - - } - - /** - * Add an attachment from ComposeActivity - * - * @param position int - position of the drawer that added a media - * @param uris List - uris of the media - */ - public void addAttachment(int position, List uris) { - if (position == -1) { - position = statusList.size() - 1; - } - // position = statusCount-1+position; - if (statusList.get(position).media_attachments == null) { - statusList.get(position).media_attachments = new ArrayList<>(); - } - if (promptDraftListener != null) { - promptDraftListener.promptDraft(); - } - int finalPosition = position; - Helper.createAttachmentFromUri(context, uris, attachments -> { - for (Attachment attachment : attachments) { - statusList.get(finalPosition).media_attachments.add(attachment); - } - notifyItemChanged(finalPosition); - }); - } - - - /** - * Add an attachment from ComposeActivity - * - * @param position int - position of the drawer that added a media - * @param attachment Attachment - media attachment - */ - public void addAttachment(int position, Attachment attachment) { - if (position == -1) { - position = statusList.size() - 1; - } - // position = statusCount-1+position; - if (statusList.get(position).media_attachments == null) { - statusList.get(position).media_attachments = new ArrayList<>(); - } - if (promptDraftListener != null) { - promptDraftListener.promptDraft(); - } - int finalPosition = position; - statusList.get(finalPosition).media_attachments.add(attachment); - notifyItemChanged(finalPosition); - - } - private static void updateCharacterCount(ComposeViewHolder composeViewHolder) { int charCount = MastodonHelper.countLength(composeViewHolder); composeViewHolder.binding.characterCount.setText(String.valueOf(charCount)); @@ -285,6 +233,55 @@ public class ComposeAdapter extends RecyclerView.Adapter - uris of the media + */ + public void addAttachment(int position, List uris) { + if (position == -1) { + position = statusList.size() - 1; + } + // position = statusCount-1+position; + if (statusList.get(position).media_attachments == null) { + statusList.get(position).media_attachments = new ArrayList<>(); + } + if (promptDraftListener != null) { + promptDraftListener.promptDraft(); + } + int finalPosition = position; + Helper.createAttachmentFromUri(context, uris, attachments -> { + for (Attachment attachment : attachments) { + statusList.get(finalPosition).media_attachments.add(attachment); + } + notifyItemChanged(finalPosition); + }); + } + + /** + * Add an attachment from ComposeActivity + * + * @param position int - position of the drawer that added a media + * @param attachment Attachment - media attachment + */ + public void addAttachment(int position, Attachment attachment) { + if (position == -1) { + position = statusList.size() - 1; + } + // position = statusCount-1+position; + if (statusList.get(position).media_attachments == null) { + statusList.get(position).media_attachments = new ArrayList<>(); + } + if (promptDraftListener != null) { + promptDraftListener.promptDraft(); + } + int finalPosition = position; + statusList.get(finalPosition).media_attachments.add(attachment); + notifyItemChanged(finalPosition); + + } + //Create text when mentioning a toot public void loadMentions(Status status) { //Get the first draft @@ -1025,19 +1022,18 @@ public class ComposeAdapter extends RecyclerView.Adapter { AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); builder.setTitle(context.getString(R.string.post_format)); @@ -1306,6 +1306,28 @@ public class ComposeAdapter extends RecyclerView.Adapter dialog.dismiss()); builder.create().show(); }); + holder.binding.buttonLocalOnly.setOnClickListener(v -> { + AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + builder.setTitle(context.getString(R.string.local_only)); + Resources res = context.getResources(); + boolean[] valArr = new boolean[]{false, true}; + String[] labelArr = res.getStringArray(R.array.set_local_only); + + int selection = 0; + int localOnly = sharedpreferences.getInt(context.getString(R.string.SET_COMPOSE_LOCAL_ONLY) + account.user_id + account.instance, 0); + if (statusDraft.local_only || localOnly == 1) { + selection = 1; + } + builder.setSingleChoiceItems(labelArr, selection, null); + builder.setPositiveButton(R.string.validate, (dialog, which) -> { + int selectedPosition = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + statusDraft.local_only = valArr[selectedPosition]; + notifyItemChanged(holder.getLayoutPosition()); + dialog.dismiss(); + }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.create().show(); + }); } else { holder.binding.buttonTextFormat.setVisibility(View.GONE); } @@ -1442,7 +1464,8 @@ public class ComposeAdapter extends RecyclerView.Adapter 0) { holder.binding.contentSpoiler.setVisibility(View.VISIBLE); } else { holder.binding.contentSpoiler.setVisibility(View.GONE); @@ -1463,7 +1486,7 @@ public class ComposeAdapter extends RecyclerView.Adapter { try { - displayEmojiPicker(holder); + displayEmojiPicker(holder, account.instance); } catch (DBException e) { e.printStackTrace(); } @@ -1850,7 +1873,7 @@ public class ComposeAdapter extends RecyclerView.Adapter 0) { GridView gridView = new GridView(context); - gridView.setAdapter(new EmojiAdapter(emojis.get(BaseMainActivity.currentInstance))); + gridView.setAdapter(new EmojiAdapter(emojis.get(instance))); gridView.setNumColumns(5); gridView.setOnItemClickListener((parent, view, position, id) -> { - holder.binding.content.getText().insert(holder.binding.content.getSelectionStart(), " :" + emojis.get(BaseMainActivity.currentInstance).get(position).shortcode + ": "); + holder.binding.content.getText().insert(holder.binding.content.getSelectionStart(), " :" + emojis.get(instance).get(position).shortcode + ": "); alertDialogEmoji.dismiss(); }); gridView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java index 5651ca478..2defcdb7c 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ConversationAdapter.java @@ -69,22 +69,6 @@ public class ConversationAdapter extends RecyclerView.Adapter { private final List fields; - private Context context; private final Account account; + private Context context; public FieldAdapter(List fields, Account account) { this.fields = fields; diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java index 82a5703d8..f58954cbe 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/InstanceRegAdapter.java @@ -40,8 +40,12 @@ import app.fedilab.android.helper.Helper; public class InstanceRegAdapter extends RecyclerView.Adapter { private final List joinMastodonInstanceList; - private Context context; public ActionClick actionClick; + private Context context; + + public InstanceRegAdapter(List joinMastodonInstanceList) { + this.joinMastodonInstanceList = joinMastodonInstanceList; + } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { @@ -67,11 +71,6 @@ public class InstanceRegAdapter extends RecyclerView.Adapter actionClick.trends(position)); } - - public InstanceRegAdapter(List joinMastodonInstanceList) { - this.joinMastodonInstanceList = joinMastodonInstanceList; - } - public int getCount() { return joinMastodonInstanceList.size(); } @@ -88,12 +87,6 @@ public class InstanceRegAdapter extends RecyclerView.Adapter(holderStatus.bindingNotification.status.displayName), title), TextView.BufferType.SPANNABLE); - holderStatus.bindingNotification.status.displayName.setText(title, TextView.BufferType.SPANNABLE); holderStatus.bindingNotification.status.username.setText(String.format("@%s", notification.account.acct)); holderStatus.bindingNotification.status.actionButtons.setVisibility(View.GONE); } diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java index 7665ca4e5..d23cb7a23 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ReorderTabAdapter.java @@ -141,6 +141,10 @@ public class ReorderTabAdapter extends RecyclerView.Adapter. */ + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.core.app.ActivityOptionsCompat; + +import com.bumptech.glide.Glide; +import com.smarteist.autoimageslider.SliderViewAdapter; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.activities.MediaActivity; +import app.fedilab.android.client.entities.api.Attachment; +import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.databinding.DrawerSliderBinding; +import app.fedilab.android.helper.Helper; + +public class SliderAdapter extends SliderViewAdapter { + + private final Status status; + private final List mSliderItems; + private Context context; + + public SliderAdapter(Status status) { + this.status = status; + this.mSliderItems = status.media_attachments; + } + + + public void addItem(Attachment sliderItem) { + this.mSliderItems.add(sliderItem); + notifyDataSetChanged(); + } + + @Override + public SliderAdapterVH onCreateViewHolder(ViewGroup parent) { + context = parent.getContext(); + DrawerSliderBinding itemBinding = DrawerSliderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new SliderAdapterVH(itemBinding); + } + + @Override + public void onBindViewHolder(SliderAdapterVH viewHolder, final int position) { + + Attachment sliderItem = mSliderItems.get(position); + + Glide.with(viewHolder.itemView) + .load(sliderItem.preview_url) + .centerCrop() + .into(viewHolder.binding.ivAutoImageSlider); + viewHolder.itemView.setOnClickListener(v -> { + Intent mediaIntent = new Intent(context, MediaActivity.class); + Bundle b = new Bundle(); + b.putInt(Helper.ARG_MEDIA_POSITION, position + 1); + b.putSerializable(Helper.ARG_MEDIA_ARRAY, new ArrayList<>(status.media_attachments)); + mediaIntent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, viewHolder.binding.ivAutoImageSlider, status.media_attachments.get(0).url); + // start the new activity + context.startActivity(mediaIntent, options.toBundle()); + }); + } + + @Override + public int getCount() { + return mSliderItems.size(); + } + + static class SliderAdapterVH extends ViewHolder { + DrawerSliderBinding binding; + + SliderAdapterVH(DrawerSliderBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java index 8d9661394..a3bda4bb7 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java @@ -71,6 +71,7 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.PopupMenu; import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.app.ActivityOptionsCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; @@ -87,6 +88,8 @@ import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; import com.github.stom79.mytransl.MyTransL; +import com.smarteist.autoimageslider.SliderAnimations; +import com.smarteist.autoimageslider.SliderView; import com.vanniktech.emoji.EmojiManager; import com.vanniktech.emoji.EmojiPopup; import com.vanniktech.emoji.one.EmojiOneProvider; @@ -128,6 +131,7 @@ import app.fedilab.android.databinding.DrawerStatusFilteredBinding; import app.fedilab.android.databinding.DrawerStatusFilteredHideBinding; import app.fedilab.android.databinding.DrawerStatusHiddenBinding; import app.fedilab.android.databinding.DrawerStatusNotificationBinding; +import app.fedilab.android.databinding.DrawerStatusPixelfedBinding; import app.fedilab.android.databinding.DrawerStatusReportBinding; import app.fedilab.android.databinding.LayoutMediaBinding; import app.fedilab.android.databinding.LayoutPollItemBinding; @@ -157,6 +161,7 @@ public class StatusAdapter extends RecyclerView.Adapter public static final int STATUS_ART = 2; public static final int STATUS_FILTERED = 3; public static final int STATUS_FILTERED_HIDE = 4; + public static final int STATUS_PIXELFED = 5; private final List statusList; private final boolean minified; private final Timeline.TimeLineEnum timelineType; @@ -164,6 +169,7 @@ public class StatusAdapter extends RecyclerView.Adapter private final boolean checkRemotely; public FetchMoreCallBack fetchMoreCallBack; private Context context; + private boolean visiblePixelfed; private RecyclerView mRecyclerView; @@ -188,6 +194,14 @@ public class StatusAdapter extends RecyclerView.Adapter return -1; } + + private static boolean isVisiblePixelfed(Status status) { + if (status.reblog != null) { + status = status.reblog; + } + return status.media_attachments != null && status.media_attachments.size() > 0; + } + private static boolean isVisible(Timeline.TimeLineEnum timelineType, Status status) { if (timelineType == Timeline.TimeLineEnum.HOME && !show_boosts && status.reblog != null) { return false; @@ -392,6 +406,7 @@ public class StatusAdapter extends RecyclerView.Adapter boolean confirmFav = sharedpreferences.getBoolean(context.getString(R.string.SET_NOTIF_VALIDATION_FAV), false); boolean confirmBoost = sharedpreferences.getBoolean(context.getString(R.string.SET_NOTIF_VALIDATION), true); boolean fullAttachement = sharedpreferences.getBoolean(context.getString(R.string.SET_FULL_PREVIEW), false); + boolean expand_media = sharedpreferences.getBoolean(context.getString(R.string.SET_EXPAND_MEDIA), false); boolean displayBookmark = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_BOOKMARK) + MainActivity.currentUserID + MainActivity.currentInstance, true); boolean displayTranslate = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_TRANSLATE) + MainActivity.currentUserID + MainActivity.currentInstance, false); boolean displayCounters = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_COUNTER_FAV_BOOST), false); @@ -399,6 +414,14 @@ public class StatusAdapter extends RecyclerView.Adapter boolean extraFeatures = sharedpreferences.getBoolean(context.getString(R.string.SET_EXTAND_EXTRA_FEATURES) + MainActivity.currentUserID + MainActivity.currentInstance, false); boolean displayQuote = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_QUOTES) + MainActivity.currentUserID + MainActivity.currentInstance, true); boolean displayReactions = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_REACTIONS) + MainActivity.currentUserID + MainActivity.currentInstance, true); + boolean compactButtons = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_COMPACT_ACTION_BUTTON), false); + + if (compactButtons) { + ConstraintSet set = new ConstraintSet(); + set.clone(holder.binding.actionButtons); + set.clear(R.id.status_emoji, ConstraintSet.END); + set.applyTo(holder.binding.actionButtons); + } if (removeLeftMargin) { LinearLayoutCompat.MarginLayoutParams p = (LinearLayoutCompat.MarginLayoutParams) holder.binding.spoiler.getLayoutParams(); @@ -1053,6 +1076,13 @@ public class StatusAdapter extends RecyclerView.Adapter ressource = R.drawable.ic_baseline_mail_24; break; } + + if (statusToDeal.local_only) { + holder.binding.localOnly.setVisibility(View.VISIBLE); + } else { + holder.binding.localOnly.setVisibility(View.GONE); + } + if (status.isFocused) { holder.binding.statusInfo.setVisibility(View.VISIBLE); holder.binding.reblogsCount.setText(String.valueOf(status.reblogs_count)); @@ -1271,7 +1301,7 @@ public class StatusAdapter extends RecyclerView.Adapter boolean singleMedia = statusToDeal.media_attachments.size() == 1; for (Attachment attachment : statusToDeal.media_attachments) { LayoutMediaBinding layoutMediaBinding = LayoutMediaBinding.inflate(LayoutInflater.from(context)); - if (fullAttachement && !statusToDeal.sensitive) { + if (fullAttachement && (!statusToDeal.sensitive || expand_media)) { float ratio = 1.0f; float mediaH = -1.0f; @@ -1310,7 +1340,7 @@ public class StatusAdapter extends RecyclerView.Adapter loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, -1.f, -1.f, -1.f, statusToDeal, attachment, singleMedia); } mediaPosition++; - if ((fullAttachement && !statusToDeal.sensitive) || singleMedia) { + if ((fullAttachement && (!statusToDeal.sensitive || expand_media)) || singleMedia) { holder.binding.mediaContainer.addView(layoutMediaBinding.getRoot()); } else { holder.binding.attachmentsList.addView(layoutMediaBinding.getRoot()); @@ -1713,6 +1743,9 @@ public class StatusAdapter extends RecyclerView.Adapter statusDraft.statusReplyList = new ArrayList<>(); statusToDeal.text = statusSource.text; statusToDeal.spoiler_text = statusSource.spoiler_text; + if (statusToDeal.spoiler_text != null && statusToDeal.spoiler_text.length() > 0) { + statusToDeal.spoilerChecked = true; + } statusDraft.statusDraftList.add(statusToDeal); intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft); intent.putExtra(Helper.ARG_EDIT_STATUS_ID, statusToDeal.id); @@ -2023,7 +2056,6 @@ public class StatusAdapter extends RecyclerView.Adapter drawerFetchMoreBinding.fetchMoreMin.setOnClickListener(v -> { status.isFetchMore = false; int position = holder.getBindingAdapterPosition(); - int position2 = getStatusPosition(statusList, status); adapter.notifyItemChanged(position); if (position < statusList.size() - 1) { String fromId; @@ -2093,7 +2125,7 @@ public class StatusAdapter extends RecyclerView.Adapter boolean expand_media = sharedpreferences.getBoolean(context.getString(R.string.SET_EXPAND_MEDIA), false); LinearLayout.LayoutParams lp; - if (fullAttachement && mediaH > 0 && !statusToDeal.sensitive) { + if (fullAttachement && mediaH > 0 && (!statusToDeal.sensitive || expand_media)) { lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, (int) (mediaH * ratio)); layoutMediaBinding.media.setScaleType(ImageView.ScaleType.FIT_CENTER); } else { @@ -2207,7 +2239,7 @@ public class StatusAdapter extends RecyclerView.Adapter adapter.notifyItemChanged(holder.getBindingAdapterPosition()); }); - if (!statusToDeal.sensitive && (fullAttachement || singleImage)) { + if ((!statusToDeal.sensitive || expand_media) && (fullAttachement || singleImage)) { layoutMediaBinding.getRoot().setPadding(0, 0, 0, 10); } else { layoutMediaBinding.getRoot().setPadding(0, 0, 10, 0); @@ -2215,28 +2247,6 @@ public class StatusAdapter extends RecyclerView.Adapter } - @Override - public void onAttachedToRecyclerView(RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - - mRecyclerView = recyclerView; - } - - /* private static boolean mediaObfuscated(Status status) { - //Media is not sensitive and doesn't have a spoiler text - if (!status.isMediaObfuscated) { - return false; - } - if (!status.sensitive && (status.spoiler_text == null || status.spoiler_text.trim().isEmpty())) { - return false; - } - if (status.isMediaObfuscated && status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { - return true; - } else { - return status.sensitive; - } - }*/ - /** * Send a broadcast to other open fragments that content a timeline * @@ -2261,68 +2271,20 @@ public class StatusAdapter extends RecyclerView.Adapter LocalBroadcastManager.getInstance(context).sendBroadcast(intentBC); } - - @Override - public int getItemViewType(int position) { - if (timelineType == Timeline.TimeLineEnum.ART) { - return STATUS_ART; + /* private static boolean mediaObfuscated(Status status) { + //Media is not sensitive and doesn't have a spoiler text + if (!status.isMediaObfuscated) { + return false; + } + if (!status.sensitive && (status.spoiler_text == null || status.spoiler_text.trim().isEmpty())) { + return false; + } + if (status.isMediaObfuscated && status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { + return true; } else { - if (statusList.get(position).filteredByApp != null) { - if (statusList.get(position).filteredByApp.filter_action.equals("warn")) { - return STATUS_FILTERED; - } else { //These messages should not be displayed unless they contain a fetch more button - if (!statusList.get(position).isFetchMore) { - return STATUS_HIDDEN; - } else { - return STATUS_FILTERED_HIDE; - } - } - } else { - return isVisible(timelineType, statusList.get(position)) ? STATUS_VISIBLE : STATUS_HIDDEN; - } - + return status.sensitive; } - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - context = parent.getContext(); - if (viewType == STATUS_HIDDEN) { //Hidden statuses - ie: filtered - DrawerStatusHiddenBinding itemBinding = DrawerStatusHiddenBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new StatusViewHolder(itemBinding); - } else if (viewType == STATUS_ART) { //Art statuses - DrawerStatusArtBinding itemBinding = DrawerStatusArtBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new StatusViewHolder(itemBinding); - } else if (viewType == STATUS_FILTERED) { //Filtered warn - DrawerStatusFilteredBinding itemBinding = DrawerStatusFilteredBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new StatusViewHolder(itemBinding); - } else if (viewType == STATUS_FILTERED_HIDE) { //Filtered hide - DrawerStatusFilteredHideBinding itemBinding = DrawerStatusFilteredHideBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new StatusViewHolder(itemBinding); - } else { //Classic statuses - if (!minified) { - DrawerStatusBinding itemBinding = DrawerStatusBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new StatusViewHolder(itemBinding); - } else { - DrawerStatusReportBinding itemBinding = DrawerStatusReportBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new StatusViewHolder(itemBinding); - } - } - } - - public int getCount() { - return statusList.size(); - } - - public Status getItem(int position) { - return statusList.get(position); - } - - - public long getItemId(int position) { - return position; - } + }*/ public static void applyColor(Context context, StatusViewHolder holder) { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -2410,6 +2372,87 @@ public class StatusAdapter extends RecyclerView.Adapter } } + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + + mRecyclerView = recyclerView; + } + + @Override + public int getItemViewType(int position) { + if (timelineType == Timeline.TimeLineEnum.ART) { + return STATUS_ART; + } else { + if (statusList.get(position).filteredByApp != null) { + if (statusList.get(position).filteredByApp.filter_action.equals("warn")) { + return STATUS_FILTERED; + } else { //These messages should not be displayed unless they contain a fetch more button + if (!statusList.get(position).isFetchMore) { + return STATUS_HIDDEN; + } else { + return STATUS_FILTERED_HIDE; + } + } + } else { + if (isVisible(timelineType, statusList.get(position))) { + if (visiblePixelfed && isVisiblePixelfed(statusList.get(position)) && timelineType != Timeline.TimeLineEnum.UNKNOWN) { + return STATUS_PIXELFED; + } else { + return STATUS_VISIBLE; + } + } else { + return STATUS_HIDDEN; + } + } + + } + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + visiblePixelfed = sharedpreferences.getBoolean(context.getString(R.string.SET_PIXELFED_PRESENTATION) + MainActivity.currentUserID + MainActivity.currentInstance, false); + if (viewType == STATUS_HIDDEN) { //Hidden statuses - ie: filtered + DrawerStatusHiddenBinding itemBinding = DrawerStatusHiddenBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else if (viewType == STATUS_ART) { //Art statuses + DrawerStatusArtBinding itemBinding = DrawerStatusArtBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else if (viewType == STATUS_PIXELFED) { //Art statuses + DrawerStatusPixelfedBinding itemBinding = DrawerStatusPixelfedBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else if (viewType == STATUS_FILTERED) { //Filtered warn + DrawerStatusFilteredBinding itemBinding = DrawerStatusFilteredBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else if (viewType == STATUS_FILTERED_HIDE) { //Filtered hide + DrawerStatusFilteredHideBinding itemBinding = DrawerStatusFilteredHideBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else { //Classic statuses + if (!minified) { + DrawerStatusBinding itemBinding = DrawerStatusBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } else { + DrawerStatusReportBinding itemBinding = DrawerStatusReportBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new StatusViewHolder(itemBinding); + } + } + } + + public int getCount() { + return statusList.size(); + } + + public Status getItem(int position) { + return statusList.get(position); + } + + public long getItemId(int position) { + return position; + } + @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { //Nothing to do with hidden statuses @@ -2577,6 +2620,44 @@ public class StatusAdapter extends RecyclerView.Adapter intent.putExtra(Helper.ARG_STATUS, status); context.startActivity(intent); }); + } else if (viewHolder.getItemViewType() == STATUS_PIXELFED) { + Status statusToDeal = status.reblog != null ? status.reblog : status; + StatusViewHolder holder = (StatusViewHolder) viewHolder; + + if (status.reblog != null) { + MastodonHelper.loadPPMastodon(holder.bindingPixelfed.artReblogPp, status.account); + holder.bindingPixelfed.artReblogPp.setVisibility(View.VISIBLE); + } else { + holder.bindingPixelfed.artReblogPp.setVisibility(View.GONE); + } + + MastodonHelper.loadPPMastodon(holder.bindingPixelfed.artPp, statusToDeal.account); + SliderAdapter adapter = new SliderAdapter(statusToDeal); + holder.bindingPixelfed.artMedia.setSliderAdapter(adapter); + holder.bindingPixelfed.artMedia.setSliderTransformAnimation(SliderAnimations.SIMPLETRANSFORMATION); + holder.bindingPixelfed.artMedia.setAutoCycleDirection(SliderView.AUTO_CYCLE_DIRECTION_BACK_AND_FORTH); + holder.bindingPixelfed.artMedia.setScrollTimeInSec(4); + holder.bindingPixelfed.artMedia.startAutoCycle(); + holder.bindingPixelfed.commentNumber.setText(String.valueOf(statusToDeal.replies_count)); + holder.bindingPixelfed.artUsername.setText( + statusToDeal.account.getSpanDisplayName(context, + new WeakReference<>(holder.bindingPixelfed.artUsername)), + TextView.BufferType.SPANNABLE); + holder.bindingPixelfed.artAcct.setText(String.format(Locale.getDefault(), "@%s", statusToDeal.account.acct)); + holder.bindingPixelfed.artPp.setOnClickListener(v -> { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, statusToDeal.account); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.bindingPixelfed.artPp, context.getString(R.string.activity_porfile_pp)); + context.startActivity(intent, options.toBundle()); + }); + holder.bindingPixelfed.bottomBanner.setOnClickListener(v -> { + Intent intent = new Intent(context, ContextActivity.class); + intent.putExtra(Helper.ARG_STATUS, statusToDeal); + context.startActivity(intent); + }); } } @@ -2602,8 +2683,10 @@ public class StatusAdapter extends RecyclerView.Adapter DrawerStatusReportBinding bindingReport; DrawerStatusNotificationBinding bindingNotification; DrawerStatusArtBinding bindingArt; + DrawerStatusPixelfedBinding bindingPixelfed; DrawerStatusFilteredBinding bindingFiltered; DrawerStatusFilteredHideBinding bindingFilteredHide; + StatusViewHolder(DrawerStatusBinding itemView) { super(itemView.getRoot()); binding = itemView; @@ -2632,6 +2715,11 @@ public class StatusAdapter extends RecyclerView.Adapter bindingArt = itemView; } + StatusViewHolder(DrawerStatusPixelfedBinding itemView) { + super(itemView.getRoot()); + bindingPixelfed = itemView; + } + StatusViewHolder(DrawerStatusFilteredBinding itemView) { super(itemView.getRoot()); bindingFiltered = itemView; diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java index 2aad4a379..8f8cc33ba 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/admin/FragmentAdminReport.java @@ -200,5 +200,4 @@ public class FragmentAdminReport extends Fragment { } - } \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java index 8b4be76dd..6840be3a6 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/login/FragmentLoginMain.java @@ -68,11 +68,11 @@ import es.dmoral.toasty.Toasty; public class FragmentLoginMain extends Fragment { + private static final int REQUEST_CODE = 5412; + private final int PICK_IMPORT = 5557; private FragmentLoginMainBinding binding; private boolean searchInstanceRunning = false; private String oldSearch; - private static final int REQUEST_CODE = 5412; - private final int PICK_IMPORT = 5557; private ActivityResultLauncher permissionLauncher; public View onCreateView(@NonNull LayoutInflater inflater, diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMediaProfile.java b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMediaProfile.java index 2bfc17215..86670549e 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMediaProfile.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMediaProfile.java @@ -46,6 +46,8 @@ import es.dmoral.toasty.Toasty; public class FragmentMediaProfile extends Fragment { + String tempToken; + String tempInstance; private FragmentPaginationBinding binding; private AccountsVM accountsVM; private Account accountTimeline; @@ -53,8 +55,6 @@ public class FragmentMediaProfile extends Fragment { private List mediaStatuses; private String max_id; private ImageAdapter imageAdapter; - String tempToken; - String tempInstance; private boolean checkRemotely; private String accountId; diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentExtraFeaturesSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentExtraFeaturesSettings.java index 985faf95e..d30b5e033 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentExtraFeaturesSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentExtraFeaturesSettings.java @@ -76,6 +76,13 @@ public class FragmentExtraFeaturesSettings extends PreferenceFragmentCompat impl String format = sharedpreferences.getString(getString(R.string.SET_POST_FORMAT) + MainActivity.currentUserID + MainActivity.currentInstance, "text/plain"); SET_POST_FORMAT.setValue(format); } + + ListPreference SET_COMPOSE_LOCAL_ONLY = findPreference(getString(R.string.SET_COMPOSE_LOCAL_ONLY)); + if (SET_COMPOSE_LOCAL_ONLY != null) { + SET_COMPOSE_LOCAL_ONLY.getContext().setTheme(Helper.dialogStyle()); + int localOnly = sharedpreferences.getInt(getString(R.string.SET_COMPOSE_LOCAL_ONLY) + MainActivity.currentUserID + MainActivity.currentInstance, 0); + SET_COMPOSE_LOCAL_ONLY.setValue(String.valueOf(localOnly)); + } } @Override @@ -119,6 +126,12 @@ public class FragmentExtraFeaturesSettings extends PreferenceFragmentCompat impl editor.putString(getString(R.string.SET_POST_FORMAT) + MainActivity.currentUserID + MainActivity.currentInstance, SET_POST_FORMAT.getValue()); } } + if (key.compareToIgnoreCase(getString(R.string.SET_COMPOSE_LOCAL_ONLY)) == 0) { + ListPreference SET_COMPOSE_LOCAL_ONLY = findPreference(getString(R.string.SET_COMPOSE_LOCAL_ONLY)); + if (SET_COMPOSE_LOCAL_ONLY != null) { + editor.putInt(getString(R.string.SET_COMPOSE_LOCAL_ONLY) + MainActivity.currentUserID + MainActivity.currentInstance, Integer.parseInt(SET_COMPOSE_LOCAL_ONLY.getValue())); + } + } editor.apply(); } } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java index b6b407e5d..62ca1258c 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java @@ -32,7 +32,6 @@ import es.dmoral.toasty.Toasty; public class FragmentThemingSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { - @Override public void onCreatePreferences(Bundle bundle, String s) { createPref(); diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java index 49867c7f4..05606bc6c 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentTimelinesSettings.java @@ -73,6 +73,12 @@ public class FragmentTimelinesSettings extends PreferenceFragmentCompat implemen boolean checked = sharedpreferences.getBoolean(getString(R.string.SET_DISPLAY_TRANSLATE) + MainActivity.currentUserID + MainActivity.currentInstance, false); SET_DISPLAY_TRANSLATE.setChecked(checked); } + + SwitchPreferenceCompat SET_PIXELFED_PRESENTATION = findPreference(getString(R.string.SET_PIXELFED_PRESENTATION)); + if (SET_PIXELFED_PRESENTATION != null) { + boolean checked = sharedpreferences.getBoolean(getString(R.string.SET_PIXELFED_PRESENTATION) + MainActivity.currentUserID + MainActivity.currentInstance, false); + SET_PIXELFED_PRESENTATION.setChecked(checked); + } } @Override @@ -95,6 +101,12 @@ public class FragmentTimelinesSettings extends PreferenceFragmentCompat implemen editor.putBoolean(getString(R.string.SET_DISPLAY_TRANSLATE) + MainActivity.currentUserID + MainActivity.currentInstance, SET_DISPLAY_TRANSLATE.isChecked()); } } + if (key.compareToIgnoreCase(getString(R.string.SET_PIXELFED_PRESENTATION)) == 0) { + SwitchPreferenceCompat SET_PIXELFED_PRESENTATION = findPreference(getString(R.string.SET_PIXELFED_PRESENTATION)); + if (SET_PIXELFED_PRESENTATION != null) { + editor.putBoolean(getString(R.string.SET_PIXELFED_PRESENTATION) + MainActivity.currentUserID + MainActivity.currentInstance, SET_PIXELFED_PRESENTATION.isChecked()); + } + } editor.apply(); } } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java index ffa1d1299..0e3352de5 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonContext.java @@ -50,12 +50,11 @@ import app.fedilab.android.viewmodel.mastodon.StatusesVM; public class FragmentMastodonContext extends Fragment { + public FirstMessage firstMessage; private FragmentPaginationBinding binding; private StatusesVM statusesVM; private List statuses; private StatusAdapter statusAdapter; - public FirstMessage firstMessage; - //Handle actions that can be done in other fragments private final BroadcastReceiver receive_action = new BroadcastReceiver() { @Override diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java index dcedecffc..817975069 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java @@ -209,6 +209,7 @@ public class FragmentMastodonNotification extends Fragment implements Notificati excludeType.add("status"); excludeType.add("admin.sign_up"); excludeType.add("admin.report"); + excludeType.add("pleroma:emoji_reaction"); if (notificationType == NotificationTypeEnum.ALL) { aggregateNotification = sharedpreferences.getBoolean(getString(R.string.SET_AGGREGATE_NOTIFICATION), true); if (excludedCategories != null) { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java index 82d2aeff1..b3e2e6b42 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -16,7 +16,6 @@ package app.fedilab.android.ui.fragment.timeline; import static app.fedilab.android.BaseMainActivity.currentInstance; -import static app.fedilab.android.BaseMainActivity.currentUserID; import static app.fedilab.android.BaseMainActivity.networkAvailable; import android.content.BroadcastReceiver; @@ -52,6 +51,7 @@ import app.fedilab.android.client.entities.api.Attachment; import app.fedilab.android.client.entities.api.Pagination; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.client.entities.api.Statuses; +import app.fedilab.android.client.entities.app.BubbleTimeline; import app.fedilab.android.client.entities.app.PinnedTimeline; import app.fedilab.android.client.entities.app.RemoteInstance; import app.fedilab.android.client.entities.app.StatusCache; @@ -84,9 +84,6 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. private StatusAdapter statusAdapter; private Timeline.TimeLineEnum timelineType; private List timelineStatuses; - private boolean checkRemotely; - private String accountIDInRemoteInstance; - //Handle actions that can be done in other fragments private final BroadcastReceiver receive_action = new BroadcastReceiver() { @Override @@ -103,12 +100,24 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. if (receivedStatus != null && statusAdapter != null) { int position = getPosition(receivedStatus); if (position >= 0) { - timelineStatuses.get(position).reblog = receivedStatus.reblog; - timelineStatuses.get(position).reblogged = receivedStatus.reblogged; - timelineStatuses.get(position).favourited = receivedStatus.favourited; - timelineStatuses.get(position).bookmarked = receivedStatus.bookmarked; - timelineStatuses.get(position).reblogs_count = receivedStatus.reblogs_count; - timelineStatuses.get(position).favourites_count = receivedStatus.favourites_count; + if (receivedStatus.reblog != null) { + timelineStatuses.get(position).reblog = receivedStatus.reblog; + } + if (timelineStatuses.get(position).reblog != null) { + timelineStatuses.get(position).reblog.reblogged = receivedStatus.reblogged; + timelineStatuses.get(position).reblog.favourited = receivedStatus.favourited; + timelineStatuses.get(position).reblog.bookmarked = receivedStatus.bookmarked; + timelineStatuses.get(position).reblog.reblogs_count = receivedStatus.reblogs_count; + timelineStatuses.get(position).reblog.favourites_count = receivedStatus.favourites_count; + } else { + timelineStatuses.get(position).reblogged = receivedStatus.reblogged; + timelineStatuses.get(position).favourited = receivedStatus.favourited; + timelineStatuses.get(position).bookmarked = receivedStatus.bookmarked; + timelineStatuses.get(position).reblogs_count = receivedStatus.reblogs_count; + timelineStatuses.get(position).favourites_count = receivedStatus.favourites_count; + } + + statusAdapter.notifyItemChanged(position); } } else if (delete_statuses_for_user != null && statusAdapter != null) { @@ -162,10 +171,13 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } } }; + private boolean checkRemotely; + private String accountIDInRemoteInstance; private boolean isViewInitialized; private Statuses initialStatuses; private String list_id; private TagTimeline tagTimeline; + private BubbleTimeline bubbleTimeline; private LinearLayoutManager mLayoutManager; private Account accountTimeline; private boolean exclude_replies, exclude_reblogs, show_pinned, media_only, minified; @@ -179,6 +191,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. private int lockForResumeCall; private boolean isNotPinnedTimeline; private int extraCalls; + //Allow to recreate data when detaching/attaching fragment public void recreate() { initialStatuses = null; @@ -241,7 +254,10 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. return -1; } for (Status _status : timelineStatuses) { - if (_status.id != null && _status.id.compareTo(status.id) == 0) { + if (_status.reblog == null && _status.id != null && _status.id.compareTo(status.id) == 0) { + found = true; + break; + } else if (_status.reblog != null && _status.reblog.id != null && _status.reblog.id.compareTo(status.id) == 0) { found = true; break; } @@ -332,6 +348,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. isViewInitialized = getArguments().getBoolean(Helper.ARG_INITIALIZE_VIEW, true); isNotPinnedTimeline = isViewInitialized; tagTimeline = (TagTimeline) getArguments().getSerializable(Helper.ARG_TAG_TIMELINE); + bubbleTimeline = (BubbleTimeline) getArguments().getSerializable(Helper.ARG_BUBBLE_TIMELINE); accountTimeline = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT); exclude_replies = !getArguments().getBoolean(Helper.ARG_SHOW_REPLIES, true); checkRemotely = getArguments().getBoolean(Helper.ARG_CHECK_REMOTELY, false); @@ -342,18 +359,26 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. minified = getArguments().getBoolean(Helper.ARG_MINIFIED, false); statusReport = (Status) getArguments().getSerializable(Helper.ARG_STATUS_REPORT); } + //When visiting a profile without being authenticated if (checkRemotely) { String[] acctArray = accountTimeline.acct.split("@"); if (acctArray.length > 1) { remoteInstance = acctArray[1]; } + if (remoteInstance != null && remoteInstance.equalsIgnoreCase(currentInstance)) { + checkRemotely = false; + } else if (remoteInstance == null) { + checkRemotely = false; + } } if (tagTimeline != null) { ident = "@T@" + tagTimeline.name; if (tagTimeline.isART) { timelineType = Timeline.TimeLineEnum.ART; } + } else if (bubbleTimeline != null) { + ident = "@B@Bubble"; } else if (list_id != null) { ident = "@l@" + list_id; } else if (remoteInstance != null && !checkRemotely) { @@ -387,7 +412,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. binding.swipeContainer.setRefreshing(false); binding.loadingNextElements.setVisibility(View.GONE); flagLoading = false; - + int currentPosition = mLayoutManager.findFirstVisibleItemPosition(); if (timelineStatuses != null && fetched_statuses != null && fetched_statuses.statuses != null && fetched_statuses.statuses.size() > 0) { try { if (statusToUpdate != null) { @@ -450,7 +475,10 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. update.onUpdate(0, timelineType, slug); } if (direction == DIRECTION.TOP && fetchingMissing) { - binding.recyclerView.scrollToPosition(getPosition(fetched_statuses.statuses.get(fetched_statuses.statuses.size() - 1)) + 1); + int newPosition = currentPosition + fetched_statuses.statuses.size() + 1; + if (newPosition < timelineStatuses.size()) { + binding.recyclerView.scrollToPosition(newPosition); + } } if (!fetchingMissing) { if (fetched_statuses.pagination.max_id == null) { @@ -619,7 +647,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } }); //For first tab we fetch new messages, if we keep position - if (slug != null && slug.compareTo(Helper.getSlugOfFirstFragment(requireActivity(), currentUserID, currentInstance)) == 0 && rememberPosition) { + if (slug != null /*&& slug.compareTo(Helper.getSlugOfFirstFragment(requireActivity(), currentUserID, currentInstance)) == 0*/ && rememberPosition) { route(DIRECTION.FETCH_NEW, true); } } @@ -712,6 +740,14 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelineParams.local = false; timelineParams.remote = true; break; + case BUBBLE: + if (bubbleTimeline != null) { + timelineParams.onlyMedia = bubbleTimeline.only_media; + timelineParams.remote = bubbleTimeline.remote; + timelineParams.replyVisibility = bubbleTimeline.reply_visibility; + timelineParams.excludeVisibilities = bubbleTimeline.exclude_visibilities; + } + break; case LIST: timelineParams.listId = list_id; break; @@ -898,6 +934,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. routeCommon(direction, fetchingMissing, statusToUpdate); } else if (timelineType == Timeline.TimeLineEnum.PUBLIC) { //PUBLIC TIMELINE routeCommon(direction, fetchingMissing, statusToUpdate); + } else if (timelineType == Timeline.TimeLineEnum.BUBBLE) { //BUBBLE TIMELINE + routeCommon(direction, fetchingMissing, statusToUpdate); } else if (timelineType == Timeline.TimeLineEnum.REMOTE) { //REMOTE TIMELINE //NITTER TIMELINES if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java index 76bec3fb4..a7a27092c 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java @@ -36,6 +36,7 @@ public class FragmentProfileTimeline extends Fragment { private Account account; private FragmentProfileTimelinesBinding binding; private boolean checkRemotely; + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java index aada3c696..326b3c654 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java @@ -137,6 +137,8 @@ public class FedilabPageAdapter extends FragmentStatePagerAdapter { bundle.putSerializable(Helper.ARG_TAG_TIMELINE, pinnedTimeline.tagTimeline); } else if (pinnedTimeline.type == Timeline.TimeLineEnum.REMOTE) { bundle.putSerializable(Helper.ARG_REMOTE_INSTANCE, pinnedTimeline); + } else if (pinnedTimeline.type == Timeline.TimeLineEnum.BUBBLE) { + bundle.putSerializable(Helper.ARG_BUBBLE_TIMELINE, pinnedTimeline.bubbleTimeline); } } diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java index 372f7659c..6d7a10e37 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java @@ -30,8 +30,8 @@ import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; public class FedilabProfilePageAdapter extends FragmentStatePagerAdapter { private final Account account; - private Fragment mCurrentFragment; private final boolean checkRemotely; + private Fragment mCurrentFragment; public FedilabProfilePageAdapter(FragmentManager fm, Account account, boolean remotely) { super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java index ca2ebb26a..582aa9d44 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java @@ -30,8 +30,8 @@ import app.fedilab.android.ui.fragment.timeline.FragmentProfileTimeline; public class FedilabProfileTLPageAdapter extends FragmentStatePagerAdapter { private final Account account; - private Fragment mCurrentFragment; private final boolean checkRemotely; + private Fragment mCurrentFragment; public FedilabProfileTLPageAdapter(FragmentManager fm, Account account, boolean remotely) { super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java index d7ee2dd4b..02e63b43c 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/NotificationsVM.java @@ -304,12 +304,17 @@ public class NotificationsVM extends AndroidViewModel { boolean favourite, boolean reblog, boolean mention, - boolean poll) { + boolean poll, + boolean status, + boolean updates, + boolean signup, + boolean report + ) { pushSubscriptionMutableLiveData = new MutableLiveData<>(); MastodonNotificationsService mastodonNotificationsService = init(instance); new Thread(() -> { PushSubscription pushSubscription = null; - Call pushSubscriptionCall = mastodonNotificationsService.pushSubscription(token, endpoint, keys_p256dh, keys_auth, follow, favourite, reblog, mention, poll); + Call pushSubscriptionCall = mastodonNotificationsService.pushSubscription(token, endpoint, keys_p256dh, keys_auth, follow, favourite, reblog, mention, poll, status, updates, signup, report); if (pushSubscriptionCall != null) { try { Response pushSubscriptionResponse = pushSubscriptionCall.execute(); diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java index ec3eccaaf..e1e293429 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java @@ -417,6 +417,9 @@ public class TimelinesVM extends AndroidViewModel { case PUBLIC: timelineCall = mastodonTimelinesService.getPublic(timelineParams.token, false, true, timelineParams.onlyMedia, timelineParams.maxId, timelineParams.sinceId, timelineParams.minId, timelineParams.limit); break; + case BUBBLE: + timelineCall = mastodonTimelinesService.getBubble(timelineParams.token, timelineParams.onlyMedia, timelineParams.remote, timelineParams.withMuted, timelineParams.excludeVisibilities, timelineParams.replyVisibility, timelineParams.maxId, timelineParams.sinceId, timelineParams.minId, timelineParams.limit); + break; case ART: case TAG: timelineCall = mastodonTimelinesService.getHashTag(timelineParams.token, timelineParams.hashtagTrim, timelineParams.local, timelineParams.onlyMedia, timelineParams.all, timelineParams.any, timelineParams.none, timelineParams.maxId, timelineParams.sinceId, timelineParams.minId, timelineParams.limit); @@ -949,6 +952,7 @@ public class TimelinesVM extends AndroidViewModel { public String userId; public Boolean remote; public Boolean onlyMedia; + public Boolean withMuted; public String hashtagTrim; public List all; public List any; @@ -961,6 +965,8 @@ public class TimelinesVM extends AndroidViewModel { public int limit = 40; public Boolean local; public List excludeType; + public List excludeVisibilities; + public String replyVisibility; public TimelineParams(@NonNull Timeline.TimeLineEnum timeLineEnum, @Nullable FragmentMastodonTimeline.DIRECTION timelineDirection, @Nullable String ident) { if (type != Timeline.TimeLineEnum.REMOTE) { diff --git a/app/src/main/res/drawable/ic_baseline_bubble_chart_24.xml b/app/src/main/res/drawable/ic_baseline_bubble_chart_24.xml new file mode 100644 index 000000000..8850e91f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_bubble_chart_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_local_only_24.xml b/app/src/main/res/drawable/ic_baseline_local_only_24.xml new file mode 100644 index 000000000..ba4d830ec --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_local_only_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_bubble_exclude_visibility.xml b/app/src/main/res/layout/dialog_bubble_exclude_visibility.xml new file mode 100644 index 000000000..ca956bb7a --- /dev/null +++ b/app/src/main/res/layout/dialog_bubble_exclude_visibility.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_bubble_reply_visibility.xml b/app/src/main/res/layout/dialog_bubble_reply_visibility.xml new file mode 100644 index 000000000..7739fb3d2 --- /dev/null +++ b/app/src/main/res/layout/dialog_bubble_reply_visibility.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_slider.xml b/app/src/main/res/layout/drawer_slider.xml new file mode 100644 index 000000000..33f3f3302 --- /dev/null +++ b/app/src/main/res/layout/drawer_slider.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_status.xml b/app/src/main/res/layout/drawer_status.xml index 7dcc0a61d..d28cfe2bb 100644 --- a/app/src/main/res/layout/drawer_status.xml +++ b/app/src/main/res/layout/drawer_status.xml @@ -159,6 +159,14 @@ android:singleLine="true" tools:text="@tools:sample/full_names" /> + + @@ -103,6 +103,17 @@ app:layout_constraintTop_toBottomOf="@id/button_emoji_one" tools:visibility="visible" /> + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/option_bubble_timeline.xml b/app/src/main/res/menu/option_bubble_timeline.xml new file mode 100644 index 000000000..e3b5c1eeb --- /dev/null +++ b/app/src/main/res/menu/option_bubble_timeline.xml @@ -0,0 +1,27 @@ + +

+ + + + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7b1e3eacc..54e26a9c2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -951,4 +951,24 @@ Klíč pro API překladače Verze Verze překladače + Viditelnost ikon + Můžete bezpečně skrýt tyto ikony v dolní oblasti, abyste získali více prostoru. Jsou k dispozici i v podnabídce. + Formát příspěvků + Ikony pro extra funkce + Pokud vaše instance nepodporuje některé extra funkce, můžete tyto ikony skýt + Zobrazit tlačítko „Citovat“ + Zobrazit tlačítko „Reakce“ + Povolením této volby se zobrazí extra funkce. Jsou určeny pro softwary sociálních sítí jako Pleroma, Akkoma nebo Glitch Social + Extra funkce + Formát příspěvků + Bublina + K získání všech zpráv bude aplikace veřejně zobrazovat profily. Interakce budou k federaci zpráv potřebovat dodatečný krok. + Seznam + Vzdálené profily + Zobrazit tlačítko „Jen místní“ + Jen místní + Stav e-mailu + Stav přihlášení + Připojil(a) se + Ztišen(a) \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 99c71d98e..bf2c0d0f7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -41,7 +41,7 @@ Benutzername Entwürfe Favoriten - Neue Folgende + Neue Follower Erwähnungen Geteilte Beiträge Geteilte Beiträge anzeigen @@ -52,30 +52,30 @@ Startseite Lokale Zeitleiste Stummgeschaltete Nutzer - Blockierte Nutzer + Geblockte Nutzer Benachrichtigungen Folgeanfragen Einstellungen E-Mail senden - Geplante Nachrichten + Geplante Beiträge Die folgenden Informationen könnten das Profil des Nutzers unvollständig wiedergeben. Emoji einfügen Die App verfügt derzeit nicht über benutzerdefinierte Emojis. Bist Du sicher, dass Du Dich von @%1$s@%2$s abmelden möchtest\? - Keine Nachrichten zum Anzeigen - Diese Nachricht Deinen Favoriten hinzufügen\? - Diese Nachricht aus Ihren Favoriten entfernen\? - Diese Nachricht teilen\? - Geteilte Nachricht zurückziehen\? + Kein Beitrag zum Anzeigen + Diesen Beitrag Deinen Favoriten hinzufügen\? + Diesen Beitrag aus Deinen Favoriten entfernen\? + Diesen Beitrag teilen\? + Geteilten Beitrag zurückziehen\? Stummschalten - Sperren + Blockieren Melden Entfernen Kopieren Teilen Erwähnen - Zeitlich begrenzt stumm schalten + Zeitlich begrenzt stummschalten Löschen & neu entwerfen Lautlosmodus für dieses Konto aktivieren? @@ -120,11 +120,11 @@ %d Tage - Ein Fehler während der Auswahl ist aufgetreten! - Diese Datei löschen? - Deine Nachricht ist leer! - Die Nachricht wurde gesendet! - Vertrauliche Inhalte? + Bei der Medien-Auswahl ist ein Fehler aufgetreten! + Diese Datei entfernen\? + Dein Beitrag ist leer! + Der Beitrag wurde gesendet! + Sensibler Inhalt\? Keine Entwürfe vorhanden! Konto auswählen Konten auswählen @@ -141,48 +141,49 @@ Instanzensuche: - Keinen Nutzer gefunden - Keine Anfragen zu Folgen - Nachrichten + Kein Konto zum Anzeigen + Keine Folgeanfragen + Beiträge \n %1$s Folgt \n %1$s - Folgende \n %1$s + Follower +\n %1$s Ablehnen - Keine geplanten Nachrichten vorhanden! - Geplante Nachricht löschen\? - Die Nachricht wurde geplant! + Keine geplanten Beiträge zum Anzeigen! + Geplanten Beitrag löschen\? + Der Beitrag wurde geplant! Der geplante Termin muss in der Zukunft liegen! Die Dauer für die Stummschaltung sollte mehr als eine Minute betragen. - %1$s wurde bis %2$s lautlos geschaltet. -\n Du kannst die Stummschaltung für diese Person beenden, indem Du ihr Profil besuchst. - %1$s ist bis %2$s stumm geschaltet. + %1$s wurde bis %2$s stummgeschaltet. +\n Du kannst die Stummschaltung für dieses Konto beenden, indem Du die Profilseite besuchst. + %1$s ist bis %2$s stummgeschaltet. \n Hier antippen, um die Stummschaltung zu beenden. - Keine Benachrichtigungen + Keine Benachrichtigung zum Anzeigen hat dich erwähnt - hat eine neue Nachricht geschrieben + hat einen neuen Beitrag verfasst hat Deinen Beitrag geteilt hat Deinen Status favorisiert - folgt dir - fragte, dir zu folgen - Entferne alle Benachrichtigungen? + folgt Dir + fragt Dir zu folgen + Lösche alle Benachrichtigungen\? Alle Benachrichtigungen wurden gelöscht! - Folgende + Follower Fehler beim Laden der Client ID! - Konto wurde gesperrt! - Das Konto wurde entsperrt! - Nutzer wurde stumm geschaltet! + Das Konto wurde geblockt! + Das Konto wurde entblockt! + Das Konto wurde stummgeschaltet! Stummschaltung für dieses Konto aufgehoben! - Du folgst dem Nutzer! - Du folgst dem Nutzer nicht mehr! - Die Nachricht wurde geteilt! - Die Nachricht wird nicht länger geteilt! - Die Nachricht wurde Deinen Favoriten hinzugefügt! - Die Nachricht wurde aus Deinen Favoriten entfernt! + Du folgst dem Konto! + Du folgst dem Konto nicht mehr! + Der Beitrag wurde geteilt! + Der Beitrag wird nicht länger geteilt! + Der Beitrag wurde Deinen Favoriten hinzugefügt! + Der Beitrag wurde aus Deinen Favoriten entfernt! Es ist ein Fehler aufgetreten! Ein Fehler ist aufgetreten! Die Instanz hat keinen Autorisierungscode gesendet! Der Name der Instanz scheint ungültig zu sein! @@ -191,7 +192,7 @@ Keine Aktion kann durchgeführt werden Während der Übersetzung ist ein Fehler aufgetreten! - Anzahl der Nachrichten pro Ladevorgang + Anzahl der Beiträge pro Ladevorgang GIF Avatare deaktivieren Benachrichtigung wenn mir jemand folgt Benachrichtigung wenn jemand meinen Beitrag teilt @@ -199,14 +200,14 @@ Benachrichtigung wenn ich erwähnt werde Benachrichtigung wenn eine Umfrage beendet ist Benachrichtigung über neue Beiträge - Bestätigungsdialog vor dem Teilen anzeigen + Bestätigungsdialog vor dem Teilen eines Beitrags anzeigen Bestätigungsdialog vor dem Favorisieren anzeigen Benachrichtigen? - Leise Benachrichtigungen - NSFW Anzeigedauer (in Sekunden, 0 ≙ deaktiviert) - Timeout der Medienbeschreibung (Sekunden, 0 bedeutet aus) + Stille Benachrichtigungen + Anzeigedauer heikler Inhalte (NSFW) (Sekunden, 0 = deaktiviert) + Timeout der Medien-Beschreibung (Sekunden, 0 = deaktiviert) Benutzerdefiniertes Teilen - Ihre benutzerdefinierte Freigabeadresse … + Deine persönliche Freigabeadresse … Konto sperren Änderungen speichern Vorschaubilder anpassen @@ -214,7 +215,7 @@ und Benutze internen Browser In-App öffnen mit externem Browser - Inhaltswarnungen (CWs) automatisch einblenden + Inhaltswarnungen (CW) automatisch einblenden LED Farbe auswählen: Blau @@ -230,9 +231,9 @@ Stumm Stummschaltung beenden Anfrage gesendet - folgt dir + folgt Dir Ersten Buchstaben bei Antworten groß schreiben - Bildgröße verändern + Bildgröße ändern Videogröße ändern @@ -258,7 +259,7 @@ Zur Liste hinzufügen Liste löschen Neuer Listentitel - Das Konto wurde zur Liste hinzugefügt! + Das Konto wurde der Liste hinzugefügt! Du hast noch keine Listen! %1$s wurde verschoben nach %2$s @@ -270,7 +271,7 @@ Port Login Passwort - Nachricht-Details beim Teilen hinzufügen + Details des Beitrags beim Teilen hinzufügen Unterstütze die app auf Liberapay Es gibt einen Fehler im regulären Ausdruck! Es wurden keine Timelines in dieser Instanz gefunden! @@ -281,17 +282,17 @@ Auf Profil hervorheben Geteilte Beiträge von %s anzeigen Nicht mehr im Profil hervorheben - Direktnachrichten + Direktnachricht Filter Keine Filter vorhanden. Du kannst durch Klicken auf \"+\" neue Filter erstellen. Schlagwort oder Phrase Startseite Öffentliche Timeline Benachrichtigungen - Unterhaltungen - Wird unabhängig vom umgebenen Text oder Inhaltswarnung einer Nachricht verglichen + Unterhaltung + Wird unabhängig von der Groß-/Kleinschreibung in dem Text oder der Inhaltswarnung eines Beitrags verglichen Entfernen anstatt zu verstecken - Gefilterte Nachrichten werden unwiderruflich verschwinden, selbst wenn der Filter später entfernt wird + Gefilterte Beiträge werden unwiderruflich verschwinden, selbst wenn der Filter später entfernt wird Wenn das Schlüsselwort oder -phrase nur Buchstaben und Zahlen enthält, wird es nur angewendet werden, wenn es dem ganzen Wort entspricht Ganzes Wort Kontext filtern @@ -301,36 +302,36 @@ Filter aktualisieren Du hast noch keine Liste erstellt. Drücke auf den \"+\" Knopf um eine anzulegen. Versteckte Medien automatisch einblenden - Neuer Folgender - Neu geteilt + Neuer Follower + Neuer geteilter Beitrag Neuer Favorit Neue Erwähnung Umfrage beendet - Nachrichten-Sicherung + Sicherung der Beiträge Neue Beiträge - Medien Download + Medien-Download Klingelton auswählen Zeitfenster aktivieren - Möchtest Du %s wirklich sperren\? + Möchtest Du %s wirklich blockieren\? \n -\nEs werden keine Inhalte aus dieser Domain in einer öffentlichen Timeline oder in Deinen Benachrichtigungen angezeigt. Deine Follower aus dieser Domain werden entfernt. +\nDu wirst keine Inhalte mehr von dieser Domain in einer öffentlichen Timeline oder in Deinen Benachrichtigungen sehen. Deine Follower von dieser Domain werden entfernt. Blockiere Domäne - Die Domäne ist blockiert + Die Domäne ist geblockt Rufe entfernten Status ab Peertube Instanz Emoji One verwenden Information - Vorschau in allen Nachrichten anzeigen + Vorschau in allen Beiträgen anzeigen Konto-ID wurde in die Zwischenablage kopiert! Sprache ändern - Lange Nachrichten kürzen - Kürze Nachrichten mit mehr als x Zeilen. Null bedeutet deaktiviert. + Lange Beiträge kürzen + Kürze Beiträge mit mehr als \'x\' Zeilen. Null bedeutet deaktiviert. Mehr anzeigen Weniger anzeigen Dieser Hashtag existiert bereits! - Teilen des Beitrags planen - Teilen geplant! - Nichts geplant! + Planen eines geteilten Beitrags + Geteilter Beitrag ist geplant! + Keine geplanten Beiträge zum Anzeigen! Öffne Menü Profilbild Profilbanner @@ -340,7 +341,7 @@ Unterhaltung erweitern Benutzerdefinierte Emoji Auswahl Favicon - Medien zum Hinzufügen einer Beschreibung + Eine Beschreibung für Medien hinzufügen (für visuell eingeschränkte Personen) Nie 30 Minuten @@ -353,16 +354,16 @@ Sprachen Nur Medien - Heikle Inhalte anzeigen + Heikle Inhalte (NSFW) anzeigen Bot Pixelfed-Instanz Mastodon-Instanz - Irgendwelche davon + Eines davon Alle davon Keine davon - Eines dieser Wörter (durch Leerzeichen getrennt) - Alle diese Wörter (durch Leerzeichen getrennt) - Wörter zum Filter hinzufügen (durch Leerzeichen getrennt) + Einer dieser Hashtags (durch Leerzeichen getrennt) + Alle dieser Hashtags (durch Leerzeichen getrennt) + Hashtags zum Filter hinzufügen (durch Leerzeichen getrennt) Spaltenname ändern Misskey Instanz Angesagt @@ -370,8 +371,8 @@ Kategorie Beschreibung Teilen - Nachrichten (Server) - Nachrichten (Gerät) + Beiträge (Server) + Beiträge (Gerät) Timelines Benutzeroberfläche Kontakte @@ -393,7 +394,7 @@ endet um %s Abstimmen Eine Umfrage, in der Du abgestimmt hast, ist beendet - Eine Ihrer Umfragen wurde beendet + Eine Deiner Umfragen wurde beendet Kategorien Timeline verschieben Timeline ausblenden @@ -406,7 +407,7 @@ Medien immer als sensibel kennzeichnen GNU-Instanz Hashtags in Antworten übernehmen - Lange Drücken, um Medien zu speichern + Lange drücken, um Medien zu speichern Hashtags verwalten Anzeigename Emoji @@ -418,10 +419,10 @@ Grafik erfolgreich gespeichert! Grafik konnte nicht gespeichert werden Umfrageelement hinzufügen - Thema nicht mehr benachrichtigen - Thema erneut benachrichtigen - Nachrichten zu diesem Thema werden wieder benachrichtigt! - Nachrichten zu diesem Thema werden nicht mehr benachrichtigt + Unterhaltung stummschalten + Stummschaltung der Unterhaltung aufheben + Die Unterhaltung ist nicht länger stummgeschaltet! Du wirst wieder über Beiträge in dieser Unterhaltung informiert! + Die Unterhaltung ist stummgeschaltet! Du wirst nicht mehr über Beiträge in dieser Unterhaltung informiert Allgemein Regional Kunst @@ -439,7 +440,7 @@ Server-Regeln Nutzungsbedingungen Registrieren - Diese Instanz verwendet Einladungen. Ihr Konto muss von einem Administrator manuell genehmigt werden, bevor es verwendet werden kann. + Diese Instanz verwendet Einladungen. Dein Konto muss von einem Administrator manuell genehmigt werden, bevor es verwendet werden kann. Die Passwörter stimmen nicht überein! Ungültige E-Mail-Adresse! Du erhältst eine Bestätigungs-E-Mail @@ -454,7 +455,7 @@ \nDu kannst nun Dein Konto verbinden, indem Du %1$s in das erste Feld schreibst und auf die Schaltfläche Verbinden klickst. \n \nWichtig: Wenn Deine Instanz eine Bestätigung benötigt, erhältst Du eine E-Mail, sobald sie geprüft wurde! - Möchtest Du die Nachricht als Entwurf speichern\? + Möchtest Du den Beitrag als Entwurf speichern\? Verwaltung Berichte Ungelöst @@ -473,15 +474,15 @@ Unterbrechen widerrufen Die Anwendung benötigt Zugriff auf die Audioaufzeichnung Sprachnachricht - Innerhalb des Zeitfensters sendet die App Benachrichtigungen. Du kannst dieses Zeitfenster mit dem rechten Schieber zurücksetzen (d. h. leise). + Innerhalb des Zeitfensters sendet die App Benachrichtigungen. Du kannst dieses Zeitfenster mit dem Auswahlfeld rechts zurücksetzen (z. B. Still). Vorschauen in den Timelines werden nicht beschnitten Automatisches Einfügen eines Zeilenumbruchs nach einer Erwähnung, um den ersten Buchstaben groß zu schreiben - Ermöglicht es Inhaltserstellern, den Status ihrer RSS-Feeds zu teilen + Ermöglicht es Inhaltserstellern, ihre Beiträge als RSS-Feed zu teilen Verfassen Auswählen Instanz hinzufügen Absturzberichte aktivieren - Wenn aktiviert, wird ein Absturzbericht erstellt. Anschließend kannst Du diesen teilen. + Wenn aktiviert, wird ein Absturzbericht auf Deinem Mobiltelefon erstellt. Anschließend kannst Du diesen teilen. Fedilab ist abgestürzt :( Du kannst mir den Fehlerreport per E-Mail senden. Dies hilft mir bei der Fehlerbehebung:)\n\nDu kannst weitere Inhalte hinzufügen. Danke! Sichtbarkeit @@ -504,10 +505,10 @@ 3 Tage 7 Tage - Ihre Umfrage kann keine doppelten Optionen haben! - Cache beim Verlassen löschen - Der Cache (Medien, zwischengespeicherte Nachrichten, Daten aus dem eingebauten Browser) wird automatisch gelöscht, wenn die Anwendung verlassen wird. - Möchtest Du diesem Konto nicht mehr folgen\? + Deine Umfrage kann keine doppelten Optionen haben! + Zwischenspeicher beim Verlassen löschen + Der Zwischenspeicher (Medien, zwischengespeicherte Beiträge, Daten aus dem eingebauten Browser) wird automatisch gelöscht, wenn die Anwendung verlassen wird. + Möchtest Du diesem Konto entfolgen\? Bestätigungsdialog vor dem Entfolgen anzeigen Medium Nutze eine alternative Benutzeroberfläche für Medium @@ -519,10 +520,10 @@ Erlaube das Komprimieren von Videos während die Qualität erhalten bleibt. Sortieren nach Links - Ändert die Farbe der Links (URLs, Erwähnungen, Hashtags, etc.) in Nachrichten - Titelzeile des geteilten Beitrags - Ändern der Farbe des Anzeigenamens oben in den Nachrichten - Ändern der Farbe des Benutzernamens am Anfang von Nachrichten + Ändert die Farbe der Links (URLs, Erwähnungen, Hashtags, etc.) in Beiträgen + Kopfzeile des geteilten Beitrags + Ändert die Farbe des Anzeigenamens am Beginn eines Beitrags + Ändert die Farbe des Benutzernamens am Beginn eines Beitrags Ändert die Farbe der Kopfzeile von geteilten Beiträgen Beiträge Hintergrundfarbe der Beiträge in den Timelines @@ -536,7 +537,7 @@ Aktion ausführen Übersetzung Textfarbe - Ändere die Textfarbe in Nachrichten + Ändert die Textfarbe der Beiträge Verwende ein benutzerdefiniertes Design Farbschema Das Theme wurde exportiert @@ -567,11 +568,11 @@ Verifiziert von %1$s (%2$s) Aktion deaktiviert Entfolgen - Etwas ist schief gelaufen. Bitte überprüfen Sie Ihr Downloadverzeichnis in den Einstellungen. + Etwas ist schief gelaufen. Bitte überprüfe Dein Download-Verzeichnis in den Einstellungen. Ankündigungen Keine Ankündigungen! Eine Reaktion hinzufügen - Video-Cache in MB, Null bedeutet keinen Cache. + Video-Zwischenspeicher in MB, Null bedeutet keinen Zwischenspeicher. Wasserzeichen Automatisches Hinzufügen eines Wasserzeichens am unteren Rand von Bildern. Der Text kann für jedes Konto angepasst werden. Keine Dienste gefunden! @@ -584,7 +585,7 @@ Diese Instanz scheint nicht gültig zu sein! Geteilt von Favoritisiert von - Nur für Follower + Nur Follower Z. B.: Sensibler Inhalt Status hinzufügen Status entfernen @@ -593,8 +594,8 @@ Aufnahme anhalten Ich mag es nicht Es ist Spam - Die Nachricht wurde zu Deinen Lesezeichen hinzugefügt! - Die Nachricht wurde von Deinen Lesezeichen entfernt! + Der Beitrag wurde Deinen Lesezeichen hinzugefügt! + Der Beitrag wurde aus Deinen Lesezeichen entfernt! Anzahl der Konten pro Ladevorgang Musik Dieses Feld kann nicht leer sein! @@ -626,18 +627,18 @@ Weiterleiten an %1$s Komm ins Fediverse Erwähnungen - „Mastodon ist nicht wie Twitter oder Facebook, es besteht aus einem Netzwerk von tausenden, durch unterschiedliche Organisationen und Einzelpersonen betriebene, Gemeinschaften, die ein nahtloses Soziale-Medien-Erlebnis bieten.“ + „Mastodon ist keine einzelne Webseite wie Twitter oder Facebook, es besteht aus einem Netzwerk von tausenden, durch unterschiedliche Organisationen und Einzelpersonen betriebene Gemeinschaften, die ein nahtloses Soziale-Medien-Erlebnis bieten.“ Favoriten Änderungen speichern Gesperrt Entsperrt Feld hinzufügen Filter hinzufügen - Bist Du Dir sicher, dass Du alle Benachrichtigungen löschen willst\? Das kann nicht rückgängig gemacht werden. + Bist Du sicher, dass Du alle Benachrichtigungen löschen willst\? Das kann nicht rückgängig gemacht werden. Zeige alle Kategorien Ergebnisse der Umfrage Alle Benachrichtigungen als gelesen markieren - Alle Benachrichtigungen löschen + Alle Benachrichtigungen entfernen Geplant Profil wurde aktualisiert! Listenname ist nicht gültig! @@ -647,7 +648,7 @@ Art der Benachrichtigungen Benachrichtigungen deaktivieren Benachrichtigungstöne - Wähle die Art der Benachrichtigung + Wähle die Art der Benachrichtigungen Themen der Mitwirkenden Timelines anpassen Ein Thema wählen @@ -670,7 +671,7 @@ Bösartige Links, gefälschtes Engagement oder sich wiederholende Antworten Sage uns, was es mit diesem Beitrag auf sich hat Stummschalten %1$s - Blockieren %1$s + Blockiere %1$s Hallo! Wir laden Dich ein, dem Fediverse beizutreten. Bot-Konto Interaktionen @@ -678,7 +679,7 @@ Erlaubt die Erstellung des eigenen Themas Wähle, ob die Basis des Themas dunkel oder hell sein soll Arten der anzuzeigenden Benachrichtigungen - Standardmäßige Sichtbarkeit der Nachrichten: + Standardmäßige Sichtbarkeit der Beiträge: Anzahl an Benachrichtigungen pro Ladevorgang Nutze eine alternative Benutzeroberfläche für Instagram Instagram Frontend Domain @@ -687,15 +688,15 @@ Grundlage des Themas Während dieses Zeitfensters Schaltfläche \"Lesezeichen\" immer anzeigen - Das Konto stammt von einem anderen Server. Die anonymisierte Kopie des Berichts auch dorthin senden\? + Das Konto stammt von einem anderen Server. Eine anonymisierte Kopie des Berichts auch dorthin senden\? Wähle die beste Übereinstimmung Entfolge %1$s Du folgst diesem Konto. Um die Beiträge nicht mehr auf Deiner Startseite zu sehen, entfolge ihm. Du wirst ihre Beiträge nicht mehr sehen. Sie können Dir immer noch folgen und Deine Beiträge sehen und wissen nicht, dass sie stummgeschaltet sind. - Du wirst ihre Beiträge nicht mehr sehen. Sie werden Deine Beiträge nicht sehen und Dir nicht mehr folgen können. Sie werden erkennen können, dass sie blockiert sind. + Du wirst ihre Beiträge nicht mehr sehen. Sie werden Deine Beiträge nicht sehen und Dir nicht mehr folgen können. Sie können erkennen, dass sie geblockt sind. Geteilte Beiträge Aktualisierungen Anderer - Folgende + Folgt Auch geteilt von: Entfolgen bestätigen "Auch favorisiert von: " @@ -712,38 +713,38 @@ Hier tippen, um die Umfrage zu aktualisieren Linie Eckig - Datei-Cache Größe - Mehr Nachrichten laden… - Nachrichten in den Entwürfen gespeichert + Größe des Datei-Zwischenspeichers + Mehr Beiträge laden… + Beiträge als Entwürfe gespeichert Rund Radier-Modus Ankündigung · %1$s - %2$s - Cache leeren - Nachrichten im Cache für Startseite - Nachrichten im Cache für andere Timelines - Cache leeren - Bist Du Dir sicher den Cache zu leeren\? Angehängte Bilder/Videos werden in gespeicherten Entwürfen geschlöscht. + Zwischenspeicher löschen + Zwischenspeicher für Beiträge auf der Startseite + Zwischenspeicher für Beiträge in den anderen Timelines + Zwischenspeicher leeren + Bist Du Dir sicher, den Zwischenspeicher zu löschen\? Wenn Du gespeicherte Entwürfe mit angehängten Medien hast, gehen die Anhänge verloren. Verlassen, ohne das Bild zu speichern\? Form Domäne Personal - Nachrichtensprache + Sprache der Beiträge Meine Instanz Symbolgröße Einstellungen exportieren Vorschaubilder für Medien laden Timelines anzeigen - Die Anzahl neuer Nachrichten in den Timelines wird in der Registerkarte angezeigt + Die Anzahl neuer Beiträge wird in der Registerkarte der Timeline angezeigt Position in den Timelines merken Timelines werden zwischengespeichert, um die Anwendung zu beschleunigen. - Zwischengespeicherte Nachricht + Zwischengespeicherter Beitrag Antwort Anzeigeeinstellungen - Counter anzeigen + Anzahl anzeigen Meine App Mein Konto Versionshinweise - Medien in Benachrichtigung anzeigen + Medien in Benachrichtigungen anzeigen Logo auswählen Logo ändern Ändert das App-Logo auf dem Gerät @@ -754,20 +755,20 @@ Entwurf öffnen Einstellungen importieren Berechtigung nicht gestattet! - Nachricht anheften - Nachricht nicht mehr anheften + Beitrag anheften + Beitrag nicht mehr anheften Exportierte Einstellungen laden - Die Nachricht ist nicht mehr angeheftet! - Zeige Zähler für Nachrichten - Die Nachricht wurde angeheftet - Nachrichten übersetzen - Cache verwenden + Der Beitrag ist nicht mehr angeheftet! + Zeige Zähler für Beiträge + Der Beitrag wurde angeheftet + Beitrag übersetzen + Zwischenspeicher verwenden Medien anzeigen Timelines als Liste Wenn aktiv, werden alle angepinnten Timelines in einem Dropdown-Menü angezeigt Einfache Actionbar - Originalnachricht öffnen - Benachrichtigungsabrufzeit + Original-Beitrag öffnen + Abruf-Intervall Hole Benachrichtigungen Wenn aktiv, hat die App nur eine Anzeigeleiste für alle Timelines Datei konnte nicht hochgeladen werden! @@ -776,7 +777,7 @@ Verzögerung zwischen jedem Aktualisieren setzen Push-Dienst Die App konnte kein Token abrufen - Nachricht bearbeiten + Beitrag bearbeiten Angepinnte Timelines löschen\? Bericht eingereicht Datenschutz-Bestimmungen @@ -784,9 +785,9 @@ Bist Du sicher, dass diese Timeline nicht mehr angepinnt sein soll\? Geblockte Domänen %1$s bearbeitet %2$s - Domäne freigeben + Domäne entsperren Du hast keine Domänen geblockt - Sicher, dass Du %1$s wieder freigeben willst\? + Sicher, dass Du das Blockieren von %1$s aufheben möchtest\? Vorschläge Nicht interessiert Zuweisung aufheben @@ -798,9 +799,9 @@ Konto abgelehnt Konto genehmigt Meldung - Medien in Benachrichtigungen für geteilte Beiträge und Favoriten werden angezeigt + Medien werden in Benachrichtigungen für geteilte Beiträge und Favoriten angezeigt Übersetzung in eine bestimmte Sprache erzwingen. Wähle den ersten Wert, um auf Geräteeinstellungen zurückzusetzen - Nachrichten-Verlauf + Beitragsverlauf Bearbeitet am %1$s Erstellt am %1$s Nicht gelistete Antworten @@ -810,7 +811,7 @@ Aktuelle IP Erlauben Warnen - Benutzer per eMail benachrichtigen + Benutzer per E-Mail benachrichtigen Benutzerdefinierte Warnung Status von Meldungen Stummgeschaltet @@ -826,20 +827,20 @@ App neu starten\? Neustart Du musst die App neu starten um die Änderungen anzuwenden. - Du folgst bisher keinen Hashtags! - Hashtag nicht mehr folgen - Bist Du sicher dass Du diesem Hashtag nicht mehr folgen willst\? - Nicht mehr folgen + Du folgst keinen Hashtags! + Hashtag entfolgen + Bist Du sicher, dass Du diesem Hashtag entfolgen möchtest\? + Entfolgen Hashtag folgen Schreibe den Hashtag, dem Du folgen möchtest - Gefolgten Hashtags + Gefolgte Hashtags Hashtag folgen - Falls aktiv wird die App alle zusammenhängenden Benachrichtigungen einklappen + Wenn aktiviert, wird die App alle zusammengehörenden Benachrichtigungen einklappen Liste bearbeiten Profile Deine Instanz scheint diese Funktion nicht zu unterstützen! Erkunde Trends dieser Instanz - Nachricht bearbeiten + Beitrag wurde bearbeitet Aktualisierungen Mit Warnung verstecken Vollständig verstecken @@ -854,13 +855,13 @@ Timeline löschen Angemeldet Neue Registrierung - Eine Benutzer hat sich registriert + Ein Benutzer hat sich registriert Remote-Profil anzeigen Die App kann keine Remote-Daten finden! - Über Updates benachrichtigen + Benachrichtigung über Updates Domänen Neues Update - Eine Nachricht die Du geteilt hast wurde bearbeitet + Ein Beitrag, den Du geteilt hast, wurde bearbeitet Ein Benutzer hat eine Meldung gesendet Registrierungen Neue Meldung @@ -875,11 +876,11 @@ Betrifft nur \"öffentliche\" Antworten. Falls aktiv, werden Deine Antworten automatisch \"nicht gelistet\" statt \"öffentlich\" Anmeldestatus Sprachen in der Auswahl - Erlaube die Liste der Sprachen in der Auswahl beim Verfassen einer Nachricht zu reduzieren. + Reduziert die Liste der Sprachen-Auswahl beim Verfassen eines Beitrags. Bezeichnung des Hashtags ist nicht zulässig! Schweregrad Medien ablehnen - Mediendateien ablehnen + Medien-Dateien ablehnen Ignoriere alle Meldungen die von dieser Domäne kommen. Für Suspendierungen irrelevant Berichte ablehnen Domänenname verschleiern @@ -890,7 +891,7 @@ Domänen-Blockierung erstellen Mit einem anderen Konto öffnen Berichte ablehnen - Die Sperrung der Domäne verhindert nicht die Erstellung von Konto-Einträgen in der Datenbank, sondern wendet rückwirkend und automatisch bestimmte Moderations-Methoden auf diese Konten an. + Das Blockieren der Domäne verhindert nicht die Erstellung von Konto-Einträgen in der Datenbank, sondern wendet rückwirkend und automatisch bestimmte Moderations-Methoden auf diese Konten an. Ignoriere alle Meldungen die von dieser Domäne kommen. Für Suspendierungen irrelevant Stummschaltung macht die Beiträge des Kontos für alle unsichtbar, die ihm nicht folgen. Suspendierung entfernt alle Inhalte, Medien und Profildaten des Kontos. Verwende Keine, wenn Du nur die Mediendateien ablehnen möchtest. Verschleiere teilweise den Domänennamen in der Liste, wenn die Verteilung der Liste der Domänen-Beschränkunden aktiviert ist @@ -916,15 +917,15 @@ Eigene Farben auswählen Hell - Eigene Farben Dunkel - Eigene Farben - Entfernte Konversation anzeigen - Die Konversation begann auf Deiner Instanz! - Die Anwendung hat die entfernte Nachricht nicht gefunden. + Entfernte Unterhaltung (auf einer anderen Instanz) anzeigen + Die Unterhaltung begann auf Deiner Instanz! + Die Anwendung konnte den Beitrag (einer anderen Instanz) nicht finden. Hashtag stummschalten Anpinnen des Hashtags aufheben Stummschaltung des Hashtags aufheben Bitte später nochmal versuchen. Hashtag anpinnen - Alle Konten auf der Startseite stumm schalten. + Alle Konten auf der Startseite stummschalten. Alle Benutzer auf der Startseite stummschalten Daten importieren Gruppiere geteilte Beiträge auf der Startseite @@ -934,8 +935,8 @@ Hashtag entfolgen Auf der Startseite stummschalten Stummschaltung auf der Startseite aufheben - Entfernt den linken Rand in den Timelines, um Nachrichten kompakter darzustellen - Übersetzungs-Knopf immer anzeigen + Entfernt den linken Rand in den Timelines, um Beiträge kompakter darzustellen + Schaltfläche \"Übersetzen\" immer anzeigen Kartenansicht Entferne linken Rand Version @@ -943,4 +944,24 @@ Übersetzungsdienst Übersetzungsdienst-Version Übersetzungsdienst + Sichtbarkeit der Symbole + Du kannst die Symbole am unteren Rand bedenkenlos entfernen, um mehr Platz zu erhalten. Sie befinden sich auch im Untermenü. + Format des Beitrags + Format des Beitrags + Symbole für zusätzliche Funktionen + Zeige den Knopf zum \"Zitieren\" + Zeige die Knöpfe für \"Reaktionen\" + Zusätzliche Funktionen + Wenn diese Option aktiviert ist, zeigt die App zusätzliche Funktionen an. Diese Funktion ist für soziale Plattformen wie Pleroma, Akkoma oder Glitch Social + Sofern Deine Instanz nicht alle zusätzlichen Funktionen unterstützt, kannst Du diese Symbole ausblenden + Liste + Eigene + Gefolgte + Sichtbarkeit von Antworten + Sichtbarkeit ausschließen + Blase + Die Anwendung ruft öffentlich verfügbare Profile ab, um deren Beiträge darzustellen. Interaktionen mit föderierten Beiträgen benötigen einen zusätzlichen Arbeitsschritt. + Profile auf anderen Instanzen + Nur Lokal + Zeige den Knopf \"Nur Lokal\" \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1cc6350d9..f472dd914 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -933,4 +933,29 @@ Xestionar contas Eliminar marxe esquerda Eliminar a marxe esquerda nas cronoloxías para compactar máis as mensaxes + Tradutor + Mostrar botón \"Só local\" + Chave da API do tradutor + Versión + Ao activar esta opción a app mostrará características extra. Temos esta función para software social tipo Pleroma, Akkoma ou Glitch Social + Formato da publicación + Iconas para Extras + Se a túa instancia non acepta características extra, podes agochar estas iconas + Mostrar botón \"Cita\" + Mostrar botóns de \"Reaccións\" + Propio + Seguindo + Perfís remotos + Só local + Burbulla + Lista + Visibilidade da resposta + Visibilidade da exclusión + Tradutor + Características extra + Visibilidade das iconas + Formato da publicación + Versión do tradutor + Podes agochar tranquilamente estas iconas ao pé para ter máis espazo. Están tamén no submenú. + A app mostrará públicamente os perfís para obter tódalas mensaxes. As interaccións precisarán un paso extra para federar as mensaxes. \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c67ed5d53..676ecadd9 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -185,7 +185,7 @@ 何もすることはできません 翻訳中にエラーが発生しました! - 一回あたりに読み込む投稿数 + 1回あたりに読み込む投稿の数 GIFアバターを無効にする フォローされたときに通知する 投稿がブーストされたときに通知する @@ -501,7 +501,7 @@ このアカウントのフォローを解除しますか? フォローを解除する前に確認を表示する Medium - Mediumの代替フロントエンドを使用する + Mediumの代替フロントエンドを使用します Mediumフロントエンドのドメイン プッシュ通知システムを使用してリアルタイムに通知を取得します。 メモを追加 @@ -613,11 +613,11 @@ 1回あたりに読み込む通知の数 音楽 Twitter - Twitterの代替フロントエンドを使用する + Twitterの代替フロントエンドを使用します Twitterフロントエンドのドメイン Instagramフロントエンドのドメイン Reddit - Redditの代替フロントエンドを使用する + Redditの代替フロントエンドを使用します 続行 カスタム 次のユーザーにブーストされました @@ -660,11 +660,11 @@ 1回あたりに読み込むアカウントの数 この項目は空欄にできません! YouTube - YouTubeの代替フロントエンドを使用する + YouTubeの代替フロントエンドを使用します YouTubeフロントエンドのドメイン Redditフロントエンドのドメイン Instagram - Instagramの代替フロントエンドを使用する + Instagramの代替フロントエンドを使用します フォロワーのみ ステータスを追加 このサーバーは無効なようです! @@ -861,7 +861,7 @@ 登録しました データをインポート 通報を却下 - 新着情報の通知 + 新着情報を通知する 新しい通報(モデレーター) 新着情報 新規登録 @@ -890,7 +890,7 @@ 通知を消去しない ダイナミックカラー 個人設定の壁紙の配色に合わせます。 - 翻訳ボタンを常に表示します + 翻訳ボタンを常に表示する 通報を却下 ドメイン名の難読化 公開コメント @@ -904,7 +904,7 @@ カスタムカラーの設定 時間をおいて再試行してください。 ホームでのミュート - タイムライン上の左のマージンを削除し、投稿をよりコンパクトにします + タイムライン上の左の余白をなくし、投稿をよりコンパクトにします 通報しました ドメイン 新規登録(モデレーター) @@ -921,12 +921,37 @@ 全てのアカウントがホームタイムライン上でミュートされます。 全てのユーザーをホームでのミュート対象として追加 選択をミュート - ホームタイムラインでブーストをグループ化 + ホームタイムラインでブーストをグループ化する アカウントの管理 - 左のマージンを削除 + 左の余白を詰める このドメインからの通報を全て無視します。サスペンドとは無関係です ドメイン制限リストの公開が有効なとき、リスト内のドメイン名を一部難読化します 内部コメント このドメイン制限に関するコメントで、モデレーターによって内部で使用されます。 このドメイン制限に関するコメントで、ドメイン制限リストの公開が有効な場合に一般外部向けに使用されます。 + 翻訳ツールのバージョン + 「引用」ボタンを表示 + バブル + 表示設定を除外する + フォロー中 + 自分 + リモートプロフィール + 翻訳 + 翻訳ツールのAPIキー + バージョン + 投稿形式 + スペースを増やすために下部にあるこれらのアイコンを安全に非表示にできます。サブメニューからも利用可能です。 + 追加機能 + このオプションを有効にするとアプリは追加の機能を表示します。これはPleromaやAkkoma、Glitch Socialなどのソフトウェア向きの機能です + アイコンの表示設定 + 投稿形式 + 追加機能のアイコン + サーバーが一部の追加機能を無効にしている場合、それらのアイコンを非表示にできます + アプリは全ての投稿を表示するために公開プロフィールにアクセスします。交流する場合は投稿を連合させるために追加の操作を必要とします。 + 「リアクション」ボタンを表示 + 返信の公開範囲 + ローカルのみ + リスト + 「ローカルのみ」ボタンを表示 + 翻訳 \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f49402012..6874f8d7c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -609,7 +609,7 @@ Profiel is bijgewerkt! Kies een thema Soorten meldingen tonen - Laad meer berichten… + Meer laden… Ovaal Rechthoek Wis modus @@ -935,4 +935,22 @@ Negeer tag Vertaalknop altijd tonen Maak tag los + Importeren gegevens + Beheer accounts + Verwijder linker kantlijn + Vertaler + Vertaler + Vertaler API key + Versie + Vertaler versie + Zet optie aan zodat de app meer mogelijkheden laat zien. Dit is voor social softwares zoals Pleroma, Akkoma or Glitch Social + Extra mogelijkheden + Zichtbaarheid iconen + Je kan deze iconen onderaan veilig verbergen voor meer ruimte. Ze staan ook in het submenu. + Iconen voor extra mogelijkheden + Als je instance niet alle mogelijkheden toestaat, kan je deze iconen verbergen + Toon de \"Quote\" knop + Toon \"Reacties\" knop + Groepeer reblogs in eigen tijdlijn + Verwijder linker kantlijn in tijdlijnen voor compactere berichten \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e9e0e54a4..b6debd1a0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -513,7 +513,7 @@ Вы хотите отписаться от этого аккаунта? Предупреждать перед отменой подписки Replace Medium links - Replace medium.com links with an open source alternative front-end focused on privacy. + Использовать альтернативный интерфейс для Medium Default: scribe.rip Использовать систему push-уведомлений для получения уведомлений в режиме реального времени. Добавить примечания @@ -581,4 +581,139 @@ Вам нужен дистрибьютор для получения push-уведомлений.\nВы найдете более подробную информацию по адресу %1$s.\n\nВы также можете отключить push-уведомления в настройках для игнорирования этого сообщения. Выберите дистрибьютора Размер значков + YouTube + Домен интерфейса YouTube + Использовать альтернативный интерфейс для YouTube + Twitter + Добавить фильтр + Сохранить изменения + Бот-аккаунт + Запланировано + Выберите тему + Упоминания + Обновления у людей + Вы уверены, что хотите удалить это поле\? + Также продвинуто: + Я модератор + Овал + Прямоугольник + Линия + Фигура + Выйти без сохранения изображения\? + Нажмите здесь, чтобы обновить опрос + Получать уведомления каждые: + Экспорт настроек + Репосты + Взаимодействия + Удалить поле + Тип уведомлений + Импорт настроек + Выберите, должна ли основа темы быть темной или светлой + Избранные + Результаты опросов + Подписки + Отображать все категории + Профиль обновлён! + Недопустимое имя списка! + Для этого списка не найдено ни одного аккаунта! + Список изменений + Использовать системный язык по умолчанию + Мое приложение + Сообщение отправлено! + Добавить поле + Удалить все уведомления + Пометить все уведомления как прочитанные + Режим ластика + Настроить ленты + Всегда отображать кнопку закладки + Выберите тип уведомлений + Звук уведомлений + Отключить уведомления + В это время + "Также в избранном у: " + Мой аккаунт + Медиафайлы не могут быть загружены! + Подтвердить отписку + Тип опроса: + Длительность опроса: + Всегда отображать кнопку перевода + Удалить кеш + Вы уверены, что хотите удалить все уведомления\? Это не может быть отменено. + Загрузить экспортированные настройки + Использовать альтернативный интерфейс для Twitter + Домен интерфейса Twitter + Instagram + Использовать альтернативный интерфейс для Instagram + Домен интерфейса Instagram + Использовать альтернативный интерфейс для Reddit + Reddit + Домен интерфейса Reddit + Перезапустить + Это нарушает правила сервера + Вы знаете, что это нарушает определенные правила + Изменить логотип + Пользователь + Модератор + Мне это не нравится + Количество аккаунтов на загрузку + Загружать предпросмотр + Подтверждено + Не подтверждено + Недопустимое имя тега! + Переводчик + Версия переводчика + Администратор + Содержимое лент будет временно сохраняться у вас, чтобы ускорить работу приложения. + Отображать медиа + Видимость иконок + Переводчик + API-ключ переводчика + Количество уведомлений на загрузку + Скажите нам, что не так с этим постом\? + Выберите наиболее подходящее + Отписаться от тега + Вы уверены, что хотите отписаться от этого тега\? + Подписаться на тег + Вы не подписаны ни на один тег! + Отписаться + Отслеживаемые теги + Подписаться на тег + Другое + Показать содержимое > + Присоединяйтесь к fediverse + Привет! Приглашаем Вас присоединиться к Fediverse. + Проблема не подпадает под другие категории + У вас нет аккаунта\? + Мой инстанс + Профили + Отображать медиа в уведомлениях + Запоминать позицию на ленте + Будут отображаться медиа в уведомлениях для репостов и избранного + Версия + Скрыть содержимое < + Пожаловаться на %1$s + Это НЕ то что вы бы хотели видеть + Использовать кэш + Это спам + Вредоносные ссылки, повторяющиеся ответы + «Mastodon — это не один веб-сайт, такой как Twitter или Facebook, это сеть из тысяч сообществ, управляемых различными организациями и частными лицами, которые обеспечивают бесперебойную работу в социальных сетях». + Измените логотип приложения на вашем устройстве + Перезапустить приложение\? + Вы должны перезапустить приложение, чтобы применить изменения. + Напишите тег для подписки + Похоже, ваш инстанс не поддерживает эту функцию! + Дополнительные функции + Включив эту опцию, приложение будет отображать дополнительные функции. Это предназначается для социальных платформ, таких как Pleroma, Akkoma или Glitch Social + Вы можете смело спрятать эти значки внизу, чтобы было больше места. Они также находятся в подменю. + Это поле не может быть пустым! + Добавлен в избранное + Только для подписчиков + Музыка + Продолжить + Пользовательский + Другое + Настройки успешно экспортированы + Настройки успешно импортированы + Настройки экспортированы + Продвинут \ No newline at end of file diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 3a4733e8e..dc9289c41 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -929,4 +929,25 @@ Importa datos Agrupa is cumpartziduras in sa lìnia de tempus printzipale Amministra is contos + Funtzionalidades extra + Formadu de publicatzione + Iconas pro funtzionalidades extra + Visibilidade de is iconas + Formadu de publicatzione + Ammustra su butone \"Tzita\" + Ammustra su butone \"Reatziones\" + Profilos remotos + Bullunca + Lista + Sighende + Boga su màrgine a manca + Boga su màrgine a manca de is lìnias de tempus pro chi is messàgios siant prus cumpatos + Tradutore + Tradutore + Crae API de su tradutore + Versione + Versione de su tradutore + Ativende cussa optzione s\'aplicatzione at a ammustrare funtzionalidades extra. Custa funtzionalidade b\'est pro programmas sotziales che a Pleroma, Akkoma o Glitch Social + Podes cuare custas iconas in manera segura in fundu pro tènnere prus logu. Sunt fintzas in su suta-menù. + Si s\'istàntzia tua no atzetat unas cantas funtzionalidades extra podes cuare custas iconas \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 712ab75be..784df89b2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -942,4 +942,24 @@ Çevirmen Çevirmen API anahtarı Sürüm + Ek özellikler + Bu seçeneği etkinleştirdiğinizde uygulama ek özellikler gösterecektir. Bu özellik Pleroma, Akkoma veya Glitch Social gibi sosyal yazılımlar için yapılır + Gönderi biçimi + Gönderi biçimi + Ek özellikler için simgeler + Sunucunuz bazı ek özellikleri kabul etmiyorsa, bu simgeleri gizleyebilirsiniz + \"Alıntı\" düğmesini göster + \"Tepkiler\" düğmelerini göster + Daha fazla alana sahip olmak için bu simgeleri alt kısımda güvenle gizleyebilirsiniz. Ayrıca alt menüde de bulunurlar. + Simge görünürlüğü + Hariç tutma görünürlüğü + Listele + Takip edilenler + Baloncuk + Yanıt görünürlüğü + Kendi + Uzak profiller + Uygulama, tüm mesajları almak için herkese açık profilleri görüntüleyecektir. Etkileşimlerin mesajları birleştirmek için ek bir adıma ihtiyacı olacaktır. + \"Yalnızca yerel\" düğmesini göster + Yalnızca yerel \ No newline at end of file diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml index d73f4a359..22d7f0043 100644 --- a/app/src/main/res/values-w1240dp/dimens.xml +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -1,3 +1,3 @@ - 200dp + 48dp \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml index 22d7f0043..2a235a023 100644 --- a/app/src/main/res/values-w600dp/dimens.xml +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -1,3 +1,3 @@ - 48dp + 32dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e929584c..e83cc5cc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -770,6 +770,16 @@ text/x.misskeymarkdown + + 0 + 1 + + + + No + Yes + + Always Wifi only @@ -1361,10 +1371,14 @@ SET_UNFOLLOW_VALIDATION SET_USE_SINGLE_TOPBAR SET_DISPLAY_COUNTERS + SET_DISPLAY_COMPACT_ACTION_BUTTON + SET_TIMELINES_IN_A_LIST SET_LED_COLOUR_VAL_N SET_SHOW_BOOSTS SET_SHOW_REPLIES + + SET_DISABLE_ANIMATED_EMOJI SET_CAPITALIZE SET_THEME_BASE @@ -1428,11 +1442,13 @@ SET_FILTER_REGEX_PUBLIC SET_NOTIF_VALIDATION SET_DISPLAY_BOOKMARK + SET_PIXELFED_PRESENTATION SET_DISPLAY_QUOTES SET_DISPLAY_REACTIONS SET_DISPLAY_TRANSLATE SET_POST_FORMAT + SET_COMPOSE_LOCAL_ONLY SET_TRANSLATOR SET_TRANSLATOR_VERSION @@ -1443,8 +1459,10 @@ SET_NOTIF_VALIDATION_FAV SET_DISPLAY_COUNTER_FAV_BOOST SET_REMOVE_LEFT_MARGIN - SET_EXTAND_EXTRA_FEATURES + SET_PROFILE_REMOTELY + SET_EXTAND_EXTRA_FEATURES + SET_DISPLAY_LOCAL_ONLY SET_INNER_MARKER SET_NOTIF_SILENT SET_REMEMBER_POSITION @@ -2174,4 +2192,17 @@ If your instance does not accept some extra features, you can hide these icons Display the \"Quote\" button Display \"Reactions\" buttons + Bubble + Exclude visibility + Reply visibility + List + Following + Self + Remote profiles + The app will display publicly profiles to get all messages. Interactions will need an extra step to federate messages. + Local only + Display \"Local only\" button + Pixelfed presentation for media + Compact action buttons + Buttons at the bottom of messages will not take the whole width \ No newline at end of file diff --git a/app/src/main/res/xml/pref_extra_features.xml b/app/src/main/res/xml/pref_extra_features.xml index 91d0a2637..26af2edf3 100644 --- a/app/src/main/res/xml/pref_extra_features.xml +++ b/app/src/main/res/xml/pref_extra_features.xml @@ -47,6 +47,12 @@ app:key="@string/SET_DISPLAY_REACTIONS" app:singleLineTitle="false" app:title="@string/set_display_reaction_indication" /> + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_interface.xml b/app/src/main/res/xml/pref_interface.xml index 55b5f58ec..da7f64c32 100644 --- a/app/src/main/res/xml/pref_interface.xml +++ b/app/src/main/res/xml/pref_interface.xml @@ -28,6 +28,14 @@ app:summary="@string/set_remove_left_margin" app:title="@string/set_remove_left_margin_title" /> + + + + - + Interface) + +Changed: +- Full rework on links in messages (also mentions and tags) + +Fixed: +- Spoiler text when editing +- Fix watermarks \ No newline at end of file diff --git a/src/fdroid/fastlane/metadata/android/en/changelogs/459.txt b/src/fdroid/fastlane/metadata/android/en/changelogs/459.txt new file mode 100644 index 000000000..e5cf9e108 --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/en/changelogs/459.txt @@ -0,0 +1,5 @@ +Added: +- Glitch: Allow to post messages locally (Can be turned off in Settings) + +Fixed: +- Crashes \ No newline at end of file diff --git a/src/fdroid/fastlane/metadata/android/en/changelogs/460.txt b/src/fdroid/fastlane/metadata/android/en/changelogs/460.txt new file mode 100644 index 000000000..5aa91b71d --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/en/changelogs/460.txt @@ -0,0 +1,8 @@ +Fixed: +- Cross-compose: Wrong instance emojis +- Custom emojis not displayed in notifications +- Fav/Boost markers with shared messages +- Empty notifications +- Fix cw removed when replying +- Fix expand media with fit preview images when sensitive +- Fix an issue with fetch more displayed too often (cache clear will help or wait new messages) \ No newline at end of file diff --git a/src/fdroid/fastlane/metadata/android/en/changelogs/461.txt b/src/fdroid/fastlane/metadata/android/en/changelogs/461.txt new file mode 100644 index 000000000..c42cfb77a --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/en/changelogs/461.txt @@ -0,0 +1,13 @@ +Added: +- Pixelfed: Custom layout to display Media fully +*(Settings > Timelines > Pixelfed Presentation) - Also works for other softwares when there are media + +Changed: +- Add pinned tag in "any" to avoid to lose it when renaming timeline + +Fixed: +- Fix push notifications with several accounts +- Fix quotes with tags/mentions +- Fix notifications +- Fix sending multiple media +- Some crashes \ No newline at end of file diff --git a/src/fdroid/fastlane/metadata/android/en/changelogs/462.txt b/src/fdroid/fastlane/metadata/android/en/changelogs/462.txt new file mode 100644 index 000000000..f737820eb --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/en/changelogs/462.txt @@ -0,0 +1,21 @@ +Added: + +- Add Bubble timeline support in extra-features with filters +- Allow to display public profiles by default to get all messages (Settings > Interface) +- Glitch: Allow to post messages locally (Can be turned off in Settings) +- Pixelfed: Custom layout to display Media fully (Also works for other software when there are media) +- Allow to align left action buttons in messages + +Changed: +- Full rework on links in messages (also mentions and tags) +- Add pinned tag in "any" to avoid to lose it when renaming timeline + +Fixed: +- Links to messages not handled by the app +- CW when editing a message +- Fix push notifications with several accounts +- New messages or edition notifications not pushed +- Fix quotes with tags/mentions +- Fix notifications +- Fix sending multiple media +- Fix crashes \ No newline at end of file