From f382192862893c48cf97f13e9fbfb85b80cdc97d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 6 Apr 2022 22:53:29 +0200 Subject: [PATCH 1/5] Add pagination for trending statuses in web UI (#17976) --- app/javascript/mastodon/actions/trends.js | 54 +++++++++++++++++-- .../mastodon/features/explore/statuses.js | 15 ++++-- .../mastodon/reducers/status_lists.js | 7 +++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 304bbebefb..edda0b5b5d 100644 --- a/app/javascript/mastodon/actions/trends.js +++ b/app/javascript/mastodon/actions/trends.js @@ -1,4 +1,4 @@ -import api from '../api'; +import api, { getLinks } from '../api'; import { importFetchedStatuses } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; @@ -13,6 +13,10 @@ export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST'; export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS'; export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL'; +export const TRENDS_STATUSES_EXPAND_REQUEST = 'TRENDS_STATUSES_EXPAND_REQUEST'; +export const TRENDS_STATUSES_EXPAND_SUCCESS = 'TRENDS_STATUSES_EXPAND_SUCCESS'; +export const TRENDS_STATUSES_EXPAND_FAIL = 'TRENDS_STATUSES_EXPAND_FAIL'; + export const fetchTrendingHashtags = () => (dispatch, getState) => { dispatch(fetchTrendingHashtagsRequest()); @@ -68,11 +72,16 @@ export const fetchTrendingLinksFail = error => ({ }); export const fetchTrendingStatuses = () => (dispatch, getState) => { + if (getState().getIn(['status_lists', 'trending', 'isLoading'])) { + return; + } + dispatch(fetchTrendingStatusesRequest()); - api(getState).get('/api/v1/trends/statuses').then(({ data }) => { - dispatch(importFetchedStatuses(data)); - dispatch(fetchTrendingStatusesSuccess(data)); + api(getState).get('/api/v1/trends/statuses').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(fetchTrendingStatusesSuccess(response.data, next ? next.uri : null)); }).catch(err => dispatch(fetchTrendingStatusesFail(err))); }; @@ -81,9 +90,10 @@ export const fetchTrendingStatusesRequest = () => ({ skipLoading: true, }); -export const fetchTrendingStatusesSuccess = statuses => ({ +export const fetchTrendingStatusesSuccess = (statuses, next) => ({ type: TRENDS_STATUSES_FETCH_SUCCESS, statuses, + next, skipLoading: true, }); @@ -93,3 +103,37 @@ export const fetchTrendingStatusesFail = error => ({ skipLoading: true, skipAlert: true, }); + + +export const expandTrendingStatuses = () => (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'trending', 'next'], null); + + if (url === null || getState().getIn(['status_lists', 'trending', 'isLoading'])) { + return; + } + + dispatch(expandTrendingStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); + dispatch(expandTrendingStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandTrendingStatusesFail(error)); + }); +}; + +export const expandTrendingStatusesRequest = () => ({ + type: TRENDS_STATUSES_EXPAND_REQUEST, +}); + +export const expandTrendingStatusesSuccess = (statuses, next) => ({ + type: TRENDS_STATUSES_EXPAND_SUCCESS, + statuses, + next, +}); + +export const expandTrendingStatusesFail = error => ({ + type: TRENDS_STATUSES_EXPAND_FAIL, + error, +}); diff --git a/app/javascript/mastodon/features/explore/statuses.js b/app/javascript/mastodon/features/explore/statuses.js index 4e5530d84c..33e5b41796 100644 --- a/app/javascript/mastodon/features/explore/statuses.js +++ b/app/javascript/mastodon/features/explore/statuses.js @@ -4,11 +4,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import StatusList from 'mastodon/components/status_list'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { fetchTrendingStatuses } from 'mastodon/actions/trends'; +import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends'; +import { debounce } from 'lodash'; const mapStateToProps = state => ({ statusIds: state.getIn(['status_lists', 'trending', 'items']), isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true), + hasMore: !!state.getIn(['status_lists', 'trending', 'next']), }); export default @connect(mapStateToProps) @@ -17,6 +19,7 @@ class Statuses extends React.PureComponent { static propTypes = { statusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, + hasMore: PropTypes.bool, multiColumn: PropTypes.bool, dispatch: PropTypes.func.isRequired, }; @@ -26,8 +29,13 @@ class Statuses extends React.PureComponent { dispatch(fetchTrendingStatuses()); } + handleLoadMore = debounce(() => { + const { dispatch } = this.props; + dispatch(expandTrendingStatuses()); + }, 300, { leading: true }) + render () { - const { isLoading, statusIds, multiColumn } = this.props; + const { isLoading, hasMore, statusIds, multiColumn } = this.props; const emptyMessage = ; @@ -36,8 +44,9 @@ class Statuses extends React.PureComponent { trackScroll statusIds={statusIds} scrollKey='explore-statuses' - hasMore={false} + hasMore={hasMore} isLoading={isLoading} + onLoadMore={this.handleLoadMore} emptyMessage={emptyMessage} bindToDocument={!multiColumn} withCounters diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 49bc94a40d..a7c56cc195 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -21,6 +21,9 @@ import { TRENDS_STATUSES_FETCH_REQUEST, TRENDS_STATUSES_FETCH_SUCCESS, TRENDS_STATUSES_FETCH_FAIL, + TRENDS_STATUSES_EXPAND_REQUEST, + TRENDS_STATUSES_EXPAND_SUCCESS, + TRENDS_STATUSES_EXPAND_FAIL, } from '../actions/trends'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { @@ -111,11 +114,15 @@ export default function statusLists(state = initialState, action) { case BOOKMARKED_STATUSES_EXPAND_SUCCESS: return appendToList(state, 'bookmarks', action.statuses, action.next); case TRENDS_STATUSES_FETCH_REQUEST: + case TRENDS_STATUSES_EXPAND_REQUEST: return state.setIn(['trending', 'isLoading'], true); case TRENDS_STATUSES_FETCH_FAIL: + case TRENDS_STATUSES_EXPAND_FAIL: return state.setIn(['trending', 'isLoading'], false); case TRENDS_STATUSES_FETCH_SUCCESS: return normalizeList(state, 'trending', action.statuses, action.next); + case TRENDS_STATUSES_EXPAND_SUCCESS: + return appendToList(state, 'trending', action.statuses, action.next); case FAVOURITE_SUCCESS: return prependOneToList(state, 'favourites', action.status); case UNFAVOURITE_SUCCESS: From 1b91359a4508b3068207ef4fd798a56549575591 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Apr 2022 11:27:35 +0200 Subject: [PATCH 2/5] Fix older items possibly disappearing on timeline updates (#17980) In some rare cases, when receiving statuses out of order from the streaming API then polling from the REST API, it was possible for the `expandNormalizedTimeline` function to remove older items from the timeline. This commit ensures that any item from the replaced slice that is older than the oldest item retrieved from the API gets added back to the replaced slice. --- app/javascript/mastodon/reducers/timelines.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 301997567f..53a644e47b 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -66,13 +66,22 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that // is newer than the most recent fetched one, as it delimits a section comprised of only - // items present in `newIds` (or that were deleted from the server, so should be removed + // items older or within `newIds` (or that were deleted from the server, so should be removed // anyway). // Stop the gap *after* that item. const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1; - // Make sure we aren't inserting duplicates - let insertedIds = ImmutableOrderedSet(newIds).subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)).toList(); + let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => { + // It is possible, though unlikely, that the slice we are replacing contains items older + // than the elements we got from the API. Get them and add them back at the back of the + // slice. + const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => id !== null && compareId(id, newIds.last()) < 0); + insertedIds.union(olderIds); + + // Make sure we aren't inserting duplicates + insertedIds.subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)); + }).toList(); + // Finally, insert a gap marker if the data is marked as partial by the server if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) { insertedIds = insertedIds.unshift(null); From 8c03b45fffc54a66d1c9c3eec75a7c6f741d0947 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 7 Apr 2022 13:32:12 +0200 Subject: [PATCH 3/5] Fix unset `SMTP_RETURN_PATH` environment variable causing e-mail not to send (#17982) --- config/environments/production.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index b003cce9ed..95f8a6f32a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -95,11 +95,12 @@ Rails.application.configure do config.action_mailer.default_options = { from: outgoing_email_address, - reply_to: ENV['SMTP_REPLY_TO'], - return_path: ENV['SMTP_RETURN_PATH'], message_id: -> { "<#{Mail.random_tag}@#{outgoing_email_domain}>" }, } + config.action_mailer.default_options[:reply_to] = ENV['SMTP_REPLY_TO'] if ENV['SMTP_REPLY_TO'].present? + config.action_mailer.default_options[:return_path] = ENV['SMTP_RETURN_PATH'] if ENV['SMTP_RETURN_PATH'].present? + config.action_mailer.smtp_settings = { :port => ENV['SMTP_PORT'], :address => ENV['SMTP_SERVER'], From ce9dcbea32e7212dc19d83bbb7a21afe8756ced0 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Apr 2022 14:47:30 +0200 Subject: [PATCH 4/5] Fix failure when sending warning emails with custom text (#17983) * Add tests * Fix failure when sending warning emails with custom text --- app/mailers/user_mailer.rb | 1 + spec/mailers/user_mailer_spec.rb | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index ce36dd6f5f..e47bedec6b 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -7,6 +7,7 @@ class UserMailer < Devise::Mailer helper :application helper :instance helper :statuses + helper :formatting helper RoutingHelper diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 9c866788f2..2ed33c1e4a 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -83,4 +83,15 @@ describe UserMailer, type: :mailer do include_examples 'localized subject', 'devise.mailer.email_changed.subject' end + + describe 'warning' do + let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') } + let(:mail) { UserMailer.warning(receiver, strike) } + + it 'renders warning notification' do + receiver.update!(locale: nil) + expect(mail.body.encoded).to include I18n.t("user_mailer.warning.title.suspend", acct: receiver.account.acct) + expect(mail.body.encoded).to include strike.text + end + end end From ed8a0bfbb80a759dc6944f599b89e52acb1e83b0 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 7 Apr 2022 11:27:35 +0200 Subject: [PATCH 5/5] [Glitch] Fix older items possibly disappearing on timeline updates Port 1b91359a4508b3068207ef4fd798a56549575591 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/reducers/timelines.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index b40e74c5bc..29e02a8640 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -66,13 +66,22 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that // is newer than the most recent fetched one, as it delimits a section comprised of only - // items present in `newIds` (or that were deleted from the server, so should be removed + // items older or within `newIds` (or that were deleted from the server, so should be removed // anyway). // Stop the gap *after* that item. const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0) + 1; - // Make sure we aren't inserting duplicates - let insertedIds = ImmutableOrderedSet(newIds).subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)).toList(); + let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => { + // It is possible, though unlikely, that the slice we are replacing contains items older + // than the elements we got from the API. Get them and add them back at the back of the + // slice. + const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => id !== null && compareId(id, newIds.last()) < 0); + insertedIds.union(olderIds); + + // Make sure we aren't inserting duplicates + insertedIds.subtract(oldIds.take(firstIndex), oldIds.skip(lastIndex)); + }).toList(); + // Finally, insert a gap marker if the data is marked as partial by the server if (isPartial && (firstIndex === 0 || oldIds.get(firstIndex - 1) !== null)) { insertedIds = insertedIds.unshift(null);