Added promotions to redux and added selector for setting promotions if not PRO

• Added:
- promotions to redux
- selector for setting promotions if not PRO

• Updated:
- StatusList, SidebarPanelGroup to use promotions from redux
This commit is contained in:
mgabdev 2020-11-09 13:28:43 -06:00
parent f806fddb5f
commit 21937d9e09
9 changed files with 123 additions and 23 deletions

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class Api::V1::PromotionsController < EmptyController
def index
data = ActiveModelSerializers::SerializableResource.new(Promotion.active, each_serializer: REST::PromotionSerializer)
render json: data.to_json, content_type: 'application/json'
end
end

View File

@ -0,0 +1,34 @@
import api from '../api'
import { me } from '../initial_state'
export const PROMOTIONS_FETCH_REQUEST = 'PROMOTIONS_FETCH_REQUEST'
export const PROMOTIONS_FETCH_SUCCESS = 'PROMOTIONS_FETCH_SUCCESS'
export const PROMOTIONS_FETCH_FAIL = 'PROMOTIONS_FETCH_FAIL'
export const fetchPromotions = () => {
return (dispatch, getState) => {
if (!me) return
dispatch(fetchPromotionsRequest())
api(getState).get('/api/v1/promotions').then((response) => {
dispatch(fetchPromotionsSuccess(response.data))
}).catch((error) => {
dispatch(fetchPromotionsFail(error))
})
}
}
const fetchPromotionsRequest = () => ({
type: PROMOTIONS_FETCH_REQUEST,
})
const fetchPromotionsSuccess = (items) => ({
type: PROMOTIONS_FETCH_SUCCESS,
items,
})
const fetchPromotionsFail = (error, listType) => ({
type: PROMOTIONS_FETCH_FAIL,
error,
})

View File

