diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 9a5b2e6da..3a9d64084 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -19,13 +19,14 @@ export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; -export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { +export function refreshTimelineSuccess(timeline, statuses, skipLoading, next, partial) { return { type: TIMELINE_REFRESH_SUCCESS, timeline, statuses, skipLoading, next, + partial, }; }; @@ -88,7 +89,7 @@ export function refreshTimeline(timelineId, path, params = {}) { return function (dispatch, getState) { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); - if (timeline.get('isLoading') || timeline.get('online')) { + if (timeline.get('isLoading') || (timeline.get('online') && !timeline.get('isPartial'))) { return; } @@ -104,8 +105,12 @@ export function refreshTimeline(timelineId, path, params = {}) { dispatch(refreshTimelineRequest(timelineId, skipLoading)); api(getState).get(path, { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null)); + if (response.status === 206) { + dispatch(refreshTimelineSuccess(timelineId, [], skipLoading, null, true)); + } else { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null, false)); + } }).catch(error => { dispatch(refreshTimelineFail(timelineId, error, skipLoading)); }); diff --git a/app/javascript/flavours/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js index 87df7f61c..70d8c3b98 100644 --- a/app/javascript/flavours/glitch/components/missing_indicator.js +++ b/app/javascript/flavours/glitch/components/missing_indicator.js @@ -2,9 +2,14 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; const MissingIndicator = () => ( -
+
- +
+ +
+ + +
); diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index f190ba6ab..f253f0fdc 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import StatusContainer from 'flavours/glitch/containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ScrollableList from './scrollable_list'; +import { FormattedMessage } from 'react-intl'; export default class StatusList extends ImmutablePureComponent { @@ -16,6 +17,7 @@ export default class StatusList extends ImmutablePureComponent { trackScroll: PropTypes.bool, shouldUpdateScroll: PropTypes.func, isLoading: PropTypes.bool, + isPartial: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, emptyMessage: PropTypes.node, @@ -48,8 +50,23 @@ export default class StatusList extends ImmutablePureComponent { } render () { - const { statusIds, ...other } = this.props; - const { isLoading } = other; + const { statusIds, ...other } = this.props; + const { isLoading, isPartial } = other; + + if (isPartial) { + return ( +
+
+
+ +
+ + +
+
+
+ ); + } const scrollableContent = (isLoading || statusIds.size > 0) ? ( statusIds.map((statusId) => ( diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js index 2dfec6bbe..c20c0244a 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.js +++ b/app/javascript/flavours/glitch/features/home_timeline/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; +import { expandHomeTimeline, refreshHomeTimeline } from 'flavours/glitch/actions/timelines'; import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import Column from 'flavours/glitch/components/column'; @@ -16,6 +16,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, + isPartial: state.getIn(['timelines', 'home', 'isPartial'], false), }); @connect(mapStateToProps) @@ -26,6 +27,7 @@ export default class HomeTimeline extends React.PureComponent { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, hasUnread: PropTypes.bool, + isPartial: PropTypes.bool, columnId: PropTypes.string, multiColumn: PropTypes.bool, }; @@ -57,6 +59,39 @@ export default class HomeTimeline extends React.PureComponent { this.props.dispatch(expandHomeTimeline()); } + componentDidMount () { + this._checkIfReloadNeeded(false, this.props.isPartial); + } + + componentDidUpdate (prevProps) { + this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial); + } + + componentWillUnmount () { + this._stopPolling(); + } + + _checkIfReloadNeeded (wasPartial, isPartial) { + const { dispatch } = this.props; + + if (wasPartial === isPartial) { + return; + } else if (!wasPartial && isPartial) { + this.polling = setInterval(() => { + dispatch(refreshHomeTimeline()); + }, 3000); + } else if (wasPartial && !isPartial) { + this._stopPolling(); + } + } + + _stopPolling () { + if (this.polling) { + clearInterval(this.polling); + this.polling = null; + } + } + render () { const { intl, hasUnread, columnId, multiColumn } = this.props; const pinned = !!columnId; diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js index c6a89a920..f9476d92d 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.js +++ b/app/javascript/flavours/glitch/features/list_timeline/index.js @@ -120,13 +120,17 @@ export default class ListTimeline extends React.PureComponent { if (typeof list === 'undefined') { return ( - +
+ +
); } else if (list === false) { return ( - +
+ +
); } diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js index eca85b8e6..f85a2eeb8 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js @@ -51,6 +51,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { timelineId }) => ({ statusIds: getStatusIds(state, { type: timelineId }), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), + isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: !!state.getIn(['timelines', timelineId, 'next']), }); diff --git a/app/javascript/flavours/glitch/images/elephant_ui_disappointed.svg b/app/javascript/flavours/glitch/images/elephant_ui_disappointed.svg new file mode 100644 index 000000000..580c15a13 --- /dev/null +++ b/app/javascript/flavours/glitch/images/elephant_ui_disappointed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/flavours/glitch/images/elephant_ui_working.svg b/app/javascript/flavours/glitch/images/elephant_ui_working.svg new file mode 100644 index 000000000..8ba475db0 --- /dev/null +++ b/app/javascript/flavours/glitch/images/elephant_ui_working.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 679e1601e..4c62d8df3 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -30,7 +30,7 @@ const initialTimeline = ImmutableMap({ items: ImmutableList(), }); -const normalizeTimeline = (state, timeline, statuses, next) => { +const normalizeTimeline = (state, timeline, statuses, next, isPartial) => { const oldIds = state.getIn([timeline, 'items'], ImmutableList()); const ids = ImmutableList(statuses.map(status => status.get('id'))).filter(newId => !oldIds.includes(newId)); const wasLoaded = state.getIn([timeline, 'loaded']); @@ -41,6 +41,7 @@ const normalizeTimeline = (state, timeline, statuses, next) => { mMap.set('isLoading', false); if (!hadNext) mMap.set('next', next); mMap.set('items', wasLoaded ? ids.concat(oldIds) : ids); + mMap.set('isPartial', isPartial); })); }; @@ -125,7 +126,7 @@ export default function timelines(state = initialState, action) { case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next); + return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial); case TIMELINE_EXPAND_SUCCESS: return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next); case TIMELINE_UPDATE: diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 8f051f7a0..8f06209c6 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -838,21 +838,10 @@ } .missing-indicator { - text-align: center; - font-size: 16px; - font-weight: 500; - color: lighten($ui-base-color, 16%); - background: $ui-base-color; - cursor: default; - display: flex; - flex: 1 1 auto; - align-items: center; - justify-content: center; + padding-top: 20px + 48px; - & > div { - background: url('~images/mastodon-not-found.png') no-repeat center -50px; - padding-top: 210px; - width: 100%; + .regeneration-indicator__figure { + background-image: url('~flavours/glitch/images/elephant_ui_disappointed.svg'); } } @@ -1162,6 +1151,7 @@ noscript { @import 'metadata'; @import 'composer'; @import 'columns'; +@import 'regeneration_indicator'; @import 'search'; @import 'emoji'; @import 'doodle'; diff --git a/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss b/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss new file mode 100644 index 000000000..9c1873cdf --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss @@ -0,0 +1,53 @@ +.regeneration-indicator { + text-align: center; + font-size: 16px; + font-weight: 500; + color: lighten($ui-base-color, 16%); + background: $ui-base-color; + cursor: default; + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; + padding: 20px; + + & > div { + width: 100%; + background: transparent; + padding-top: 0; + } + + &__figure { + background: url('~flavours/glitch/images/elephant_ui_working.svg') no-repeat center 0; + width: 100%; + height: 160px; + background-size: contain; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + &.missing-indicator { + padding-top: 20px + 48px; + + .regeneration-indicator__figure { + background-image: url('~flavours/glitch/images/elephant_ui_disappointed.svg'); + } + } + + &__label { + margin-top: 200px; + + strong { + display: block; + margin-bottom: 10px; + color: lighten($ui-base-color, 34%); + } + + span { + font-size: 15px; + font-weight: 400; + } + } +}