Added Timeline Injections

• Added:
- Timeline Injections
- FeaturedGroupsInjection, GroupCategoriesInjection, ProUpgradeInjection, PWAInjection, ShopInjection, TimelineInjectionBase, TimelineInjectionLayout, TimelineInjectionRoot, UserSuggestionsInjection
- Constants
- Redux for timeline_injections
- settings for setting
- popover for dismissing and saving weight
This commit is contained in:
mgabdev 2020-09-14 11:40:42 -05:00
parent 41f48ea886
commit d198695bdb
16 changed files with 989 additions and 7 deletions

@ -0,0 +1,32 @@
import {
TIMELINE_INJECTION_WEIGHT_DEFAULT,
TIMELINE_INJECTION_WEIGHT_MULTIPLIER,
TIMELINE_INJECTION_WEIGHT_SUBTRACTOR,
TIMELINE_INJECTION_WEIGHT_MIN,
} from '../constants'
import { changeSetting } from './settings'
export const TIMELINE_INJECTION_SHOW = 'TIMELINE_INJECTION_SHOW'
export const TIMELINE_INJECTION_HIDE = 'TIMELINE_INJECTION_HIDE'
export const showTimelineInjection = (injectionId) => (dispatch) => {
dispatch({
type: TIMELINE_INJECTION_SHOW,
injectionId,
})
}
export const hideTimelineInjection = (injectionId) => (dispatch, getState) => {
const existingInjectionWeight = getState().getIn(['settings', 'injections', injectionId], null)
if (!existingInjectionWeight) return false
const newInjectionWeight = Math.max(existingInjectionWeight - 0.005, 0.01)
dispatch(changeSetting(['injections', injectionId], newInjectionWeight))
dispatch({
type: TIMELINE_INJECTION_HIDE,
injectionId,
})
}

@ -14,6 +14,7 @@ import {
POPOVER_STATUS_OPTIONS,
POPOVER_STATUS_EXPIRATION_OPTIONS,
POPOVER_STATUS_VISIBILITY,
POPOVER_TIMELINE_INJECTION_OPTIONS,
POPOVER_USER_INFO,
POPOVER_VIDEO_STATS,
} from '../../constants'
@ -32,6 +33,7 @@ import {
StatusExpirationOptionsPopover,
StatusOptionsPopover,
StatusVisibilityPopover,
TimelineInjectionOptionsPopover,
UserInfoPopover,
VideoStatsPopover,
} from '../../features/ui/util/async_components'
@ -64,6 +66,7 @@ POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover
POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover
POPOVER_COMPONENTS[POPOVER_STATUS_EXPIRATION_OPTIONS] = StatusExpirationOptionsPopover
POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover
POPOVER_COMPONENTS[POPOVER_TIMELINE_INJECTION_OPTIONS] = TimelineInjectionOptionsPopover
POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover
POPOVER_COMPONENTS[POPOVER_VIDEO_STATS] = VideoStatsPopover

@ -0,0 +1,66 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { closePopover } from '../../actions/popover'
import { hideTimelineInjection } from '../../actions/timeline_injections'
import PopoverLayout from './popover_layout'
import List from '../list'
class TimelineInjectionOptionsPopover extends React.PureComponent {
handleOnClick = () => {
this.props.onDismissInjection()
this.props.onDismiss()
this.props.onClosePopover()
}
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const { intl, isXS } = this.props
return (
<PopoverLayout
width={280}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<List
size={isXS ? 'large' : 'small'}
scrollKey='timeline_injection_options'
items={[{
hideArrow: true,
title: intl.formatMessage(messages.dismissMessage),
onClick: this.handleOnClick,
}]}
/>
</PopoverLayout>
)
}
}
const messages = defineMessages({
dismissMessage: { id: 'timeline_injection_popover.dismiss_message', defaultMessage: 'Show this content less often' },
})
const mapDispatchToProps = (dispatch, { timelineInjectionId }) => ({
onDismissInjection() {
dispatch(hideTimelineInjection(timelineInjectionId))
},
onClosePopover: () => dispatch(closePopover()),
})
TimelineInjectionOptionsPopover.propTypes = {
intl: PropTypes.object.isRequired,
isXS: PropTypes.bool,
timelineInjectionId: PropTypes.string.isRequired,
onClosePopover: PropTypes.func.isRequired,
onDismissInjection: PropTypes.func.isRequired,
onDismiss: PropTypes.func.isRequired,
}
export default injectIntl(connect(null, mapDispatchToProps)(TimelineInjectionOptionsPopover))