@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { me, promotions } from '../initial_state' import { connect } from 'react-redux'
import { me } from '../initial_state'
import { getPromotions } from '../selectors'
import Bundle from '../features/ui/util/bundle' import Bundle from '../features/ui/util/bundle'
import WrappedBundle from '../features/ui/util/wrapped_bundle' import WrappedBundle from '../features/ui/util/wrapped_bundle'
import { import {
@ -10,16 +12,20 @@ import {
class SidebarPanelGroup extends React.PureComponent { class SidebarPanelGroup extends React.PureComponent {
render() { render() {
const { layout, page } = this.props const {
layout,
page,
promotions,
} = this.props
if (Array.isArray(promotions) && Array.isArray(layout) && !!me) { if (!!promotions && promotions.count() > 0 && Array.isArray(layout) && !!me) {
const sidebarPromotionPageId = `${page}.sidebar` const sidebarPromotionPageId = `${page}.sidebar`
const promotion = promotions.find(p => p.timeline_id === sidebarPromotionPageId) const promotion = promotions.find((p) => p.get('timeline_id') === sidebarPromotionPageId)
if (!!promotion) { if (!!promotion) {
const correctedPosition = promotion.position - 1 > layout.length ? layout.length - 1 : promotion.position const correctedPosition = promotion.get('position') - 1 > layout.length ? layout.length - 1 : promotion.get('position')
if (!layout.find(p => p.key === 'status-promotion-panel')) { if (!layout.find(p => p.key === 'status-promotion-panel')) {
layout.splice(correctedPosition, 0, <WrappedBundle key='status-promotion-panel' component={StatusPromotionPanel} componentParams={{ statusId: promotion.status_id }} />) layout.splice(correctedPosition, 0, <WrappedBundle key='status-promotion-panel' component={StatusPromotionPanel} componentParams={{ statusId: promotion.get('status_id') }} />)
} }
} }
} }
@ -56,10 +62,14 @@ class SidebarPanelGroup extends React.PureComponent {
} }
const mapStateToProps = (state) => ({
promotions: getPromotions()(state)
})
SidebarPanelGroup.propTypes = { SidebarPanelGroup.propTypes = {
layout: PropTypes.array.isRequired, layout: PropTypes.array.isRequired,
page: PropTypes.string.isRequired, page: PropTypes.string.isRequired,
promotion: PropTypes.object, promotion: PropTypes.object,
} }
export default SidebarPanelGroup export default connect(mapStateToProps)(SidebarPanelGroup)

View File

@ -6,7 +6,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import { me, promotions } from '../initial_state' import { me } from '../initial_state'
import { getPromotions } from '../selectors'
import { import {
TIMELINE_INJECTION_FEATURED_GROUPS, TIMELINE_INJECTION_FEATURED_GROUPS,
TIMELINE_INJECTION_GROUP_CATEGORIES, TIMELINE_INJECTION_GROUP_CATEGORIES,
@ -45,15 +46,16 @@ class StatusList extends ImmutablePureComponent {
promotedStatuses, promotedStatuses,
timelineId, timelineId,
statusIds, statusIds,
promotions,
} = this.props } = this.props
if (Array.isArray(promotions)) { if (!!promotions && promotions.count() > 0) {
promotions.forEach((promotionBlock) => { promotions.forEach((promotion) => {
if (promotionBlock.timeline_id === timelineId && if (promotion.get('timeline_id') === timelineId &&
statusIds.count() >= promotionBlock.position && statusIds.count() >= promotion.get('position') &&
!promotedStatuses[promotionBlock.status_id]) { !promotedStatuses[promotion.get('status_id')]) {
onFetchStatus(promotionBlock.status_id) onFetchStatus(promotion.get('status_id'))
} }
}) })
@ -158,9 +160,12 @@ class StatusList extends ImmutablePureComponent {
emptyMessage, emptyMessage,
onScrollToTop, onScrollToTop,
onScroll, onScroll,
promotions,
} = this.props } = this.props
const { fetchedContext, isRefreshing } = this.state const { fetchedContext, isRefreshing } = this.state
console.log("promotions:", promotions)
if (isPartial || (isLoading && statusIds.size === 0)) { if (isPartial || (isLoading && statusIds.size === 0)) {
return ( return (
<React.Fragment> <React.Fragment>
@ -200,13 +205,13 @@ class StatusList extends ImmutablePureComponent {
/> />
) )
} else { } else {
if (Array.isArray(promotions)) { if (!!promotions && promotions.count() > 0) {
const promotionBlock = promotions.find(p => (p.position === i && p.timeline_id === timelineId)) const promotion = promotions.find((p) => (p.get('position') === i && p.get('timeline_id') === timelineId))
if (promotionBlock) { if (promotion) {
scrollableContent.push( scrollableContent.push(
<StatusContainer <StatusContainer
key={`promotion-${i}-${promotionBlock.status_id}`} key={`promotion-${i}-${promotion.get('status_id')}`}
id={promotionBlock.status_id} id={promotion.get('status_id')}
onMoveUp={this.handleMoveUp} onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown} onMoveDown={this.handleMoveDown}
contextType={timelineId} contextType={timelineId}
@ -340,21 +345,24 @@ const mapStateToProps = (state, { timelineId }) => {
if (!timelineId) return {} if (!timelineId) return {}
const getStatusIds = makeGetStatusIds() const getStatusIds = makeGetStatusIds()
const promotions = getPromotions()(state)
const statusIds = getStatusIds(state, { const statusIds = getStatusIds(state, {
type: timelineId.substring(0, 5) === 'group' ? 'group' : timelineId, type: timelineId.substring(0, 5) === 'group' ? 'group' : timelineId,
id: timelineId id: timelineId
}) })
const promotedStatuses = Array.isArray(promotions) ? const promotedStatuses = (!!promotions && promotions.count() > 0) ?
promotions.map((block) => { promotions.map((promotion) => {
const s = {} const s = {}
s[block.status_id] = state.getIn(['statuses', block.status_id]) s[promotion.get('status_id')] = state.getIn(['statuses', promotion.get('status_id')])
return s return s
}) : [] }) : []
return { return {
statusIds, statusIds,
promotions,
promotedStatuses, promotedStatuses,
isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),

View File

@ -10,6 +10,7 @@ import moment from 'moment-mini'
import { ScrollContext } from 'react-router-scroll-4' import { ScrollContext } from 'react-router-scroll-4'
import { IntlProvider, addLocaleData } from 'react-intl' import { IntlProvider, addLocaleData } from 'react-intl'
import { fetchCustomEmojis } from '../actions/custom_emojis' import { fetchCustomEmojis } from '../actions/custom_emojis'
import { fetchPromotions } from '../actions/promotions'
import { hydrateStore } from '../actions/store' import { hydrateStore } from '../actions/store'
import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants' import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants'
import { import {
@ -32,6 +33,7 @@ const hydrateAction = hydrateStore(initialState)
store.dispatch(hydrateAction) store.dispatch(hydrateAction)
store.dispatch(fetchCustomEmojis()) store.dispatch(fetchCustomEmojis())
store.dispatch(fetchPromotions())
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
accountCreatedAt: !!me ? state.getIn(['accounts', me, 'created_at']) : undefined, accountCreatedAt: !!me ? state.getIn(['accounts', me, 'created_at']) : undefined,

View File

@ -25,6 +25,7 @@ import news from './news'
import notifications from './notifications' import notifications from './notifications'
import polls from './polls' import polls from './polls'
import popover from './popover' import popover from './popover'
import promotions from './promotions'
import push_notifications from './push_notifications' import push_notifications from './push_notifications'
import relationships from './relationships' import relationships from './relationships'
import reports from './reports' import reports from './reports'
@ -70,6 +71,7 @@ const reducers = {
notifications, notifications,
polls, polls,
popover, popover,
promotions,
push_notifications, push_notifications,
relationships, relationships,
reports, reports,

View File

@ -0,0 +1,23 @@
import {
List as ImmutableList,
fromJS,
}from 'immutable'
import {
PROMOTIONS_FETCH_REQUEST,
PROMOTIONS_FETCH_SUCCESS,
PROMOTIONS_FETCH_FAIL,
} from '../actions/promotions'
const initialState = ImmutableList()
export default function promotions(state = initialState, action) {
switch (action.type) {
case PROMOTIONS_FETCH_REQUEST:
case PROMOTIONS_FETCH_FAIL:
return initialState
case PROMOTIONS_FETCH_SUCCESS:
return fromJS(action.items)
default:
return state
}
}

View File

@ -38,6 +38,16 @@ const toServerSideType = columnType => {
export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date()))); export const getFilters = (state, { contextType }) => state.get('filters', ImmutableList()).filter(filter => contextType && filter.get('context').includes(toServerSideType(contextType)) && (filter.get('expires_at') === null || Date.parse(filter.get('expires_at')) > (new Date())));
export const getPromotions = () => {
return createSelector([
(state) => state,
(state) => state.getIn(['accounts', me, 'is_pro']),
(state) => state.get('promotions'),
], (state, isPro, promotions) => {
return !isPro ? promotions : ImmutableList()
})
}
const escapeRegExp = string => const escapeRegExp = string =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string

View File

@ -345,6 +345,7 @@ Rails.application.routes.draw do
end end
get '/search', to: 'search#index', as: :search get '/search', to: 'search#index', as: :search
resources :promotions, only: [:index]
get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex get '/account_by_username/:username', to: 'account_by_username#show', username: username_regex