Add onboarding prompt when home feed too slow in web UI (#25267)
This commit is contained in:
		
							parent
							
								
									1d622c8033
								
							
						
					
					
						commit
						00ec43914a
					
				
					 11 changed files with 131 additions and 39 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								app/javascript/images/friends-cropped.png
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/javascript/images/friends-cropped.png
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 189 KiB | 
|  | @ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent { | |||
|           <ColumnSettingsContainer columnId={columnId} /> | ||||
|         </ColumnHeader> | ||||
| 
 | ||||
|         <DismissableBanner id='community_timeline'> | ||||
|           <FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /> | ||||
|         </DismissableBanner> | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>} | ||||
|           trackScroll={!pinned} | ||||
|           scrollKey={`community_timeline-${columnId}`} | ||||
|           timelineId={`community${onlyMedia ? ':media' : ''}`} | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ class Links extends PureComponent { | |||
| 
 | ||||
|     const banner = ( | ||||
|       <DismissableBanner id='explore/links'> | ||||
|         <FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These news stories are being talked about by people on this and other servers of the decentralized network right now.' /> | ||||
|         <FormattedMessage id='dismissable_banner.explore_links' defaultMessage='These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.' /> | ||||
|       </DismissableBanner> | ||||
|     ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ class Statuses extends PureComponent { | |||
|     return ( | ||||
|       <> | ||||
|         <DismissableBanner id='explore/statuses'> | ||||
|           <FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These posts from this and other servers in the decentralized network are gaining traction on this server right now.' /> | ||||
|           <FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' /> | ||||
|         </DismissableBanner> | ||||
| 
 | ||||
|         <StatusList | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ class Tags extends PureComponent { | |||
| 
 | ||||
|     const banner = ( | ||||
|       <DismissableBanner id='explore/tags'> | ||||
|         <FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These hashtags are gaining traction among people on this and other servers of the decentralized network right now.' /> | ||||
|         <FormattedMessage id='dismissable_banner.explore_tags' defaultMessage='These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.' /> | ||||
|       </DismissableBanner> | ||||
|     ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,23 @@ | |||
| import React from 'react'; | ||||
| 
 | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import { Link } from 'react-router-dom'; | ||||
| 
 | ||||
| import background from 'mastodon/../images/friends-cropped.png'; | ||||
| import DismissableBanner from 'mastodon/components/dismissable_banner'; | ||||
| 
 | ||||
| 
 | ||||
| export const ExplorePrompt = () => ( | ||||
|   <DismissableBanner id='home.explore_prompt'> | ||||
|     <img src={background} alt='' className='dismissable-banner__background-image' /> | ||||
| 
 | ||||
|     <h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1> | ||||
|     <p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p> | ||||
| 
 | ||||
|     <div className='dismissable-banner__message__actions'> | ||||
|       <Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link> | ||||
|       <Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link> | ||||
|     </div> | ||||
|   </DismissableBanner> | ||||
| ); | ||||
|  | @ -5,9 +5,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | |||
| 
 | ||||
| import classNames from 'classnames'; | ||||
| import { Helmet } from 'react-helmet'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| 
 | ||||
| import { List as ImmutableList } from 'immutable'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { createSelector } from 'reselect'; | ||||
| 
 | ||||
| import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements'; | ||||
| import { IconWithBadge } from 'mastodon/components/icon_with_badge'; | ||||
|  | @ -20,6 +21,7 @@ import Column from '../../components/column'; | |||
| import ColumnHeader from '../../components/column_header'; | ||||
| import StatusListContainer from '../ui/containers/status_list_container'; | ||||
| 
 | ||||
| import { ExplorePrompt } from './components/explore_prompt'; | ||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|  | @ -28,12 +30,36 @@ const messages = defineMessages({ | |||
|   hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, | ||||
| }); | ||||
| 
 | ||||
| const getHomeFeedSpeed = createSelector([ | ||||
|   state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), | ||||
|   state => state.get('statuses'), | ||||
| ], (statusIds, statusMap) => { | ||||
|   const statuses = statusIds.take(20).map(id => statusMap.get(id)); | ||||
|   const uniqueAccountIds = (new Set(statuses.map(status => status.get('account')).toArray())).size; | ||||
|   const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); | ||||
|   const newest = new Date(statuses.getIn([0, 'created_at'], 0)); | ||||
|   const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds | ||||
| 
 | ||||
|   return { | ||||
|     unique: uniqueAccountIds, | ||||
|     gap: averageGap, | ||||
|     newest, | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const homeTooSlow = createSelector(getHomeFeedSpeed, speed => | ||||
|   speed.unique < 5 // If there are fewer than 5 different accounts visible | ||||
|   || speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes | ||||
|   || (Date.now() - speed.newest) > (1000 * 3600) // If the most recent post is from over an hour ago | ||||
| ); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
|   hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, | ||||
|   isPartial: state.getIn(['timelines', 'home', 'isPartial']), | ||||
|   hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), | ||||
|   unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')), | ||||
|   showAnnouncements: state.getIn(['announcements', 'show']), | ||||
|   tooSlow: homeTooSlow(state), | ||||
| }); | ||||
| 
 | ||||
| class HomeTimeline extends PureComponent { | ||||
|  | @ -52,6 +78,7 @@ class HomeTimeline extends PureComponent { | |||
|     hasAnnouncements: PropTypes.bool, | ||||
|     unreadAnnouncements: PropTypes.number, | ||||
|     showAnnouncements: PropTypes.bool, | ||||
|     tooSlow: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   handlePin = () => { | ||||
|  | @ -121,11 +148,11 @@ class HomeTimeline extends PureComponent { | |||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; | ||||
|     const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; | ||||
|     const pinned = !!columnId; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     let announcementsButton = null; | ||||
|     let announcementsButton, banner; | ||||
| 
 | ||||
|     if (hasAnnouncements) { | ||||
|       announcementsButton = ( | ||||
|  | @ -141,6 +168,10 @@ class HomeTimeline extends PureComponent { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (tooSlow) { | ||||
|       banner = <ExplorePrompt />; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> | ||||
|         <ColumnHeader | ||||
|  | @ -160,11 +191,13 @@ class HomeTimeline extends PureComponent { | |||
| 
 | ||||
|         {signedIn ? ( | ||||
|           <StatusListContainer | ||||
|             prepend={banner} | ||||
|             alwaysPrepend | ||||
|             trackScroll={!pinned} | ||||
|             scrollKey={`home_timeline-${columnId}`} | ||||
|             onLoadMore={this.handleLoadMore} | ||||
|             timelineId='home' | ||||
|             emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />} | ||||
|             emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />} | ||||
|             bindToDocument={!multiColumn} | ||||
|           /> | ||||
|         ) : <NotSignedInIndicator />} | ||||
|  |  | |||
|  | @ -142,11 +142,8 @@ class PublicTimeline extends PureComponent { | |||
|           <ColumnSettingsContainer columnId={columnId} /> | ||||
|         </ColumnHeader> | ||||
| 
 | ||||
|         <DismissableBanner id='public_timeline'> | ||||
|           <FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' /> | ||||
|         </DismissableBanner> | ||||
| 
 | ||||
|         <StatusListContainer | ||||
|           prepend={<DismissableBanner id='public_timeline'><FormattedMessage id='dismissable_banner.public_timeline' defaultMessage='These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.' /></DismissableBanner>} | ||||
|           timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`} | ||||
|           onLoadMore={this.handleLoadMore} | ||||
|           trackScroll={!pinned} | ||||
|  |  | |||
|  | @ -197,9 +197,9 @@ | |||
|   "disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.", | ||||
|   "dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.", | ||||
|   "dismissable_banner.dismiss": "Dismiss", | ||||
|   "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", | ||||
|   "dismissable_banner.explore_statuses": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.", | ||||
|   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", | ||||
|   "dismissable_banner.explore_links": "These are news stories being shared the most on the social web today. Newer news stories posted by more different people are ranked higher.", | ||||
|   "dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.", | ||||
|   "dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.", | ||||
|   "dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.", | ||||
|   "embed.instructions": "Embed this post on your website by copying the code below.", | ||||
|   "embed.preview": "Here is what it will look like:", | ||||
|  | @ -232,8 +232,7 @@ | |||
|   "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", | ||||
|   "empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.", | ||||
|   "empty_column.hashtag": "There is nothing in this hashtag yet.", | ||||
|   "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", | ||||
|   "empty_column.home.suggestions": "See some suggestions", | ||||
|   "empty_column.home": "Your home timeline is empty! Follow more people to fill it up.", | ||||
|   "empty_column.list": "There is nothing in this list yet. When members of this list publish new posts, they will appear here.", | ||||
|   "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", | ||||
|   "empty_column.mutes": "You haven't muted any users yet.", | ||||
|  | @ -292,9 +291,13 @@ | |||
|   "hashtag.column_settings.tag_toggle": "Include additional tags for this column", | ||||
|   "hashtag.follow": "Follow hashtag", | ||||
|   "hashtag.unfollow": "Unfollow hashtag", | ||||
|   "home.actions.go_to_explore": "See what's trending", | ||||
|   "home.actions.go_to_suggestions": "Find people to follow", | ||||
|   "home.column_settings.basic": "Basic", | ||||
|   "home.column_settings.show_reblogs": "Show boosts", | ||||
|   "home.column_settings.show_replies": "Show replies", | ||||
|   "home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:", | ||||
|   "home.explore_prompt.title": "This is your home base within Mastodon.", | ||||
|   "home.hide_announcements": "Hide announcements", | ||||
|   "home.show_announcements": "Show announcements", | ||||
|   "interaction_modal.description.favourite": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.", | ||||
|  |  | |||
|  | @ -653,11 +653,6 @@ html { | |||
|   border: 1px solid lighten($ui-base-color, 8%); | ||||
| } | ||||
| 
 | ||||
| .dismissable-banner { | ||||
|   border-left: 1px solid lighten($ui-base-color, 8%); | ||||
|   border-right: 1px solid lighten($ui-base-color, 8%); | ||||
| } | ||||
| 
 | ||||
| .status__content, | ||||
| .reply-indicator__content { | ||||
|   a { | ||||
|  |  | |||
|  | @ -8695,27 +8695,71 @@ noscript { | |||
| } | ||||
| 
 | ||||
| .dismissable-banner { | ||||
|   background: $ui-base-color; | ||||
|   border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 30px; | ||||
|   position: relative; | ||||
|   margin: 10px; | ||||
|   margin-bottom: 5px; | ||||
|   border-radius: 8px; | ||||
|   border: 1px solid $highlight-text-color; | ||||
|   background: rgba($highlight-text-color, 0.15); | ||||
|   padding-inline-end: 45px; | ||||
|   overflow: hidden; | ||||
| 
 | ||||
|   &__background-image { | ||||
|     width: 125%; | ||||
|     position: absolute; | ||||
|     bottom: -25%; | ||||
|     inset-inline-end: -25%; | ||||
|     z-index: -1; | ||||
|     opacity: 0.15; | ||||
|     mix-blend-mode: luminosity; | ||||
|   } | ||||
| 
 | ||||
|   &__message { | ||||
|     flex: 1 1 auto; | ||||
|     padding: 20px 15px; | ||||
|     cursor: default; | ||||
|     font-size: 14px; | ||||
|     line-height: 18px; | ||||
|     padding: 15px; | ||||
|     font-size: 15px; | ||||
|     line-height: 22px; | ||||
|     font-weight: 500; | ||||
|     color: $primary-text-color; | ||||
| 
 | ||||
|     p { | ||||
|       margin-bottom: 15px; | ||||
| 
 | ||||
|       &:last-child { | ||||
|         margin-bottom: 0; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     h1 { | ||||
|       color: $highlight-text-color; | ||||
|       font-size: 22px; | ||||
|       line-height: 33px; | ||||
|       font-weight: 700; | ||||
|       margin-bottom: 15px; | ||||
|     } | ||||
| 
 | ||||
|     &__actions { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       gap: 4px; | ||||
|       margin-top: 30px; | ||||
|     } | ||||
| 
 | ||||
|     .button-tertiary { | ||||
|       background: rgba($ui-base-color, 0.15); | ||||
|       backdrop-filter: blur(8px); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &__action { | ||||
|     padding: 15px; | ||||
|     flex: 0 0 auto; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     position: absolute; | ||||
|     inset-inline-end: 0; | ||||
|     top: 0; | ||||
|     padding: 10px; | ||||
| 
 | ||||
|     .icon-button { | ||||
|       color: $highlight-text-color; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Reference in a new issue