@ -0,0 +1,73 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { fetchGroups } from '../../actions/groups'
import GroupCollectionItem from '../group_collection_item'
import TimelineInjectionLayout from './timeline_injection_layout'
class FeaturedGroupsInjection extends ImmutablePureComponent {
componentDidMount() {
if (!this.props.isFetched) {
this.props.onFetchGroups('featured')
}
}
render() {
const {
groupIds,
isLoading,
isFetched,
isXS,
injectionId,
} = this.props
if (isFetched && groupIds.size === 0) {
return <div />
}
return (
<TimelineInjectionLayout
id={injectionId}
title='Featured groups'
buttonLink='/groups/browse/featured'
buttonTitle='See more featured groups'
isXS={isXS}
>
{
groupIds.map((groupId) => (
<div className={[_s.d, _s.w300PX].join(' ')}>
<GroupCollectionItem
isAddable
id={groupId}
/>
</div>
))
}
</TimelineInjectionLayout>
)
}
}
const mapStateToProps = (state) => ({
groupIds: state.getIn(['group_lists', 'featured', 'items']),
isFetched: state.getIn(['group_lists', 'featured', 'isFetched']),
isLoading: state.getIn(['group_lists', 'featured', 'isLoading']),
})
const mapDispatchToProps = (dispatch) => ({
onFetchGroups: (tab) => dispatch(fetchGroups(tab)),
})
FeaturedGroupsInjection.propTypes = {
groupIds: ImmutablePropTypes.list,
isFetched: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
onFetchGroups: PropTypes.func.isRequired,
injectionId: PropTypes.string.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(FeaturedGroupsInjection)

@ -0,0 +1,101 @@
import React from 'react'
import PropTypes from 'prop-types'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { connect } from 'react-redux'
import { makeGetAccount } from '../../selectors'
import { fetchGroupCategories } from '../../actions/group_categories'
import slugify from '../../utils/slugify'
import Button from '../button'
import Text from '../text'
import TimelineInjectionLayout from './timeline_injection_layout'
class FeaturedGroupsInjection extends React.PureComponent {
componentDidMount() {
this.props.dispatch(fetchGroupCategories())
}
render() {
const {
categories,
isXS,
injectionId,
} = this.props
let categoriesOptions = []
if (categories) {
for (let i = 0; i < categories.count(); i++) {
const c = categories.get(i)
const title = c.get('text')
categoriesOptions.push({
title,
to: `/groups/browse/categories/${slugify(title)}`,
})
}
}
const split1Arr = categoriesOptions.splice(0, Math.ceil(categoriesOptions.length /2));
return (
<TimelineInjectionLayout
id={injectionId}
title='Popular group categories'
subtitle='Find a group by browsing top categories.'
buttonLink='/groups/browse/categories'
buttonTitle='Browse all categories'
isXS={isXS}
>
<div className={[_s.d, _s.pb10].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.mb5].join(' ')}>
{
split1Arr.map((block) => (
<Button
isNarrow
to={block.to}
color='primary'
backgroundColor='tertiary'
className={[_s.mr10].join(' ')}
>
<Text color='inherit'>
{block.title}
</Text>
</Button>
))
}
</div>
<div className={[_s.d, _s.flexRow].join(' ')}>
{
categoriesOptions.map((block) => (
<Button
isNarrow
to={block.to}
color='primary'
backgroundColor='tertiary'
className={[_s.mr10].join(' ')}
>
<Text color='inherit'>
{block.title}
</Text>
</Button>
))
}
</div>
</div>
</TimelineInjectionLayout>
)
}
}
const mapStateToProps = (state) => ({
categories: state.getIn(['group_categories', 'items']),
})
FeaturedGroupsInjection.propTypes = {
categories: ImmutablePropTypes.list.isRequired,
injectionId: PropTypes.string.isRequired,
}
export default connect(mapStateToProps)(FeaturedGroupsInjection)

