Merge branch 'refs/heads/glitch-soc' into develop

This commit is contained in:
Jeremy Kescher 2024-06-22 13:30:26 +02:00
commit b3f731abac
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
39 changed files with 253 additions and 214 deletions

View file

@ -20,10 +20,6 @@ module.exports = defineConfig({
es6: true, es6: true,
}, },
globals: {
ATTACHMENT_HOST: false,
},
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: [ plugins: [
@ -79,7 +75,7 @@ module.exports = defineConfig({
], ],
}, },
], ],
'no-empty': 'off', 'no-empty': ['error', { "allowEmptyCatch": true }],
'no-restricted-properties': [ 'no-restricted-properties': [
'error', 'error',
{ property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substring', message: 'Use .slice instead of .substring.' },
@ -94,7 +90,6 @@ module.exports = defineConfig({
message: "Use '·' (middle dot) instead of '•' (bullet)", message: "Use '·' (middle dot) instead of '•' (bullet)",
}, },
], ],
'no-self-assign': 'off',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
@ -119,12 +114,10 @@ module.exports = defineConfig({
'react/jsx-tag-spacing': 'error', 'react/jsx-tag-spacing': 'error',
'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-uses-react': 'off', // not needed with new JSX transform
'react/jsx-wrap-multilines': 'error', 'react/jsx-wrap-multilines': 'error',
'react/no-deprecated': 'off',
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
'react/self-closing-comp': 'error', 'react/self-closing-comp': 'error',
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46 // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/label-has-associated-control': 'off',
'jsx-a11y/media-has-caption': 'off', 'jsx-a11y/media-has-caption': 'off',
@ -139,23 +132,6 @@ module.exports = defineConfig({
// ], // ],
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off',
// recommended rule is: // recommended rule is:
// 'jsx-a11y/no-noninteractive-element-interactions': [
// 'error',
// {
// body: ['onError', 'onLoad'],
// iframe: ['onError', 'onLoad'],
// img: ['onError', 'onLoad'],
// },
// ],
'jsx-a11y/no-noninteractive-element-interactions': [
'warn',
{
handlers: [
'onClick',
],
},
],
// recommended rule is:
// 'jsx-a11y/no-noninteractive-tabindex': [ // 'jsx-a11y/no-noninteractive-tabindex': [
// 'error', // 'error',
// { // {
@ -165,7 +141,6 @@ module.exports = defineConfig({
// }, // },
// ], // ],
'jsx-a11y/no-noninteractive-tabindex': 'off', 'jsx-a11y/no-noninteractive-tabindex': 'off',
'jsx-a11y/no-onchange': 'off',
// recommended is full 'error' // recommended is full 'error'
'jsx-a11y/no-static-element-interactions': [ 'jsx-a11y/no-static-element-interactions': [
'warn', 'warn',

View file

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.63.5. # using RuboCop version 1.64.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -55,7 +55,6 @@ Style/FetchEnvVar:
- 'config/initializers/vapid.rb' - 'config/initializers/vapid.rb'
- 'lib/mastodon/redis_config.rb' - 'lib/mastodon/redis_config.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
- 'spec/system/profile_spec.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns.
@ -66,40 +65,10 @@ Style/FormatStringToken:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb' - 'lib/paperclip/color_extractor.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
Exclude:
- 'config/environments/development.rb'
- 'config/environments/production.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause: Style/GuardClause:
Exclude: Enabled: false
- 'app/lib/activitypub/activity/block.rb'
- 'app/lib/request.rb'
- 'app/lib/request_pool.rb'
- 'app/lib/webfinger.rb'
- 'app/lib/webfinger_resource.rb'
- 'app/models/concerns/account/counters.rb'
- 'app/models/concerns/user/ldap_authenticable.rb'
- 'app/models/tag.rb'
- 'app/models/user.rb'
- 'app/services/fan_out_on_write_service.rb'
- 'app/services/post_status_service.rb'
- 'app/services/process_hashtags_service.rb'
- 'app/workers/move_worker.rb'
- 'app/workers/redownload_avatar_worker.rb'
- 'app/workers/redownload_header_worker.rb'
- 'app/workers/redownload_media_worker.rb'
- 'app/workers/remote_account_refresh_worker.rb'
- 'config/initializers/devise.rb'
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
- 'lib/mastodon/cli/accounts.rb'
- 'lib/mastodon/cli/maintenance.rb'
- 'lib/mastodon/cli/media.rb'
- 'lib/tasks/repo.rake'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues: Style/HashTransformValues:
@ -121,16 +90,10 @@ Style/MutableConstant:
- 'app/services/delete_account_service.rb' - 'app/services/delete_account_service.rb'
- 'lib/mastodon/migration_warning.rb' - 'lib/mastodon/migration_warning.rb'
# This cop supports safe autocorrection (--autocorrect).
Style/NilLambda:
Exclude:
- 'config/initializers/paperclip.rb'
# Configuration parameters: AllowedMethods. # Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing? # AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter: Style/OptionalBooleanParameter:
Exclude: Exclude:
- 'app/helpers/admin/account_moderation_notes_helper.rb'
- 'app/helpers/jsonld_helper.rb' - 'app/helpers/jsonld_helper.rb'
- 'app/lib/admin/system_check/message.rb' - 'app/lib/admin/system_check/message.rb'
- 'app/lib/request.rb' - 'app/lib/request.rb'
@ -154,13 +117,6 @@ Style/RedundantConstantBase:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/sidekiq.rb' - 'config/initializers/sidekiq.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
# AllowedMethods: present?, blank?, presence, try, try!
Style/SafeNavigation:
Exclude:
- 'app/models/concerns/account/finder_concern.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: WordRegex. # Configuration parameters: WordRegex.
# SupportedStyles: percent, brackets # SupportedStyles: percent, brackets

View file

@ -708,7 +708,7 @@ GEM
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.0) rspec-core (3.13.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.0) rspec-expectations (3.13.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-github (2.4.0) rspec-github (2.4.0)
@ -716,7 +716,7 @@ GEM
rspec-mocks (3.13.1) rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (6.1.2) rspec-rails (6.1.3)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
@ -897,7 +897,7 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.15) zeitwerk (2.6.16)
PLATFORMS PLATFORMS
ruby ruby

View file

@ -15,7 +15,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
end end
render json: @notifications.map { |notification| NotificationGroup.from_notification(notification) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata render json: @notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
end end
def show def show

View file

@ -77,7 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}, },
onDisconnect() { onDisconnect() {
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline({ timeline: timelineId }));
if (options.fallback) { if (options.fallback) {
// @ts-expect-error // @ts-expect-error

View file

@ -7,9 +7,11 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import {timelineDelete} from './timelines_typed';
export { disconnectTimeline } from './timelines_typed';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
@ -18,7 +20,6 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
@ -73,16 +74,10 @@ export function updateTimeline(timeline, status, accept) {
export function deleteFromTimelines(id) { export function deleteFromTimelines(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']); const accountId = getState().getIn(['statuses', id, 'account']);
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON();
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
dispatch({ dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf }));
type: TIMELINE_DELETE,
id,
accountId,
references,
reblogOf,
});
}; };
} }
@ -237,12 +232,6 @@ export function connectTimeline(timeline) {
}; };
} }
export const disconnectTimeline = timeline => ({
type: TIMELINE_DISCONNECT,
timeline,
usePendingItems: preferPendingItems,
});
export const markAsPartial = timeline => ({ export const markAsPartial = timeline => ({
type: TIMELINE_MARK_AS_PARTIAL, type: TIMELINE_MARK_AS_PARTIAL,
timeline, timeline,

View file

@ -0,0 +1,20 @@
import { createAction } from '@reduxjs/toolkit';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
export const disconnectTimeline = createAction(
'timeline/disconnect',
({ timeline }: { timeline: string }) => ({
payload: {
timeline,
usePendingItems: preferPendingItems,
},
}),
);
export const timelineDelete = createAction<{
statusId: string;
accountId: string;
references: string[];
reblogOf: string | null;
}>('timelines/delete');

View file

@ -1,5 +1,7 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { import {
COMPOSE_MOUNT, COMPOSE_MOUNT,
COMPOSE_UNMOUNT, COMPOSE_UNMOUNT,
@ -54,7 +56,6 @@ import {
} from '../actions/compose'; } from '../actions/compose';
import { REDRAFT } from '../actions/statuses'; import { REDRAFT } from '../actions/statuses';
import { STORE_HYDRATE } from '../actions/store'; import { STORE_HYDRATE } from '../actions/store';
import { TIMELINE_DELETE } from '../actions/timelines';
import { me, defaultContentType, pollLimits } from '../initial_state'; import { me, defaultContentType, pollLimits } from '../initial_state';
import { recoverHashtags } from '../utils/hashtag'; import { recoverHashtags } from '../utils/hashtag';
import { unescapeHTML } from '../utils/html'; import { unescapeHTML } from '../utils/html';
@ -554,10 +555,10 @@ export default function compose(state = initialState, action) {
return updateSuggestionTags(state, action.token); return updateSuggestionTags(state, action.token);
case COMPOSE_TAG_HISTORY_UPDATE: case COMPOSE_TAG_HISTORY_UPDATE:
return state.set('tagHistory', fromJS(action.tags)); return state.set('tagHistory', fromJS(action.tags));
case TIMELINE_DELETE: case timelineDelete.type:
if (action.id === state.get('in_reply_to')) { if (action.payload.statusId === state.get('in_reply_to')) {
return state.set('in_reply_to', null); return state.set('in_reply_to', null);
} else if (action.id === state.get('id')) { } else if (action.payload.statusId === state.get('id')) {
return state.set('id', null); return state.set('id', null);
} else { } else {
return state; return state;

View file

@ -1,11 +1,13 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { import {
blockAccountSuccess, blockAccountSuccess,
muteAccountSuccess, muteAccountSuccess,
} from '../actions/accounts'; } from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; import { TIMELINE_UPDATE } from '../actions/timelines';
import { compareId } from '../compare_id'; import { compareId } from '../compare_id';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@ -97,8 +99,8 @@ export default function replies(state = initialState, action) {
return filterContexts(state, action.payload.relationship, action.payload.statuses); return filterContexts(state, action.payload.relationship, action.payload.statuses);
case CONTEXT_FETCH_SUCCESS: case CONTEXT_FETCH_SUCCESS:
return normalizeContext(state, action.id, action.ancestors, action.descendants); return normalizeContext(state, action.id, action.ancestors, action.descendants);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteFromContexts(state, [action.id]); return deleteFromContexts(state, [action.payload.statusId]);
case TIMELINE_UPDATE: case TIMELINE_UPDATE:
return updateContext(state, action.status); return updateContext(state, action.status);
default: default:

View file

@ -1,10 +1,11 @@
import type { Reducer } from '@reduxjs/toolkit'; import type { Reducer } from '@reduxjs/toolkit';
import { Record as ImmutableRecord, Stack } from 'immutable'; import { Record as ImmutableRecord, Stack } from 'immutable';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
import type { ModalType } from '../actions/modal'; import type { ModalType } from '../actions/modal';
import { openModal, closeModal } from '../actions/modal'; import { openModal, closeModal } from '../actions/modal';
import { TIMELINE_DELETE } from '../actions/timelines';
export type ModalProps = Record<string, unknown>; export type ModalProps = Record<string, unknown>;
interface Modal { interface Modal {
@ -72,10 +73,10 @@ export const modalReducer: Reducer<State> = (state = initialState, action) => {
// TODO: type those actions // TODO: type those actions
else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS) else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false }); return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
else if (action.type === TIMELINE_DELETE) else if (timelineDelete.match(action))
return state.update('stack', (stack) => return state.update('stack', (stack) =>
stack.filterNot( stack.filterNot(
(modal) => modal.get('modalProps').statusId === action.id, (modal) => modal.get('modalProps').statusId === action.payload.statusId,
), ),
); );
else return state; else return state;

View file

@ -1,6 +1,7 @@
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { blockDomainSuccess } from 'flavours/glitch/actions/domain_blocks'; import { blockDomainSuccess } from 'flavours/glitch/actions/domain_blocks';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { import {
authorizeFollowRequestSuccess, authorizeFollowRequestSuccess,
@ -33,7 +34,7 @@ import {
NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_SUPPORT,
NOTIFICATIONS_SET_BROWSER_PERMISSION, NOTIFICATIONS_SET_BROWSER_PERMISSION,
} from '../actions/notifications'; } from '../actions/notifications';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { disconnectTimeline } from '../actions/timelines';
import { compareId } from '../compare_id'; import { compareId } from '../compare_id';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@ -333,11 +334,11 @@ export default function notifications(state = initialState, action) {
return filterNotifications(state, [action.payload.id], 'follow_request'); return filterNotifications(state, [action.payload.id], 'follow_request');
case NOTIFICATIONS_CLEAR: case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteByStatus(state, action.id); return deleteByStatus(state, action.payload.statusId);
case TIMELINE_DISCONNECT: case disconnectTimeline.type:
return action.timeline === 'home' ? return action.payload.timeline === 'home' ?
state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state.update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
state; state;
case NOTIFICATIONS_SET_BROWSER_SUPPORT: case NOTIFICATIONS_SET_BROWSER_SUPPORT:
return state.set('browserSupport', action.value); return state.set('browserSupport', action.value);

View file

@ -4,8 +4,7 @@ import {
deployPictureInPictureAction, deployPictureInPictureAction,
removePictureInPicture, removePictureInPicture,
} from 'flavours/glitch/actions/picture_in_picture'; } from 'flavours/glitch/actions/picture_in_picture';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { TIMELINE_DELETE } from '../actions/timelines';
export interface PIPMediaProps { export interface PIPMediaProps {
src: string; src: string;
@ -49,8 +48,9 @@ export const pictureInPictureReducer: Reducer<PIPState> = (
...action.payload.props, ...action.payload.props,
}; };
else if (removePictureInPicture.match(action)) return initialState; else if (removePictureInPicture.match(action)) return initialState;
else if (action.type === TIMELINE_DELETE) else if (timelineDelete.match(action))
if (state.type && state.statusId === action.id) return initialState; if (state.type && state.statusId === action.payload.statusId)
return initialState;
return state; return state;
}; };

View file

@ -1,5 +1,7 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { normalizeStatusTranslation } from '../actions/importer/normalizer'; import { normalizeStatusTranslation } from '../actions/importer/normalizer';
import { import {
@ -32,7 +34,6 @@ import {
STATUS_FETCH_REQUEST, STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL, STATUS_FETCH_FAIL,
} from '../actions/statuses'; } from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
const importStatus = (state, status) => state.set(status.id, fromJS(status)); const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -164,8 +165,8 @@ export default function statuses(state = initialState, action) {
}); });
case STATUS_COLLAPSE: case STATUS_COLLAPSE:
return state.setIn([action.id, 'collapsed'], action.isCollapsed); return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteStatus(state, action.id, action.references); return deleteStatus(state, action.payload.statusId, action.payload.references);
case STATUS_TRANSLATE_SUCCESS: case STATUS_TRANSLATE_SUCCESS:
return statusTranslateSuccess(state, action.id, action.translation); return statusTranslateSuccess(state, action.id, action.translation);
case STATUS_TRANSLATE_UNDO: case STATUS_TRANSLATE_UNDO:

View file

@ -1,5 +1,7 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import { import {
blockAccountSuccess, blockAccountSuccess,
muteAccountSuccess, muteAccountSuccess,
@ -7,19 +9,18 @@ import {
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
TIMELINE_UPDATE, TIMELINE_UPDATE,
TIMELINE_DELETE,
TIMELINE_CLEAR, TIMELINE_CLEAR,
TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_SUCCESS,
TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_REQUEST,
TIMELINE_EXPAND_FAIL, TIMELINE_EXPAND_FAIL,
TIMELINE_SCROLL_TOP, TIMELINE_SCROLL_TOP,
TIMELINE_CONNECT, TIMELINE_CONNECT,
TIMELINE_DISCONNECT,
TIMELINE_LOAD_PENDING, TIMELINE_LOAD_PENDING,
TIMELINE_MARK_AS_PARTIAL, TIMELINE_MARK_AS_PARTIAL,
TIMELINE_INSERT, TIMELINE_INSERT,
TIMELINE_GAP, TIMELINE_GAP,
TIMELINE_SUGGESTIONS, TIMELINE_SUGGESTIONS,
disconnectTimeline,
} from '../actions/timelines'; } from '../actions/timelines';
import { compareId } from '../compare_id'; import { compareId } from '../compare_id';
@ -164,7 +165,7 @@ const filterTimelines = (state, relationship, statuses) => {
return; return;
} }
references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')); references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')).valueSeq().toJSON();
state = deleteStatus(state, status.get('id'), references, relationship.id); state = deleteStatus(state, status.get('id'), references, relationship.id);
}); });
@ -207,8 +208,8 @@ export default function timelines(state = initialState, action) {
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
case TIMELINE_UPDATE: case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered); return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteStatus(state, action.id, action.references, action.reblogOf); return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf);
case TIMELINE_CLEAR: case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline); return clearTimeline(state, action.timeline);
case blockAccountSuccess.type: case blockAccountSuccess.type:
@ -220,11 +221,11 @@ export default function timelines(state = initialState, action) {
return updateTop(state, action.timeline, action.top); return updateTop(state, action.timeline, action.top);
case TIMELINE_CONNECT: case TIMELINE_CONNECT:
return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems)); return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems));
case TIMELINE_DISCONNECT: case disconnectTimeline.type:
return state.update( return state.update(
action.timeline, action.payload.timeline,
initialTimeline, initialTimeline,
map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items), map => map.set('online', false).update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items),
); );
case TIMELINE_MARK_AS_PARTIAL: case TIMELINE_MARK_AS_PARTIAL:
return state.update( return state.update(

View file

@ -82,13 +82,19 @@ export function createThunk<Arg = void, Returned = void>(
const discardLoadDataInPayload = Symbol('discardLoadDataInPayload'); const discardLoadDataInPayload = Symbol('discardLoadDataInPayload');
type DiscardLoadData = typeof discardLoadDataInPayload; type DiscardLoadData = typeof discardLoadDataInPayload;
type OnData<LoadDataResult, ReturnedData> = ( type OnData<ActionArg, LoadDataResult, ReturnedData> = (
data: LoadDataResult, data: LoadDataResult,
api: AppThunkApi & { api: AppThunkApi & {
actionArg: ActionArg;
discardLoadData: DiscardLoadData; discardLoadData: DiscardLoadData;
}, },
) => ReturnedData | DiscardLoadData | Promise<ReturnedData | DiscardLoadData>; ) => ReturnedData | DiscardLoadData | Promise<ReturnedData | DiscardLoadData>;
type LoadData<Args, LoadDataResult> = (
args: Args,
api: AppThunkApi,
) => Promise<LoadDataResult>;
type ArgsType = Record<string, unknown> | undefined; type ArgsType = Record<string, unknown> | undefined;
// Overload when there is no `onData` method, the payload is the `onData` result // Overload when there is no `onData` method, the payload is the `onData` result
@ -101,18 +107,18 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>( export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: onDataOrThunkOptions?:
| AppThunkOptions | AppThunkOptions
| OnData<LoadDataResult, DiscardLoadData>, | OnData<Args, LoadDataResult, DiscardLoadData>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions,
): ReturnType<typeof createThunk<Args, void>>; ): ReturnType<typeof createThunk<Args, void>>;
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result // Overload when the `onData` method returns nothing, then the mayload is the `onData` result
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>( export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, void>, onDataOrThunkOptions?: AppThunkOptions | OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions,
): ReturnType<typeof createThunk<Args, LoadDataResult>>; ): ReturnType<typeof createThunk<Args, LoadDataResult>>;
@ -123,8 +129,10 @@ export function createDataLoadingThunk<
Returned, Returned,
>( >(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, Returned>, onDataOrThunkOptions?:
| AppThunkOptions
| OnData<Args, LoadDataResult, Returned>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions,
): ReturnType<typeof createThunk<Args, Returned>>; ): ReturnType<typeof createThunk<Args, Returned>>;
@ -159,11 +167,13 @@ export function createDataLoadingThunk<
Returned, Returned,
>( >(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, Returned>, onDataOrThunkOptions?:
| AppThunkOptions
| OnData<Args, LoadDataResult, Returned>,
maybeThunkOptions?: AppThunkOptions, maybeThunkOptions?: AppThunkOptions,
) { ) {
let onData: OnData<LoadDataResult, Returned> | undefined; let onData: OnData<Args, LoadDataResult, Returned> | undefined;
let thunkOptions: AppThunkOptions | undefined; let thunkOptions: AppThunkOptions | undefined;
if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions;
@ -177,7 +187,10 @@ export function createDataLoadingThunk<
return createThunk<Args, Returned>( return createThunk<Args, Returned>(
name, name,
async (arg, { getState, dispatch }) => { async (arg, { getState, dispatch }) => {
const data = await loadData(arg); const data = await loadData(arg, {
dispatch,
getState,
});
if (!onData) return data as Returned; if (!onData) return data as Returned;
@ -185,6 +198,7 @@ export function createDataLoadingThunk<
dispatch, dispatch,
getState, getState,
discardLoadData: discardLoadDataInPayload, discardLoadData: discardLoadDataInPayload,
actionArg: arg,
}); });
// if there is no return in `onData`, we return the `onData` result // if there is no return in `onData`, we return the `onData` result

View file

@ -77,7 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}, },
onDisconnect() { onDisconnect() {
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline({ timeline: timelineId }));
if (options.fallback) { if (options.fallback) {
// @ts-expect-error // @ts-expect-error

View file

@ -6,9 +6,11 @@ import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import {timelineDelete} from './timelines_typed';
export { disconnectTimeline } from './timelines_typed';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
@ -17,7 +19,6 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
@ -62,16 +63,10 @@ export function updateTimeline(timeline, status, accept) {
export function deleteFromTimelines(id) { export function deleteFromTimelines(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']); const accountId = getState().getIn(['statuses', id, 'account']);
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON();
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
dispatch({ dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf }));
type: TIMELINE_DELETE,
id,
accountId,
references,
reblogOf,
});
}; };
} }
@ -225,12 +220,6 @@ export function connectTimeline(timeline) {
}; };
} }
export const disconnectTimeline = timeline => ({
type: TIMELINE_DISCONNECT,
timeline,
usePendingItems: preferPendingItems,
});
export const markAsPartial = timeline => ({ export const markAsPartial = timeline => ({
type: TIMELINE_MARK_AS_PARTIAL, type: TIMELINE_MARK_AS_PARTIAL,
timeline, timeline,

View file

@ -0,0 +1,20 @@
import { createAction } from '@reduxjs/toolkit';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
export const disconnectTimeline = createAction(
'timeline/disconnect',
({ timeline }: { timeline: string }) => ({
payload: {
timeline,
usePendingItems: preferPendingItems,
},
}),
);
export const timelineDelete = createAction<{
statusId: string;
accountId: string;
references: string[];
reblogOf: string | null;
}>('timelines/delete');

View file

@ -133,7 +133,7 @@ class AccountTimeline extends ImmutablePureComponent {
} }
if (prevProps.accountId === me && accountId !== me) { if (prevProps.accountId === me && accountId !== me) {
dispatch(disconnectTimeline(`account:${me}`)); dispatch(disconnectTimeline({ timeline: `account:${me}` }));
} }
} }
@ -141,7 +141,7 @@ class AccountTimeline extends ImmutablePureComponent {
const { dispatch, accountId } = this.props; const { dispatch, accountId } = this.props;
if (accountId === me) { if (accountId === me) {
dispatch(disconnectTimeline(`account:${me}`)); dispatch(disconnectTimeline({ timeline: `account:${me}` }));
} }
} }

