Added verified accounts/suggestions panel, updated suggestions route

• Added:
- verified accounts/suggestions panel

• Updated:
- suggestions route
This commit is contained in:
mgabdev 2020-07-01 21:36:53 -04:00
parent 095e646661
commit f41274efc7
6 changed files with 227 additions and 58 deletions

@ -5,12 +5,21 @@ class Api::V1::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read } before_action -> { doorkeeper_authorize! :read }
before_action :require_user! before_action :require_user!
before_action :set_accounts
respond_to :json respond_to :json
def index 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 end
def destroy def destroy
@ -18,9 +27,4 @@ class Api::V1::SuggestionsController < Api::BaseController
render_empty render_empty
end end
private
def set_accounts
@accounts = PotentialFriendshipTracker.get(current_account.id)
end
end end

@ -1,57 +1,77 @@
import api from '../api' import api from '../api'
import { importFetchedAccounts } from './importer' import { importFetchedAccounts } from './importer'
import { me } from '../initial_state' 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_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS' export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL' 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) => { return (dispatch, getState) => {
if (!me) return false if (!me) return false
dispatch(fetchSuggestionsRequest()); dispatch(fetchSuggestionsRequest(SUGGESTION_TYPE_VERIFIED))
api(getState).get('/api/v1/suggestions').then(response => { api(getState).get(`/api/v1/suggestions?type=${SUGGESTION_TYPE_VERIFIED}`).then(response => {
dispatch(importFetchedAccounts(response.data)); dispatch(importFetchedAccounts(response.data))
dispatch(fetchSuggestionsSuccess(response.data)); dispatch(fetchSuggestionsSuccess(response.data, SUGGESTION_TYPE_VERIFIED))
}).catch(error => dispatch(fetchSuggestionsFail(error))); }).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 { return {
type: SUGGESTIONS_FETCH_REQUEST, type: SUGGESTIONS_FETCH_REQUEST,
skipLoading: true, skipLoading: true,
}; suggestionType,
}; }
}
export function fetchSuggestionsSuccess(accounts) { export function fetchSuggestionsSuccess(accounts, suggestionType) {
return { return {
type: SUGGESTIONS_FETCH_SUCCESS, type: SUGGESTIONS_FETCH_SUCCESS,
accounts,
skipLoading: true, skipLoading: true,
}; accounts,
}; suggestionType
}
}
export function fetchSuggestionsFail(error) { export function fetchSuggestionsFail(error, suggestionType) {
return { return {
type: SUGGESTIONS_FETCH_FAIL, type: SUGGESTIONS_FETCH_FAIL,
error,
skipLoading: true, skipLoading: true,
skipAlert: true, skipAlert: true,
}; error,
}; suggestionType,
}
}
export const dismissSuggestion = accountId => (dispatch, getState) => { export const dismissRelatedSuggestion = (accountId) => (dispatch, getState) => {
if (!me) return; if (!me) return
dispatch({ dispatch({
type: SUGGESTIONS_DISMISS, type: SUGGESTIONS_DISMISS,
id: accountId, id: accountId,
}); })
api(getState).delete(`/api/v1/suggestions/${accountId}`); api(getState).delete(`/api/v1/suggestions/related/${accountId}`)
}; }

@ -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 (
<PanelLayout
noPadding
title={intl.formatMessage(messages.title)}
// footerButtonTitle={intl.formatMessage(messages.show_more)}
// footerButtonTo='/explore'
>
<div className={_s.default}>
{
suggestions.map(accountId => (
<Account
compact
key={accountId}
id={accountId}
/>
))
}
</div>
</PanelLayout>
)
}
}

