Add indicator of unread content to window title when web UI is out of focus (#11560)
Fix #1288
This commit is contained in:
		
							
								
								
									
										10
									
								
								app/javascript/mastodon/actions/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/javascript/mastodon/actions/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| export const APP_FOCUS   = 'APP_FOCUS'; | ||||
| export const APP_UNFOCUS = 'APP_UNFOCUS'; | ||||
|  | ||||
| export const focusApp = () => ({ | ||||
|   type: APP_FOCUS, | ||||
| }); | ||||
|  | ||||
| export const unfocusApp = () => ({ | ||||
|   type: APP_UNFOCUS, | ||||
| }); | ||||
| @@ -0,0 +1,41 @@ | ||||
| import { PureComponent } from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { title } from 'mastodon/initial_state'; | ||||
|  | ||||
| const mapStateToProps = state => ({ | ||||
|   unread: state.getIn(['missed_updates', 'unread']), | ||||
| }); | ||||
|  | ||||
| export default @connect(mapStateToProps) | ||||
| class DocumentTitle extends PureComponent { | ||||
|  | ||||
|   static propTypes = { | ||||
|     unread: PropTypes.number.isRequired, | ||||
|   }; | ||||
|  | ||||
|   componentDidMount () { | ||||
|     this._sideEffects(); | ||||
|   } | ||||
|  | ||||
|   componentDidUpdate() { | ||||
|     this._sideEffects(); | ||||
|   } | ||||
|  | ||||
|   _sideEffects () { | ||||
|     const { unread } = this.props; | ||||
|  | ||||
|     if (unread > 99) { | ||||
|       document.title = `(*) ${title}`; | ||||
|     } else if (unread > 0) { | ||||
|       document.title = `(${unread}) ${title}`; | ||||
|     } else { | ||||
|       document.title = title; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   render () { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -15,9 +15,11 @@ import { expandHomeTimeline } from '../../actions/timelines'; | ||||
| import { expandNotifications } from '../../actions/notifications'; | ||||
| import { fetchFilters } from '../../actions/filters'; | ||||
| import { clearHeight } from '../../actions/height_cache'; | ||||
| import { focusApp, unfocusApp } from 'mastodon/actions/app'; | ||||
| import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; | ||||
| import UploadArea from './components/upload_area'; | ||||
| import ColumnsAreaContainer from './containers/columns_area_container'; | ||||
| import DocumentTitle from './components/document_title'; | ||||
| import { | ||||
|   Compose, | ||||
|   Status, | ||||
| @@ -226,7 +228,7 @@ class UI extends React.PureComponent { | ||||
|     draggingOver: false, | ||||
|   }; | ||||
|  | ||||
|   handleBeforeUnload = (e) => { | ||||
|   handleBeforeUnload = e => { | ||||
|     const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props; | ||||
|  | ||||
|     if (isComposing && (hasComposingText || hasMediaAttachments)) { | ||||
| @@ -237,6 +239,14 @@ class UI extends React.PureComponent { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   handleWindowFocus = () => { | ||||
|     this.props.dispatch(focusApp()); | ||||
|   } | ||||
|  | ||||
|   handleWindowBlur = () => { | ||||
|     this.props.dispatch(unfocusApp()); | ||||
|   } | ||||
|  | ||||
|   handleLayoutChange = () => { | ||||
|     // The cached heights are no longer accurate, invalidate | ||||
|     this.props.dispatch(clearHeight()); | ||||
| @@ -314,6 +324,8 @@ class UI extends React.PureComponent { | ||||
|   } | ||||
|  | ||||
|   componentWillMount () { | ||||
|     window.addEventListener('focus', this.handleWindowFocus, false); | ||||
|     window.addEventListener('blur', this.handleWindowBlur, false); | ||||
|     window.addEventListener('beforeunload', this.handleBeforeUnload, false); | ||||
|  | ||||
|     document.addEventListener('dragenter', this.handleDragEnter, false); | ||||
| @@ -343,7 +355,10 @@ class UI extends React.PureComponent { | ||||
|   } | ||||
|  | ||||
|   componentWillUnmount () { | ||||
|     window.removeEventListener('focus', this.handleWindowFocus); | ||||
|     window.removeEventListener('blur', this.handleWindowBlur); | ||||
|     window.removeEventListener('beforeunload', this.handleBeforeUnload); | ||||
|  | ||||
|     document.removeEventListener('dragenter', this.handleDragEnter); | ||||
|     document.removeEventListener('dragover', this.handleDragOver); | ||||
|     document.removeEventListener('drop', this.handleDrop); | ||||
| @@ -502,6 +517,7 @@ class UI extends React.PureComponent { | ||||
|           <LoadingBarContainer className='loading-bar' /> | ||||
|           <ModalContainer /> | ||||
|           <UploadArea active={draggingOver} onClose={this.closeUploadModal} /> | ||||
|           <DocumentTitle /> | ||||
|         </div> | ||||
|       </HotKeys> | ||||
|     ); | ||||
|   | ||||
| @@ -23,5 +23,6 @@ export const forceSingleColumn = !getMeta('advanced_layout'); | ||||
| export const useBlurhash = getMeta('use_blurhash'); | ||||
| export const usePendingItems = getMeta('use_pending_items'); | ||||
| export const showTrends = getMeta('trends'); | ||||
| export const title = getMeta('title'); | ||||
|  | ||||
| export default initialState; | ||||
|   | ||||
| @@ -32,6 +32,7 @@ import suggestions from './suggestions'; | ||||
| import polls from './polls'; | ||||
| import identity_proofs from './identity_proofs'; | ||||
| import trends from './trends'; | ||||
| import missed_updates from './missed_updates'; | ||||
|  | ||||
| const reducers = { | ||||
|   dropdown_menu, | ||||
| @@ -67,6 +68,7 @@ const reducers = { | ||||
|   suggestions, | ||||
|   polls, | ||||
|   trends, | ||||
|   missed_updates, | ||||
| }; | ||||
|  | ||||
| export default combineReducers(reducers); | ||||
|   | ||||
							
								
								
									
										23
									
								
								app/javascript/mastodon/reducers/missed_updates.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/javascript/mastodon/reducers/missed_updates.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { Map as ImmutableMap } from 'immutable'; | ||||
| import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications'; | ||||
| import { TIMELINE_UPDATE } from 'mastodon/actions/timelines'; | ||||
| import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app'; | ||||
|  | ||||
| const initialState = ImmutableMap({ | ||||
|   focused: true, | ||||
|   unread: 0, | ||||
| }); | ||||
|  | ||||
| export default function missed_updates(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|   case APP_FOCUS: | ||||
|     return state.set('focused', true).set('unread', 0); | ||||
|   case APP_UNFOCUS: | ||||
|     return state.set('focused', false); | ||||
|   case NOTIFICATIONS_UPDATE: | ||||
|   case TIMELINE_UPDATE: | ||||
|     return state.get('focused') ? state : state.update('unread', x => x + 1); | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
| }; | ||||
| @@ -12,6 +12,7 @@ class InitialStateSerializer < ActiveModel::Serializer | ||||
|       access_token: object.token, | ||||
|       locale: I18n.locale, | ||||
|       domain: Rails.configuration.x.local_domain, | ||||
|       title: instance_presenter.site_title, | ||||
|       admin: object.admin&.id&.to_s, | ||||
|       search_enabled: Chewy.enabled?, | ||||
|       repository: Mastodon::Version.repository, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user