@ -0,0 +1,73 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { me } from '../../initial_state'
import { URL_GAB_PRO } from '../../constants'
import Button from '../button'
import Text from '../text'
class ProUpgradeInjection extends React.PureComponent {
deferredPrompt = null
componentDidMount() {
}
handleOnClick = () => {
}
render() {
const { isPro } = this.props
if (isPro) return <div />
return (
<div className={[_s.d, _s.w100PC, _s.px15, _s.mb15].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.py15, _s.px10, _s.boxShadowBlock, _s.radiusSmall, _s.bgPrimary].join(' ')}>
<div className={[_s.d, _s.py15, _s.px10].join(' ')}>
<Text size='extraLarge' align='center' weight='bold' className={_s.mb15}>
Upgrade to GabPRO
</Text>
<Text size='large' color='secondary' align='center'>
Please consider supporting us on our mission to defend free expression online for all people.
</Text>
</div>
<div className={[_s.d, _s.mt10, _s.mb5, _s.flexRow, _s.mlAuto, _s.mrAuto].join(' ')}>
<Button
backgroundColor='secondary'
color='secondary'
onClick={this.handleOnClick}
className={_s.mr10}
>
<Text color='inherit' className={_s.px5}>
Not now
</Text>
</Button>
<Button href={URL_GAB_PRO}>
<Text color='inherit' weight='medium' className={_s.px15}>
Learn More
</Text>
</Button>
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state) => ({
isPro: state.getIn(['accounts', me, 'is_pro']),
})
ProUpgradeInjection.propTypes = {
isPro: PropTypes.bool.isRequired,
injectionId: PropTypes.string,
isXS: PropTypes.bool,
}
export default connect(mapStateToProps)(ProUpgradeInjection)

@ -0,0 +1,109 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../button'
import Text from '../text'
class PWAInjection extends React.PureComponent {
deferredPrompt=null
componentDidMount() {
window.addEventListener('beforeinstallprompt',(e) => {
console.log("e:",e)
// Prevent the mini-infobar from appearing on mobile
e.preventDefault()
// Stash the event so it can be triggered later.
this.deferredPrompt=e
// Update UI notify the user they can install the PWA
// showInstallPromotion()
})
window.addEventListener('appinstalled',(evt) => {
// Log install to analytics
console.log('INSTALL: Success')
})
window.addEventListener('DOMContentLoaded',() => {
let displayMode='browser tab'
if(navigator.standalone) {
displayMode='standalone-ios'
}
if(window.matchMedia('(display-mode: standalone)').matches) {
displayMode='standalone'
}
// Log launch display mode to analytics
console.log('DISPLAY_MODE_LAUNCH:',displayMode)
window.matchMedia('(display-mode: standalone)').addListener((evt) => {
let displayMode='browser tab';
if(evt.matches) {
displayMode='standalone';
}
// Log display mode change to analytics
console.log('DISPLAY_MODE_CHANGED',displayMode);
});
})
}
handleOnClick=() => {
// Hide the app provided install promotion
// hideMyInstallPromotion()
// Show the install prompt
this.deferredPrompt.prompt()
// Wait for the user to respond to the prompt
this.deferredPrompt.userChoice.then((choiceResult) => {
if(choiceResult.outcome==='accepted') {
console.log('User accepted the install prompt')
} else {
console.log('User dismissed the install prompt')
}
})
}
render() {
// : todo :
return <div />
return (
<div className={[_s.d,_s.w100PC,_s.px15,_s.mb15].join(' ')}>
<div className={[_s.d,_s.w100PC,_s.py15,_s.px10,_s.boxShadowBlock,_s.radiusSmall,_s.bgPrimary].join(' ')}>
<div className={[_s.d,_s.py15,_s.px10].join(' ')}>
<Text size='large' align='center' className={_s.mb10}>
Were not on the app stores, but you can still get the Gab app on your phone.
</Text>
<Text size='large' align='center'>
Click install to learn how.
</Text>
</div>
<div className={[_s.d,_s.mt10,_s.mb5,_s.flexRow,_s.mlAuto,_s.mrAuto].join(' ')}>
<Button
backgroundColor='none'
color='secondary'
className={_s.mr15}
>
Not now
</Button>
<Button
onClick={this.handleOnClick}
>
<Text color='inherit' weight='medium' className={_s.px10}>
Install
</Text>
</Button>
</div>
</div>
</div>
)
}
}
PWAInjection.propTypes = {
injectionId: PropTypes.string,
isXS: PropTypes.string,
}
export default PWAInjection

@ -0,0 +1,77 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { fetchFeaturedProducts } from '../../actions/shop'
import { URL_DISSENTER_SHOP } from '../../constants'
import ShopItem from '../shop_item'
import TimelineInjectionLayout from './timeline_injection_layout'
class ShopInjection extends React.PureComponent {
componentDidMount() {
const { items } = this.props
const doFetch = !Array.isArray(items) || (Array.isArray(items) && items.length === 0)
if (doFetch) {
this.props.onFetchFeaturedProducts()
}
}
render() {
const {
intl,
items,
isError,
injectionId,
} = this.props
if (!items || isError || !Array.isArray(items)) return <div />
return (
<TimelineInjectionLayout
id={injectionId}
title={intl.formatMessage(messages.title)}
buttonHref={URL_DISSENTER_SHOP}
buttonTitle={intl.formatMessage(messages.shop_now)}
>
{
items.map((block, i) => (
<ShopItem
key={`shop-item-injection-${i}`}
image={block.image}
name={block.name}
link={block.link}
price={block.price}
/>
))
}
</TimelineInjectionLayout>
)
}
}
const messages = defineMessages({
title: { id: 'shop_panel.title', defaultMessage: 'Dissenter Shop' },
shop_now: { id: 'shop_panel.shop_now', defaultMessage: 'Visit the Dissenter Shop' },
})
const mapStateToProps = (state) => ({
items: state.getIn(['shop', 'featured', 'items']),
isError: state.getIn(['shop', 'featured', 'isError']),
})
const mapDispatchToProps = (dispatch) => ({
onFetchFeaturedProducts: () => dispatch(fetchFeaturedProducts()),
})
ShopInjection.propTypes = {
intl: PropTypes.object.isRequired,
products: PropTypes.array,
onFetchFeaturedProducts: PropTypes.func.isRequired,
isError: PropTypes.bool.isRequired,
injectionId: PropTypes.string,
}
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ShopInjection))