View file

@ -80,6 +80,7 @@
"admin.dashboard.retention.cohort_size": "नये उपयोगकर्ता", "admin.dashboard.retention.cohort_size": "नये उपयोगकर्ता",
"admin.impact_report.instance_accounts": "ये अकाउंट प्रोफाइल मिटा देगा", "admin.impact_report.instance_accounts": "ये अकाउंट प्रोफाइल मिटा देगा",
"admin.impact_report.instance_followers": "हमारे यूजर्स इन फॉलोअर्स को खो देंगे", "admin.impact_report.instance_followers": "हमारे यूजर्स इन फॉलोअर्स को खो देंगे",
"admin.impact_report.instance_follows": "उनके उपयोगकर्ता इतने फ़ॉलोअर खो देंगे",
"admin.impact_report.title": "प्रभावकां सारांश", "admin.impact_report.title": "प्रभावकां सारांश",
"alert.rate_limited.message": "कृप्या {retry_time, time, medium} के बाद दुबारा कोशिश करें", "alert.rate_limited.message": "कृप्या {retry_time, time, medium} के बाद दुबारा कोशिश करें",
"alert.rate_limited.title": "सीमित दर", "alert.rate_limited.title": "सीमित दर",
@ -88,6 +89,7 @@
"announcement.announcement": "घोषणा", "announcement.announcement": "घोषणा",
"attachments_list.unprocessed": "(असंसाधित)", "attachments_list.unprocessed": "(असंसाधित)",
"audio.hide": "हाईड ऑडियो", "audio.hide": "हाईड ऑडियो",
"block_modal.remote_users_caveat": "हम {domain} को आपके निर्णय का सम्मान करने को कहेंगे।हालाकि इसकी आपूर्ति कि प्रत्याभूति नहीं हे। क्योंकि कुछ सर्वर ब्लॉक को अलग तरह से निभा सकते हे। अभी भी सार्वजानिक पोस्ट लोग- इन बगैर के उपयोगकर्ताओं को दिख सकती हैं।",
"block_modal.show_less": "कम दिखाएं", "block_modal.show_less": "कम दिखाएं",
"block_modal.show_more": "और दिखाएँ", "block_modal.show_more": "और दिखाएँ",
"block_modal.they_cant_mention": "वे आपको मेंशन या फॉलो नहीं कर सकते", "block_modal.they_cant_mention": "वे आपको मेंशन या फॉलो नहीं कर सकते",
@ -205,7 +207,12 @@
"dismissable_banner.dismiss": "डिसमिस", "dismissable_banner.dismiss": "डिसमिस",
"dismissable_banner.explore_links": "इन समाचारों के बारे में लोगों द्वारा इस पर और डेसेंट्रलीसेड नेटवर्क के अन्य सर्वरों पर अभी बात की जा रही है।", "dismissable_banner.explore_links": "इन समाचारों के बारे में लोगों द्वारा इस पर और डेसेंट्रलीसेड नेटवर्क के अन्य सर्वरों पर अभी बात की जा रही है।",
"dismissable_banner.explore_tags": "ये हैशटैग अभी इस पर और डेसेंट्रलीसेड नेटवर्क के अन्य सर्वरों पर लोगों के बीच कर्षण प्राप्त कर रहे हैं।", "dismissable_banner.explore_tags": "ये हैशटैग अभी इस पर और डेसेंट्रलीसेड नेटवर्क के अन्य सर्वरों पर लोगों के बीच कर्षण प्राप्त कर रहे हैं।",
"dismissable_banner.public_timeline": "यह ताजा सार्वजनिक पोस्ट है जिसका सामाजिक वेब {domain} के लोगो द्वारा अनुसरण हो रहा हैं।",
"domain_block_modal.block": "सर्वर ब्लॉक करें", "domain_block_modal.block": "सर्वर ब्लॉक करें",
"domain_block_modal.block_account_instead": "इसकी जगह यह @{name} रखें",
"domain_block_modal.they_can_interact_with_old_posts": "इस सर्वर की लोग आपकी पूरानी पोस्ट्स का अनुसरण किया जा sakta है।",
"domain_block_modal.they_cant_follow": "इस सर्वर मेसे कोई भी आपका अनुसरण नहीं कर सकता।",
"domain_block_modal.they_wont_know": "उनको पता नहीं चलेगा कि वे अवरोधित किए गए है।",
"domain_block_modal.title": "डोमेन ब्लॉक करें", "domain_block_modal.title": "डोमेन ब्लॉक करें",
"domain_pill.server": "सर्वर", "domain_pill.server": "सर्वर",
"domain_pill.username": "यूज़रनेम", "domain_pill.username": "यूज़रनेम",

