Add indicator of unread content to window title when web UI is out of focus (#11560)

Fix #1288
This commit is contained in:
Eugen Rochko 2019-08-13 12:22:16 +02:00 committed by GitHub
parent 5f63339744
commit c09ecbc53e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 1 deletions

View 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,
});

View file

@ -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;
}
}

View file

@ -15,9 +15,11 @@ import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications'; import { expandNotifications } from '../../actions/notifications';
import { fetchFilters } from '../../actions/filters'; import { fetchFilters } from '../../actions/filters';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
import { focusApp, unfocusApp } from 'mastodon/actions/app';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import UploadArea from './components/upload_area'; import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container'; import ColumnsAreaContainer from './containers/columns_area_container';
import DocumentTitle from './components/document_title';
import { import {
Compose, Compose,
Status, Status,
@ -226,7 +228,7 @@ class UI extends React.PureComponent {
draggingOver: false, draggingOver: false,
}; };
handleBeforeUnload = (e) => { handleBeforeUnload = e => {
const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props; const { intl, isComposing, hasComposingText, hasMediaAttachments } = this.props;
if (isComposing && (hasComposingText || hasMediaAttachments)) { if (isComposing && (hasComposingText || hasMediaAttachments)) {
@ -237,6 +239,14 @@ class UI extends React.PureComponent {
} }
} }
handleWindowFocus = () => {
this.props.dispatch(focusApp());
}
handleWindowBlur = () => {
this.props.dispatch(unfocusApp());
}
handleLayoutChange = () => { handleLayoutChange = () => {
// The cached heights are no longer accurate, invalidate // The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight()); this.props.dispatch(clearHeight());
@ -314,6 +324,8 @@ class UI extends React.PureComponent {
} }
componentWillMount () { componentWillMount () {
window.addEventListener('focus', this.handleWindowFocus, false);
window.addEventListener('blur', this.handleWindowBlur, false);
window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('beforeunload', this.handleBeforeUnload, false);
document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragenter', this.handleDragEnter, false);
@ -343,7 +355,10 @@ class UI extends React.PureComponent {
} }
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('blur', this.handleWindowBlur);
window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('beforeunload', this.handleBeforeUnload);
document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('dragover', this.handleDragOver);
document.removeEventListener('drop', this.handleDrop); document.removeEventListener('drop', this.handleDrop);
@ -502,6 +517,7 @@ class UI extends React.PureComponent {
<LoadingBarContainer className='loading-bar' /> <LoadingBarContainer className='loading-bar' />
<ModalContainer /> <ModalContainer />
<UploadArea active={draggingOver} onClose={this.closeUploadModal} /> <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
<DocumentTitle />
</div> </div>
</HotKeys> </HotKeys>
); );

View file

@ -23,5 +23,6 @@ export const forceSingleColumn = !getMeta('advanced_layout');
export const useBlurhash = getMeta('use_blurhash'); export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items'); export const usePendingItems = getMeta('use_pending_items');
export const showTrends = getMeta('trends'); export const showTrends = getMeta('trends');
export const title = getMeta('title');
export default initialState; export default initialState;

View file

@ -32,6 +32,7 @@ import suggestions from './suggestions';
import polls from './polls'; import polls from './polls';
import identity_proofs from './identity_proofs'; import identity_proofs from './identity_proofs';
import trends from './trends'; import trends from './trends';
import missed_updates from './missed_updates';
const reducers = { const reducers = {
dropdown_menu, dropdown_menu,
@ -67,6 +68,7 @@ const reducers = {
suggestions, suggestions,
polls, polls,
trends, trends,
missed_updates,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);

View 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;
}
};

View file

@ -12,6 +12,7 @@ class InitialStateSerializer < ActiveModel::Serializer
access_token: object.token, access_token: object.token,
locale: I18n.locale, locale: I18n.locale,
domain: Rails.configuration.x.local_domain, domain: Rails.configuration.x.local_domain,
title: instance_presenter.site_title,
admin: object.admin&.id&.to_s, admin: object.admin&.id&.to_s,
search_enabled: Chewy.enabled?, search_enabled: Chewy.enabled?,
repository: Mastodon::Version.repository, repository: Mastodon::Version.repository,