From 59797ee233db88db047773294225eb8c8701adb7 Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Wed, 17 Jan 2018 23:00:23 +0100 Subject: Weblate translations (#6284) * Translated using Weblate (French) Currently translated at 100.0% (56 of 56 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/fr/ * Translated using Weblate (Catalan) Currently translated at 100.0% (56 of 56 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ca/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (260 of 260 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (45 of 45 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/pt_BR/ * Translated using Weblate (Dutch) Currently translated at 100.0% (529 of 529 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (45 of 45 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/nl/ * Translated using Weblate (Galician) Currently translated at 100.0% (529 of 529 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/gl/ * Translated using Weblate (Galician) Currently translated at 100.0% (45 of 45 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/gl/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.6% (527 of 529 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (528 of 529 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (45 of 45 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (260 of 260 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/pt_BR/ * Translated using Weblate (Slovak) Currently translated at 100.0% (56 of 56 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/sk/ * Translated using Weblate (Slovak) Currently translated at 37.2% (197 of 529 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/sk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (260 of 260 strings) Translation: Mastodon/React Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/frontend/sk/ * Translated using Weblate (Russian) Currently translated at 99.0% (526 of 531 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ru/ * Translated using Weblate (Catalan) Currently translated at 100.0% (45 of 45 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ca/ * Translated using Weblate (Catalan) Currently translated at 99.8% (530 of 531 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ca/ * Translated using Weblate (Japanese) Currently translated at 92.8% (52 of 56 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/ja/ * Translated using Weblate (Japanese) Currently translated at 75.8% (47 of 62 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ja/ * Translated using Weblate (Polish) Currently translated at 77.4% (48 of 62 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/pl/ * Translated using Weblate (Slovak) Currently translated at 38.3% (204 of 532 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/sk/ * Translated using Weblate (Japanese) Currently translated at 100.0% (62 of 62 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ja/ * Translated using Weblate (Catalan) Currently translated at 100.0% (62 of 62 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/ca/ * Translated using Weblate (Polish) Currently translated at 100.0% (62 of 62 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/pl/ * Translated using Weblate (Polish) Currently translated at 100.0% (75 of 75 strings) Translation: Mastodon/Doorkeeper Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/doorkeeper/pl/ * Translated using Weblate (Polish) Currently translated at 100.0% (62 of 62 strings) Translation: Mastodon/Devise Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/devise/pl/ * Translated using Weblate (Polish) Currently translated at 100.0% (56 of 56 strings) Translation: Mastodon/Preferences Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/simple_form/pl/ * Translated using Weblate (Russian) Currently translated at 96.8% (525 of 542 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ru/ * Translated using Weblate (Japanese) Currently translated at 99.0% (537 of 542 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ja/ * Translated using Weblate (Polish) Currently translated at 99.8% (541 of 542 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/pl/ * Translated using Weblate (Japanese) Currently translated at 99.0% (538 of 543 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/ja/ * Translated using Weblate (Dutch) Currently translated at 97.4% (529 of 543 strings) Translation: Mastodon/Backend Translate-URL: https://weblate.joinmastodon.org/projects/mastodon/backend/nl/ * Normalize translations Ran i18n-tasks normalize && yarn manage:translations --- app/javascript/mastodon/locales/pt-BR.json | 4 ++-- app/javascript/mastodon/locales/sk.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 947c6fb2b..381b80639 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -50,7 +50,7 @@ "column_header.unpin": "Desafixar", "column_subheading.navigation": "Navegação", "column_subheading.settings": "Configurações", - "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.hashtag_warning": "Esse toot não será listado em nenhuma hashtag por ser não listado. Somente toots públicos podem ser pesquisados por hashtag.", "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.", "compose_form.lock_disclaimer.lock": "trancada", "compose_form.placeholder": "No que você está pensando?", @@ -223,7 +223,7 @@ "status.media_hidden": "Mídia escondida", "status.mention": "Mencionar @{name}", "status.more": "Mais", - "status.mute": "Mute @{name}", + "status.mute": "Silenciar @{name}", "status.mute_conversation": "Silenciar conversa", "status.open": "Expandir", "status.pin": "Fixar no perfil", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 41ebfbd0e..c38bfd3c6 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -195,8 +195,8 @@ "privacy.private.short": "Iba sledujúci", "privacy.public.long": "Pošli všetkým", "privacy.public.short": "Verejne", - "privacy.unlisted.long": "Neposielať verejne", - "privacy.unlisted.short": "Nie je v zozname", + "privacy.unlisted.long": "Neposielať do verejných časových osí", + "privacy.unlisted.short": "Verejne mimo osí", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", "relative_time.just_now": "now", -- cgit From 7badad7797b487b411a2ab34e0f7413741974bb4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 17 Jan 2018 23:56:03 +0100 Subject: Fix home regeneration (#6251) * Fix regeneration marker not being removed after completion * Return HTTP 206 from /api/v1/timelines/home if regeneration in progress Prioritize RegenerationWorker by putting it into default queue * Display loading indicator and poll home timeline while it regenerates * Add graphic to regeneration message * Make "not found" indicator consistent with home regeneration --- .../api/v1/timelines/home_controller.rb | 10 ++++- app/javascript/images/elephant-friend.png | Bin 24466 -> 0 bytes app/javascript/images/elephant_ui_disappointed.svg | 1 + app/javascript/images/elephant_ui_working.svg | 1 + app/javascript/images/mastodon-not-found.png | Bin 19560 -> 0 bytes app/javascript/mastodon/actions/timelines.js | 13 +++++-- .../mastodon/components/missing_indicator.js | 9 ++++- app/javascript/mastodon/components/status_list.js | 21 +++++++++- .../mastodon/features/home_timeline/index.js | 37 +++++++++++++++++- .../mastodon/features/list_timeline/index.js | 8 +++- .../ui/containers/status_list_container.js | 1 + app/javascript/mastodon/reducers/timelines.js | 5 ++- app/javascript/styles/mastodon/components.scss | 43 +++++++++++++++++++-- app/services/precompute_feed_service.rb | 1 + app/workers/regeneration_worker.rb | 2 +- .../concerns/user_tracking_concern_spec.rb | 40 +++++++++++++++---- 16 files changed, 165 insertions(+), 27 deletions(-) delete mode 100644 app/javascript/images/elephant-friend.png create mode 100644 app/javascript/images/elephant_ui_disappointed.svg create mode 100644 app/javascript/images/elephant_ui_working.svg delete mode 100644 app/javascript/images/mastodon-not-found.png (limited to 'app/javascript') diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index db6cd8568..bbbcf7f90 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -9,7 +9,11 @@ class Api::V1::Timelines::HomeController < Api::BaseController def show @statuses = load_statuses - render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + + render json: @statuses, + each_serializer: REST::StatusSerializer, + relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), + status: regeneration_in_progress? ? 206 : 200 end private @@ -57,4 +61,8 @@ class Api::V1::Timelines::HomeController < Api::BaseController def pagination_since_id @statuses.first.id end + + def regeneration_in_progress? + Redis.current.exists("account:#{current_account.id}:regeneration") + end end diff --git a/app/javascript/images/elephant-friend.png b/app/javascript/images/elephant-friend.png deleted file mode 100644 index 3c5145ba9..000000000 Binary files a/app/javascript/images/elephant-friend.png and /dev/null differ diff --git a/app/javascript/images/elephant_ui_disappointed.svg b/app/javascript/images/elephant_ui_disappointed.svg new file mode 100644 index 000000000..580c15a13 --- /dev/null +++ b/app/javascript/images/elephant_ui_disappointed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/images/elephant_ui_working.svg b/app/javascript/images/elephant_ui_working.svg new file mode 100644 index 000000000..8ba475db0 --- /dev/null +++ b/app/javascript/images/elephant_ui_working.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/images/mastodon-not-found.png b/app/javascript/images/mastodon-not-found.png deleted file mode 100644 index 76108d41f..000000000 Binary files a/app/javascript/images/mastodon-not-found.png and /dev/null differ diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index f8843d1d9..df6a36379 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/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/mastodon/components/missing_indicator.js b/app/javascript/mastodon/components/missing_indicator.js index 87df7f61c..70d8c3b98 100644 --- a/app/javascript/mastodon/components/missing_indicator.js +++ b/app/javascript/mastodon/components/missing_indicator.js @@ -2,9 +2,14 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; const MissingIndicator = () => ( -
+
- +
+ +
+ + +
); diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 58a7b228a..5acaf714e 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import StatusContainer from '../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/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index a4bc60fac..31f5a3c8b 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { expandHomeTimeline } from '../../actions/timelines'; +import { expandHomeTimeline, refreshHomeTimeline } from '../../actions/timelines'; import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../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/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index ae136e48f..3b97ac62a 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/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/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index a0aec4403..59b53d823 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -47,6 +47,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/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 9984c3b5d..7b7b5470f 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/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); })); }; @@ -124,7 +125,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/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index dbb277af9..6fbecee7c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2303,7 +2303,7 @@ } } -.missing-indicator { +.regeneration-indicator { text-align: center; font-size: 16px; font-weight: 500; @@ -2314,11 +2314,46 @@ flex: 1 1 auto; align-items: center; justify-content: center; + padding: 20px; & > div { - background: url('../images/mastodon-not-found.png') no-repeat center -50px; - padding-top: 210px; width: 100%; + background: transparent; + padding-top: 0; + } + + &__figure { + background: url('../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('../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; + } } } @@ -2749,7 +2784,6 @@ @keyframes heartbeat { from { transform: scale(1); - transform-origin: center center; animation-timing-function: ease-out; } @@ -2775,6 +2809,7 @@ } .pulse-loading { + transform-origin: center center; animation: heartbeat 1.5s ease-in-out infinite both; } diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 36aabaa00..4f771ff72 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -3,5 +3,6 @@ class PrecomputeFeedService < BaseService def call(account) FeedManager.instance.populate_feed(account) + Redis.current.del("account:#{account.id}:regeneration") end end diff --git a/app/workers/regeneration_worker.rb b/app/workers/regeneration_worker.rb index 8cee21ae1..5c6a040bd 100644 --- a/app/workers/regeneration_worker.rb +++ b/app/workers/regeneration_worker.rb @@ -3,7 +3,7 @@ class RegenerationWorker include Sidekiq::Worker - sidekiq_options queue: 'pull', backtrace: true, unique: :until_executed + sidekiq_options unique: :until_executed def perform(account_id, _ = :home) account = Account.find(account_id) diff --git a/spec/controllers/concerns/user_tracking_concern_spec.rb b/spec/controllers/concerns/user_tracking_concern_spec.rb index 168d44ba6..d08095ef8 100644 --- a/spec/controllers/concerns/user_tracking_concern_spec.rb +++ b/spec/controllers/concerns/user_tracking_concern_spec.rb @@ -43,15 +43,39 @@ describe ApplicationController, type: :controller do expect_updated_sign_in_at(user) end - it 'regenerates feed when sign in is older than two weeks' do - allow(RegenerationWorker).to receive(:perform_async) - user.update(current_sign_in_at: 3.weeks.ago) - sign_in user, scope: :user - get :show + describe 'feed regeneration' do + before do + alice = Fabricate(:account) + bob = Fabricate(:account) - expect_updated_sign_in_at(user) - expect(Redis.current.get("account:#{user.account_id}:regeneration")).to eq 'true' - expect(RegenerationWorker).to have_received(:perform_async) + user.account.follow!(alice) + user.account.follow!(bob) + + Fabricate(:status, account: alice, text: 'hello world') + Fabricate(:status, account: bob, text: 'yes hello') + Fabricate(:status, account: user.account, text: 'test') + + user.update(last_sign_in_at: 'Tue, 04 Jul 2017 14:45:56 UTC +00:00', current_sign_in_at: 'Wed, 05 Jul 2017 22:10:52 UTC +00:00') + + sign_in user, scope: :user + end + + it 'sets a regeneration marker while regenerating' do + allow(RegenerationWorker).to receive(:perform_async) + get :show + + expect_updated_sign_in_at(user) + expect(Redis.current.get("account:#{user.account_id}:regeneration")).to eq 'true' + expect(RegenerationWorker).to have_received(:perform_async) + end + + it 'regenerates feed when sign in is older than two weeks' do + get :show + + expect_updated_sign_in_at(user) + expect(Redis.current.zcard(FeedManager.instance.key(:home, user.account_id))).to eq 3 + expect(Redis.current.get("account:#{user.account_id}:regeneration")).to be_nil + end end def expect_updated_sign_in_at(user) -- cgit From bcd86404daf90b120eeb339f0cfeee7a6c25dd94 Mon Sep 17 00:00:00 2001 From: David Yip Date: Thu, 18 Jan 2018 10:25:37 -0600 Subject: Port 7badad7797b487b411a2ab34e0f7413741974bb4 to glitch frontend --- .../flavours/glitch/actions/timelines.js | 13 ++++-- .../glitch/components/missing_indicator.js | 9 +++- .../flavours/glitch/components/status_list.js | 21 ++++++++- .../glitch/features/home_timeline/index.js | 37 ++++++++++++++- .../glitch/features/list_timeline/index.js | 8 +++- .../ui/containers/status_list_container.js | 1 + .../glitch/images/elephant_ui_disappointed.svg | 1 + .../flavours/glitch/images/elephant_ui_working.svg | 1 + .../flavours/glitch/reducers/timelines.js | 5 +- .../flavours/glitch/styles/components/index.scss | 18 ++------ .../styles/components/regeneration_indicator.scss | 53 ++++++++++++++++++++++ 11 files changed, 140 insertions(+), 27 deletions(-) create mode 100644 app/javascript/flavours/glitch/images/elephant_ui_disappointed.svg create mode 100644 app/javascript/flavours/glitch/images/elephant_ui_working.svg create mode 100644 app/javascript/flavours/glitch/styles/components/regeneration_indicator.scss (limited to 'app/javascript') 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; + } + } +} -- cgit From 3896cd5d79d8b46473b935b00caf31ef943fb21d Mon Sep 17 00:00:00 2001 From: David Yip Date: Thu, 18 Jan 2018 10:25:54 -0600 Subject: Use absolute paths for new working/not-found SVGs This allows these component styles to be used in i.e. the win95 skin. --- app/javascript/styles/mastodon/components.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 4ac0deea1..d13a18ad7 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2323,7 +2323,7 @@ } &__figure { - background: url('../images/elephant_ui_working.svg') no-repeat center 0; + background: url('~images/elephant_ui_working.svg') no-repeat center 0; width: 100%; height: 160px; background-size: contain; @@ -2337,7 +2337,7 @@ padding-top: 20px + 48px; .regeneration-indicator__figure { - background-image: url('../images/elephant_ui_disappointed.svg'); + background-image: url('~images/elephant_ui_disappointed.svg'); } } -- cgit