View file

@ -32,7 +32,7 @@
"account.featured_tags.last_status_never": "게시물 없음", "account.featured_tags.last_status_never": "게시물 없음",
"account.featured_tags.title": "{name} 님의 추천 해시태그", "account.featured_tags.title": "{name} 님의 추천 해시태그",
"account.follow": "팔로우", "account.follow": "팔로우",
"account.follow_back": "맞팔로우", "account.follow_back": "맞팔로우 하기",
"account.followers": "팔로워", "account.followers": "팔로워",
"account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.", "account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.",
"account.followers_counter": "{counter} 팔로워", "account.followers_counter": "{counter} 팔로워",
@ -53,7 +53,7 @@
"account.mute_notifications_short": "알림 뮤트", "account.mute_notifications_short": "알림 뮤트",
"account.mute_short": "뮤트", "account.mute_short": "뮤트",
"account.muted": "뮤트됨", "account.muted": "뮤트됨",
"account.mutual": "상호 팔로우", "account.mutual": "맞팔로우 중",
"account.no_bio": "제공된 설명이 없습니다.", "account.no_bio": "제공된 설명이 없습니다.",
"account.open_original_page": "원본 페이지 열기", "account.open_original_page": "원본 페이지 열기",
"account.posts": "게시물", "account.posts": "게시물",

