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]