@ -0,0 +1,67 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { me } from '../../initial_state'
import {
TIMELINE_INJECTION_PWA,
TIMELINE_INJECTION_WEIGHT_MULTIPLIER,
} from '../../constants'
import TimelineInjectionRoot from './timeline_injection_root'
class TimelineInjectionBase extends ImmutablePureComponent {
state = {
injectionType: {}
}
componentDidMount() {
const { injectionWeights } = this.props
const keys = injectionWeights.keySeq().toArray()
const values = injectionWeights.valueSeq().toArray()
const weights = values.map((a) => Math.max(Math.ceil(a * TIMELINE_INJECTION_WEIGHT_MULTIPLIER), 0.01))
const totalWeight = weights.reduce((a, b) => a + b, 0)
let weighedElems = []
let currentElem = 0
while (currentElem < keys.length) {
for (let i = 0; i < weights[currentElem]; i++) {
weighedElems[weighedElems.length] = currentElem
}
currentElem++
}
const rnd = Math.floor(Math.random() * totalWeight)
const index = weighedElems[rnd]
const injectionType = keys[index]
this.setState({ injectionType })
}
render() {
const { injectionType } = this.state
// : todo :
// hide PWA for now
if (injectionType === TIMELINE_INJECTION_PWA) return <div />
if (!me) return <div />
return <TimelineInjectionRoot type={injectionType} />
}
}
const mapStateToProps = (state) => ({
injectionWeights: state.getIn(['settings', 'injections']),
})
TimelineInjectionBase.propTypes = {
index: PropTypes.number,
injectionWeights: ImmutablePropTypes.map,
}
export default connect(mapStateToProps)(TimelineInjectionBase)

