diff --git a/app/build.gradle b/app/build.gradle index 2046c73dd..e39b62ae5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { defaultConfig { minSdk 21 targetSdk 33 - versionCode 462 - versionName "3.14.0" + versionCode 463 + versionName "3.14.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } flavorDimensions "default" @@ -97,6 +97,10 @@ dependencies { implementation 'org.framagit.tom79:SparkButton:1.0.13' implementation "com.github.bumptech.glide:glide:4.14.2" implementation "com.github.bumptech.glide:okhttp3-integration:4.14.2" + implementation("com.github.bumptech.glide:recyclerview-integration:4.14.2") { + // Excludes the support library because it's already included by Glide. + transitive = false + } implementation "org.jsoup:jsoup:1.15.1" diff --git a/app/src/main/assets/release_notes/notes.json b/app/src/main/assets/release_notes/notes.json index c55a9703b..53210353f 100644 --- a/app/src/main/assets/release_notes/notes.json +++ b/app/src/main/assets/release_notes/notes.json @@ -1,4 +1,9 @@ [ + { + "version": "3.14.1", + "code": "463", + "note": "Added:\n- Search bar: display suggestions when starting by \"@\" or \"#\"\n\nChanged:\n- Preload media in timelines to avoid jumps\n- Search: Automatically switch to account tab if no results for tags\n\nFixed:\n- Fix jumps with the fetch more feature\n- Fix videos cannot be saved\n- Tags cannot be pinned when there are no custom tabs\n- PixelFed view: NSFW not honored\n- Fix crashes" + }, { "version": "3.14.0", "code": "462", diff --git a/app/src/main/java/app/fedilab/android/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/BaseMainActivity.java index 43b4ddb3b..ff7cffd95 100644 --- a/app/src/main/java/app/fedilab/android/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/BaseMainActivity.java @@ -23,11 +23,13 @@ import static app.fedilab.android.ui.drawer.StatusAdapter.sendAction; import android.Manifest; import android.annotation.SuppressLint; +import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.database.MatrixCursor; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -35,6 +37,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.provider.BaseColumns; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; @@ -63,6 +66,7 @@ import androidx.appcompat.widget.SearchView; import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityOptionsCompat; import androidx.core.view.GravityCompat; +import androidx.cursoradapter.widget.CursorAdapter; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; @@ -75,7 +79,9 @@ import androidx.preference.PreferenceManager; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.gif.GifDrawable; +import com.bumptech.glide.request.FutureTarget; import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; @@ -91,6 +97,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -129,6 +136,7 @@ import app.fedilab.android.client.entities.api.Filter; import app.fedilab.android.client.entities.api.Instance; import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.app.Account; import app.fedilab.android.client.entities.app.BaseAccount; import app.fedilab.android.client.entities.app.BottomMenu; @@ -146,12 +154,15 @@ import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.PinnedTimelineHelper; import app.fedilab.android.helper.PushHelper; +import app.fedilab.android.ui.drawer.AccountsSearchTopBarAdapter; +import app.fedilab.android.ui.drawer.TagSearchTopBarAdapter; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonConversation; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; import app.fedilab.android.ui.fragment.timeline.FragmentNotificationContainer; import app.fedilab.android.viewmodel.mastodon.AccountsVM; import app.fedilab.android.viewmodel.mastodon.FiltersVM; import app.fedilab.android.viewmodel.mastodon.InstancesVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; import app.fedilab.android.viewmodel.mastodon.TimelinesVM; import app.fedilab.android.viewmodel.mastodon.TopBarVM; import es.dmoral.toasty.Toasty; @@ -777,6 +788,75 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt @Override public boolean onQueryTextChange(String newText) { + String pattern = "^(@[\\w_-]+@[a-z0-9.\\-]+|@[\\w_-]+)"; + final Pattern mentionPattern = Pattern.compile(pattern); + String patternTag = "^#([\\w-]{2,})$"; + final Pattern tagPattern = Pattern.compile(patternTag); + Matcher matcherMention, matcherTag; + matcherMention = mentionPattern.matcher(newText); + matcherTag = tagPattern.matcher(newText); + if (newText.trim().isEmpty()) { + binding.toolbarSearch.setSuggestionsAdapter(null); + } + if (matcherMention.matches()) { + String[] from = new String[]{SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_TEXT_1}; + int[] to = new int[]{R.id.account_pp, R.id.account_un}; + String searchGroup = matcherMention.group(); + AccountsVM accountsVM = new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class); + MatrixCursor cursor = new MatrixCursor(new String[]{BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_ICON_1, + SearchManager.SUGGEST_COLUMN_TEXT_1}); + accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, 5, false, false) + .observe(BaseMainActivity.this, accounts -> { + if (accounts == null) { + return; + } + AccountsSearchTopBarAdapter cursorAdapter = new AccountsSearchTopBarAdapter(BaseMainActivity.this, accounts, R.layout.drawer_account_search, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); + binding.toolbarSearch.setSuggestionsAdapter(cursorAdapter); + new Thread(() -> { + int i = 0; + for (app.fedilab.android.client.entities.api.Account account : accounts) { + FutureTarget futureTarget = Glide + .with(BaseMainActivity.this.getApplicationContext()) + .load(account.avatar_static) + .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); + File cacheFile; + try { + cacheFile = futureTarget.get(); + cursor.addRow(new String[]{String.valueOf(i), cacheFile.getAbsolutePath(), "@" + account.acct}); + i++; + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + runOnUiThread(() -> cursorAdapter.changeCursor(cursor)); + }).start(); + + }); + } else if (matcherTag.matches()) { + SearchVM searchVM = new ViewModelProvider(BaseMainActivity.this).get(SearchVM.class); + String[] from = new String[]{SearchManager.SUGGEST_COLUMN_TEXT_1}; + int[] to = new int[]{R.id.tag_name}; + String searchGroup = matcherTag.group(); + MatrixCursor cursor = new MatrixCursor(new String[]{BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_TEXT_1}); + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, null, + "hashtags", false, true, false, 0, + null, null, 10).observe(BaseMainActivity.this, + results -> { + if (results == null || results.hashtags == null) { + return; + } + TagSearchTopBarAdapter cursorAdapter = new TagSearchTopBarAdapter(BaseMainActivity.this, results.hashtags, R.layout.drawer_tag_search, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); + binding.toolbarSearch.setSuggestionsAdapter(cursorAdapter); + int i = 0; + for (Tag tag : results.hashtags) { + cursor.addRow(new String[]{String.valueOf(i), "#" + tag.name}); + i++; + } + runOnUiThread(() -> cursorAdapter.changeCursor(cursor)); + }); + } return false; } }); 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 af6b0c533..4843f4670 100644 --- a/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/HashTagActivity.java @@ -105,21 +105,22 @@ public class HashTagActivity extends BaseActivity { }); ReorderVM reorderVM = new ViewModelProvider(HashTagActivity.this).get(ReorderVM.class); reorderVM.getAllPinned().observe(HashTagActivity.this, pinned -> { - if (pinned != null) { - this.pinned = pinned; - pinnedTag = false; - if (pinned.pinnedTimelines != null) { - for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { - if (pinnedTimeline.tagTimeline != null) { - if (pinnedTimeline.tagTimeline.name.equalsIgnoreCase(stripTag)) { - this.pinnedTimeline = pinnedTimeline; - pinnedTag = true; - break; - } + if (pinned == null) { + pinned = new Pinned(); + pinned.pinnedTimelines = new ArrayList<>(); + } + pinnedTag = false; + if (pinned.pinnedTimelines != null) { + for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { + if (pinnedTimeline.tagTimeline != null) { + if (pinnedTimeline.tagTimeline.name.equalsIgnoreCase(stripTag)) { + this.pinnedTimeline = pinnedTimeline; + pinnedTag = true; + break; } } - invalidateOptionsMenu(); } + invalidateOptionsMenu(); } }); if (MainActivity.filterFetched && MainActivity.mainFilters != null) { diff --git a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java index 2e6584a83..0f9c732a7 100644 --- a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java @@ -16,7 +16,9 @@ package app.fedilab.android.activities; import android.app.SearchManager; import android.content.Context; +import android.database.MatrixCursor; import android.os.Bundle; +import android.provider.BaseColumns; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -26,22 +28,38 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; +import androidx.cursoradapter.widget.CursorAdapter; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.lifecycle.ViewModelProvider; import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.FutureTarget; +import com.bumptech.glide.request.target.Target; import com.google.android.material.tabs.TabLayout; import org.jetbrains.annotations.NotNull; +import java.io.File; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; +import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.databinding.ActivitySearchResultTabsBinding; import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.drawer.AccountsSearchTopBarAdapter; +import app.fedilab.android.ui.drawer.TagSearchTopBarAdapter; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; +import app.fedilab.android.viewmodel.mastodon.SearchVM; import es.dmoral.toasty.Toasty; @@ -140,6 +158,75 @@ public class SearchResultTabActivity extends BaseBarActivity { @Override public boolean onQueryTextChange(String newText) { + String pattern = "^(@[\\w_-]+@[a-z0-9.\\-]+|@[\\w_-]+)"; + final Pattern mentionPattern = Pattern.compile(pattern); + String patternTag = "^#([\\w-]{2,})$"; + final Pattern tagPattern = Pattern.compile(patternTag); + Matcher matcherMention, matcherTag; + matcherMention = mentionPattern.matcher(newText); + matcherTag = tagPattern.matcher(newText); + if (newText.trim().isEmpty()) { + searchView.setSuggestionsAdapter(null); + } + if (matcherMention.matches()) { + String[] from = new String[]{SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_TEXT_1}; + int[] to = new int[]{R.id.account_pp, R.id.account_un}; + String searchGroup = matcherMention.group(); + AccountsVM accountsVM = new ViewModelProvider(SearchResultTabActivity.this).get(AccountsVM.class); + MatrixCursor cursor = new MatrixCursor(new String[]{BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_ICON_1, + SearchManager.SUGGEST_COLUMN_TEXT_1}); + accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, 5, false, false) + .observe(SearchResultTabActivity.this, accounts -> { + if (accounts == null) { + return; + } + AccountsSearchTopBarAdapter cursorAdapter = new AccountsSearchTopBarAdapter(SearchResultTabActivity.this, accounts, R.layout.drawer_account_search, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); + searchView.setSuggestionsAdapter(cursorAdapter); + new Thread(() -> { + int i = 0; + for (app.fedilab.android.client.entities.api.Account account : accounts) { + FutureTarget futureTarget = Glide + .with(SearchResultTabActivity.this.getApplicationContext()) + .load(account.avatar_static) + .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL); + File cacheFile; + try { + cacheFile = futureTarget.get(); + cursor.addRow(new String[]{String.valueOf(i), cacheFile.getAbsolutePath(), "@" + account.acct}); + i++; + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + runOnUiThread(() -> cursorAdapter.changeCursor(cursor)); + }).start(); + + }); + } else if (matcherTag.matches()) { + SearchVM searchVM = new ViewModelProvider(SearchResultTabActivity.this).get(SearchVM.class); + String[] from = new String[]{SearchManager.SUGGEST_COLUMN_TEXT_1}; + int[] to = new int[]{R.id.tag_name}; + String searchGroup = matcherTag.group(); + MatrixCursor cursor = new MatrixCursor(new String[]{BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_TEXT_1}); + searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, null, + "hashtags", false, true, false, 0, + null, null, 10).observe(SearchResultTabActivity.this, + results -> { + if (results == null || results.hashtags == null) { + return; + } + TagSearchTopBarAdapter cursorAdapter = new TagSearchTopBarAdapter(SearchResultTabActivity.this, results.hashtags, R.layout.drawer_tag_search, null, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); + searchView.setSuggestionsAdapter(cursorAdapter); + int i = 0; + for (Tag tag : results.hashtags) { + cursor.addRow(new String[]{String.valueOf(i), "#" + tag.name}); + i++; + } + runOnUiThread(() -> cursorAdapter.changeCursor(cursor)); + }); + } return false; } }); @@ -188,6 +275,10 @@ public class SearchResultTabActivity extends BaseBarActivity { } + public void moveToAccount() { + binding.searchViewpager.setCurrentItem(1); + } + /** * Pager adapter for the 4 fragments */ diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java b/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java index f569a2422..0ccbacc27 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java @@ -45,13 +45,14 @@ public class Attachment implements Serializable { public String local_path; @SerializedName("meta") public Meta meta; + @SerializedName("sensitive") + public boolean sensitive = false; public String peertubeHost = null; public String peertubeId = null; public String focus = null; public String translation = null; - public float measuredWidth = -1.f; public static class Meta implements Serializable { @SerializedName("focus") 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 310edb0d4..2d31e6059 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -522,9 +522,8 @@ public class Helper { long months = days / 30; long years = days / 365; - String format = DateFormat.getDateInstance(DateFormat.SHORT).format(date); if (years > 0) { - return format; + return DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(date); } else if (months > 0 || days > 7) { //Removes the year depending of the locale from DateFormat.SHORT format SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()); 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 770436b5f..b6f65f7c7 100644 --- a/app/src/main/java/app/fedilab/android/helper/MediaHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java @@ -94,30 +94,14 @@ public class MediaHelper { try { request = new DownloadManager.Request(Uri.parse(url.trim())); } catch (Exception e) { + e.printStackTrace(); Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); return -1; } try { String mime = getMimeType(url); - final String fileName = URLUtil.guessFileName(url, null, null); request.allowScanningByMediaScanner(); - String myDir; - if (mime.toLowerCase().startsWith("video")) { - myDir = Environment.DIRECTORY_MOVIES + "/" + context.getString(R.string.app_name); - } else if (mime.toLowerCase().startsWith("audio")) { - myDir = Environment.DIRECTORY_MUSIC + "/" + context.getString(R.string.app_name); - } else { - myDir = Environment.DIRECTORY_DOWNLOADS; - } - - if (!new File(myDir).exists()) { - boolean created = new File(myDir).mkdir(); - if (!created) { - Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); - return -1; - } - } if (mime.toLowerCase().startsWith("video")) { request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MOVIES, context.getString(R.string.app_name) + "/" + fileName); } else if (mime.toLowerCase().startsWith("audio")) { 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 f95e42c38..7a39946da 100644 --- a/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java @@ -464,6 +464,11 @@ public class PinnedTimelineHelper { break; case NITTER: item.setIcon(R.drawable.nitter); + if (pinnedTimeline.remoteInstance.displayName.trim().length() > 0) { + item.setTitle(pinnedTimeline.remoteInstance.displayName); + } else { + item.setTitle(pinnedTimeline.remoteInstance.host); + } break; } break; 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 f53c1172a..e030eeae9 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -99,7 +99,9 @@ public class SpannableHelper { public static Spannable convert(Context context, String text, Status status, Account account, Announcement announcement, WeakReference viewWeakReference, Status.Callback callback) { - + if (text == null) { + return null; + } SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; boolean customLight = sharedpreferences.getBoolean(context.getString(R.string.SET_CUSTOMIZE_LIGHT_COLORS), false); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/AccountsSearchTopBarAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/AccountsSearchTopBarAdapter.java new file mode 100644 index 000000000..6de07a1b3 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/AccountsSearchTopBarAdapter.java @@ -0,0 +1,79 @@ +package app.fedilab.android.ui.drawer; +/* 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 android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.core.app.ActivityOptionsCompat; +import androidx.cursoradapter.widget.SimpleCursorAdapter; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.ProfileActivity; +import app.fedilab.android.client.entities.api.Account; +import app.fedilab.android.helper.Helper; + + +public class AccountsSearchTopBarAdapter extends SimpleCursorAdapter { + + private final int layout; + private final LayoutInflater inflater; + private final List accountList; + + public AccountsSearchTopBarAdapter(Context context, List accounts, int layout, Cursor c, String[] from, int[] to, int flags) { + super(context, layout, c, from, to, flags); + this.layout = layout; + this.inflater = LayoutInflater.from(context); + this.accountList = accounts; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return inflater.inflate(layout, null); + } + + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + + LinearLayoutCompat container = view.findViewById(R.id.account_container); + container.setTag(cursor.getPosition()); + ImageView account_pp = view.findViewById(R.id.account_pp); + container.setOnClickListener(v -> { + int position = (int) v.getTag(); + if (accountList != null && accountList.size() > position) { + Intent intent = new Intent(context, ProfileActivity.class); + Bundle b = new Bundle(); + b.putSerializable(Helper.ARG_ACCOUNT, accountList.get(position)); + intent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, account_pp, context.getString(R.string.activity_porfile_pp)); + // start the new activity + context.startActivity(intent, options.toBundle()); + } + }); + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/SliderAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/SliderAdapter.java index de0975c47..e34051b74 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/SliderAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/SliderAdapter.java @@ -17,23 +17,29 @@ package app.fedilab.android.ui.drawer; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.CountDownTimer; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.core.app.ActivityOptionsCompat; +import androidx.preference.PreferenceManager; import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; import com.smarteist.autoimageslider.SliderViewAdapter; import java.util.ArrayList; import java.util.List; +import app.fedilab.android.R; 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; +import jp.wasabeef.glide.transformations.BlurTransformation; public class SliderAdapter extends SliderViewAdapter { @@ -63,21 +69,48 @@ public class SliderAdapter extends SliderViewAdapter { - 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()); + if (status.sensitive && !expand_media) { + status.sensitive = false; + notifyDataSetChanged(); + if (timeout > 0) { + new CountDownTimer((timeout * 1000L), 1000) { + public void onTick(long millisUntilFinished) { + } + + public void onFinish() { + status.sensitive = true; + notifyDataSetChanged(); + } + }.start(); + } + } else { + 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()); + } }); } 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 a3bda4bb7..baa1a162c 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 @@ -60,7 +60,6 @@ import android.widget.GridView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RadioButton; -import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; @@ -84,8 +83,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; +import com.bumptech.glide.ListPreloader; 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; @@ -137,7 +136,9 @@ import app.fedilab.android.databinding.LayoutMediaBinding; import app.fedilab.android.databinding.LayoutPollItemBinding; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.CrossActionHelper; +import app.fedilab.android.helper.GlideApp; import app.fedilab.android.helper.GlideFocus; +import app.fedilab.android.helper.GlideRequests; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.LongClickLinkMovementMethod; import app.fedilab.android.helper.MastodonHelper; @@ -155,7 +156,7 @@ import es.dmoral.toasty.Toasty; import jp.wasabeef.glide.transformations.BlurTransformation; -public class StatusAdapter extends RecyclerView.Adapter { +public class StatusAdapter extends RecyclerView.Adapter implements ListPreloader.PreloadModelProvider { public static final int STATUS_HIDDEN = 0; public static final int STATUS_VISIBLE = 1; public static final int STATUS_ART = 2; @@ -172,6 +173,8 @@ public class StatusAdapter extends RecyclerView.Adapter private boolean visiblePixelfed; private RecyclerView mRecyclerView; + private static float measuredWidth = -1; + private static float measuredWidthArt = -1; public StatusAdapter(List statuses, Timeline.TimeLineEnum timelineType, boolean minified, boolean canBeFederated, boolean checkRemotely) { this.statusList = statuses; @@ -181,19 +184,6 @@ public class StatusAdapter extends RecyclerView.Adapter this.checkRemotely = checkRemotely; } - public static int getStatusPosition(List timelineStatuses, Status status) { - int position = 0; - if (timelineStatuses != null && status != null) { - for (Status _s : timelineStatuses) { - if (_s.id.compareTo(status.id) == 0) { - return position; - } - position++; - } - } - return -1; - } - private static boolean isVisiblePixelfed(Status status) { if (status.reblog != null) { @@ -434,7 +424,7 @@ public class StatusAdapter extends RecyclerView.Adapter psc.setMarginStart((int) Helper.convertDpToPixel(6, context)); holder.binding.statusContent.setLayoutParams(psc); LinearLayoutCompat.MarginLayoutParams pct = (LinearLayoutCompat.MarginLayoutParams) holder.binding.containerTrans.getLayoutParams(); - psc.setMarginStart((int) Helper.convertDpToPixel(6, context)); + pct.setMarginStart((int) Helper.convertDpToPixel(6, context)); holder.binding.containerTrans.setLayoutParams(psc); LinearLayoutCompat.MarginLayoutParams pcv = (LinearLayoutCompat.MarginLayoutParams) holder.binding.card.getLayoutParams(); pcv.setMarginStart((int) Helper.convertDpToPixel(6, context)); @@ -583,6 +573,9 @@ public class StatusAdapter extends RecyclerView.Adapter gridView.setAdapter(new EmojiAdapter(emojis.get(BaseMainActivity.currentInstance))); gridView.setNumColumns(5); gridView.setOnItemClickListener((parent, view, index, id) -> { + if (emojis.get(BaseMainActivity.currentInstance) == null) { + return; + } String emojiStr = emojis.get(BaseMainActivity.currentInstance).get(index).shortcode; String url = emojis.get(BaseMainActivity.currentInstance).get(index).url; String static_url = emojis.get(BaseMainActivity.currentInstance).get(index).static_url; @@ -772,9 +765,7 @@ public class StatusAdapter extends RecyclerView.Adapter CrossActionHelper.doCrossAction(context, CrossActionHelper.TypeOfCrossAction.BOOKMARK_ACTION, null, statusToDeal); return true; }); - holder.binding.actionButtonTranslate.setOnClickListener(v -> { - translate(context, statusToDeal, holder, adapter); - }); + holder.binding.actionButtonTranslate.setOnClickListener(v -> translate(context, statusToDeal, holder, adapter)); holder.binding.actionButtonBookmark.setOnClickListener(v -> { if (remote) { Toasty.info(context, context.getString(R.string.retrieve_remote_status), Toasty.LENGTH_SHORT).show(); @@ -1283,7 +1274,18 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.mediaContainer.setVisibility(View.GONE); holder.binding.card.setVisibility(View.GONE); } - + if (measuredWidth <= 0 && statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { + holder.binding.mediaContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + holder.binding.mediaContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); + measuredWidth = holder.binding.mediaContainer.getWidth(); + if (adapter != null && statusList != null) { + adapter.notifyItemChanged(0, statusList.size()); + } + } + }); + } LayoutInflater inflater = ((Activity) context).getLayoutInflater(); //--- MEDIA ATTACHMENT --- if (statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { @@ -1294,7 +1296,9 @@ public class StatusAdapter extends RecyclerView.Adapter holder.binding.displayMedia.setVisibility(View.VISIBLE); holder.binding.displayMedia.setOnClickListener(v -> { statusToDeal.canLoadMedia = true; - adapter.notifyItemChanged(holder.getBindingAdapterPosition()); + if (adapter != null) { + adapter.notifyItemChanged(holder.getBindingAdapterPosition()); + } }); } else { int mediaPosition = 1; @@ -1304,38 +1308,16 @@ public class StatusAdapter extends RecyclerView.Adapter if (fullAttachement && (!statusToDeal.sensitive || expand_media)) { float ratio = 1.0f; float mediaH = -1.0f; - - if (attachment.measuredWidth > 0) { - float viewWidth = attachment.measuredWidth; - if (attachment.meta != null && attachment.meta.small != null) { - mediaH = attachment.meta.small.height; - float mediaW = attachment.meta.small.width; - if (mediaW != 0) { - ratio = viewWidth / mediaW; - } + float mediaW = -1.0f; + if (attachment.meta != null && attachment.meta.small != null) { + mediaH = attachment.meta.small.height; + mediaW = attachment.meta.small.width; + if (mediaW != 0) { + ratio = measuredWidth > 0 ? measuredWidth / mediaW : 1.0f; } - loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, viewWidth, mediaH, ratio, statusToDeal, attachment, singleMedia); - } else { - int finalMediaPosition = mediaPosition; - layoutMediaBinding.media.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - layoutMediaBinding.media.getViewTreeObserver().removeOnGlobalLayoutListener(this); - attachment.measuredWidth = layoutMediaBinding.media.getWidth(); - float ratio = 1.0f; - float mediaH = -1.0f; - float viewWidth = attachment.measuredWidth; - if (attachment.meta != null && attachment.meta.small != null) { - mediaH = attachment.meta.small.height; - float mediaW = attachment.meta.small.width; - if (mediaW != 0) { - ratio = viewWidth / mediaW; - } - } - loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, finalMediaPosition, viewWidth, mediaH, ratio, statusToDeal, attachment, singleMedia); - } - }); } + loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, mediaW, mediaH, ratio, statusToDeal, attachment, singleMedia); + } else { loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, -1.f, -1.f, -1.f, statusToDeal, attachment, singleMedia); } @@ -1813,9 +1795,7 @@ public class StatusAdapter extends RecyclerView.Adapter builderInner.setMessage(statusToDeal.account.acct); builderInner.setNeutralButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); builderInner.setPositiveButton(R.string.action_mute, (dialog, which) -> accountsVM.muteHome(currentAccount, statusToDeal.account) - .observe((LifecycleOwner) context, account -> { - Toasty.info(context, context.getString(R.string.toast_mute), Toasty.LENGTH_LONG).show(); - })); + .observe((LifecycleOwner) context, account -> Toasty.info(context, context.getString(R.string.toast_mute), Toasty.LENGTH_LONG).show())); builderInner.show(); } else if (itemId == R.id.action_mute_conversation) { if (statusToDeal.muted) { @@ -2113,10 +2093,42 @@ public class StatusAdapter extends RecyclerView.Adapter } } + private static RequestBuilder prepareRequestBuilder(Context context, Attachment attachment, + float mediaW, float mediaH, + float focusX, float focusY, boolean isSensitive, boolean isArt) { + + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); + boolean fullAttachement = sharedpreferences.getBoolean(context.getString(R.string.SET_FULL_PREVIEW), false); + if (isArt) { + fullAttachement = true; + } + boolean expand_media = sharedpreferences.getBoolean(context.getString(R.string.SET_EXPAND_MEDIA), false); + RequestBuilder requestBuilder; + GlideRequests glideRequests = GlideApp.with(context); + if (!isSensitive || expand_media) { + requestBuilder = glideRequests.asDrawable(); + if (!fullAttachement) { + requestBuilder = requestBuilder.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))); + requestBuilder = requestBuilder.dontAnimate(); + } else { + requestBuilder = requestBuilder.placeholder(R.color.transparent_grey); + requestBuilder = requestBuilder.dontAnimate(); + requestBuilder = requestBuilder.apply(new RequestOptions().override((int) mediaW, (int) mediaH)); + requestBuilder = requestBuilder.fitCenter(); + } + } else { + requestBuilder = glideRequests.asDrawable() + .dontAnimate() + .apply(new RequestOptions().transform(new BlurTransformation(50, 3))); + // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + } + return requestBuilder; + } + private static void loadAndAddAttachment(Context context, LayoutMediaBinding layoutMediaBinding, StatusViewHolder holder, RecyclerView.Adapter adapter, - int mediaPosition, float viewWidth, float mediaH, float ratio, + int mediaPosition, float mediaW, float mediaH, float ratio, Status statusToDeal, Attachment attachment, boolean singleImage) { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); final int timeout = sharedpreferences.getInt(context.getString(R.string.SET_NSFW_TIMEOUT), 5); @@ -2179,26 +2191,13 @@ public class StatusAdapter extends RecyclerView.Adapter layoutMediaBinding.viewDescription.setVisibility(View.GONE); } + RequestBuilder requestBuilder = prepareRequestBuilder(context, attachment, mediaW * ratio, mediaH * ratio, focusX, focusY, statusToDeal.sensitive, false); if (!statusToDeal.sensitive || expand_media) { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); - RequestBuilder requestBuilder = Glide.with(layoutMediaBinding.media.getContext()) - .load(attachment.preview_url); - if (!fullAttachement) { - requestBuilder = requestBuilder.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))); - } else { - requestBuilder = requestBuilder.placeholder(R.color.transparent_grey); - requestBuilder = requestBuilder.apply(new RequestOptions().override((int) viewWidth, (int) mediaH)); - requestBuilder = requestBuilder.fitCenter(); - } - requestBuilder.into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); - Glide.with(layoutMediaBinding.media.getContext()) - .load(attachment.preview_url) - .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) - // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) - .into(layoutMediaBinding.media); } + requestBuilder.load(attachment.preview_url).into(layoutMediaBinding.media); if (statusToDeal.sensitive) { Helper.changeDrawableColor(context, layoutMediaBinding.viewHide, ThemeHelper.getAttColor(context, R.attr.colorError)); } else { @@ -2247,6 +2246,53 @@ public class StatusAdapter extends RecyclerView.Adapter } + @NonNull + @Override + public List getPreloadItems(int position) { + List attachments = new ArrayList<>(); + if (position == 0 && statusList.size() > 0) { + for (Status status : statusList.subList(0, 1)) { + Status statusToDeal = status.reblog != null ? status.reblog : status; + if (statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { + attachments.addAll(statusToDeal.media_attachments); + } + } + } else if (position > 0 && position < (statusList.size() - 1)) { + for (Status status : statusList.subList(position - 1, position + 1)) { + Status statusToDeal = status.reblog != null ? status.reblog : status; + if (statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { + attachments.addAll(statusToDeal.media_attachments); + } + } + } else { + for (Status status : statusList.subList(position, position)) { + Status statusToDeal = status.reblog != null ? status.reblog : status; + if (statusToDeal.media_attachments != null && statusToDeal.media_attachments.size() > 0) { + attachments.addAll(statusToDeal.media_attachments); + } + } + } + return attachments; + } + + @Nullable + @Override + public RequestBuilder getPreloadRequestBuilder(@NonNull Attachment attachment) { + float focusX = 0.f; + float focusY = 0.f; + if (attachment.meta != null && attachment.meta.focus != null) { + focusX = attachment.meta.focus.x; + focusY = attachment.meta.focus.y; + } + int mediaH = 0; + int mediaW = 0; + if (attachment.meta != null && attachment.meta.small != null) { + mediaH = attachment.meta.small.height; + mediaW = attachment.meta.small.width; + } + return prepareRequestBuilder(context, attachment, mediaW, mediaH, focusX, focusY, attachment.sensitive, timelineType == Timeline.TimeLineEnum.ART).load(attachment); + } + /** * Send a broadcast to other open fragments that content a timeline * @@ -2549,45 +2595,29 @@ public class StatusAdapter extends RecyclerView.Adapter } else if (viewHolder.getItemViewType() == STATUS_ART) { StatusViewHolder holder = (StatusViewHolder) viewHolder; MastodonHelper.loadPPMastodon(holder.bindingArt.artPp, status.account); - if (status.art_attachment != null) { - + if (measuredWidthArt <= 0) { holder.bindingArt.artMedia.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { holder.bindingArt.artMedia.getViewTreeObserver().removeOnGlobalLayoutListener(this); - if (status.art_attachment.meta != null && status.art_attachment.meta.small != null) { - float viewWidth = holder.bindingArt.artMedia.getWidth(); - ConstraintLayout.LayoutParams lp; - float mediaH = status.art_attachment.meta.small.height; - float mediaW = status.art_attachment.meta.small.width; - float ratio = 1.0f; - if (mediaW != 0) { - ratio = viewWidth / mediaW; - } - lp = new ConstraintLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, (int) (mediaH * ratio)); - holder.bindingArt.artMedia.setScaleType(ImageView.ScaleType.FIT_CENTER); - holder.bindingArt.artMedia.setLayoutParams(lp); - } - + measuredWidthArt = holder.bindingArt.artMedia.getWidth(); + notifyItemChanged(0, statusList.size()); } }); + } + if (status.art_attachment != null) { if (status.art_attachment.meta != null && status.art_attachment.meta.small != null) { - float viewWidth = holder.bindingArt.artMedia.getWidth(); ConstraintLayout.LayoutParams lp; float mediaH = status.art_attachment.meta.small.height; float mediaW = status.art_attachment.meta.small.width; - float ratio = 1.0f; - if (mediaW != 0) { - ratio = viewWidth / mediaW; - } + float ratio = measuredWidthArt > 0 ? measuredWidthArt / mediaW : 1.0f; lp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, (int) (mediaH * ratio)); holder.bindingArt.artMedia.setScaleType(ImageView.ScaleType.FIT_CENTER); holder.bindingArt.artMedia.setLayoutParams(lp); + RequestBuilder requestBuilder = prepareRequestBuilder(context, status.art_attachment, mediaW * ratio, mediaH * ratio, 1.0f, 1.0f, status.sensitive, true); + requestBuilder.into(holder.bindingArt.artMedia); } - Glide.with(holder.bindingArt.artMedia.getContext()) - .load(status.art_attachment.preview_url) - .apply(new RequestOptions().transform(new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) - .into(holder.bindingArt.artMedia); + } holder.bindingArt.artUsername.setText( status.account.getSpanDisplayName(context, @@ -2671,6 +2701,7 @@ public class StatusAdapter extends RecyclerView.Adapter super.onViewRecycled(holder); } + public interface FetchMoreCallBack { void onClickMinId(String min_id, Status statusToUpdate); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/TagSearchTopBarAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/TagSearchTopBarAdapter.java new file mode 100644 index 000000000..3b36f6599 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/TagSearchTopBarAdapter.java @@ -0,0 +1,72 @@ +package app.fedilab.android.ui.drawer; +/* 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 android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.appcompat.widget.LinearLayoutCompat; +import androidx.cursoradapter.widget.SimpleCursorAdapter; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.HashTagActivity; +import app.fedilab.android.client.entities.api.Tag; +import app.fedilab.android.helper.Helper; + + +public class TagSearchTopBarAdapter extends SimpleCursorAdapter { + + private final int layout; + private final LayoutInflater inflater; + private final List tags; + + public TagSearchTopBarAdapter(Context context, List tags, int layout, Cursor c, String[] from, int[] to, int flags) { + super(context, layout, c, from, to, flags); + this.layout = layout; + this.inflater = LayoutInflater.from(context); + this.tags = tags; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return inflater.inflate(layout, null); + } + + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + + LinearLayoutCompat container = view.findViewById(R.id.tag_container); + container.setTag(cursor.getPosition()); + container.setOnClickListener(v -> { + int position = (int) v.getTag(); + if (tags != null && tags.size() > position) { + Intent intent = new Intent(context, HashTagActivity.class); + Bundle b = new Bundle(); + b.putString(Helper.ARG_SEARCH_KEYWORD, tags.get(position).name.trim()); + intent.putExtras(b); + context.startActivity(intent); + } + }); + } +} 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 86670549e..fe42a549e 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 @@ -74,7 +74,7 @@ public class FragmentMediaProfile extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); flagLoading = false; - accountsVM = new ViewModelProvider(FragmentMediaProfile.this).get(AccountsVM.class); + accountsVM = new ViewModelProvider(requireActivity()).get(AccountsVM.class); mediaStatuses = new ArrayList<>(); if (checkRemotely) { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java index b6bf6b18e..97af17208 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java @@ -33,6 +33,7 @@ import java.util.List; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; +import app.fedilab.android.activities.SearchResultTabActivity; import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.FragmentPaginationBinding; @@ -144,6 +145,9 @@ public class FragmentMastodonTag extends Fragment { router(); }); if (tags == null || tags.size() == 0) { + if (requireActivity() instanceof SearchResultTabActivity) { + ((SearchResultTabActivity) requireActivity()).moveToAccount(); + } binding.recyclerView.setVisibility(View.GONE); binding.noAction.setVisibility(View.VISIBLE); binding.noActionText.setText(R.string.no_tags); 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 b3e2e6b42..d4a84a5fd 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 @@ -40,6 +40,9 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; +import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; +import com.bumptech.glide.util.ViewPreloadSizeProvider; + import java.util.ArrayList; import java.util.List; @@ -60,6 +63,7 @@ import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.CrossActionHelper; +import app.fedilab.android.helper.GlideApp; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.ui.drawer.StatusAdapter; @@ -84,6 +88,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. private StatusAdapter statusAdapter; private Timeline.TimeLineEnum timelineType; private List timelineStatuses; + private static final int PRELOAD_AHEAD_ITEMS = 10; + private ViewPreloadSizeProvider preloadSizeProvider; //Handle actions that can be done in other fragments private final BroadcastReceiver receive_action = new BroadcastReceiver() { @Override @@ -161,8 +167,10 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. if (toRemove.size() > 0) { for (int i = 0; i < toRemove.size(); i++) { int position = getPosition(toRemove.get(i)); - timelineStatuses.remove(position); - statusAdapter.notifyItemRemoved(position); + if (position >= 0) { + timelineStatuses.remove(position); + statusAdapter.notifyItemRemoved(position); + } } } } else if (refreshAll) { @@ -266,6 +274,29 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. return found ? position : -1; } + + /** + * Return the position of the status in the ArrayList + * + * @param status - Status to fetch + * @return position or -1 if not found + */ + private int getAbsolutePosition(Status status) { + int position = 0; + boolean found = false; + if (status.id == null) { + return -1; + } + for (Status _status : timelineStatuses) { + if (_status.id != null && _status.id.compareTo(status.id) == 0) { + found = true; + break; + } + position++; + } + return found ? position : -1; + } + /** * Returned list of checked status id for reports * @@ -395,6 +426,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. if (timelineType != null) { slug = timelineType != Timeline.TimeLineEnum.ART ? timelineType.getValue() + (ident != null ? "|" + ident : "") : Timeline.TimeLineEnum.TAG.getValue() + (ident != null ? "|" + ident : ""); } + + LocalBroadcastManager.getInstance(requireActivity()).registerReceiver(receive_action, new IntentFilter(Helper.RECEIVE_STATUS_ACTION)); binding = FragmentPaginationBinding.inflate(inflater, container, false); return binding.getRoot(); @@ -412,7 +445,6 @@ 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) { @@ -475,9 +507,10 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. update.onUpdate(0, timelineType, slug); } if (direction == DIRECTION.TOP && fetchingMissing) { - int newPosition = currentPosition + fetched_statuses.statuses.size() + 1; - if (newPosition < timelineStatuses.size()) { - binding.recyclerView.scrollToPosition(newPosition); + int position = getAbsolutePosition(fetched_statuses.statuses.get(fetched_statuses.statuses.size() - 1)); + + if (position != -1) { + binding.recyclerView.scrollToPosition(position + 1); } } if (!fetchingMissing) { @@ -611,6 +644,14 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(statusAdapter); + + preloadSizeProvider = new ViewPreloadSizeProvider<>(); + RecyclerViewPreloader preloader = + new RecyclerViewPreloader<>( + GlideApp.with(this), statusAdapter, preloadSizeProvider, PRELOAD_AHEAD_ITEMS); + binding.recyclerView.addOnScrollListener(preloader); + binding.recyclerView.setItemViewCacheSize(0); + if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override diff --git a/app/src/main/res/layout/drawer_tag_search.xml b/app/src/main/res/layout/drawer_tag_search.xml index c0c284bc4..03b7921fe 100644 --- a/app/src/main/res/layout/drawer_tag_search.xml +++ b/app/src/main/res/layout/drawer_tag_search.xml @@ -16,6 +16,7 @@ --> diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 54e26a9c2..67ecae103 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -971,4 +971,6 @@ Stav přihlášení Připojil(a) se Ztišen(a) + Kompaktní tlačítka akcí + Tlačítka v dolní části zpráv nezaberou celou šířku \ 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 bf2c0d0f7..ec4f897f1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -398,7 +398,7 @@ Kategorien Timeline verschieben Timeline ausblenden - Verwaltung der Timelines + Timelines verwalten Liste endgültig gelöscht Gefolgte Instanz entfernt Angehefteter Hashtag entfernt @@ -639,7 +639,7 @@ Ergebnisse der Umfrage Alle Benachrichtigungen als gelesen markieren Alle Benachrichtigungen entfernen - Geplant + Beiträge planen Profil wurde aktualisiert! Listenname ist nicht gültig! Keine Konten für diese Liste gefunden! @@ -964,4 +964,7 @@ Profile auf anderen Instanzen Nur Lokal Zeige den Knopf \"Nur Lokal\" + Pixelfed-Präsentation für Medien + Knöpfe am unteren Rand von Beiträgen benötigen nicht die gesamte Breite + Kompakte Aktions-Knöpfe \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d92b41d00..d922fc800 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -938,4 +938,21 @@ Commentaire à propos de la limitation de ce domaine au public, si l\'annonce de la liste de limitation de domaines est activée. Re-blog de groupe dans la timeline d’accueil Ajouter tout les utilisateurs masqués à l’accueil + Clé API du traducteur + Si votre instance n\'accepte pas certaines fonctionnalités supplémentaires, vous pouvez masquer ces icônes + En activant cette option, l\'application affichera des fonctionnalités supplémentaires. Cette fonctionnalité est utilisée pour les logiciels sociaux comme Pleroma, Akkoma ou Glitch Social + Version du traducteur + Fonctionnalités supplémentaires + Vous pouvez sans risque cacher ces icônes en bas pour avoir plus d\'espace. Elles se trouvent également dans le sous-menu. + Icônes pour les fonctions supplémentaires + Bulle + Visibilité des réponses + Liste + Suivant + Visibilité des icônes + Traducteur + Traducteur + Supprimer la marge de gauche + Suppression de la marge de gauche dans les lignes de temps pour rendre les messages plus compacts + Version \ 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 f472dd914..3e44f4ded 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -958,4 +958,7 @@ 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. + Presentación Pixelfed para multimedia + Compactar botóns de accións + Os botóns ao pé das mensaxes non ocuparán todo o ancho \ 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 6874f8d7c..0aaf1afc0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -3,7 +3,7 @@ Over Over deze server Privacy - Buffer + Cache Uitloggen Sluiten @@ -548,7 +548,7 @@ Status telling Server-teller Eindigt over %s - Deze server is niet beschikbaar + Deze server is niet beschikbaar op https://instances.social Add a comment Deel link Open met een andere app @@ -627,7 +627,7 @@ Tijdlijnen worden in cache gezet zodat de app sneller is. Laad thumbnails voor media Toon tijdlijnen - Gebufferd bericht + Bericht in cache Toon opties Bericht losmaken Bewerkt op %1$s @@ -683,7 +683,7 @@ Berichten in cache andere tijdlijnen Onderste menu Klik hier om poll bij te werken - Buffer leegmaken + Cache leegmaken Wil je afsluiten zonder de afbeelding te bewaren\? Gebruik de standaard systeemtaal Stel je maximale karakter limiet in @@ -953,4 +953,19 @@ Toon \"Reacties\" knop Groepeer reblogs in eigen tijdlijn Verwijder linker kantlijn in tijdlijnen voor compactere berichten + Volgend + Zelf + Remote profielen + Zichtbaarheid van antwoorden + Alleen lokaal + Compacte actieknoppen + Knoppen aan de onderkant van berichten nemen niet de hele breedte in + Pixelfed presentatie voor media + De app toont openbare profielen om alle berichten te ontvangen. Interacties hebben een extra stap nodig om berichten te bundelen. + Bubbel + Zichtbaarheid uitsluiten + Lijst + Toon \"Alleen lokaal\" knop + Bericht indeling + Bericht indeling \ 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 b6debd1a0..e4804d4ae 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -316,7 +316,9 @@ Загрузка медиа Выбрать сигнал Активировать временной интервал - Вы уверены, что хотите заблокировать %s\? + Вы уверены, что хотите заблокировать %s\? +\n +\nВы не будете получать уведомлений и видеть публикации из этого домена. Ваши подписчики из этого домена будут удалены. Заблокировать домен Этот домен заблокирован Получение внешнего статуса @@ -716,4 +718,26 @@ Настройки успешно импортированы Настройки экспортированы Продвинут + Вы не будете видеть публикации этого ползователя. Он не сможет видеть ваши публикации или подписаться на вас. Пользователь сможет понять, что был заблокирован. + Какие правила были нарушены\? + Выберите подходящие пункты + Этот аккаунт принадлежит другому серверу. Отправить анонимную копию жалобы туда\? + Кнопки внизу сообщений не будут занимать всю ширину + Проверено в: %s + Отправка сообщения %d/%d + Не работает! + Не хотите это видеть\? + Вы подписаны на этот аккаунт. Чтобы перестать видеть его публикации, отпишитесь от него. + Заблокировать %1$s + Новая регистрация + Сообщение, которым вы поделились, было изменено + Онлайн: %,.2f %% + Есть ли какие-либо публикации, которые подтверждают жалобу\? + Жалоба отправлена! + Добавить статус + Убрать статус + Сообщение публикуется… + Работает! + Дополнительные комментарии + Есть ли что-то ещё, что нам надо знать\? \ 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 dc9289c41..baea2f950 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -771,7 +771,7 @@ Isbloca su messàgiu Su messàgiu no est prus apicadu! Su messàgiu est istadu apicadu - Borta sos messàgios + Bortare sos messàgios Fortza sa tradutzione a una limba ispetzificada. Issèbera su primu valore pro torrare a is cunfiguratziones de su dispositivu Modìfica su messàgiu %1$s at modificadu %2$s @@ -950,4 +950,11 @@ 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 + S\'aplicatzione at a ammustrare profilos in manera pùblica pro retzire totu is messàgios. Is interatziones ant a bisongiare de unu passu in prus pro federare is messàgios. + Presentatzione de Pixelfed pro is mèdios + Butones de atziones cumpatos + Is butones in fundu a is messàgios no ant a pigare totu sa largària + Mustra su butone \"In locale ebbia\" + In locale ebbia + Visibilidade de is rispostas \ 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 784df89b2..f3623a786 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -962,4 +962,7 @@ 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 + Medya için Pixelfed sunumu + Küçük eylem düğmeleri + Mesajların altındaki düğmeler tüm genişliği kaplamayacak \ No newline at end of file diff --git a/src/fdroid/fastlane/metadata/android/en/changelogs/463.txt b/src/fdroid/fastlane/metadata/android/en/changelogs/463.txt new file mode 100644 index 000000000..4fe4c5fe5 --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/en/changelogs/463.txt @@ -0,0 +1,13 @@ +Added: +- Search bar: display suggestions when starting by "@" or "#" + +Changed: +- Preload media in timelines to avoid jumps +- Search: Automatically switch to account tab if no results for tags + +Fixed: +- Fix jumps with the fetch more feature +- Fix videos cannot be saved +- Tags cannot be pinned when there are no custom tabs +- PixelFed view: NSFW not honored +- Fix crashes \ No newline at end of file diff --git a/src/fdroid/fastlane/metadata/android/nl/title.txt b/src/fdroid/fastlane/metadata/android/nl/title.txt new file mode 100644 index 000000000..e6f369e8c --- /dev/null +++ b/src/fdroid/fastlane/metadata/android/nl/title.txt @@ -0,0 +1 @@ +Fedilab