Add indicator of unread content to window title when web UI is out of focus (#11560)
Fix #1288pull/1199/head
parent
5f63339744
commit
c09ecbc53e
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
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,
|
||||||
|
|
Loading…
Reference in New Issue