@ -0,0 +1,115 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { openPopover } from '../../actions/popover'
import {
CX,
POPOVER_TIMELINE_INJECTION_OPTIONS,
} from '../../constants'
import Button from '../button'
import Text from '../text'
class TimelineInjectionLayout extends React.PureComponent {
state = {
dismissed: false,
}
handleOnOptionsClick = () => {
this.props.dispatch(openPopover(POPOVER_TIMELINE_INJECTION_OPTIONS, {
targetRef: this.optionsBtn,
timelineInjectionId: this.props.id,
onDismiss: this.handleOnDismiss,
}))
}
handleOnDismiss = () => {
this.setState({ dismissed: true })
}
setOptionsBtn = (n) => {
this.optionsBtn = n
}
render() {
const {
title,
subtitle,
children,
buttonLink,
buttonTitle,
isXS,
} = this.props
const { dismissed } = this.state
if (dismissed) return <div />
const containerClasses = CX({
d: 1,
w100PC: 1,
mb10: 1,
borderTop1PX: isXS,
borderBottom1PX: isXS,
border1PX: !isXS,
radiusSmall: !isXS,
borderColorSecondary: 1,
bgPrimary: 1,
overflowHidden: 1,
})
return (
<div className={containerClasses}>
<div className={[_s.d, _s.px15, _s.py5, _s.flexRow, _s.jcCenter, _s.aiCenter].join(' ')}>
<div className={[_s.d, _s.pr10].join(' ')}>
<Text size='medium'>
{title}
</Text>
{
!!subtitle &&
<Text size='small' weight='medium' color='secondary' className={[_s.pt5, _s.pb10].join(' ')}>
{subtitle}
</Text>
}
</div>
<Button
backgroundColor='none'
color='secondary'
iconSize='16px'
icon='ellipsis'
onClick={this.handleOnOptionsClick}
buttonRef={this.setOptionsBtn}
className={[_s.mlAuto].join(' ')}
/>
</div>
<div className={[_s.d, _s.px10, _s.flexRow, _s.width100PC, _s.overflowHidden, _s.overflowXScroll, _s.noScrollbar, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
{children}
</div>
<div className={_s.d}>
<Button
isText
color='none'
backgroundColor='none'
to={buttonLink}
className={[_s.px15, _s.py15, _s.bgSubtle_onHover].join(' ')}
>
<Text color='brand' align='center' size='medium'>
{buttonTitle}
</Text>
</Button>
</div>
</div>
)
}
}
TimelineInjectionLayout.propTypes = {
title: PropTypes.string,
buttonLink: PropTypes.string,
buttonTitle: PropTypes.string,
id: PropTypes.string.isRequired,
subtitle: PropTypes.string,
isXS: PropTypes.bool,
}
export default connect()(TimelineInjectionLayout)

@ -0,0 +1,104 @@
import {
BREAKPOINT_EXTRA_SMALL,
TIMELINE_INJECTION_FEATURED_GROUPS,
TIMELINE_INJECTION_GROUP_CATEGORIES,
TIMELINE_INJECTION_PRO_UPGRADE,
TIMELINE_INJECTION_PWA,
TIMELINE_INJECTION_SHOP,
TIMELINE_INJECTION_USER_SUGGESTIONS,
} from '../../constants'
import {
FeaturedGroupsInjection,
GroupCategoriesInjection,
ProUpgradeInjection,
PWAInjection,
ShopInjection,
UserSuggestionsInjection,
} from '../../features/ui/util/async_components'
import React from 'react'
import PropTypes from 'prop-types'
import { getWindowDimension } from '../../utils/is_mobile'
import Bundle from '../../features/ui/util/bundle'
const initialState = getWindowDimension()
const INJECTION_COMPONENTS = {}
INJECTION_COMPONENTS[TIMELINE_INJECTION_FEATURED_GROUPS] = FeaturedGroupsInjection
INJECTION_COMPONENTS[TIMELINE_INJECTION_GROUP_CATEGORIES] = GroupCategoriesInjection
INJECTION_COMPONENTS[TIMELINE_INJECTION_PRO_UPGRADE] = ProUpgradeInjection
INJECTION_COMPONENTS[TIMELINE_INJECTION_PWA] = PWAInjection
INJECTION_COMPONENTS[TIMELINE_INJECTION_SHOP] = ShopInjection
INJECTION_COMPONENTS[TIMELINE_INJECTION_USER_SUGGESTIONS] = UserSuggestionsInjection
class TimelineInjectionRoot extends React.PureComponent {
state = {
width: initialState.width,
}
componentDidMount() {
this.handleResize()
window.addEventListener('resize', this.handleResize, false)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false)
}
handleResize = () => {
const { width } = getWindowDimension()
this.setState({ width })
}
renderLoading = () => {
return <div />
}
renderError = () => {
return <div />
}
render() {
const { type } = this.props
const { width } = this.state
const visible = !!type
if (!visible) return <div />
const isXS = width <= BREAKPOINT_EXTRA_SMALL
//If is not XS and popover is pwa, dont show
//Since not on mobile this should not be visible
if (!isXS && type === TIMELINE_INJECTION_PWA) return <div />
return (
<div>
<Bundle
fetchComponent={INJECTION_COMPONENTS[type]}
loading={this.renderLoading}
error={this.renderError}
renderDelay={150}
>
{
(Component) => (
<Component
isXS={isXS}
injectionId={type}
/>
)
}
</Bundle>
</div>
)
}
}
TimelineInjectionRoot.propTypes = {
type: PropTypes.string,
}
export default TimelineInjectionRoot

@ -0,0 +1,100 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { defineMessages, injectIntl } from 'react-intl'
import {
fetchRelatedSuggestions,
fetchPopularSuggestions,
} from '../../actions/suggestions'
import Account from '../account'
import TimelineInjectionLayout from './timeline_injection_layout'
class UserSuggestionsInjection extends ImmutablePureComponent {
componentDidMount() {
this.handleFetch()
}
handleFetch = () => {
if (this.props.suggestionType === 'verified') {
this.props.fetchPopularSuggestions()
} else {
this.props.fetchRelatedSuggestions()
}
}
render() {
const {
intl,
isLoading,
isXS,
suggestions,
suggestionType,
injectionId,
} = this.props
if (suggestions.isEmpty()) return <div />
const title = suggestionType === 'verified' ? intl.formatMessage(messages.verifiedTitle) : intl.formatMessage(messages.relatedTitle)
return (
<TimelineInjectionLayout
id={injectionId}
title={title}
buttonLink='/suggestions'
buttonTitle='See more reccomendations'
isXS={isXS}
>
{
suggestions.map((accountId) => (
<Account
isCard
key={`user_suggestion_injection_${accountId}`}
id={accountId}
/>
))
}
</TimelineInjectionLayout>
)
}
}
const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
relatedTitle: { id: 'who_to_follow.title', defaultMessage: 'Who to Follow' },
verifiedTitle: { id: 'who_to_follow.verified_title', defaultMessage: 'Verified Accounts to Follow' },
show_more: { id: 'who_to_follow.more', defaultMessage: 'Show more' },
})
const mapStateToProps = (state, { suggestionType = 'related' }) => ({
suggestions: state.getIn(['suggestions', suggestionType, 'items']),
isLoading: state.getIn(['suggestions', suggestionType, 'isLoading']),
})
const mapDispatchToProps = (dispatch) => ({
fetchRelatedSuggestions: () => dispatch(fetchRelatedSuggestions()),
fetchPopularSuggestions: () => dispatch(fetchPopularSuggestions()),
})
UserSuggestionsInjection.propTypes = {
suggestionType: PropTypes.oneOf([
'related',
'verified'
]),
fetchRelatedSuggestions: PropTypes.func.isRequired,
fetchPopularSuggestions: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
isXS: PropTypes.bool,
injectionId: PropTypes.string,
}
UserSuggestionsInjection.defaultProps = {
suggestionType: 'related',
}
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UserSuggestionsInjection))

