diff options
Diffstat (limited to 'app/javascript/flavours')
9 files changed, 277 insertions, 1 deletions
diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 05955963c..9b3bd0d56 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -24,6 +24,7 @@ export function normalizeAccount(account) { account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); account.note_emojified = emojify(account.note, emojiMap); + account.note_plain = unescapeHTML(account.note); if (account.fields) { account.fields = account.fields.map(pair => ({ diff --git a/app/javascript/flavours/glitch/actions/suggestions.js b/app/javascript/flavours/glitch/actions/suggestions.js index e867bccf7..b8ce5825c 100644 --- a/app/javascript/flavours/glitch/actions/suggestions.js +++ b/app/javascript/flavours/glitch/actions/suggestions.js @@ -1,5 +1,6 @@ import api from 'flavours/glitch/util/api'; import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; @@ -7,13 +8,17 @@ export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL'; export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS'; -export function fetchSuggestions() { +export function fetchSuggestions(withRelationships = false) { return (dispatch, getState) => { dispatch(fetchSuggestionsRequest()); api(getState).get('/api/v2/suggestions').then(response => { dispatch(importFetchedAccounts(response.data.map(x => x.account))); dispatch(fetchSuggestionsSuccess(response.data)); + + if (withRelationships) { + dispatch(fetchRelationships(response.data.map(item => item.account.id))); + } }).catch(error => dispatch(fetchSuggestionsFail(error))); }; }; diff --git a/app/javascript/flavours/glitch/components/logo.js b/app/javascript/flavours/glitch/components/logo.js new file mode 100644 index 000000000..d1c7f08a9 --- /dev/null +++ b/app/javascript/flavours/glitch/components/logo.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const Logo = () => ( + <svg viewBox='0 0 216.4144 232.00976' className='logo'> + <use xlinkHref='#mastodon-svg-logo' /> + </svg> +); + +export default Logo; diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js new file mode 100644 index 000000000..bee9f9384 --- /dev/null +++ b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js @@ -0,0 +1,85 @@ +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 'flavours/glitch/selectors'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import Permalink from 'flavours/glitch/components/permalink'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { injectIntl, defineMessages } from 'react-intl'; +import { followAccount, unfollowAccount } from 'flavours/glitch/actions/accounts'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, props) => ({ + account: getAccount(state, props.id), + }); + + return mapStateToProps; +}; + +const getFirstSentence = str => { + const arr = str.split(/(([\.\?!]+\s)|[.。?!])/); + + return arr[0]; +}; + +export default @connect(makeMapStateToProps) +@injectIntl +class Account extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + }; + + handleFollow = () => { + const { account, dispatch } = this.props; + + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + dispatch(unfollowAccount(account.get('id'))); + } else { + dispatch(followAccount(account.get('id'))); + } + } + + render () { + const { account, intl } = this.props; + + let button; + + if (account.getIn(['relationship', 'following'])) { + button = <IconButton icon='check' title={intl.formatMessage(messages.unfollow)} active onClick={this.handleFollow} />; + } else { + button = <IconButton icon='plus' title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} />; + } + + return ( + <div className='account follow-recommendations-account'> + <div className='account__wrapper'> + <Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> + + <DisplayName account={account} /> + + <div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div> + </Permalink> + + <div className='account__relationship'> + {button} + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.js b/app/javascript/flavours/glitch/features/follow_recommendations/index.js new file mode 100644 index 000000000..ac75062e0 --- /dev/null +++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.js @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import { fetchSuggestions } from 'flavours/glitch/actions/suggestions'; +import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings'; +import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; +import Column from 'flavours/glitch/features/ui/components/column'; +import Account from './components/account'; +import Logo from 'flavours/glitch/components/logo'; +import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; +import Button from 'flavours/glitch/components/button'; + +const mapStateToProps = state => ({ + suggestions: state.getIn(['suggestions', 'items']), + isLoading: state.getIn(['suggestions', 'isLoading']), +}); + +export default @connect(mapStateToProps) +class FollowRecommendations extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object.isRequired, + }; + + static propTypes = { + dispatch: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + isLoading: PropTypes.bool, + }; + + componentDidMount () { + const { dispatch, suggestions } = this.props; + + // Don't re-fetch if we're e.g. navigating backwards to this page, + // since we don't want followed accounts to disappear from the list + + if (suggestions.size === 0) { + dispatch(fetchSuggestions(true)); + } + } + + handleDone = () => { + const { dispatch } = this.props; + const { router } = this.context; + + dispatch(requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); + dispatch(saveSettings()); + } + })); + + router.history.push('/timelines/home'); + } + + render () { + const { suggestions, isLoading } = this.props; + + return ( + <Column> + <div className='scrollable'> + <div className='column-title'> + <Logo /> + <h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3> + <p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p> + </div> + + {!isLoading && ( + <React.Fragment> + <div> + {suggestions.map(suggestion => ( + <Account key={suggestion.get('account')} id={suggestion.get('account')} /> + ))} + </div> + + <div className='column-actions'> + <img src={imageGreeting} alt='' className='column-actions__background' /> + <Button onClick={this.handleDone}><FormattedMessage id='follow_recommendations.done' defaultMessage='Done' /></Button> + </div> + </React.Fragment> + )} + </div> + </Column> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 61a34fd2b..1149eb14e 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -50,9 +50,11 @@ import { Search, GettingStartedMisc, Directory, + FollowRecommendations, } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import { me } from 'flavours/glitch/util/initial_state'; +import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; // Dummy import, to make sure that <Status /> ends up in the application bundle. @@ -75,6 +77,7 @@ const mapStateToProps = state => ({ showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']), hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']), moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]), + firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, }); const keyMap = { @@ -207,6 +210,7 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> + <WrappedRoute path='/start' component={FollowRecommendations} content={children} /> <WrappedRoute path='/search' component={Search} content={children} /> <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} /> @@ -260,6 +264,7 @@ class UI extends React.Component { unreadNotifications: PropTypes.number, showFaviconBadge: PropTypes.bool, moved: PropTypes.map, + firstLaunch: PropTypes.bool, }; state = { @@ -378,6 +383,12 @@ class UI extends React.Component { this.favicon = new Favico({ animation:"none" }); + // On first launch, redirect to the follow recommendations page + if (this.props.firstLaunch) { + this.context.router.history.replace('/start'); + this.props.dispatch(closeOnboarding()); + } + this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 6a629353d..377cdd91f 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -11,6 +11,19 @@ overflow: hidden; text-decoration: none; font-size: 14px; + + &--with-note { + strong { + display: inline; + } + } + } + + &__note { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: $ui-secondary-color; } &.small { @@ -26,6 +39,16 @@ } } +.follow-recommendations-account { + .icon-button { + color: $ui-primary-color; + + &.active { + color: $valid-value-color; + } + } +} + .account__wrapper { display: flex; } diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 42d64c135..c6045e96e 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -796,3 +796,46 @@ text-align: center; } } + +.column-title { + text-align: center; + padding: 40px; + + .logo { + fill: $primary-text-color; + width: 50px; + margin: 0 auto; + margin-bottom: 40px; + } + + h3 { + font-size: 24px; + line-height: 1.5; + font-weight: 700; + margin-bottom: 10px; + } + + p { + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: $darker-text-color; + } +} + +.column-actions { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + padding-top: 40px; + padding-bottom: 200px; + + &__background { + position: absolute; + left: 0; + bottom: 0; + height: 220px; + width: auto; + } +} diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index 26255bbb7..a6c6ab0ab 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -169,3 +169,7 @@ export function Tesseract () { export function Directory () { return import(/* webpackChunkName: "features/glitch/async/directory" */'flavours/glitch/features/directory'); } + +export function FollowRecommendations () { + return import(/* webpackChunkName: "features/glitch/async/follow_recommendations" */'flavours/glitch/features/follow_recommendations'); +} |