Commit 2385de94 authored by Thomas's avatar Thomas
Browse files

Merge branch 'improvements' into develop

parents a12ea240 27e65a5c
Loading
Loading
Loading
Loading
+24 −13
Original line number Diff line number Diff line
@@ -966,30 +966,41 @@ public class Helper {
                                       @Nullable Bundle args,
                                       @Nullable String tag,
                                       @Nullable String backStackName) {
        // Check if FragmentManager is in a valid state
        if (fragmentManager.isDestroyed() || fragmentManager.isStateSaved()) {
            return fragment;
        }

        Fragment _fragment = fragmentManager.findFragmentByTag(tag);
        FragmentTransaction ft = fragmentManager.beginTransaction();
        ft.setCustomAnimations(R.anim.enter, R.anim.exit, R.anim.pop_enter, R.anim.pop_exit);
        Fragment _fragment = fragmentManager.findFragmentByTag(tag);

        if (_fragment != null && _fragment.isAdded()) {
            ft.show(_fragment).commitAllowingStateLoss();
            fragment = _fragment;
            // Reuse existing fragment
            ft.show(_fragment);
            try {
                ft.commit();
            } catch (IllegalStateException e) {
                // State already saved, cannot commit
                return _fragment;
            }
            return _fragment;
        } else {
            // Add new fragment
            if (args != null) fragment.setArguments(args);
            ft = fragmentManager.beginTransaction();
            ft.add(containerViewId, fragment, tag);
            if (backStackName != null) {
                try {
                ft.addToBackStack(backStackName);
                }catch (Exception ignored){}
            }
            if (!fragmentManager.isDestroyed()) {
                ft.commitAllowingStateLoss();
            }
            }
        if(!fragmentManager.isDestroyed()) {
            fragmentManager.executePendingTransactions();
            try {
                ft.commit();
            } catch (IllegalStateException e) {
                // State already saved, cannot commit
                return fragment;
            }
            return fragment;
        }
    }

    /**
     * Load a media into a view
+188 −101
Original line number Diff line number Diff line
@@ -75,7 +75,7 @@ public class TimelineHelper {
     * @return filtered List<Status>
     */
    public static List<Status> filterStatus(Context context, List<Status> statuses, Timeline.TimeLineEnum filterTimeLineType) {
        //A security to make sure filters have been fetched before displaying messages
        // Ensure filters have been fetched before displaying messages
        if (!BaseMainActivity.filterFetched && BaseMainActivity.filterFetchedRetry < 3) {
            MastodonFiltersService mastodonFiltersService = initv2(context);
            List<Filter> filterList;
@@ -95,27 +95,40 @@ public class TimelineHelper {
            BaseMainActivity.filterFetchedRetry++;
        }

        //If there are filters:
        if (BaseMainActivity.mainFilters != null && !BaseMainActivity.mainFilters.isEmpty() && statuses != null && !statuses.isEmpty()) {
        if (statuses == null || statuses.isEmpty()) {
            return statuses;
        }

            //Loop through filters
        // Precompile patterns for all active filters
        List<CompiledFilter> compiledFilters = new ArrayList<>();
        if (BaseMainActivity.mainFilters != null && !BaseMainActivity.mainFilters.isEmpty()) {
            Date now = new Date();
            for (Filter filter : BaseMainActivity.mainFilters) {
                if (filter.expires_at != null && filter.expires_at.before(new Date())) {
                    //Expired filter
                // Skip expired filters
                if (filter.expires_at != null && filter.expires_at.before(now)) {
                    continue;
                }

                // Check context
                boolean contextMatches = false;
                if (filterTimeLineType == Timeline.TimeLineEnum.HOME || filterTimeLineType == Timeline.TimeLineEnum.LIST) {
                    if (!filter.context.contains("home")) continue;
                    contextMatches = filter.context.contains("home");
                } else if (filterTimeLineType == Timeline.TimeLineEnum.NOTIFICATION) {
                    if (!filter.context.contains("notifications")) continue;
                    contextMatches = filter.context.contains("notifications");
                } else if (filterTimeLineType == Timeline.TimeLineEnum.CONTEXT) {
                    if (!filter.context.contains("thread")) continue;
                    contextMatches = filter.context.contains("thread");
                } else if (filterTimeLineType == Timeline.TimeLineEnum.ACCOUNT_TIMELINE) {
                    if (!filter.context.contains("account")) continue;
                    contextMatches = filter.context.contains("account");
                } else {
                    if (!filter.context.contains("public")) continue;
                    contextMatches = filter.context.contains("public");
                }
                if (filter.keywords != null && !filter.keywords.isEmpty()) {

                if (!contextMatches || filter.keywords == null || filter.keywords.isEmpty()) {
                    continue;
                }

                // Precompile patterns for this filter
                List<Pattern> patterns = new ArrayList<>();
                for (Filter.KeywordsAttributes filterKeyword : filter.keywords) {
                    String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).find() ? "\\b" : "";
                    String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).find() ? "\\b" : "";
@@ -125,103 +138,177 @@ public class TimelineHelper {
                    } else {
                        p = Pattern.compile("(" + Pattern.quote(filterKeyword.keyword) + ")", Pattern.CASE_INSENSITIVE);
                    }
                    patterns.add(p);
                }
                compiledFilters.add(new CompiledFilter(filter, patterns));
            }
        }

        // Apply filters to statuses (inverted loop order for early exit)
        for (Status status : statuses) {
            // Skip user's own statuses
            if (status.account.id.equals(MainActivity.currentUserID)) {
                continue;
            }
                            String content;
                            try {
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                    content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
                                else
                                    content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
                                if(status.reblog == null && status.getQuote() != null) {
                                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                        content += Html.fromHtml(status.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                                    else
                                        content += Html.fromHtml(status.getQuote().content).toString();
                                } else if(status.reblog != null && status.reblog.getQuote() != null) {
                                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                        content += Html.fromHtml(status.reblog.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                                    else
                                        content += Html.fromHtml(status.reblog.getQuote().content).toString();

            // Cache parsed HTML content
            String content = parseStatusContent(status);
            String spoilerText = parseStatusSpoiler(status);
            List<Attachment> mediaAttachments = status.reblog != null ? status.reblog.media_attachments : status.media_attachments;

            // Check against all compiled filters
            for (CompiledFilter compiledFilter : compiledFilters) {
                boolean matched = false;

                // Check content
                for (Pattern pattern : compiledFilter.patterns) {
                    if (pattern.matcher(content).find()) {
                        matched = true;
                        break;
                    }
                            } catch (Exception e) {
                                content = status.reblog != null ? status.reblog.content : status.content;
                }
                            Matcher m = p.matcher(content);
                            if (m.find()) {
                                status.filteredByApp = filter;
                                continue;

                // Check spoiler text
                if (!matched && spoilerText != null) {
                    for (Pattern pattern : compiledFilter.patterns) {
                        if (pattern.matcher(spoilerText).find()) {
                            matched = true;
                            break;
                        }
                            if (status.spoiler_text != null) {
                                String spoilerText;
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                    spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
                                else
                                    spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
                                Matcher ms = p.matcher(spoilerText);
                                if (ms.find()) {
                                    status.filteredByApp = filter;
                                    continue;
                    }
                }
                            List<Attachment> mediaAttachments = status.reblog != null ? status.reblog.media_attachments : status.media_attachments;
                            if(mediaAttachments != null && !mediaAttachments.isEmpty()) {

                // Check media attachments
                if (!matched && mediaAttachments != null && !mediaAttachments.isEmpty()) {
                    for (Attachment attachment : mediaAttachments) {
                        if (attachment.description != null) {
                                        Matcher ms = p.matcher(attachment.description );
                                        if (ms.find()) {
                                            status.filteredByApp = filter;
                            for (Pattern pattern : compiledFilter.patterns) {
                                if (pattern.matcher(attachment.description).find()) {
                                    matched = true;
                                    break;
                                }
                            }
                            if (matched) break;
                        }
                    }
                }

                if (matched) {
                    status.filteredByApp = compiledFilter.filter;
                    break; // Stop checking other filters for this status
                }
            }
        }
        }
        if (statuses != null && !statuses.isEmpty()) {

        // Apply additional filters for HOME timeline
        if (filterTimeLineType == Timeline.TimeLineEnum.HOME) {
            SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
            boolean groupReblogs = sharedpreferences.getBoolean(context.getString(R.string.SET_GROUP_REBLOGS), true);
            if (filterTimeLineType == Timeline.TimeLineEnum.HOME) {

            // Index seen reblog IDs for O(1) lookup
            java.util.HashSet<String> seenReblogIds = new java.util.HashSet<>();

            for (int i = 0; i < statuses.size(); i++) {
                Status currentStatus = statuses.get(i);

                // Check filtered accounts
                if (filteredAccounts != null && !filteredAccounts.isEmpty()) {
                    for (Account account : filteredAccounts) {
                            if (account.acct.equals(statuses.get(i).account.acct) || (statuses.get(i).reblog != null && account.acct.equals(statuses.get(i).reblog.account.acct))) {
                        if (account.acct.equals(currentStatus.account.acct) ||
                            (currentStatus.reblog != null && account.acct.equals(currentStatus.reblog.account.acct))) {
                            Filter filterCustom = new Filter();
                            filterCustom.filter_action = "hide";
                            ArrayList<String> contextCustom = new ArrayList<>();
                            contextCustom.add("home");
                            filterCustom.title = "Fedilab";
                            filterCustom.context = contextCustom;
                                statuses.get(i).filteredByApp = filterCustom;
                            currentStatus.filteredByApp = filterCustom;
                            break;
                        }
                    }
                }
                    //Group boosts
                    if (groupReblogs && statuses.get(i).filteredByApp == null && statuses.get(i).reblog != null) {
                        for (int j = 0; j < i; j++) {
                            if (statuses.get(j).reblog != null && statuses.get(j).reblog.id.equals(statuses.get(i).reblog.id)) {

                // Group duplicate reblogs using HashSet
                if (groupReblogs && currentStatus.filteredByApp == null && currentStatus.reblog != null) {
                    if (seenReblogIds.contains(currentStatus.reblog.id)) {
                        Filter filterCustom = new Filter();
                        filterCustom.filter_action = "hide";
                        ArrayList<String> contextCustom = new ArrayList<>();
                        contextCustom.add("home");
                        filterCustom.title = "Fedilab reblog";
                        filterCustom.context = contextCustom;
                                statuses.get(i).filteredByApp = filterCustom;
                        currentStatus.filteredByApp = filterCustom;
                    } else {
                        seenReblogIds.add(currentStatus.reblog.id);
                    }
                }
            }
        }

        return statuses;
    }

    /**
     * Parse HTML content from status (cached per status)
     */
    private static String parseStatusContent(Status status) {
        try {
            String content;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
            } else {
                content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
            }

            // Append quote content if present
            if (status.reblog == null && status.getQuote() != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    content += Html.fromHtml(status.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                } else {
                    content += Html.fromHtml(status.getQuote().content).toString();
                }
            } else if (status.reblog != null && status.reblog.getQuote() != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    content += Html.fromHtml(status.reblog.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                } else {
                    content += Html.fromHtml(status.reblog.getQuote().content).toString();
                }
            }
            return content;
        } catch (Exception e) {
            return status.reblog != null ? status.reblog.content : status.content;
        }
    }

    /**
     * Parse HTML spoiler text from status
     */
    private static String parseStatusSpoiler(Status status) {
        if (status.spoiler_text == null) {
            return null;
        }
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                return Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
            } else {
                return Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
            }
        } catch (Exception e) {
            return status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text;
        }
    }

    /**
     * Helper class to hold precompiled filter patterns
     */
    private static class CompiledFilter {
        final Filter filter;
        final List<Pattern> patterns;

        CompiledFilter(Filter filter, List<Pattern> patterns) {
            this.filter = filter;
            this.patterns = patterns;
        }
        return statuses;
    }

    /**
+31 −8
Original line number Diff line number Diff line
@@ -1746,9 +1746,6 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                    } else {
                        measuredWidth = holder.binding.media.mediaContainer.getWidth();
                    }
                    if (adapter != null && statusList != null) {
                        adapter.notifyItemChanged(0, statusList.size());
                    }
                }
            });
        }
@@ -1939,6 +1936,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                                player.setMediaSource(videoSource);
                                player.prepare();
                                player.setPlayWhenReady(true);
                                holder.activePlayers.add(player);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
@@ -1951,7 +1949,10 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                                    adapter.notifyItemChanged(position);

                                    if (timeout > 0) {
                                        new CountDownTimer((timeout * 1000L), 1000) {
                                        if (holder.activeTimer != null) {
                                            holder.activeTimer.cancel();
                                        }
                                        holder.activeTimer = new CountDownTimer((timeout * 1000L), 1000) {
                                            public void onTick(long millisUntilFinished) {
                                            }

@@ -1959,7 +1960,8 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                                                statusToDeal.sensitive = true;
                                                adapter.notifyItemChanged(position);
                                            }
                                        }.start();
                                        };
                                        holder.activeTimer.start();
                                    }
                                    return;
                                }
@@ -2032,6 +2034,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                                player.setMediaSource(videoSource);
                                player.prepare();
                                player.setPlayWhenReady(true);
                                holder.activePlayers.add(player);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
@@ -2043,7 +2046,10 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                                    adapter.notifyItemChanged(position);

                                    if (timeout > 0) {
                                        new CountDownTimer((timeout * 1000L), 1000) {
                                        if (holder.activeTimer != null) {
                                            holder.activeTimer.cancel();
                                        }
                                        holder.activeTimer = new CountDownTimer((timeout * 1000L), 1000) {
                                            public void onTick(long millisUntilFinished) {
                                            }

@@ -2051,7 +2057,8 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                                                statusToDeal.sensitive = true;
                                                adapter.notifyItemChanged(position);
                                            }
                                        }.start();
                                        };
                                        holder.activeTimer.start();
                                    }
                                    return;
                                }
@@ -3910,7 +3917,21 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
    public void onViewRecycled(@NonNull RecyclerView.ViewHolder viewHolder) {
        super.onViewRecycled(viewHolder);
        if (viewHolder instanceof StatusViewHolder holder) {
            //Release players
            //Cancel active timer
            if (holder.activeTimer != null) {
                holder.activeTimer.cancel();
                holder.activeTimer = null;
            }

            //Release all tracked players
            for (ExoPlayer player : holder.activePlayers) {
                if (player != null) {
                    player.release();
                }
            }
            holder.activePlayers.clear();

            //Release players in views (legacy cleanup)
            if (holder.binding != null) { //Cropped views
                if(holder.binding.media.getRoot().getChildCount() > 0) {
                    for(int i = 0 ; i < holder.binding.media.getRoot().getChildCount() ; i++ ) {
@@ -3950,6 +3971,8 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
        DrawerStatusPixelfedBinding bindingPixelfed;
        DrawerStatusFilteredBinding bindingFiltered;
        DrawerStatusFilteredHideBinding bindingFilteredHide;
        List<ExoPlayer> activePlayers = new ArrayList<>();
        CountDownTimer activeTimer;

        StatusViewHolder(DrawerStatusBinding itemView) {
            super(itemView.getRoot());