View file

@ -1,5 +1,7 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { import {
COMPOSE_MOUNT, COMPOSE_MOUNT,
COMPOSE_UNMOUNT, COMPOSE_UNMOUNT,
@ -51,7 +53,6 @@ import {
} from '../actions/compose'; } from '../actions/compose';
import { REDRAFT } from '../actions/statuses'; import { REDRAFT } from '../actions/statuses';
import { STORE_HYDRATE } from '../actions/store'; import { STORE_HYDRATE } from '../actions/store';
import { TIMELINE_DELETE } from '../actions/timelines';
import { me, pollLimits } from '../initial_state'; import { me, pollLimits } from '../initial_state';
import { unescapeHTML } from '../utils/html'; import { unescapeHTML } from '../utils/html';
import { uuid } from '../uuid'; import { uuid } from '../uuid';
@ -449,10 +450,10 @@ export default function compose(state = initialState, action) {
return updateSuggestionTags(state, action.token); return updateSuggestionTags(state, action.token);
case COMPOSE_TAG_HISTORY_UPDATE: case COMPOSE_TAG_HISTORY_UPDATE:
return state.set('tagHistory', fromJS(action.tags)); return state.set('tagHistory', fromJS(action.tags));
case TIMELINE_DELETE: case timelineDelete.type:
if (action.id === state.get('in_reply_to')) { if (action.payload.statusId === state.get('in_reply_to')) {
return state.set('in_reply_to', null); return state.set('in_reply_to', null);
} else if (action.id === state.get('id')) { } else if (action.payload.statusId === state.get('id')) {
return state.set('id', null); return state.set('id', null);
} else { } else {
return state; return state;

View file

@ -1,11 +1,13 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { import {
blockAccountSuccess, blockAccountSuccess,
muteAccountSuccess, muteAccountSuccess,
} from '../actions/accounts'; } from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; import { TIMELINE_UPDATE } from '../actions/timelines';
import { compareId } from '../compare_id'; import { compareId } from '../compare_id';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@ -97,8 +99,8 @@ export default function replies(state = initialState, action) {
return filterContexts(state, action.payload.relationship, action.payload.statuses); return filterContexts(state, action.payload.relationship, action.payload.statuses);
case CONTEXT_FETCH_SUCCESS: case CONTEXT_FETCH_SUCCESS:
return normalizeContext(state, action.id, action.ancestors, action.descendants); return normalizeContext(state, action.id, action.ancestors, action.descendants);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteFromContexts(state, [action.id]); return deleteFromContexts(state, [action.payload.statusId]);
case TIMELINE_UPDATE: case TIMELINE_UPDATE:
return updateContext(state, action.status); return updateContext(state, action.status);
default: default:

View file

@ -1,10 +1,11 @@
import type { Reducer } from '@reduxjs/toolkit'; import type { Reducer } from '@reduxjs/toolkit';
import { Record as ImmutableRecord, Stack } from 'immutable'; import { Record as ImmutableRecord, Stack } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
import type { ModalType } from '../actions/modal'; import type { ModalType } from '../actions/modal';
import { openModal, closeModal } from '../actions/modal'; import { openModal, closeModal } from '../actions/modal';
import { TIMELINE_DELETE } from '../actions/timelines';
export type ModalProps = Record<string, unknown>; export type ModalProps = Record<string, unknown>;
interface Modal { interface Modal {
@ -72,10 +73,10 @@ export const modalReducer: Reducer<State> = (state = initialState, action) => {
// TODO: type those actions // TODO: type those actions
else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS) else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS)
return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false }); return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false });
else if (action.type === TIMELINE_DELETE) else if (timelineDelete.match(action))
return state.update('stack', (stack) => return state.update('stack', (stack) =>
stack.filterNot( stack.filterNot(
(modal) => modal.get('modalProps').statusId === action.id, (modal) => modal.get('modalProps').statusId === action.payload.statusId,
), ),
); );
else return state; else return state;

View file

@ -1,6 +1,7 @@
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; import { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { import {
authorizeFollowRequestSuccess, authorizeFollowRequestSuccess,
@ -30,7 +31,7 @@ import {
NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_SUPPORT,
NOTIFICATIONS_SET_BROWSER_PERMISSION, NOTIFICATIONS_SET_BROWSER_PERMISSION,
} from '../actions/notifications'; } from '../actions/notifications';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; import { disconnectTimeline } from '../actions/timelines';
import { compareId } from '../compare_id'; import { compareId } from '../compare_id';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@ -291,11 +292,11 @@ export default function notifications(state = initialState, action) {
return filterNotifications(state, [action.payload.id], 'follow_request'); return filterNotifications(state, [action.payload.id], 'follow_request');
case NOTIFICATIONS_CLEAR: case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteByStatus(state, action.id); return deleteByStatus(state, action.payload.statusId);
case TIMELINE_DISCONNECT: case disconnectTimeline.type:
return action.timeline === 'home' ? return action.payload.timeline === 'home' ?
state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state.update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
state; state;
case NOTIFICATIONS_MARK_AS_READ: case NOTIFICATIONS_MARK_AS_READ:
const lastNotification = state.get('items').find(item => item !== null); const lastNotification = state.get('items').find(item => item !== null);

View file

@ -4,8 +4,7 @@ import {
deployPictureInPictureAction, deployPictureInPictureAction,
removePictureInPicture, removePictureInPicture,
} from 'mastodon/actions/picture_in_picture'; } from 'mastodon/actions/picture_in_picture';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { TIMELINE_DELETE } from '../actions/timelines';
export interface PIPMediaProps { export interface PIPMediaProps {
src: string; src: string;
@ -49,8 +48,9 @@ export const pictureInPictureReducer: Reducer<PIPState> = (
...action.payload.props, ...action.payload.props,
}; };
else if (removePictureInPicture.match(action)) return initialState; else if (removePictureInPicture.match(action)) return initialState;
else if (action.type === TIMELINE_DELETE) else if (timelineDelete.match(action))
if (state.type && state.statusId === action.id) return initialState; if (state.type && state.statusId === action.payload.statusId)
return initialState;
return state; return state;
}; };

View file

@ -1,5 +1,7 @@
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { normalizeStatusTranslation } from '../actions/importer/normalizer'; import { normalizeStatusTranslation } from '../actions/importer/normalizer';
import { import {
@ -27,7 +29,6 @@ import {
STATUS_FETCH_REQUEST, STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL, STATUS_FETCH_FAIL,
} from '../actions/statuses'; } from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
const importStatus = (state, status) => state.set(status.id, fromJS(status)); const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -114,8 +115,8 @@ export default function statuses(state = initialState, action) {
}); });
case STATUS_COLLAPSE: case STATUS_COLLAPSE:
return state.setIn([action.id, 'collapsed'], action.isCollapsed); return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteStatus(state, action.id, action.references); return deleteStatus(state, action.payload.statusId, action.payload.references);
case STATUS_TRANSLATE_SUCCESS: case STATUS_TRANSLATE_SUCCESS:
return statusTranslateSuccess(state, action.id, action.translation); return statusTranslateSuccess(state, action.id, action.translation);
case STATUS_TRANSLATE_UNDO: case STATUS_TRANSLATE_UNDO:

View file

@ -1,5 +1,7 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import { import {
blockAccountSuccess, blockAccountSuccess,
muteAccountSuccess, muteAccountSuccess,
@ -7,19 +9,18 @@ import {
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
TIMELINE_UPDATE, TIMELINE_UPDATE,
TIMELINE_DELETE,
TIMELINE_CLEAR, TIMELINE_CLEAR,
TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_SUCCESS,
TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_REQUEST,
TIMELINE_EXPAND_FAIL, TIMELINE_EXPAND_FAIL,
TIMELINE_SCROLL_TOP, TIMELINE_SCROLL_TOP,
TIMELINE_CONNECT, TIMELINE_CONNECT,
TIMELINE_DISCONNECT,
TIMELINE_LOAD_PENDING, TIMELINE_LOAD_PENDING,
TIMELINE_MARK_AS_PARTIAL, TIMELINE_MARK_AS_PARTIAL,
TIMELINE_INSERT, TIMELINE_INSERT,
TIMELINE_GAP, TIMELINE_GAP,
TIMELINE_SUGGESTIONS, TIMELINE_SUGGESTIONS,
disconnectTimeline,
} from '../actions/timelines'; } from '../actions/timelines';
import { compareId } from '../compare_id'; import { compareId } from '../compare_id';
@ -158,7 +159,7 @@ const filterTimelines = (state, relationship, statuses) => {
return; return;
} }
references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')); references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')).valueSeq().toJSON();
state = deleteStatus(state, status.get('id'), references, relationship.id); state = deleteStatus(state, status.get('id'), references, relationship.id);
}); });
@ -201,8 +202,8 @@ export default function timelines(state = initialState, action) {
return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems);
case TIMELINE_UPDATE: case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
case TIMELINE_DELETE: case timelineDelete.type:
return deleteStatus(state, action.id, action.references, action.reblogOf); return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf);
case TIMELINE_CLEAR: case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline); return clearTimeline(state, action.timeline);
case blockAccountSuccess.type: case blockAccountSuccess.type:
@ -214,11 +215,11 @@ export default function timelines(state = initialState, action) {
return updateTop(state, action.timeline, action.top); return updateTop(state, action.timeline, action.top);
case TIMELINE_CONNECT: case TIMELINE_CONNECT:
return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems)); return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems));
case TIMELINE_DISCONNECT: case disconnectTimeline.type:
return state.update( return state.update(
action.timeline, action.payload.timeline,
initialTimeline, initialTimeline,
map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items), map => map.set('online', false).update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items),
); );
case TIMELINE_MARK_AS_PARTIAL: case TIMELINE_MARK_AS_PARTIAL:
return state.update( return state.update(

View file

@ -82,13 +82,19 @@ export function createThunk<Arg = void, Returned = void>(
const discardLoadDataInPayload = Symbol('discardLoadDataInPayload'); const discardLoadDataInPayload = Symbol('discardLoadDataInPayload');
type DiscardLoadData = typeof discardLoadDataInPayload; type DiscardLoadData = typeof discardLoadDataInPayload;
type OnData<LoadDataResult, ReturnedData> = ( type OnData<ActionArg, LoadDataResult, ReturnedData> = (
data: LoadDataResult, data: LoadDataResult,
api: AppThunkApi & { api: AppThunkApi & {
actionArg: ActionArg;
discardLoadData: DiscardLoadData; discardLoadData: DiscardLoadData;
}, },
) => ReturnedData | DiscardLoadData | Promise<ReturnedData | DiscardLoadData>; ) => ReturnedData | DiscardLoadData | Promise<ReturnedData | DiscardLoadData>;
type LoadData<Args, LoadDataResult> = (
args: Args,
api: AppThunkApi,
) => Promise<LoadDataResult>;
type ArgsType = Record<string, unknown> | undefined; type ArgsType = Record<string, unknown> | undefined;
// Overload when there is no `onData` method, the payload is the `onData` result // Overload when there is no `onData` method, the payload is the `onData` result
@ -101,18 +107,18 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>( export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: onDataOrThunkOptions?:
| AppThunkOptions | AppThunkOptions
| OnData<LoadDataResult, DiscardLoadData>, | OnData<Args, LoadDataResult, DiscardLoadData>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions,
): ReturnType<typeof createThunk<Args, void>>; ): ReturnType<typeof createThunk<Args, void>>;
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result // Overload when the `onData` method returns nothing, then the mayload is the `onData` result
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>( export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, void>, onDataOrThunkOptions?: AppThunkOptions | OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions,
): ReturnType<typeof createThunk<Args, LoadDataResult>>; ): ReturnType<typeof createThunk<Args, LoadDataResult>>;
@ -123,8 +129,10 @@ export function createDataLoadingThunk<
Returned, Returned,
>( >(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, Returned>, onDataOrThunkOptions?:
| AppThunkOptions
| OnData<Args, LoadDataResult, Returned>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions,
): ReturnType<typeof createThunk<Args, Returned>>; ): ReturnType<typeof createThunk<Args, Returned>>;
@ -159,11 +167,13 @@ export function createDataLoadingThunk<
Returned, Returned,
>( >(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<LoadDataResult, Returned>, onDataOrThunkOptions?:
| AppThunkOptions
| OnData<Args, LoadDataResult, Returned>,
maybeThunkOptions?: AppThunkOptions, maybeThunkOptions?: AppThunkOptions,
) { ) {
let onData: OnData<LoadDataResult, Returned> | undefined; let onData: OnData<Args, LoadDataResult, Returned> | undefined;
let thunkOptions: AppThunkOptions | undefined; let thunkOptions: AppThunkOptions | undefined;
if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions;
@ -177,7 +187,10 @@ export function createDataLoadingThunk<
return createThunk<Args, Returned>( return createThunk<Args, Returned>(
name, name,
async (arg, { getState, dispatch }) => { async (arg, { getState, dispatch }) => {
const data = await loadData(arg); const data = await loadData(arg, {
dispatch,
getState,
});
if (!onData) return data as Returned; if (!onData) return data as Returned;
@ -185,6 +198,7 @@ export function createDataLoadingThunk<
dispatch, dispatch,
getState, getState,
discardLoadData: discardLoadDataInPayload, discardLoadData: discardLoadDataInPayload,
actionArg: arg,
}); });
// if there is no return in `onData`, we return the `onData` result // if there is no return in `onData`, we return the `onData` result

View file

@ -3,13 +3,16 @@
class NotificationGroup < ActiveModelSerializers::Model class NotificationGroup < ActiveModelSerializers::Model
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id
def self.from_notification(notification) def self.from_notification(notification, max_id: nil)
if notification.group_key.present? if notification.group_key.present?
# TODO: caching and preloading # TODO: caching and preloading
most_recent_notifications = notification.account.notifications.where(group_key: notification.group_key).order(id: :desc).take(3) scope = notification.account.notifications.where(group_key: notification.group_key)
scope = scope.where(id: ..max_id) if max_id.present?
most_recent_notifications = scope.order(id: :desc).take(3)
most_recent_id = most_recent_notifications.first.id most_recent_id = most_recent_notifications.first.id
sample_accounts = most_recent_notifications.map(&:from_account) sample_accounts = most_recent_notifications.map(&:from_account)
notifications_count = notification.account.notifications.where(group_key: notification.group_key).count notifications_count = scope.count
else else
most_recent_id = notification.id most_recent_id = notification.id
sample_accounts = [notification.from_account] sample_accounts = [notification.from_account]
@ -29,5 +32,6 @@ class NotificationGroup < ActiveModelSerializers::Model
:target_status, :target_status,
:report, :report,
:account_relationship_severance_event, :account_relationship_severance_event,
:account_warning,
to: :notification, prefix: false to: :notification, prefix: false
end end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::NotificationSerializer < ActiveModel::Serializer class REST::NotificationSerializer < ActiveModel::Serializer
attributes :id, :type, :created_at attributes :id, :type, :created_at, :group_key
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer
@ -13,6 +13,10 @@ class REST::NotificationSerializer < ActiveModel::Serializer
object.id.to_s object.id.to_s
end end
def group_key
object.group_key || "ungrouped-#{object.id}"
end
def status_type? def status_type?
[:favourite, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type) [:favourite, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type)
end end

View file

@ -49,6 +49,7 @@ require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions' require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_extensions' require_relative '../lib/rails/engine_extensions'
require_relative '../lib/action_dispatch/remote_ip_extensions' require_relative '../lib/action_dispatch/remote_ip_extensions'
require_relative '../lib/stoplight/redis_data_store_extensions'
require_relative '../lib/active_record/database_tasks_extensions' require_relative '../lib/active_record/database_tasks_extensions'
require_relative '../lib/active_record/batches' require_relative '../lib/active_record/batches'
require_relative '../lib/active_record/with_recursive' require_relative '../lib/active_record/with_recursive'

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
# Restore compatibility with Redis < 6.2
module Stoplight
module DataStore
module RedisExtensions
def query_failures(light, transaction: @redis)
window_start = Time.now.to_i - light.window_size
transaction.zrevrangebyscore(failures_key(light), Float::INFINITY, window_start)
end
end
end
end
Stoplight::DataStore::Redis.prepend(Stoplight::DataStore::RedisExtensions)

View file

@ -6,3 +6,14 @@ RSpec::Matchers.define :match_json_schema do |schema|
JSON::Validator.validate(schema_path, input_json, validate_schema: true) JSON::Validator.validate(schema_path, input_json, validate_schema: true)
end end
end end
RSpec::Matchers.define :match_json_values do |values|
match do |string|
expect(json_str_to_hash(string))
.to include(values)
end
failure_message do |value|
"expected that #{value} would have the same values as #{values}."
end
end

View file

@ -16,7 +16,7 @@ describe ActivityPub::DistributePollUpdateWorker do
end end
it 'delivers to followers' do it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), account.id, 'http://example.com']]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), account.id, 'http://example.com']]) do
subject.perform(status.id) subject.perform(status.id)
end end
end end

