From f41274efc75449f3d6232e65a4c8d137c26e2331 Mon Sep 17 00:00:00 2001 From: mgabdev <> Date: Wed, 1 Jul 2020 21:36:53 -0400 Subject: [PATCH] Added verified accounts/suggestions panel, updated suggestions route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added: - verified accounts/suggestions panel • Updated: - suggestions route --- .../api/v1/suggestions_controller.rb | 18 ++-- .../gabsocial/actions/suggestions.js | 70 +++++++++------ .../panel/verified_accounts_panel.js | 90 +++++++++++++++++++ .../components/panel/who_to_follow_panel.js | 31 ++++--- .../gabsocial/reducers/suggestions.js | 38 +++++--- app/lib/verified_suggestions.rb | 38 ++++++++ 6 files changed, 227 insertions(+), 58 deletions(-) create mode 100644 app/javascript/gabsocial/components/panel/verified_accounts_panel.js create mode 100644 app/lib/verified_suggestions.rb diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index 19946bb2..90bd0749 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -5,12 +5,21 @@ class Api::V1::SuggestionsController < Api::BaseController before_action -> { doorkeeper_authorize! :read } before_action :require_user! - before_action :set_accounts respond_to :json def index - render json: @accounts, each_serializer: REST::AccountSerializer + type = params[:type] + + if type == 'related' + @accounts = PotentialFriendshipTracker.get(current_account.id) + render json: @accounts, each_serializer: REST::AccountSerializer + elsif type == 'verified' + @accounts = VerifiedSuggestions.get(current_account.id) + render json: @accounts, each_serializer: REST::AccountSerializer + else + raise GabSocial::NotPermittedError + end end def destroy @@ -18,9 +27,4 @@ class Api::V1::SuggestionsController < Api::BaseController render_empty end - private - - def set_accounts - @accounts = PotentialFriendshipTracker.get(current_account.id) - end end diff --git a/app/javascript/gabsocial/actions/suggestions.js b/app/javascript/gabsocial/actions/suggestions.js index 6f0c8e59..6c48dc63 100644 --- a/app/javascript/gabsocial/actions/suggestions.js +++ b/app/javascript/gabsocial/actions/suggestions.js @@ -1,57 +1,77 @@ import api from '../api' import { importFetchedAccounts } from './importer' import { me } from '../initial_state' +import { + SUGGESTION_TYPE_VERIFIED, + SUGGESTION_TYPE_RELATED, +} from '../constants' export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST' export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS' export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL' -export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; +export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS' -export function fetchSuggestions() { +export function fetchPopularSuggestions() { return (dispatch, getState) => { if (!me) return false - dispatch(fetchSuggestionsRequest()); + dispatch(fetchSuggestionsRequest(SUGGESTION_TYPE_VERIFIED)) - api(getState).get('/api/v1/suggestions').then(response => { - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchSuggestionsSuccess(response.data)); - }).catch(error => dispatch(fetchSuggestionsFail(error))); - }; -}; + api(getState).get(`/api/v1/suggestions?type=${SUGGESTION_TYPE_VERIFIED}`).then(response => { + dispatch(importFetchedAccounts(response.data)) + dispatch(fetchSuggestionsSuccess(response.data, SUGGESTION_TYPE_VERIFIED)) + }).catch(error => dispatch(fetchSuggestionsFail(error, SUGGESTION_TYPE_VERIFIED))) + } +} -export function fetchSuggestionsRequest() { +export function fetchRelatedSuggestions() { + return (dispatch, getState) => { + if (!me) return false + + dispatch(fetchSuggestionsRequest(SUGGESTION_TYPE_RELATED)) + + api(getState).get(`/api/v1/suggestions?type=${SUGGESTION_TYPE_RELATED}`).then(response => { + dispatch(importFetchedAccounts(response.data)) + dispatch(fetchSuggestionsSuccess(response.data, SUGGESTION_TYPE_RELATED)) + }).catch(error => dispatch(fetchSuggestionsFail(error, SUGGESTION_TYPE_RELATED))) + } +} + +export function fetchSuggestionsRequest(suggestionType) { return { type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true, - }; -}; + suggestionType, + } +} -export function fetchSuggestionsSuccess(accounts) { +export function fetchSuggestionsSuccess(accounts, suggestionType) { return { type: SUGGESTIONS_FETCH_SUCCESS, - accounts, skipLoading: true, - }; -}; + accounts, + suggestionType + } +} -export function fetchSuggestionsFail(error) { +export function fetchSuggestionsFail(error, suggestionType) { return { type: SUGGESTIONS_FETCH_FAIL, - error, skipLoading: true, skipAlert: true, - }; -}; + error, + suggestionType, + } +} -export const dismissSuggestion = accountId => (dispatch, getState) => { - if (!me) return; +export const dismissRelatedSuggestion = (accountId) => (dispatch, getState) => { + if (!me) return dispatch({ type: SUGGESTIONS_DISMISS, id: accountId, - }); + }) - api(getState).delete(`/api/v1/suggestions/${accountId}`); -}; + api(getState).delete(`/api/v1/suggestions/related/${accountId}`) +} \ No newline at end of file diff --git a/app/javascript/gabsocial/components/panel/verified_accounts_panel.js b/app/javascript/gabsocial/components/panel/verified_accounts_panel.js new file mode 100644 index 00000000..0168eab1 --- /dev/null +++ b/app/javascript/gabsocial/components/panel/verified_accounts_panel.js @@ -0,0 +1,90 @@ +import { defineMessages, injectIntl } from 'react-intl' +import { fetchPopularSuggestions } from '../../actions/suggestions' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import Account from '../account' +import PanelLayout from './panel_layout' + +const messages = defineMessages({ + dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, + title: { id: 'who_to_follow.title', defaultMessage: 'Verified Accounts to Follow' }, + show_more: { id: 'who_to_follow.more', defaultMessage: 'Show more' }, +}) + +const mapStateToProps = (state) => ({ + suggestions: state.getIn(['suggestions', 'verified', 'items']), +}) + +const mapDispatchToProps = (dispatch) => ({ + fetchPopularSuggestions: () => dispatch(fetchPopularSuggestions()), +}) + +export default +@connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class VerifiedAccountsPanel extends ImmutablePureComponent { + + static propTypes = { + fetchPopularSuggestions: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + suggestions: ImmutablePropTypes.list.isRequired, + isLazy: PropTypes.bool, + } + + state = { + fetched: !this.props.isLazy, + } + + updateOnProps = [ + 'suggestions', + 'isLazy', + 'shouldLoad', + ] + + static getDerivedStateFromProps(nextProps, prevState) { + if (nextProps.shouldLoad && !prevState.fetched) { + return { fetched: true } + } + + return null + } + + componentDidUpdate(prevProps, prevState) { + if (!prevState.fetched && this.state.fetched) { + this.props.fetchPopularSuggestions() + } + } + + componentDidMount() { + if (!this.props.isLazy) { + this.props.fetchPopularSuggestions() + } + } + + render() { + const { intl, suggestions } = this.props + + if (suggestions.isEmpty()) return null + + return ( + +
+ { + suggestions.map(accountId => ( + + )) + } +
+
+ ) + } +} \ No newline at end of file diff --git a/app/javascript/gabsocial/components/panel/who_to_follow_panel.js b/app/javascript/gabsocial/components/panel/who_to_follow_panel.js index ce19ea31..e87e0142 100644 --- a/app/javascript/gabsocial/components/panel/who_to_follow_panel.js +++ b/app/javascript/gabsocial/components/panel/who_to_follow_panel.js @@ -1,5 +1,8 @@ import { defineMessages, injectIntl } from 'react-intl' -import { fetchSuggestions, dismissSuggestion } from '../../actions/suggestions' +import { + fetchRelatedSuggestions, + dismissRelatedSuggestion, +} from '../../actions/suggestions' import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePropTypes from 'react-immutable-proptypes' import Account from '../../components/account' @@ -12,12 +15,12 @@ const messages = defineMessages({ }) const mapStateToProps = (state) => ({ - suggestions: state.getIn(['suggestions', 'items']), + suggestions: state.getIn(['suggestions', 'related', 'items']), }) const mapDispatchToProps = (dispatch) => ({ - fetchSuggestions: () => dispatch(fetchSuggestions()), - dismissSuggestion: (account) => dispatch(dismissSuggestion(account.get('id'))), + fetchRelatedSuggestions: () => dispatch(fetchRelatedSuggestions()), + dismissRelatedSuggestion: (account) => dispatch(dismissRelatedSuggestion(account.get('id'))), }) export default @@ -26,8 +29,8 @@ export default class WhoToFollowPanel extends ImmutablePureComponent { static propTypes = { - dismissSuggestion: PropTypes.func.isRequired, - fetchSuggestions: PropTypes.func.isRequired, + dismissRelatedSuggestion: PropTypes.func.isRequired, + fetchRelatedSuggestions: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, suggestions: ImmutablePropTypes.list.isRequired, isLazy: PropTypes.bool, @@ -53,18 +56,22 @@ class WhoToFollowPanel extends ImmutablePureComponent { componentDidUpdate(prevProps, prevState) { if (!prevState.fetched && this.state.fetched) { - this.props.fetchSuggestions() + this.props.fetchRelatedSuggestions() } } componentDidMount() { if (!this.props.isLazy) { - this.props.fetchSuggestions() + this.props.fetchRelatedSuggestions() } } render() { - const { intl, suggestions, dismissSuggestion } = this.props + const { + intl, + suggestions, + dismissRelatedSuggestion, + } = this.props if (suggestions.isEmpty()) return null @@ -72,8 +79,8 @@ class WhoToFollowPanel extends ImmutablePureComponent {
{ @@ -83,7 +90,7 @@ class WhoToFollowPanel extends ImmutablePureComponent { showDismiss key={accountId} id={accountId} - dismissAction={dismissSuggestion} + dismissAction={dismissRelatedSuggestion} /> )) } diff --git a/app/javascript/gabsocial/reducers/suggestions.js b/app/javascript/gabsocial/reducers/suggestions.js index 9f4b89d5..b31aad18 100644 --- a/app/javascript/gabsocial/reducers/suggestions.js +++ b/app/javascript/gabsocial/reducers/suggestions.js @@ -3,28 +3,38 @@ import { SUGGESTIONS_FETCH_SUCCESS, SUGGESTIONS_FETCH_FAIL, SUGGESTIONS_DISMISS, -} from '../actions/suggestions'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +} from '../actions/suggestions' +import { + Map as ImmutableMap, + List as ImmutableList, + fromJS, +} from 'immutable' const initialState = ImmutableMap({ - items: ImmutableList(), - isLoading: false, -}); + related: ImmutableMap({ + items: ImmutableList(), + isLoading: false, + }), + verified: ImmutableMap({ + items: ImmutableList(), + isLoading: false, + }), +}) export default function suggestionsReducer(state = initialState, action) { switch(action.type) { case SUGGESTIONS_FETCH_REQUEST: - return state.set('isLoading', true); + return state.setIn([action.suggestionType, 'isLoading'], true) case SUGGESTIONS_FETCH_SUCCESS: - return state.withMutations(map => { - map.set('items', fromJS(action.accounts.map(x => x.id))); - map.set('isLoading', false); - }); + return state.withMutations((map) => { + map.setIn([action.suggestionType, 'items'], fromJS(action.accounts.map(x => x.id))) + map.setIn([action.suggestionType, 'isLoading'], false) + }) case SUGGESTIONS_FETCH_FAIL: - return state.set('isLoading', false); + return state.setIn([action.suggestionType, 'isLoading'], false) case SUGGESTIONS_DISMISS: - return state.update('items', list => list.filterNot(id => id === action.id)); + return state.updateIn([action.suggestionType, 'items'], list => list.filterNot(id => id === action.id)) default: - return state; + return state } -}; +} diff --git a/app/lib/verified_suggestions.rb b/app/lib/verified_suggestions.rb new file mode 100644 index 00000000..8c80b42e --- /dev/null +++ b/app/lib/verified_suggestions.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class VerifiedSuggestions + EXPIRE_AFTER = 12.minute.seconds + MAX_ITEMS = 12 + KEY = 'popularsuggestions' + + class << self + include Redisable + + def set(account_ids) + return if account_ids.nil? || account_ids.empty? + redis.setex(KEY, EXPIRE_AFTER, account_ids) + end + + def get(account_id) + account_ids = redis.get(KEY) + + if account_ids.nil? || account_ids.empty? + account_ids = Account.searchable + .where(is_verified: true) + .discoverable + .by_recent_status + .local + .limit(MAX_ITEMS) + .pluck(:id) + + set(account_ids) if account_ids.nil? || account_ids.empty? + else + account_ids = JSON.parse(account_ids) + end + + return [] if account_ids.nil? || account_ids.empty? + + Account.where(id: account_ids) + end + end +end