forked from mastodon/mastodon
-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Change onboarding prompt to follow suggestions carousel in web UI (ma…
- Loading branch information
Showing
17 changed files
with
507 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 0 additions & 46 deletions
46
app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx
This file was deleted.
Oops, something went wrong.
201 changes: 201 additions & 0 deletions
201
app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import PropTypes from 'prop-types'; | ||
import { useEffect, useCallback, useRef, useState } from 'react'; | ||
|
||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl'; | ||
|
||
import { Link } from 'react-router-dom'; | ||
|
||
import ImmutablePropTypes from 'react-immutable-proptypes'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
|
||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; | ||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; | ||
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; | ||
import InfoIcon from '@/material-icons/400-24px/info.svg?react'; | ||
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts'; | ||
import { changeSetting } from 'mastodon/actions/settings'; | ||
import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions'; | ||
import { Avatar } from 'mastodon/components/avatar'; | ||
import { Button } from 'mastodon/components/button'; | ||
import { DisplayName } from 'mastodon/components/display_name'; | ||
import { Icon } from 'mastodon/components/icon'; | ||
import { IconButton } from 'mastodon/components/icon_button'; | ||
import { VerifiedBadge } from 'mastodon/components/verified_badge'; | ||
|
||
const messages = defineMessages({ | ||
follow: { id: 'account.follow', defaultMessage: 'Follow' }, | ||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, | ||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' }, | ||
next: { id: 'lightbox.next', defaultMessage: 'Next' }, | ||
dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" }, | ||
}); | ||
|
||
const Source = ({ id }) => { | ||
let label; | ||
|
||
switch (id) { | ||
case 'friends_of_friends': | ||
case 'similar_to_recently_followed': | ||
label = <FormattedMessage id='follow_suggestions.personalized_suggestion' defaultMessage='Personalized suggestion' />; | ||
break; | ||
case 'featured': | ||
label = <FormattedMessage id='follow_suggestions.curated_suggestion' defaultMessage="Editors' Choice" />; | ||
break; | ||
case 'most_followed': | ||
case 'most_interactions': | ||
label = <FormattedMessage id='follow_suggestions.popular_suggestion' defaultMessage='Popular suggestion' />; | ||
break; | ||
} | ||
|
||
return ( | ||
<div className='inline-follow-suggestions__body__scrollable__card__text-stack__source'> | ||
<Icon icon={InfoIcon} /> | ||
{label} | ||
</div> | ||
); | ||
}; | ||
|
||
Source.propTypes = { | ||
id: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']), | ||
}; | ||
|
||
const Card = ({ id, source }) => { | ||
const intl = useIntl(); | ||
const account = useSelector(state => state.getIn(['accounts', id])); | ||
const relationship = useSelector(state => state.getIn(['relationships', id])); | ||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at')); | ||
const dispatch = useDispatch(); | ||
const following = relationship?.get('following') ?? relationship?.get('requested'); | ||
|
||
const handleFollow = useCallback(() => { | ||
if (following) { | ||
dispatch(unfollowAccount(id)); | ||
} else { | ||
dispatch(followAccount(id)); | ||
} | ||
}, [id, following, dispatch]); | ||
|
||
const handleDismiss = useCallback(() => { | ||
dispatch(dismissSuggestion(id)); | ||
}, [id, dispatch]); | ||
|
||
return ( | ||
<div className='inline-follow-suggestions__body__scrollable__card'> | ||
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} /> | ||
|
||
<div className='inline-follow-suggestions__body__scrollable__card__avatar'> | ||
<Link to={`/@${account.get('acct')}`}><Avatar account={account} size={72} /></Link> | ||
</div> | ||
|
||
<div className='inline-follow-suggestions__body__scrollable__card__text-stack'> | ||
<Link to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link> | ||
{firstVerifiedField ? <VerifiedBadge link={firstVerifiedField.get('value')} /> : <Source id={source.get(0)} />} | ||
</div> | ||
|
||
<Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} /> | ||
</div> | ||
); | ||
}; | ||
|
||
Card.propTypes = { | ||
id: PropTypes.string.isRequired, | ||
source: ImmutablePropTypes.list, | ||
}; | ||
|
||
const DISMISSIBLE_ID = 'home/follow-suggestions'; | ||
|
||
export const InlineFollowSuggestions = ({ hidden }) => { | ||
const intl = useIntl(); | ||
const dispatch = useDispatch(); | ||
const suggestions = useSelector(state => state.getIn(['suggestions', 'items'])); | ||
const isLoading = useSelector(state => state.getIn(['suggestions', 'isLoading'])); | ||
const dismissed = useSelector(state => state.getIn(['settings', 'dismissed_banners', DISMISSIBLE_ID])); | ||
const bodyRef = useRef(); | ||
const [canScrollLeft, setCanScrollLeft] = useState(false); | ||
const [canScrollRight, setCanScrollRight] = useState(true); | ||
|
||
useEffect(() => { | ||
dispatch(fetchSuggestions()); | ||
}, [dispatch]); | ||
|
||
useEffect(() => { | ||
if (!bodyRef.current) { | ||
return; | ||
} | ||
|
||
setCanScrollLeft(bodyRef.current.scrollLeft > 0); | ||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); | ||
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]); | ||
|
||
const handleLeftNav = useCallback(() => { | ||
bodyRef.current.scrollLeft -= 200; | ||
}, [bodyRef]); | ||
|
||
const handleRightNav = useCallback(() => { | ||
bodyRef.current.scrollLeft += 200; | ||
}, [bodyRef]); | ||
|
||
const handleScroll = useCallback(() => { | ||
if (!bodyRef.current) { | ||
return; | ||
} | ||
|
||
setCanScrollLeft(bodyRef.current.scrollLeft > 0); | ||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); | ||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]); | ||
|
||
const handleDismiss = useCallback(() => { | ||
dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true)); | ||
}, [dispatch]); | ||
|
||
if (dismissed || (!isLoading && suggestions.isEmpty())) { | ||
return null; | ||
} | ||
|
||
if (hidden) { | ||
return ( | ||
<div className='inline-follow-suggestions' /> | ||
); | ||
} | ||
|
||
return ( | ||
<div className='inline-follow-suggestions'> | ||
<div className='inline-follow-suggestions__header'> | ||
<h3><FormattedMessage id='follow_suggestions.who_to_follow' defaultMessage='Who to follow' /></h3> | ||
|
||
<div className='inline-follow-suggestions__header__actions'> | ||
<button className='link-button' onClick={handleDismiss}><FormattedMessage id='follow_suggestions.dismiss' defaultMessage="Don't show again" /></button> | ||
<Link to='/explore/suggestions' className='link-button'><FormattedMessage id='follow_suggestions.view_all' defaultMessage='View all' /></Link> | ||
</div> | ||
</div> | ||
|
||
<div className='inline-follow-suggestions__body'> | ||
<div className='inline-follow-suggestions__body__scrollable' ref={bodyRef} onScroll={handleScroll}> | ||
{suggestions.map(suggestion => ( | ||
<Card | ||
key={suggestion.get('account')} | ||
id={suggestion.get('account')} | ||
source={suggestion.get('source')} | ||
/> | ||
))} | ||
</div> | ||
|
||
{canScrollLeft && ( | ||
<button className='inline-follow-suggestions__body__scroll-button left' onClick={handleLeftNav} aria-label={intl.formatMessage(messages.previous)}> | ||
<div className='inline-follow-suggestions__body__scroll-button__icon'><Icon icon={ChevronLeftIcon} /></div> | ||
</button> | ||
)} | ||
|
||
{canScrollRight && ( | ||
<button className='inline-follow-suggestions__body__scroll-button right' onClick={handleRightNav} aria-label={intl.formatMessage(messages.next)}> | ||
<div className='inline-follow-suggestions__body__scroll-button__icon'><Icon icon={ChevronRightIcon} /></div> | ||
</button> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
InlineFollowSuggestions.propTypes = { | ||
hidden: PropTypes.bool, | ||
}; |
Oops, something went wrong.