Commit d198695b authored by mgabdev's avatar mgabdev

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
parent 41f48ea8
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,
})
}
\ No newline at end of file
......@@ -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
......
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))
\ No newline at end of file
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)
\ No newline at end of file
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)
\ No newline at end of file
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)
\ No newline at end of file
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
\ No newline at end of file
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))
\ No newline at end of file
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<