@ -1,5 +1,8 @@
import { defineMessages, injectIntl } from 'react-intl' 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 ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import Account from '../../components/account' import Account from '../../components/account'
@ -12,12 +15,12 @@ const messages = defineMessages({
}) })
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
suggestions: state.getIn(['suggestions', 'items']), suggestions: state.getIn(['suggestions', 'related', 'items']),
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
fetchSuggestions: () => dispatch(fetchSuggestions()), fetchRelatedSuggestions: () => dispatch(fetchRelatedSuggestions()),
dismissSuggestion: (account) => dispatch(dismissSuggestion(account.get('id'))), dismissRelatedSuggestion: (account) => dispatch(dismissRelatedSuggestion(account.get('id'))),
}) })
export default export default
@ -26,8 +29,8 @@ export default
class WhoToFollowPanel extends ImmutablePureComponent { class WhoToFollowPanel extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dismissSuggestion: PropTypes.func.isRequired, dismissRelatedSuggestion: PropTypes.func.isRequired,
fetchSuggestions: PropTypes.func.isRequired, fetchRelatedSuggestions: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list.isRequired, suggestions: ImmutablePropTypes.list.isRequired,
isLazy: PropTypes.bool, isLazy: PropTypes.bool,
@ -53,18 +56,22 @@ class WhoToFollowPanel extends ImmutablePureComponent {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (!prevState.fetched && this.state.fetched) { if (!prevState.fetched && this.state.fetched) {
this.props.fetchSuggestions() this.props.fetchRelatedSuggestions()
} }
} }
componentDidMount() { componentDidMount() {
if (!this.props.isLazy) { if (!this.props.isLazy) {
this.props.fetchSuggestions() this.props.fetchRelatedSuggestions()
} }
} }
render() { render() {
const { intl, suggestions, dismissSuggestion } = this.props const {
intl,
suggestions,
dismissRelatedSuggestion,
} = this.props
if (suggestions.isEmpty()) return null if (suggestions.isEmpty()) return null
@ -72,8 +79,8 @@ class WhoToFollowPanel extends ImmutablePureComponent {
<PanelLayout <PanelLayout
noPadding noPadding
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
// footerButtonTitle={intl.formatMessage(messages.show_more)} footerButtonTitle={intl.formatMessage(messages.show_more)}
// footerButtonTo='/explore' footerButtonTo='/explore'
> >
<div className={_s.default}> <div className={_s.default}>
{ {
@ -83,7 +90,7 @@ class WhoToFollowPanel extends ImmutablePureComponent {
showDismiss showDismiss
key={accountId} key={accountId}
id={accountId} id={accountId}
dismissAction={dismissSuggestion} dismissAction={dismissRelatedSuggestion}
/> />
)) ))
} }

@ -3,28 +3,38 @@ import {
SUGGESTIONS_FETCH_SUCCESS, SUGGESTIONS_FETCH_SUCCESS,
SUGGESTIONS_FETCH_FAIL, SUGGESTIONS_FETCH_FAIL,
SUGGESTIONS_DISMISS, SUGGESTIONS_DISMISS,
} from '../actions/suggestions'; } from '../actions/suggestions'
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import {
Map as ImmutableMap,
List as ImmutableList,
fromJS,
} from 'immutable'
const initialState = ImmutableMap({ const initialState = ImmutableMap({
items: ImmutableList(), related: ImmutableMap({
isLoading: false, items: ImmutableList(),
}); isLoading: false,
}),
verified: ImmutableMap({
items: ImmutableList(),
isLoading: false,
}),
})
export default function suggestionsReducer(state = initialState, action) { export default function suggestionsReducer(state = initialState, action) {
switch(action.type) { switch(action.type) {
case SUGGESTIONS_FETCH_REQUEST: case SUGGESTIONS_FETCH_REQUEST:
return state.set('isLoading', true); return state.setIn([action.suggestionType, 'isLoading'], true)
case SUGGESTIONS_FETCH_SUCCESS: case SUGGESTIONS_FETCH_SUCCESS:
return state.withMutations(map => { return state.withMutations((map) => {
map.set('items', fromJS(action.accounts.map(x => x.id))); map.setIn([action.suggestionType, 'items'], fromJS(action.accounts.map(x => x.id)))
map.set('isLoading', false); map.setIn([action.suggestionType, 'isLoading'], false)
}); })
case SUGGESTIONS_FETCH_FAIL: case SUGGESTIONS_FETCH_FAIL:
return state.set('isLoading', false); return state.setIn([action.suggestionType, 'isLoading'], false)
case SUGGESTIONS_DISMISS: 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: default:
return state; return state
} }
}; }

@ -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