@ -34,6 +34,7 @@ export const POPOVER_SIDEBAR_MORE = 'SIDEBAR_MORE'
export const POPOVER_STATUS_OPTIONS = 'STATUS_OPTIONS'
export const POPOVER_STATUS_EXPIRATION_OPTIONS = 'STATUS_EXPIRATION_OPTIONS'
export const POPOVER_STATUS_VISIBILITY = 'STATUS_VISIBILITY'
export const POPOVER_TIMELINE_INJECTION_OPTIONS = 'TIMELINE_INJECTION_OPTIONS'
export const POPOVER_USER_INFO = 'USER_INFO'
export const POPOVER_VIDEO_STATS = 'VIDEO_STATS'
@ -136,4 +137,16 @@ export const GROUP_TIMELINE_SORTING_TYPE_TOP_OPTION_YEARLY = 'yearly'
export const GROUP_TIMELINE_SORTING_TYPE_TOP_OPTION_ALL_TIME = 'all_time'
export const TOAST_TYPE_ERROR = 'error'
export const TOAST_TYPE_SUCCESS = 'success'
export const TOAST_TYPE_SUCCESS = 'success'
export const TIMELINE_INJECTION_FEATURED_GROUPS = 'TI_FEATURED_GROUPS'
export const TIMELINE_INJECTION_GROUP_CATEGORIES = 'TI_GROUP_CATEGORIES'
export const TIMELINE_INJECTION_PRO_UPGRADE = 'TI_PRO_UPGRADE'
export const TIMELINE_INJECTION_PWA = 'TI_PWA'
export const TIMELINE_INJECTION_SHOP = 'TI_SHOP'
export const TIMELINE_INJECTION_USER_SUGGESTIONS = 'TI_USER_SUGGESTIONS'
export const TIMELINE_INJECTION_WEIGHT_DEFAULT = 1
export const TIMELINE_INJECTION_WEIGHT_MULTIPLIER = 100
export const TIMELINE_INJECTION_WEIGHT_SUBTRACTOR = 0.005
export const TIMELINE_INJECTION_WEIGHT_MIN = 0.01