View file

@ -19,7 +19,7 @@ describe ActivityPub::DistributionWorker do
end end
it 'delivers to followers' do it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything]]) do
subject.perform(status.id) subject.perform(status.id)
end end
end end
@ -31,7 +31,7 @@ describe ActivityPub::DistributionWorker do
end end
it 'delivers to followers' do it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'http://example.com', anything]]) do
subject.perform(status.id) subject.perform(status.id)
end end
end end
@ -46,7 +46,7 @@ describe ActivityPub::DistributionWorker do
end end
it 'delivers to mentioned accounts' do it 'delivers to mentioned accounts' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'https://foo.bar/inbox', anything]]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Create'), status.account.id, 'https://foo.bar/inbox', anything]]) do
subject.perform(status.id) subject.perform(status.id)
end end
end end

View file

@ -16,12 +16,16 @@ describe ActivityPub::MoveDistributionWorker do
end end
it 'delivers to followers and known blockers' do it 'delivers to followers and known blockers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [ expect_push_bulk_to_match(ActivityPub::DeliveryWorker, expected_migration_deliveries) do
[kind_of(String), migration.account.id, 'http://example.com'],
[kind_of(String), migration.account.id, 'http://example2.com'],
]) do
subject.perform(migration.id) subject.perform(migration.id)
end end
end end
def expected_migration_deliveries
[
[match_json_values(type: 'Move'), migration.account.id, 'http://example.com'],
[match_json_values(type: 'Move'), migration.account.id, 'http://example2.com'],
]
end
end end
end end

View file

@ -25,7 +25,7 @@ describe ActivityPub::StatusUpdateDistributionWorker do
end end
it 'delivers to followers' do it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
subject.perform(status.id) subject.perform(status.id)
end end
end end
@ -37,7 +37,7 @@ describe ActivityPub::StatusUpdateDistributionWorker do
end end
it 'delivers to followers' do it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
subject.perform(status.id) subject.perform(status.id)
end end
end end

View file

@ -14,7 +14,7 @@ describe ActivityPub::UpdateDistributionWorker do
end end
it 'delivers to followers' do it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), account.id, 'http://example.com', anything]]) do expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), account.id, 'http://example.com', anything]]) do
subject.perform(account.id) subject.perform(account.id)
end end
end end