Add added_to_collection and collection_updated notification types (#38491)
This commit is contained in:
parent
df64716b34
commit
39c70649ca
@ -3,6 +3,7 @@
|
||||
import type { AccountWarningAction } from 'mastodon/models/notification_group';
|
||||
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
import type { ApiCollectionJSON } from './collections';
|
||||
import type { ApiReportJSON } from './reports';
|
||||
import type { ApiStatusJSON } from './statuses';
|
||||
|
||||
@ -22,6 +23,8 @@ export const allNotificationTypes: NotificationType[] = [
|
||||
'moderation_warning',
|
||||
'severed_relationships',
|
||||
'annual_report',
|
||||
'added_to_collection',
|
||||
'collection_update',
|
||||
];
|
||||
|
||||
export type NotificationWithStatusType =
|
||||
@ -42,7 +45,9 @@ export type NotificationType =
|
||||
| 'severed_relationships'
|
||||
| 'admin.sign_up'
|
||||
| 'admin.report'
|
||||
| 'annual_report';
|
||||
| 'annual_report'
|
||||
| 'added_to_collection'
|
||||
| 'collection_update';
|
||||
|
||||
export interface BaseNotificationJSON {
|
||||
id: string;
|
||||
@ -83,6 +88,26 @@ interface ReportNotificationJSON extends BaseNotificationJSON {
|
||||
report: ApiReportJSON;
|
||||
}
|
||||
|
||||
interface AddedToCollectionNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'added_to_collection';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
interface AddedToCollectionNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'added_to_collection';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
interface CollectionUpdateNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'collection_update';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
interface CollectionUpdateNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'collection_update';
|
||||
collection: ApiCollectionJSON;
|
||||
}
|
||||
|
||||
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
|
||||
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: SimpleNotificationTypes;
|
||||
@ -146,7 +171,9 @@ export type ApiNotificationJSON =
|
||||
| ReportNotificationJSON
|
||||
| AccountRelationshipSeveranceNotificationJSON
|
||||
| NotificationWithStatusJSON
|
||||
| ModerationWarningNotificationJSON;
|
||||
| ModerationWarningNotificationJSON
|
||||
| AddedToCollectionNotificationJSON
|
||||
| CollectionUpdateNotificationJSON;
|
||||
|
||||
export type ApiNotificationGroupJSON =
|
||||
| SimpleNotificationGroupJSON
|
||||
@ -154,7 +181,9 @@ export type ApiNotificationGroupJSON =
|
||||
| AccountRelationshipSeveranceNotificationGroupJSON
|
||||
| NotificationGroupWithStatusJSON
|
||||
| ModerationWarningNotificationGroupJSON
|
||||
| AnnualReportNotificationGroupJSON;
|
||||
| AnnualReportNotificationGroupJSON
|
||||
| AddedToCollectionNotificationGroupJSON
|
||||
| CollectionUpdateNotificationGroupJSON;
|
||||
|
||||
export interface ApiNotificationGroupsResultJSON {
|
||||
accounts: ApiAccountJSON[];
|
||||
|
||||
@ -10,6 +10,8 @@ import type {
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import type { ApiReportJSON } from 'mastodon/api_types/reports';
|
||||
|
||||
import type { ApiCollectionJSON } from '../api_types/collections';
|
||||
|
||||
// Maximum number of avatars displayed in a notification group
|
||||
// This corresponds to the max length of `group.sampleAccountIds`
|
||||
export const NOTIFICATIONS_GROUP_MAX_AVATARS = 8;
|
||||
@ -87,6 +89,15 @@ export interface NotificationGroupAdminReport extends BaseNotification<'admin.re
|
||||
report: Report;
|
||||
}
|
||||
|
||||
type Collection = ApiCollectionJSON;
|
||||
export interface NotificationGroupAddedToCollection extends BaseNotification<'added_to_collection'> {
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
export interface NotificationGroupCollectionUpdate extends BaseNotification<'collection_update'> {
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
export type NotificationGroup =
|
||||
| NotificationGroupFavourite
|
||||
| NotificationGroupReblog
|
||||
@ -102,7 +113,9 @@ export type NotificationGroup =
|
||||
| NotificationGroupSeveredRelationships
|
||||
| NotificationGroupAdminSignUp
|
||||
| NotificationGroupAdminReport
|
||||
| NotificationGroupAnnualReport;
|
||||
| NotificationGroupAnnualReport
|
||||
| NotificationGroupAddedToCollection
|
||||
| NotificationGroupCollectionUpdate;
|
||||
|
||||
function createReportFromJSON(reportJSON: ApiReportJSON): Report {
|
||||
const { target_account, ...report } = reportJSON;
|
||||
@ -249,6 +262,13 @@ export function createNotificationGroupFromNotificationJSON(
|
||||
notification.moderation_warning,
|
||||
),
|
||||
};
|
||||
case 'added_to_collection':
|
||||
case 'collection_update':
|
||||
return {
|
||||
...group,
|
||||
type: notification.type,
|
||||
collection: notification.collection,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...group,
|
||||
|
||||
@ -26,6 +26,7 @@ class ActivityPub::Activity::FeatureRequest < ActivityPub::Activity
|
||||
collection_item_attributes(:accepted)
|
||||
)
|
||||
|
||||
notify_local_user!(collection_item)
|
||||
queue_delivery!(collection_item, ActivityPub::AcceptFeatureRequestSerializer)
|
||||
end
|
||||
|
||||
@ -52,6 +53,10 @@ class ActivityPub::Activity::FeatureRequest < ActivityPub::Activity
|
||||
{ account: @featured_account, activity_uri: @json['id'], state: }
|
||||
end
|
||||
|
||||
def notify_local_user!(collection_item)
|
||||
LocalNotificationWorker.perform_async(collection_item.account_id, collection_item.id, collection_item.class.name, 'added_to_collection')
|
||||
end
|
||||
|
||||
def queue_delivery!(collection_item, serializer)
|
||||
json = JSON.generate(serialize_payload(collection_item, serializer))
|
||||
ActivityPub::DeliveryWorker.perform_async(json, @featured_account.id, @account.inbox_url)
|
||||
|
||||
@ -33,7 +33,7 @@ class Notification < ApplicationRecord
|
||||
'Quote' => :quote,
|
||||
}.freeze
|
||||
|
||||
# Please update app/javascript/api_types/notification.ts if you change this
|
||||
# Please update app/javascript/mastodon/api_types/notifications.ts if you change this
|
||||
PROPERTIES = {
|
||||
mention: {
|
||||
filterable: true,
|
||||
@ -80,6 +80,12 @@ class Notification < ApplicationRecord
|
||||
quoted_update: {
|
||||
filterable: false,
|
||||
}.freeze,
|
||||
added_to_collection: {
|
||||
filterable: true,
|
||||
}.freeze,
|
||||
collection_update: {
|
||||
filterable: false,
|
||||
},
|
||||
}.freeze
|
||||
|
||||
TYPES = PROPERTIES.keys.freeze
|
||||
@ -112,6 +118,8 @@ class Notification < ApplicationRecord
|
||||
belongs_to :account_warning, inverse_of: false
|
||||
belongs_to :generated_annual_report, inverse_of: false
|
||||
belongs_to :quote, inverse_of: :notification
|
||||
belongs_to :collection_item, inverse_of: false # TODO: have an inverse?
|
||||
belongs_to :collection, inverse_of: false # TODO: have an inverse?
|
||||
end
|
||||
|
||||
validates :type, inclusion: { in: TYPES }
|
||||
@ -208,8 +216,10 @@ class Notification < ApplicationRecord
|
||||
case activity_type
|
||||
when 'Status'
|
||||
self.from_account_id = type == :quoted_update ? activity&.quote&.quoted_account_id : activity&.account_id
|
||||
when 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report', 'Quote'
|
||||
when 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report', 'Quote', 'Collection'
|
||||
self.from_account_id = activity&.account_id
|
||||
when 'CollectionItem'
|
||||
self.from_account_id = activity&.collection&.account_id
|
||||
when 'Mention'
|
||||
self.from_account_id = activity&.status&.account_id
|
||||
when 'Account'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::NotificationGroupSerializer < ActiveModel::Serializer
|
||||
# Please update app/javascript/api_types/notification.ts when making changes to the attributes
|
||||
# Please update app/javascript/mastodon/api_types/notifications.ts when making changes to the attributes
|
||||
attributes :group_key, :notifications_count, :type, :most_recent_notification_id
|
||||
|
||||
attribute :page_min_id, if: :paginated?
|
||||
@ -14,6 +14,7 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer
|
||||
belongs_to :account_relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer
|
||||
belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer
|
||||
belongs_to :generated_annual_report, key: :annual_report, if: :annual_report_event?, serializer: REST::AnnualReportEventSerializer
|
||||
belongs_to :collection, if: :collection_type?, serializer: REST::CollectionSerializer
|
||||
|
||||
def sample_account_ids
|
||||
object.sample_accounts.pluck(:id).map(&:to_s)
|
||||
@ -27,6 +28,10 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer
|
||||
[:favourite, :reblog, :status, :mention, :poll, :update, :quote, :quoted_update].include?(object.type)
|
||||
end
|
||||
|
||||
def collection_type?
|
||||
[:added_to_collection, :collection_update].include?(object.type)
|
||||
end
|
||||
|
||||
def report_type?
|
||||
object.type == :'admin.report'
|
||||
end
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::NotificationSerializer < ActiveModel::Serializer
|
||||
# Please update app/javascript/api_types/notification.ts when making changes to the attributes
|
||||
# Please update app/javascript/mastodon/api_types/notifications.ts when making changes to the attributes
|
||||
attributes :id, :type, :created_at, :group_key
|
||||
|
||||
attribute :filtered, if: :filtered?
|
||||
@ -11,6 +11,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||
belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer
|
||||
belongs_to :account_relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer
|
||||
belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer
|
||||
belongs_to :collection, if: :collection_type?, serializer: REST::CollectionSerializer
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
@ -24,6 +25,10 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||
[:favourite, :reblog, :status, :mention, :poll, :update, :quoted_update, :quote].include?(object.type)
|
||||
end
|
||||
|
||||
def collection_type?
|
||||
[:added_to_collection, :collection_update].include?(object.type)
|
||||
end
|
||||
|
||||
def report_type?
|
||||
object.type == :'admin.report'
|
||||
end
|
||||
|
||||
@ -25,6 +25,7 @@ class ActivityPub::ProcessFeaturedCollectionService
|
||||
end
|
||||
|
||||
process_items!
|
||||
notify_about_update!
|
||||
|
||||
@collection
|
||||
end
|
||||
@ -32,6 +33,10 @@ class ActivityPub::ProcessFeaturedCollectionService
|
||||
|
||||
private
|
||||
|
||||
def notify_about_update!
|
||||
NotifyOfCollectionUpdateService.new.call(@collection)
|
||||
end
|
||||
|
||||
def truncated_summary
|
||||
text = @json['summaryMap']&.values&.first || @json['summary'] || ''
|
||||
text[0, Collection::DESCRIPTION_LENGTH_HARD_LIMIT]
|
||||
|
||||
@ -11,6 +11,7 @@ class AddAccountToCollectionService
|
||||
|
||||
@collection_item = create_collection_item
|
||||
|
||||
notify_local_user if @account.local?
|
||||
distribute_add_activity if @account.local?
|
||||
distribute_feature_request_activity if @account.remote?
|
||||
|
||||
@ -24,6 +25,10 @@ class AddAccountToCollectionService
|
||||
@collection.collection_items.create!(account: @account, state:)
|
||||
end
|
||||
|
||||
def notify_local_user
|
||||
LocalNotificationWorker.perform_async(@account.id, @collection_item.id, @collection_item.class.name, 'added_to_collection')
|
||||
end
|
||||
|
||||
def distribute_add_activity
|
||||
ActivityPub::AccountRawDistributionWorker.perform_async(add_activity_json, @collection.account_id)
|
||||
end
|
||||
|
||||
21
app/services/notify_of_collection_update_service.rb
Normal file
21
app/services/notify_of_collection_update_service.rb
Normal file
@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class NotifyOfCollectionUpdateService
|
||||
def call(collection)
|
||||
return unless significantly_changed?(collection)
|
||||
|
||||
collection.collection_items.includes(:account).references(:account).merge(Account.local).accepted.find_each do |collection_item|
|
||||
LocalNotificationWorker.perform_async(collection_item.account_id, collection.id, collection.class.name, 'collection_update')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def significantly_changed?(collection)
|
||||
# If the collection is brand new, we don't need to look at its members
|
||||
return false if collection.previously_new_record?
|
||||
|
||||
# Only notify of change to description or name
|
||||
%i(description description_html name).any? { |attr| collection.attribute_previously_changed?(attr) }
|
||||
end
|
||||
end
|
||||
@ -14,6 +14,8 @@ class NotifyService < BaseService
|
||||
moderation_warning
|
||||
severed_relationships
|
||||
annual_report
|
||||
added_to_collection
|
||||
collection_update
|
||||
).freeze
|
||||
|
||||
class BaseCondition
|
||||
@ -21,15 +23,6 @@ class NotifyService < BaseService
|
||||
|
||||
NEW_FOLLOWER_THRESHOLD = 3.days.freeze
|
||||
|
||||
NON_FILTERABLE_TYPES = %i(
|
||||
admin.sign_up
|
||||
admin.report
|
||||
poll
|
||||
update
|
||||
account_warning
|
||||
annual_report
|
||||
).freeze
|
||||
|
||||
def initialize(notification, **options)
|
||||
@recipient = notification.account
|
||||
@sender = notification.from_account
|
||||
|
||||
@ -7,6 +7,7 @@ class UpdateCollectionService
|
||||
@collection = collection
|
||||
@collection.update!(params)
|
||||
|
||||
notify_about_update
|
||||
distribute_update_activity
|
||||
end
|
||||
|
||||
@ -18,6 +19,10 @@ class UpdateCollectionService
|
||||
ActivityPub::AccountRawDistributionWorker.perform_async(activity_json, @collection.account.id)
|
||||
end
|
||||
|
||||
def notify_about_update
|
||||
NotifyOfCollectionUpdateService.new.call(@collection)
|
||||
end
|
||||
|
||||
def activity_json
|
||||
ActiveModelSerializers::SerializableResource.new(@collection, serializer: ActivityPub::UpdateFeaturedCollectionSerializer, adapter: ActivityPub::Adapter).to_json
|
||||
end
|
||||
|
||||
@ -10,10 +10,8 @@ class LocalNotificationWorker
|
||||
# For most notification types, only one notification should exist, and the older one is
|
||||
# preferred. For updates, such as when a status is edited, the new notification
|
||||
# should replace the previous ones.
|
||||
if type == 'update'
|
||||
Notification.where(account: receiver, activity: activity, type: 'update').in_batches.delete_all
|
||||
elsif type == 'quoted_update'
|
||||
Notification.where(account: receiver, activity: activity, type: 'quoted_update').in_batches.delete_all
|
||||
if %w(update quoted_update collection_update).include?(type)
|
||||
Notification.where(account: receiver, activity: activity, type: type).in_batches.delete_all
|
||||
elsif Notification.where(account: receiver, activity: activity, type: type).any?
|
||||
return
|
||||
end
|
||||
|
||||
@ -26,9 +26,10 @@ RSpec.describe ActivityPub::Activity::FeatureRequest do
|
||||
context 'when recipient is discoverable' do
|
||||
let(:discoverable) { true }
|
||||
|
||||
it 'schedules a job to send an `Accept` activity' do
|
||||
it 'schedules a job to send an `Accept` activity as well as a notification worker' do
|
||||
expect { subject.perform }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker)
|
||||
.to enqueue_sidekiq_job(LocalNotificationWorker).with(recipient.id, anything, 'CollectionItem', 'added_to_collection')
|
||||
.and enqueue_sidekiq_job(ActivityPub::DeliveryWorker)
|
||||
.with(satisfying do |body|
|
||||
response_json = JSON.parse(body)
|
||||
response_json['type'] == 'Accept' &&
|
||||
|
||||
@ -259,6 +259,9 @@ RSpec.describe ActivityPub::Activity::Update do
|
||||
|
||||
context 'with a `FeaturedCollection` object', feature: :collections do
|
||||
let(:collection) { Fabricate(:remote_collection, account: sender, name: 'old name', discoverable: false) }
|
||||
let(:account) { Fabricate(:account) }
|
||||
let!(:collection_item) { Fabricate(:collection_item, account:, collection:, uri: 'https://example.com/featured_stamps/1') }
|
||||
|
||||
let(:featured_collection_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
@ -267,13 +270,21 @@ RSpec.describe ActivityPub::Activity::Update do
|
||||
'attributedTo' => sender.uri,
|
||||
'name' => 'Cool people',
|
||||
'summary' => 'People you should follow.',
|
||||
'totalItems' => 0,
|
||||
'totalItems' => 1,
|
||||
'sensitive' => false,
|
||||
'discoverable' => true,
|
||||
'published' => '2026-03-09T15:19:25Z',
|
||||
'updated' => Time.zone.now.iso8601,
|
||||
'orderedItems' => [
|
||||
{
|
||||
'type' => 'FeaturedItem',
|
||||
'id' => ActivityPub::TagManager.instance.uri_for(collection_item),
|
||||
'object' => ActivityPub::TagManager.instance.uri_for(account),
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
@ -282,18 +293,41 @@ RSpec.describe ActivityPub::Activity::Update do
|
||||
'object' => featured_collection_json,
|
||||
}
|
||||
end
|
||||
let(:stubbed_service) do
|
||||
instance_double(ActivityPub::ProcessFeaturedCollectionService, call: true)
|
||||
|
||||
it 'updates the collection and notifies local user' do
|
||||
expect { subject.perform }
|
||||
.to change { collection.reload.name }.to(featured_collection_json['name'])
|
||||
.and enqueue_sidekiq_job(LocalNotificationWorker).with(account.id, collection.id, 'Collection', 'collection_update')
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ActivityPub::ProcessFeaturedCollectionService).to receive(:new).and_return(stubbed_service)
|
||||
end
|
||||
context 'when the metadata does not actually change' do
|
||||
let(:featured_collection_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => collection.uri,
|
||||
'type' => 'FeaturedCollection',
|
||||
'attributedTo' => sender.uri,
|
||||
'name' => collection.name,
|
||||
'summary' => collection.description_html,
|
||||
'totalItems' => 1,
|
||||
'sensitive' => false,
|
||||
'discoverable' => true,
|
||||
'published' => '2026-03-09T15:19:25Z',
|
||||
'updated' => Time.zone.now.iso8601,
|
||||
'orderedItems' => [
|
||||
{
|
||||
'type' => 'FeaturedItem',
|
||||
'id' => ActivityPub::TagManager.instance.uri_for(collection_item),
|
||||
'object' => ActivityPub::TagManager.instance.uri_for(account),
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the collection' do
|
||||
subject.perform
|
||||
|
||||
expect(stubbed_service).to have_received(:call).with(sender, featured_collection_json)
|
||||
it 'does not notify the local user' do
|
||||
expect { subject.perform }
|
||||
.to_not enqueue_sidekiq_job(LocalNotificationWorker)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -22,10 +22,15 @@ RSpec.describe AddAccountToCollectionService do
|
||||
end
|
||||
|
||||
context 'when the account is local' do
|
||||
it 'federates an `Add` activity' do
|
||||
it 'federates an `Add` activity and schedules a notification' do
|
||||
subject.call(collection, account)
|
||||
|
||||
expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job
|
||||
expect(ActivityPub::AccountRawDistributionWorker)
|
||||
.to have_enqueued_sidekiq_job
|
||||
.with(anything, collection.account_id)
|
||||
expect(LocalNotificationWorker)
|
||||
.to have_enqueued_sidekiq_job
|
||||
.with(account.id, anything, 'CollectionItem', 'added_to_collection')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -6,21 +6,15 @@ RSpec.describe UpdateCollectionService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
let!(:collection_item) { Fabricate(:collection_item, collection:) }
|
||||
|
||||
describe '#call' do
|
||||
context 'when given valid parameters' do
|
||||
it 'updates the collection' do
|
||||
subject.call(collection, { name: 'Newly updated name' })
|
||||
|
||||
expect(collection.name).to eq 'Newly updated name'
|
||||
end
|
||||
|
||||
context 'when something actually changed' do
|
||||
it 'federates an `Update` activity' do
|
||||
subject.call(collection, { name: 'updated' })
|
||||
|
||||
expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job
|
||||
end
|
||||
it 'updates the collection, sends a notification and federates an `Update` activity' do
|
||||
expect { subject.call(collection, { name: 'Newly updated name' }) }
|
||||
.to change(collection, :name).to('Newly updated name')
|
||||
.and enqueue_sidekiq_job(LocalNotificationWorker).with(collection_item.account_id, collection.id, collection.class.name, 'collection_update')
|
||||
.and enqueue_sidekiq_job(ActivityPub::AccountRawDistributionWorker)
|
||||
end
|
||||
|
||||
context 'when nothing changed' do
|
||||
@ -28,6 +22,7 @@ RSpec.describe UpdateCollectionService do
|
||||
subject.call(collection, { name: collection.name })
|
||||
|
||||
expect(ActivityPub::AccountRawDistributionWorker).to_not have_enqueued_sidekiq_job
|
||||
expect(LocalNotificationWorker).to_not have_enqueued_sidekiq_job
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user