@ -22,14 +22,14 @@ 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 FeaturedGroupsInjection() { return import(/* webpackChunkName: "components/featured_groups_injection" */'../../../components/timeline_injections/featured_groups_injection') }
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') }
export function FollowRequests() { return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests') }
export function LikedStatuses() { return import(/* webpackChunkName: "features/liked_statuses" */'../../liked_statuses') }
export function GenericNotFound() { return import(/* webpackChunkName: "features/generic_not_found" */'../../generic_not_found') }
export function GlobalFooter() { return import(/* webpackChunkName: "components/global_footer" */'../../../components/global_footer') }
// export function GroupCategoriesInjection() { return import(/* webpackChunkName: "components/group_categories_injection" */'../../../components/timeline_injections/group_categories_injection') }
export function GroupCategoriesInjection() { return import(/* webpackChunkName: "components/group_categories_injection" */'../../../components/timeline_injections/group_categories_injection') }
export function GroupsCollection() { return import(/* webpackChunkName: "features/groups_collection" */'../../groups_collection') }
export function GroupAbout() { return import(/* webpackChunkName: "features/group_about" */'../../group_about.js') }
export function GroupCollectionTimeline() { return import(/* webpackChunkName: "features/group_collection_timeline" */'../../group_collection_timeline') }
@ -85,14 +85,16 @@ export function ProfileInfoPanel() { return import(/* webpackChunkName: "compone
export function ProfileStatsPanel() { return import(/* webpackChunkName: "components/profile_info_panel" */'../../../components/panel/profile_stats_panel') }
export function ProgressPanel() { return import(/* webpackChunkName: "components/progress_panel" */'../../../components/panel/progress_panel') }
export function ProPanel() { return import(/* webpackChunkName: "components/pro_panel" */'../../../components/panel/pro_panel') }
export function ProUpgradeInjection() { return import(/* webpackChunkName: "components/pro_upgrade_injection" */'../../../components/timeline_injections/pro_upgrade_injection') }
export function ProUpgradeModal() { return import(/* webpackChunkName: "components/pro_upgrade_modal" */'../../../components/modal/pro_upgrade_modal') }
// export function PWAInjection() { return import(/* webpackChunkName: "components/pwa_injection" */'../../../components/timeline_injections/pwa_injection') }
export function PWAInjection() { return import(/* webpackChunkName: "components/pwa_injection" */'../../../components/timeline_injections/pwa_injection') }
export function ReportModal() { return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal/report_modal') }
export function Search() { return import(/*webpackChunkName: "features/search" */'../../search') }
export function Shortcuts() { return import(/*webpackChunkName: "features/shortcuts" */'../../shortcuts') }
export function Status() { return import(/* webpackChunkName: "components/status" */'../../../components/status') }
export function StatusFeature() { return import(/* webpackChunkName: "features/status" */'../../status') }
export function SearchFilterPanel() { return import(/* webpackChunkName: "components/search_filter_panel" */'../../../components/panel/search_filter_panel') }
export function ShopInjection() { return import(/* webpackChunkName: "components/shop_injection" */'../../../components/timeline_injections/shop_injection') }
export function ShopPanel() { return import(/* webpackChunkName: "components/shop_panel" */'../../../components/panel/shop_panel') }
export function SidebarMorePopover() { return import(/* webpackChunkName: "components/sidebar_more_popover" */'../../../components/popover/sidebar_more_popover') }
export function SignUpLogInPanel() { return import(/* webpackChunkName: "components/sign_up_log_in_panel" */'../../../components/panel/sign_up_log_in_panel') }
@ -110,7 +112,7 @@ export function StatusVisibilityPopover() { return import(/* webpackChunkName: "
export function Suggestions() { return import(/* webpackChunkName: "features/suggestions" */'../../suggestions') }
export function TermsOfSale() { return import(/* webpackChunkName: "features/about/terms_of_sale" */'../../about/terms_of_sale') }
export function TermsOfService() { return import(/* webpackChunkName: "features/about/terms_of_service" */'../../about/terms_of_service') }
// export function TimelineInjectionOptionsPopover() { return import(/* webpackChunkName: "components/timeline_injection_options_popover" */'../../../components/popover/timeline_injection_options_popover') }
export function TimelineInjectionOptionsPopover() { return import(/* webpackChunkName: "components/timeline_injection_options_popover" */'../../../components/popover/timeline_injection_options_popover') }
export function TrendsPanel() { return import(/* webpackChunkName: "components/trends_panel" */'../../../components/panel/trends_panel') }
export function UnauthorizedModal() { return import(/* webpackChunkName: "components/unauthorized_modal" */'../../../components/modal/unauthorized_modal') }
export function UnfollowModal() { return import(/* webpackChunkName: "components/unfollow_modal" */'../../../components/modal/unfollow_modal') }
@ -119,5 +121,5 @@ export function UserPanel() { return import(/* webpackChunkName: "components/use
export function Video() { return import(/* webpackChunkName: "components/video" */'../../../components/video') }
export function VideoModal() { return import(/* webpackChunkName: "components/video_modal" */'../../../components/modal/video_modal') }
export function VideoStatsPopover() { return import(/* webpackChunkName: "components/video_stats_popover" */'../../../components/popover/video_stats_popover') }
// export function UserSuggestionsInjection() { return import(/* webpackChunkName: "components/user_suggestions_injection" */'../../../components/timeline_injections/user_suggestions_injection') }
export function UserSuggestionsInjection() { return import(/* webpackChunkName: "components/user_suggestions_injection" */'../../../components/timeline_injections/user_suggestions_injection') }
export function UserSuggestionsPanel() { return import(/* webpackChunkName: "components/user_suggestions_panel" */'../../../components/panel/user_suggestions_panel') }

@ -2,7 +2,17 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'
import { STORE_HYDRATE } from '../actions/store'
import { EMOJI_USE } from '../actions/emojis'
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists'
import { COMMENT_SORTING_TYPE_OLDEST } from '../constants'
import { TIMELINE_INJECTION_HIDE } from '../actions/timeline_injections'
import {
COMMENT_SORTING_TYPE_OLDEST,
TIMELINE_INJECTION_WEIGHT_DEFAULT,
TIMELINE_INJECTION_FEATURED_GROUPS,
TIMELINE_INJECTION_GROUP_CATEGORIES,
TIMELINE_INJECTION_PRO_UPGRADE,
TIMELINE_INJECTION_PWA,
TIMELINE_INJECTION_SHOP,
TIMELINE_INJECTION_USER_SUGGESTIONS,
} from '../constants'
import { Map as ImmutableMap, fromJS } from 'immutable'
import uuid from '../utils/uuid'
@ -12,6 +22,16 @@ const initialState = ImmutableMap({
skinTone: 1,
commentSorting: COMMENT_SORTING_TYPE_OLDEST,
// every dismiss reduces by half or set to zero for pwa, shop, pro
injections: ImmutableMap({
[TIMELINE_INJECTION_FEATURED_GROUPS]: TIMELINE_INJECTION_WEIGHT_DEFAULT,
[TIMELINE_INJECTION_GROUP_CATEGORIES]: TIMELINE_INJECTION_WEIGHT_DEFAULT,
[TIMELINE_INJECTION_PRO_UPGRADE]: TIMELINE_INJECTION_WEIGHT_DEFAULT,
[TIMELINE_INJECTION_PWA]: TIMELINE_INJECTION_WEIGHT_DEFAULT,
[TIMELINE_INJECTION_SHOP]: TIMELINE_INJECTION_WEIGHT_DEFAULT,
[TIMELINE_INJECTION_USER_SUGGESTIONS]: TIMELINE_INJECTION_WEIGHT_DEFAULT,
}),
displayOptions: ImmutableMap({
fontSize: 'normal',
radiusSmallDisabled: false,

@ -0,0 +1,27 @@
import Immutable from 'immutable'
import {
TIMELINE_INJECTION_SHOW,
TIMELINE_INJECTION_HIDE,
} from '../actions/timeline_injections'
const initialState = Immutable.Map({
visibleIds: Immutable.List(),
hiddenIds: Immutable.List(),
})
export default function timeline_injection(state = initialState, action) {
switch(action.type) {
case TIMELINE_INJECTION_SHOW:
return state.withMutations((map) => {
map.update('hiddenIds', (list) => list.filterNot((id) => id === action.injectionId))
map.update('visibleIds', (list) => list.push(action.injectionId))
})
case TIMELINE_INJECTION_HIDE:
return state.withMutations((map) => {
map.update('visibleIds', (list) => list.filterNot((id) => id === action.injectionId))
map.update('hiddenIds', (list) => list.push(action.injectionId))
})
default:
return state
}
}