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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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