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

View File

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

View File

@ -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}`)
}

View File

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

View File

@ -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 {
<PanelLayout
noPadding
title={intl.formatMessage(messages.title)}
// footerButtonTitle={intl.formatMessage(messages.show_more)}
// footerButtonTo='/explore'
footerButtonTitle={intl.formatMessage(messages.show_more)}
footerButtonTo='/explore'
>
<div className={_s.default}>
{
@ -83,7 +90,7 @@ class WhoToFollowPanel extends ImmutablePureComponent {
showDismiss
key={accountId}
id={accountId}
dismissAction={dismissSuggestion}
dismissAction={dismissRelatedSuggestion}
/>
))
}

View File

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

View File

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