From 12c9cf208394408324e174cb363be35def58eba6 Mon Sep 17 00:00:00 2001 From: mgabdev <> Date: Mon, 14 Sep 2020 20:20:27 -0500 Subject: [PATCH] Updated explore page functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Updated: - explore page functionality --- .../api/v1/timelines/explore_controller.rb | 179 ++++++++++++++++++ app/javascript/gabsocial/actions/timelines.js | 1 + .../gabsocial/features/explore_timeline.js | 130 +++++++++++++ app/javascript/gabsocial/features/ui/ui.js | 5 +- .../features/ui/util/async_components.js | 1 + config/routes.rb | 1 + 6 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/timelines/explore_controller.rb create mode 100644 app/javascript/gabsocial/features/explore_timeline.js diff --git a/app/controllers/api/v1/timelines/explore_controller.rb b/app/controllers/api/v1/timelines/explore_controller.rb new file mode 100644 index 00000000..1c2ae43a --- /dev/null +++ b/app/controllers/api/v1/timelines/explore_controller.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +class Api::V1::Timelines::ExploreController < Api::BaseController + before_action :set_sort_type + before_action :set_statuses + + after_action :insert_pagination_headers, unless: -> { + @statuses.empty? + } + + def show + if current_user + render json: @statuses, + each_serializer: REST::StatusSerializer, + relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id) + else + render json: @statuses, each_serializer: REST::StatusSerializer + end + end + + private + + def set_sort_type + @sort_type = 'newest' + @sort_type = params[:sort_by] if [ + 'hot', + 'newest', + 'recent', + 'top_today', + 'top_weekly', + 'top_monthly', + 'top_yearly', + 'top_all_time', + ].include? params[:sort_by] + + return @sort_type + end + + def set_statuses + @statuses = cached_explore_statuses + end + + def cached_explore_statuses + cache_collection explore_statuses, Status + end + + def explore_statuses + statuses = nil + + date_limit = 30.days.ago + top_order = 'status_stats.favourites_count DESC, status_stats.reblogs_count DESC, status_stats.replies_count DESC' + + if @sort_type == 'hot' + # : todo : + # unique groups + # unique users + date_limit = 8.hours.ago + elsif @sort_type == 'top_today' + date_limit = 24.hours.ago + elsif @sort_type == 'top_weekly' + date_limit = 7.days.ago + elsif @sort_type == 'top_monthly' + date_limit = 30.days.ago + elsif @sort_type == 'top_yearly' + date_limit = 1.year.ago + end + + if current_account + if @sort_type == 'newest' + statuses = Status.with_public_visibility.where( + reply: false + ).paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) } + elsif @sort_type == 'recent' + statuses = Status.with_public_visibility.where( + reply: false + ).joins(:status_stat).where( + 'status_stats.replies_count > 0 OR status_stats.reblogs_count > 0 OR status_stats.favourites_count > 0' + ).order('status_stats.updated_at DESC').paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) } + elsif ['top_today', 'top_weekly', 'top_monthly', 'top_yearly', 'top_all_time', 'hot'].include? @sort_type + if @sort_type == 'top_all_time' + statuses = Status.unscoped.with_public_visibility.where( + reply: false + ).joins(:status_stat).order(top_order) + .paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) } + elsif @sort_type == 'hot' + statuses = Status.unscoped.with_public_visibility.where( + reply: false + ).where( + 'statuses.created_at > ?', date_limit + ).joins(:status_stat).order(top_order).paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) } + else + statuses = Status.unscoped.with_public_visibility.where( + reply: false + ).where( + 'statuses.created_at > ?', date_limit + ).joins(:status_stat).order(top_order).paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) } + end + end + else + if @sort_type == 'newest' + statuses = Status.with_public_visibility.where( + reply: false + ).paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + elsif @sort_type == 'recent' + statuses = Status.with_public_visibility.where( + reply: false + ).joins(:status_stat).where( + 'status_stats.replies_count > 0 OR status_stats.reblogs_count > 0 OR status_stats.favourites_count > 0' + ).order('status_stats.updated_at DESC').paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + elsif ['top_today', 'top_weekly', 'top_monthly', 'top_yearly', 'top_all_time', 'hot'].include? @sort_type + if @sort_type == 'top_all_time' + statuses = Status.unscoped.with_public_visibility.where( + reply: false + ).joins(:status_stat).order(top_order) + .paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + else + statuses = Status.unscoped.with_public_visibility.where( + reply: false + ).where( + 'statuses.created_at > ?', date_limit + ).joins(:status_stat).order(top_order).paginate_by_id( + limit_param(DEFAULT_STATUSES_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + end + end + end + + statuses + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + + def next_path + api_v1_timelines_explore_url params[:id], pagination_params(max_id: pagination_max_id) + end + + def prev_path + api_v1_timelines_explore_url params[:id], pagination_params(min_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id + end +end diff --git a/app/javascript/gabsocial/actions/timelines.js b/app/javascript/gabsocial/actions/timelines.js index 7e61550c..ddbcd914 100644 --- a/app/javascript/gabsocial/actions/timelines.js +++ b/app/javascript/gabsocial/actions/timelines.js @@ -166,6 +166,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandExploreTimeline = ({ maxId, sortBy } = {}, done = noOp) => expandTimeline('explore', '/api/v1/timelines/explore', { max_id: maxId, sort_by: sortBy }, done); export const expandProTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('pro', '/api/v1/timelines/pro', { max_id: maxId }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies, commentsOnly } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${commentsOnly ? ':comments_only' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { only_comments: commentsOnly, exclude_replies: (!withReplies && !commentsOnly), max_id: maxId }); diff --git a/app/javascript/gabsocial/features/explore_timeline.js b/app/javascript/gabsocial/features/explore_timeline.js new file mode 100644 index 00000000..a95ead53 --- /dev/null +++ b/app/javascript/gabsocial/features/explore_timeline.js @@ -0,0 +1,130 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { injectIntl, defineMessages } from 'react-intl' +import { List as ImmutableList } from 'immutable' +import { me } from '../initial_state' +import { + clearTimeline, + expandExploreTimeline, +} from '../actions/timelines' +import { + setGroupTimelineSort, +} from '../actions/groups' +import { + GROUP_TIMELINE_SORTING_TYPE_HOT, + GROUP_TIMELINE_SORTING_TYPE_NEWEST, +} from '../constants' +import getSortBy from '../utils/group_sort_by' +import Text from '../components/text' +import StatusList from '../components/status_list' +import GroupSortBlock from '../components/group_sort_block' + +class ExploreTimeline extends React.PureComponent { + + state = { + //keep track of loads for if no user, + //only allow 2 loads before showing sign up msg + loadCount: 0, + } + + componentDidMount() { + const { + sortByValue, + sortByTopValue, + } = this.props + + if (sortByValue !== GROUP_TIMELINE_SORTING_TYPE_HOT) { + this.props.setFeaturedTop() + } else { + const sortBy = getSortBy(sortByValue, sortByTopValue) + this.props.onExpandExploreTimeline({ sortBy }) + } + } + + componentDidUpdate(prevProps) { + if (prevProps.sortByValue !== this.props.sortByValue || + prevProps.sortByTopValue !== this.props.sortByTopValue) { + this.props.onClearTimeline('explore') + this.handleLoadMore() + } + } + + handleLoadMore = (maxId) => { + const { + sortByValue, + sortByTopValue, + } = this.props + const { loadCount } = this.state + + if (!!maxId && !me) { + this.setState({ loadCount: this.state.loadCount + 1 }) + if (loadCount >= 2) return false + } else if (!maxId && loadCount !== 0) { + this.setState({ loadCount: 0 }) + } + + const sortBy = getSortBy(sortByValue, sortByTopValue) + const options = { sortBy, maxId } + + this.props.onExpandExploreTimeline(options) + } + + render() { + const { intl } = this.props + const { loadCount } = this.state + + const canLoadMore = loadCount < 2 && !me || !!me + + return ( + + + + + ) + } + +} + +const messages = defineMessages({ + empty: { id: 'empty_column.group_collection_timeline', defaultMessage: 'There are no gabs to display.' }, +}) + +const mapStateToProps = (state) => ({ + sortByValue: state.getIn(['group_lists', 'sortByValue']), + sortByTopValue: state.getIn(['group_lists', 'sortByTopValue']), +}) + +const mapDispatchToProps = (dispatch) => ({ + onClearTimeline(timeline) { + dispatch(clearTimeline(timeline)) + }, + onExpandExploreTimeline(options) { + dispatch(expandExploreTimeline(options)) + }, + setFeaturedTop() { + dispatch(setGroupTimelineSort(GROUP_TIMELINE_SORTING_TYPE_HOT)) + }, + setMemberNewest() { + dispatch(setGroupTimelineSort(GROUP_TIMELINE_SORTING_TYPE_NEWEST)) + }, +}) + +ExploreTimeline.propTypes = { + params: PropTypes.object.isRequired, + onClearTimeline: PropTypes.func.isRequired, + onExpandExploreTimeline: PropTypes.func.isRequired, + setFeaturedTop: PropTypes.func.isRequired, + setMemberNewest: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + sortByValue: PropTypes.string.isRequired, + sortByTopValue: PropTypes.string, + hasStatuses: PropTypes.bool.isRequired, +} + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ExploreTimeline)) \ No newline at end of file diff --git a/app/javascript/gabsocial/features/ui/ui.js b/app/javascript/gabsocial/features/ui/ui.js index 0e3683a2..4a8f8c39 100644 --- a/app/javascript/gabsocial/features/ui/ui.js +++ b/app/javascript/gabsocial/features/ui/ui.js @@ -56,6 +56,7 @@ import { CommunityTimeline, Compose, DMCA, + ExploreTimeline, // Filters, Followers, Following, @@ -170,7 +171,7 @@ class SwitchingArea extends React.PureComponent { } { !me && - + } @@ -184,7 +185,7 @@ class SwitchingArea extends React.PureComponent { - + diff --git a/app/javascript/gabsocial/features/ui/util/async_components.js b/app/javascript/gabsocial/features/ui/util/async_components.js index 297f15db..abeac429 100644 --- a/app/javascript/gabsocial/features/ui/util/async_components.js +++ b/app/javascript/gabsocial/features/ui/util/async_components.js @@ -22,6 +22,7 @@ export function EditShortcutsModal() { return import(/* webpackChunkName: "compo export function EmbedModal() { return import(/* webpackChunkName: "modals/embed_modal" */'../../../components/modal/embed_modal') } export function EmojiPicker() { return import(/* webpackChunkName: "emoji_picker" */'../../../components/emoji/emoji_picker') } export function EmojiPickerPopover() { return import(/* webpackChunkName: "components/emoji_picker_popover" */'../../../components/popover/emoji_picker_popover') } +export function ExploreTimeline() { return import(/* webpackChunkName: "features/explore_timeline" */'../../explore_timeline') } export function FeaturedGroupsInjection() { return import(/* webpackChunkName: "components/featured_groups_injection" */'../../../components/timeline_injections/featured_groups_injection') } export function Followers() { return import(/* webpackChunkName: "features/followers" */'../../followers') } export function Following() { return import(/* webpackChunkName: "features/following" */'../../following') } diff --git a/config/routes.rb b/config/routes.rb index c4bf84c3..1db4f797 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -331,6 +331,7 @@ Rails.application.routes.draw do resources :group, only: :show resources :group_collection, only: :show resources :group_pins, only: :show + resource :explore, only: :show, controller: :explore end resources :gab_trends, only: [:index]