Add indicator of unread content to window title when web UI is out of focus (#11560)
Fix #1288
This commit is contained in:
		
							parent
							
								
									5f63339744
								
							
						
					
					
						commit
						c09ecbc53e
					
				
					 7 changed files with 95 additions and 1 deletions
				
			
		
							
								
								
									
										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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue