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 1e1aa3e32..5869d7eea 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -198,9 +198,6 @@ public class SpannableHelper { final String url = content.toString().substring(matchStart, matchEnd); - /* if (!url.startsWith("http")) { - continue; - }*/ String newURL = Helper.transformURL(context, url); //If URL has been transformed if (newURL.compareTo(url) != 0) { @@ -316,7 +313,7 @@ public class SpannableHelper { } } httpsURLConnection.getInputStream().close(); - if (redirect != null && redirect.compareTo(finalURl1) != 0) { + if (redirect != null && finalURl1 != null && redirect.compareTo(finalURl1) != 0) { URL redirectURL = new URL(redirect); String host = redirectURL.getHost(); String protocol = redirectURL.getProtocol(); @@ -384,8 +381,11 @@ public class SpannableHelper { } textView.setTag(CLICKABLE_SPAN); Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$"); - Matcher matcherLink = link.matcher(finalURl2); - if (matcherLink.find() && !finalURl2.contains("medium.com")) { + 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 diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ImageAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ImageAdapter.java new file mode 100644 index 000000000..f4f8f2709 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ImageAdapter.java @@ -0,0 +1,121 @@ +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.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityOptionsCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.activities.ContextActivity; +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.DrawerMediaBinding; +import app.fedilab.android.helper.Helper; + + +public class ImageAdapter extends RecyclerView.Adapter { + private final List statuses; + private Context context; + + public ImageAdapter(List statuses) { + this.statuses = statuses; + } + + public int getCount() { + return statuses.size(); + } + + public Status getItem(int position) { + return statuses.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerMediaBinding itemBinding = DrawerMediaBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + Status status = statuses.get(position); + + final ViewHolder holder = (ViewHolder) viewHolder; + + if (Helper.isValidContextForGlide(context) && status.art_attachment != null) { + if (status.art_attachment.preview_url != null) { + Glide.with(context).load(status.art_attachment.preview_url).into(holder.binding.media); + } else if (status.art_attachment.url != null) { + Glide.with(context).load(status.art_attachment.url).into(holder.binding.media); + } + } + holder.binding.media.setOnClickListener(v -> { + Intent mediaIntent = new Intent(context, MediaActivity.class); + Bundle b = new Bundle(); + b.putInt(Helper.ARG_MEDIA_POSITION, position + 1); + ArrayList attachmentsTmp = new ArrayList<>(); + for (Status status1 : statuses) { + attachmentsTmp.add(status1.art_attachment); + } + b.putSerializable(Helper.ARG_MEDIA_ARRAY, new ArrayList<>(attachmentsTmp)); + mediaIntent.putExtras(b); + ActivityOptionsCompat options = ActivityOptionsCompat + .makeSceneTransitionAnimation((Activity) context, holder.binding.media, status.media_attachments.get(0).url); + // start the new activity + context.startActivity(mediaIntent, options.toBundle()); + }); + + holder.binding.media.setOnLongClickListener(v -> { + Intent intentContext = new Intent(context, ContextActivity.class); + intentContext.putExtra(Helper.ARG_STATUS, status); + intentContext.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intentContext); + return false; + }); + } + + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return statuses.size(); + } + + + static class ViewHolder extends RecyclerView.ViewHolder { + DrawerMediaBinding binding; + + public ViewHolder(DrawerMediaBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..d1f00d63e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/media/FragmentMediaProfile.java @@ -0,0 +1,182 @@ +package app.fedilab.android.ui.fragment.media; +/* 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.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.BaseMainActivity; +import app.fedilab.android.client.entities.api.Account; +import app.fedilab.android.client.entities.api.Attachment; +import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.api.Statuses; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.MastodonHelper; +import app.fedilab.android.ui.drawer.ImageAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class FragmentMediaProfile extends Fragment { + + private FragmentPaginationBinding binding; + private AccountsVM accountsVM; + private Account accountTimeline; + private boolean flagLoading; + private List mediaStatuses; + private String max_id; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + binding = FragmentPaginationBinding.inflate(inflater, container, false); + Bundle bundle = this.getArguments(); + if (bundle != null) { + accountTimeline = (Account) getArguments().getSerializable(Helper.ARG_ACCOUNT); + } + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + flagLoading = false; + accountsVM = new ViewModelProvider(FragmentMediaProfile.this).get(AccountsVM.class); + mediaStatuses = new ArrayList<>(); + accountsVM.getAccountStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, null, null, null, null, null, true, false, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); + } + + /** + * Intialize the common view for statuses on different timelines + * + * @param statuses {@link Statuses} + */ + private void initializeStatusesCommonView(final Statuses statuses) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + if (statuses == null || statuses.statuses == null || statuses.statuses.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + return; + } + + for (Status status : statuses.statuses) { + for (Attachment attachment : status.media_attachments) { + try { + Status statusTmp = (Status) status.clone(); + statusTmp.art_attachment = attachment; + mediaStatuses.add(statusTmp); + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + } + } + ImageAdapter imageAdapter = new ImageAdapter(mediaStatuses); + + flagLoading = statuses.pagination.max_id == null; + binding.recyclerView.setVisibility(View.VISIBLE); + + if (max_id == null || (statuses.pagination.max_id != null && statuses.pagination.max_id.compareTo(max_id) < 0)) { + max_id = statuses.pagination.max_id; + } + GridLayoutManager gvLayout = new GridLayoutManager(requireActivity(), 3); + binding.recyclerView.setLayoutManager(gvLayout); + binding.recyclerView.setAdapter(imageAdapter); + + + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (requireActivity() instanceof BaseMainActivity) { + if (dy < 0 && !((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(true); + if (dy > 0 && ((BaseMainActivity) requireActivity()).getFloatingVisibility()) + ((BaseMainActivity) requireActivity()).manageFloatingButton(false); + } + int firstVisibleItem = gvLayout.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = gvLayout.getChildCount(); + int totalItemCount = gvLayout.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + accountsVM.getAccountStatuses(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountTimeline.id, max_id, null, null, null, null, true, false, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), newStatuses -> dealWithPagination(newStatuses)); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + } + }); + + } + + + /** + * Update view and pagination when scrolling down + * + * @param fetched_statuses Statuses + */ + private synchronized void dealWithPagination(Statuses fetched_statuses) { + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.swipeContainer.setRefreshing(false); + binding.loadingNextElements.setVisibility(View.GONE); + flagLoading = false; + if (this.mediaStatuses != null && fetched_statuses != null && fetched_statuses.statuses != null && fetched_statuses.statuses.size() > 0) { + flagLoading = fetched_statuses.pagination.max_id == null; + binding.noAction.setVisibility(View.GONE); + //We have to split media in different statuses + List mediaStatusesNew = new ArrayList<>(); + for (Status status : fetched_statuses.statuses) { + if (status.media_attachments.size() > 1) { + for (Attachment attachment : status.media_attachments) { + status.media_attachments = new ArrayList<>(); + status.media_attachments.add(0, attachment); + mediaStatusesNew.add(status); + } + } + } + this.mediaStatuses.addAll(mediaStatusesNew); + if (fetched_statuses.pagination.max_id == null) { + flagLoading = true; + } else if (max_id == null || fetched_statuses.pagination.max_id.compareTo(max_id) < 0) { + max_id = fetched_statuses.pagination.max_id; + } + } else { + flagLoading = true; + } + } +} 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 5e32fbd6b..39369fb23 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 @@ -25,6 +25,7 @@ import androidx.fragment.app.FragmentStatePagerAdapter; import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.helper.Helper; +import app.fedilab.android.ui.fragment.media.FragmentMediaProfile; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; public class FedilabProfilePageAdapter extends FragmentStatePagerAdapter { @@ -73,12 +74,10 @@ public class FedilabProfilePageAdapter extends FragmentStatePagerAdapter { fragmentProfileTimeline.setArguments(bundle); return fragmentProfileTimeline; case 2: - fragmentProfileTimeline = new FragmentMastodonTimeline(); - bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE); + FragmentMediaProfile fragmentMediaProfile = new FragmentMediaProfile(); bundle.putSerializable(Helper.ARG_ACCOUNT, account); - bundle.putBoolean(Helper.ARG_SHOW_MEDIA_ONY, true); - fragmentProfileTimeline.setArguments(bundle); - return fragmentProfileTimeline; + fragmentMediaProfile.setArguments(bundle); + return fragmentMediaProfile; default: return new FragmentMastodonTimeline(); } diff --git a/app/src/main/res/layout/drawer_media.xml b/app/src/main/res/layout/drawer_media.xml new file mode 100644 index 000000000..a542a806e --- /dev/null +++ b/app/src/main/res/layout/drawer_media.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file