Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/views/admin/settings/appearance/show.html.haml`: Upstream enforced an uniform code style around lambdas, and glitch-soc had a different lambda due to its theming system. Applied the same code style changes. - `app/views/settings/preferences/appearance/show.html.haml`: Upstream enforced an uniform code style around lambdas, and glitch-soc removed some code just after the lambda. Applied the same code style changes.
This commit is contained in:
commit
c04f2d0cf7
|
@ -23,5 +23,5 @@ jobs:
|
||||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
commentOnClean: This pull request has resolved merge conflicts and is ready for review.
|
commentOnClean: This pull request has resolved merge conflicts and is ready for review.
|
||||||
commentOnDirty: This pull request has merge conflicts that must be resolved before it can be merged.
|
commentOnDirty: This pull request has merge conflicts that must be resolved before it can be merged.
|
||||||
retryMax: 10
|
retryMax: 30
|
||||||
continueOnMissingPermissions: false
|
continueOnMissingPermissions: false
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `haml-lint --auto-gen-config`
|
# `haml-lint --auto-gen-config`
|
||||||
# on 2023-07-11 23:58:05 +0200 using Haml-Lint version 0.48.0.
|
# on 2023-07-17 12:00:21 -0400 using Haml-Lint version 0.48.0.
|
||||||
# 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 lints are removed from the code base.
|
# one by one as the lints 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
|
||||||
# versions of Haml-Lint, may require this file to be generated again.
|
# versions of Haml-Lint, may require this file to be generated again.
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# Offense count: 94
|
# Offense count: 959
|
||||||
RuboCop:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# Offense count: 960
|
|
||||||
LineLength:
|
LineLength:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
@ -19,6 +15,10 @@ linters:
|
||||||
UnnecessaryStringOutput:
|
UnnecessaryStringOutput:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# Offense count: 67
|
||||||
|
RuboCop:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
# Offense count: 3
|
# Offense count: 3
|
||||||
ViewLength:
|
ViewLength:
|
||||||
exclude:
|
exclude:
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Api::V1::TagsController < Api::BaseController
|
||||||
|
|
||||||
def unfollow
|
def unfollow
|
||||||
TagFollow.find_by(account: current_account, tag: @tag)&.destroy!
|
TagFollow.find_by(account: current_account, tag: @tag)&.destroy!
|
||||||
|
TagUnmergeWorker.perform_async(@tag.id, current_account.id)
|
||||||
render json: @tag, serializer: REST::TagSerializer
|
render json: @tag, serializer: REST::TagSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,7 @@ export default class ColumnBackButton extends PureComponent {
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
// Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
|
} else if (router.history.location?.state?.fromMastodon) {
|
||||||
// When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
|
|
||||||
} else if (router.route.location.key) {
|
|
||||||
router.history.goBack();
|
router.history.goBack();
|
||||||
} else {
|
} else {
|
||||||
router.history.push('/');
|
router.history.push('/');
|
||||||
|
|
|
@ -63,10 +63,12 @@ class ColumnHeader extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleBackClick = () => {
|
handleBackClick = () => {
|
||||||
if (window.history && window.history.state) {
|
const { router } = this.context;
|
||||||
this.context.router.history.goBack();
|
|
||||||
|
if (router.history.location?.state?.fromMastodon) {
|
||||||
|
router.history.goBack();
|
||||||
} else {
|
} else {
|
||||||
this.context.router.history.push('/');
|
router.history.push('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,6 +85,7 @@ class ColumnHeader extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { router } = this.context;
|
||||||
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
|
const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props;
|
||||||
const { collapsed, animating } = this.state;
|
const { collapsed, animating } = this.state;
|
||||||
|
|
||||||
|
@ -126,7 +129,7 @@ class ColumnHeader extends PureComponent {
|
||||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pinned && (multiColumn || showBackButton)) {
|
if (!pinned && ((multiColumn && router.history.location?.state?.fromMastodon) || showBackButton)) {
|
||||||
backButton = (
|
backButton = (
|
||||||
<button onClick={this.handleBackClick} className='column-header__back-button'>
|
<button onClick={this.handleBackClick} className='column-header__back-button'>
|
||||||
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { History } from 'history';
|
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import { Router as OriginalRouter } from 'react-router';
|
import { Router as OriginalRouter } from 'react-router';
|
||||||
|
|
||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
|
|
||||||
const browserHistory = createBrowserHistory();
|
interface MastodonLocationState {
|
||||||
const originalPush = browserHistory.push.bind(browserHistory);
|
fromMastodon?: boolean;
|
||||||
|
mastodonModalKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserHistory = createBrowserHistory<
|
||||||
|
MastodonLocationState | undefined
|
||||||
|
>();
|
||||||
|
const originalPush = browserHistory.push.bind(browserHistory);
|
||||||
|
const originalReplace = browserHistory.replace.bind(browserHistory);
|
||||||
|
|
||||||
|
browserHistory.push = (path: string, state?: MastodonLocationState) => {
|
||||||
|
state = state ?? {};
|
||||||
|
state.fromMastodon = true;
|
||||||
|
|
||||||
browserHistory.push = (path: string, state: History.LocationState) => {
|
|
||||||
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
||||||
originalPush(`/deck${path}`, state);
|
originalPush(`/deck${path}`, state);
|
||||||
} else {
|
} else {
|
||||||
|
@ -18,6 +28,19 @@ browserHistory.push = (path: string, state: History.LocationState) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
browserHistory.replace = (path: string, state?: MastodonLocationState) => {
|
||||||
|
if (browserHistory.location.state?.fromMastodon) {
|
||||||
|
state = state ?? {};
|
||||||
|
state.fromMastodon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
||||||
|
originalReplace(`/deck${path}`, state);
|
||||||
|
} else {
|
||||||
|
originalReplace(path, state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
|
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -479,10 +479,12 @@ class UI extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyBack = () => {
|
handleHotkeyBack = () => {
|
||||||
if (window.history && window.history.state) {
|
const { router } = this.context;
|
||||||
this.context.router.history.goBack();
|
|
||||||
|
if (router.history.location?.state?.fromMastodon) {
|
||||||
|
router.history.goBack();
|
||||||
} else {
|
} else {
|
||||||
this.context.router.history.push('/');
|
router.history.push('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -205,6 +205,26 @@ class FeedManager
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Remove a tag's statuses from a home feed
|
||||||
|
# @param [Tag] from_tag
|
||||||
|
# @param [Account] into_account
|
||||||
|
# @return [void]
|
||||||
|
def unmerge_tag_from_home(from_tag, into_account)
|
||||||
|
timeline_key = key(:home, into_account.id)
|
||||||
|
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
|
||||||
|
|
||||||
|
# This is a bit tricky because we need posts tagged with this hashtag that are not
|
||||||
|
# also tagged with another followed hashtag or from a followed user
|
||||||
|
scope = from_tag.statuses
|
||||||
|
.where(id: timeline_status_ids)
|
||||||
|
.where.not(account: into_account.following)
|
||||||
|
.tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id))
|
||||||
|
|
||||||
|
scope.select('id, reblog_of_id').reorder(nil).find_each do |status|
|
||||||
|
remove_from_feed(:home, into_account.id, status, aggregate_reblogs: into_account.user&.aggregates_reblogs?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Clear all statuses from or mentioning target_account from a home feed
|
# Clear all statuses from or mentioning target_account from a home feed
|
||||||
# @param [Account] account
|
# @param [Account] account
|
||||||
# @param [Account] target_account
|
# @param [Account] target_account
|
||||||
|
|
|
@ -8,6 +8,8 @@ class ActivityPub::ProcessCollectionService < BaseService
|
||||||
@json = original_json = Oj.load(body, mode: :strict)
|
@json = original_json = Oj.load(body, mode: :strict)
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
|
return unless @json.is_a?(Hash)
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@json = compact(@json) if @json['signature'].is_a?(Hash)
|
@json = compact(@json) if @json['signature'].is_a?(Hash)
|
||||||
rescue JSON::LD::JsonLdError => e
|
rescue JSON::LD::JsonLdError => e
|
||||||
|
|
|
@ -10,4 +10,4 @@
|
||||||
= opengraph 'og:image:width', '400'
|
= opengraph 'og:image:width', '400'
|
||||||
= opengraph 'og:image:height', '400'
|
= opengraph 'og:image:height', '400'
|
||||||
= opengraph 'twitter:card', 'summary'
|
= opengraph 'twitter:card', 'summary'
|
||||||
= opengraph 'profile:username', acct(account)[1..-1]
|
= opengraph 'profile:username', acct(account)[1..]
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
= f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('admin.domain_blocks.new.hint'), required: true, readonly: true, disabled: true
|
= f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('admin.domain_blocks.new.hint'), required: true, readonly: true, disabled: true
|
||||||
|
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t("admin.domain_blocks.new.severity.#{type}") }, hint: t('admin.domain_blocks.new.severity.desc_html')
|
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: ->(type) { t("admin.domain_blocks.new.severity.#{type}") }, hint: t('admin.domain_blocks.new.severity.desc_html')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
|
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
= f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('.hint'), required: true
|
= f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('.hint'), required: true
|
||||||
|
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t(".severity.#{type}") }, hint: t('.severity.desc_html')
|
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: ->(type) { t(".severity.#{type}") }, hint: t('.severity.desc_html')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
|
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
= f.input :ip, as: :string, wrapper: :with_block_label, input_html: { placeholder: '192.0.2.0/24' }
|
= f.input :ip, as: :string, wrapper: :with_block_label, input_html: { placeholder: '192.0.2.0/24' }
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :expires_in, wrapper: :with_block_label, collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), label_method: lambda { |i| I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
= f.input :expires_in, wrapper: :with_block_label, collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), label_method: ->(i) { I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :severity, as: :radio_buttons, collection: IpBlock.severities.keys, include_blank: false, wrapper: :with_block_label, label_method: lambda { |severity| safe_join([I18n.t("simple_form.labels.ip_block.severities.#{severity}"), content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint')]) }
|
= f.input :severity, as: :radio_buttons, collection: IpBlock.severities.keys, include_blank: false, wrapper: :with_block_label, label_method: ->(severity) { safe_join([I18n.t("simple_form.labels.ip_block.severities.#{severity}"), content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint')]) }
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :comment, as: :string, wrapper: :with_block_label
|
= f.input :comment, as: :string, wrapper: :with_block_label
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
- (@role.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
|
- (@role.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
|
||||||
%h4= t(category, scope: 'admin.roles.categories')
|
%h4= t(category, scope: 'admin.roles.categories')
|
||||||
|
|
||||||
= f.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: lambda { |privilege| safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false, disabled: permissions.filter { |privilege| UserRole::FLAGS[privilege] & current_user.role.computed_permissions == 0 }
|
= f.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: ->(privilege) { safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false, disabled: permissions.filter { |privilege| UserRole::FLAGS[privilege] & current_user.role.computed_permissions == 0 }
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
= f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label_method: ->(value) { t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
= f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label_method: ->(value) { t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :status_page_url, wrapper: :with_block_label, input_html: { placeholder: "https://status.#{Rails.configuration.x.local_domain}" }
|
= f.input :status_page_url, wrapper: :with_block_label, input_html: { placeholder: "https://status.#{Rails.configuration.x.local_domain}" }
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
%p.lead= t('admin.settings.appearance.preamble')
|
%p.lead= t('admin.settings.appearance.preamble')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :flavour_and_skin, collection: Themes.instance.flavours_and_skins, group_label_method: lambda { |(flavour, _)| I18n.t("flavours.#{flavour}.name", default: flavour) }, wrapper: :with_label, label: t('admin.settings.flavour_and_skin.title'), include_blank: false, as: :grouped_select, label_method: :last, value_method: lambda { |value| value.join('/') }, group_method: :last
|
= f.input :flavour_and_skin, collection: Themes.instance.flavours_and_skins, group_label_method: -> (flavour_and_skin) { I18n.t("flavours.#{flavour_and_skin}.name", default: flavour_and_skin) }, wrapper: :with_label, label: t('admin.settings.flavour_and_skin.title'), include_blank: false, as: :grouped_select, label_method: :last, value_method: lambda { |value| value.join('/') }, group_method: :last
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
|
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: lambda { |mode| I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
|
= f.input :registrations_mode, collection: %w(open approved none), wrapper: :with_label, include_blank: false, label_method: ->(mode) { I18n.t("admin.settings.registrations_mode.modes.#{mode}") }
|
||||||
|
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations?
|
= f.input :require_invite_text, as: :boolean, wrapper: :with_label, disabled: !approved_registrations?
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :title, as: :string, wrapper: :with_label, hint: false
|
= f.input :title, as: :string, wrapper: :with_label, hint: false
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
|
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: ->(i) { I18n.t("invites.expires_in.#{i}") }, include_blank: I18n.t('invites.expires_in_prompt')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: lambda { |context| I18n.t("filters.contexts.#{context}") }, include_blank: false
|
= f.input :context, wrapper: :with_block_label, collection: CustomFilter::VALID_CONTEXTS, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label_method: ->(context) { I18n.t("filters.contexts.#{context}") }, include_blank: false
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: lambda { |num| I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt')
|
= f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: ->(num) { I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt')
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
= f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].map(&:to_i), label_method: ->(i) { I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :autofollow, wrapper: :with_label
|
= f.input :autofollow, wrapper: :with_label
|
||||||
|
|
|
@ -15,4 +15,4 @@
|
||||||
%span.hint= t('simple_form.hints.defaults.scopes')
|
%span.hint= t('simple_form.hints.defaults.scopes')
|
||||||
|
|
||||||
- Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each do |k, v|
|
- Doorkeeper.configuration.scopes.group_by { |s| s.split(':').first }.each do |k, v|
|
||||||
= f.input :scopes, label: false, hint: false, collection: v.sort, wrapper: :with_block_label, include_blank: false, label_method: lambda { |scope| safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, selected: f.object.scopes.all, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
= f.input :scopes, label: false, hint: false, collection: v.sort, wrapper: :with_block_label, include_blank: false, label_method: ->(scope) { safe_join([content_tag(:samp, scope, class: class_for_scope(scope)), content_tag(:span, t("doorkeeper.scopes.#{scope}"), class: 'hint')]) }, selected: f.object.scopes.all, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t("imports.titles.#{@bulk_import.type.to_s}")
|
= t("imports.titles.#{@bulk_import.type}")
|
||||||
|
|
||||||
- if @bulk_import.likely_mismatched?
|
- if @bulk_import.likely_mismatched?
|
||||||
.flash-message.warning= t("imports.mismatched_types_warning")
|
.flash-message.warning= t('imports.mismatched_types_warning')
|
||||||
|
|
||||||
- if @bulk_import.overwrite?
|
- if @bulk_import.overwrite?
|
||||||
%p.hint= t("imports.overwrite_preambles.#{@bulk_import.type.to_s}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items)
|
%p.hint= t("imports.overwrite_preambles.#{@bulk_import.type}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items)
|
||||||
- else
|
- else
|
||||||
%p.hint= t("imports.preambles.#{@bulk_import.type.to_s}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items)
|
%p.hint= t("imports.preambles.#{@bulk_import.type}_html", filename: @bulk_import.original_filename, total_items: @bulk_import.total_items)
|
||||||
|
|
||||||
.simple_form
|
.simple_form
|
||||||
.actions
|
.actions
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put, id: 'edit_user' } do |f|
|
= simple_form_for current_user, url: settings_preferences_appearance_path, html: { method: :put, id: 'edit_user' } do |f|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-group.fields-row__column.fields-row__column-6
|
.fields-group.fields-row__column.fields-row__column-6
|
||||||
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| native_locale_name(locale) }, selected: I18n.locale, hint: false
|
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: ->(locale) { native_locale_name(locale) }, selected: I18n.locale, hint: false
|
||||||
|
|
||||||
.fields-group.fields-row__column.fields-row__column-6
|
.fields-group.fields-row__column.fields-row__column-6
|
||||||
= f.input :time_zone, wrapper: :with_label, collection: ActiveSupport::TimeZone.all.map { |tz| ["(GMT#{tz.formatted_offset}) #{tz.name}", tz.tzinfo.name] }, hint: false
|
= f.input :time_zone, wrapper: :with_label, collection: ActiveSupport::TimeZone.all.map { |tz| ["(GMT#{tz.formatted_offset}) #{tz.name}", tz.tzinfo.name] }, hint: false
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
%h4= t 'appearance.sensitive_content'
|
%h4= t 'appearance.sensitive_content'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :'web.display_media', collection: ['default', 'show_all', 'hide_all'],label_method: lambda { |item| t("simple_form.hints.defaults.setting_display_media_#{item}") }, hint: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label, label: I18n.t('simple_form.labels.defaults.setting_display_media')
|
= ff.input :'web.display_media', collection: ['default', 'show_all', 'hide_all'], label_method: ->(item) { t("simple_form.hints.defaults.setting_display_media_#{item}") }, hint: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label, label: I18n.t('simple_form.labels.defaults.setting_display_media')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :'web.use_blurhash', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_use_blurhash'), hint: I18n.t('simple_form.hints.defaults.setting_use_blurhash')
|
= ff.input :'web.use_blurhash', wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_use_blurhash'), hint: I18n.t('simple_form.hints.defaults.setting_use_blurhash')
|
||||||
|
|
|
@ -22,10 +22,10 @@
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-group.fields-row__column.fields-row__column-6
|
.fields-group.fields-row__column.fields-row__column-6
|
||||||
= ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy')
|
= ff.input :default_privacy, collection: Status.selectable_visibilities, wrapper: :with_label, include_blank: false, label_method: ->(visibility) { safe_join([I18n.t("statuses.visibilities.#{visibility}"), I18n.t("statuses.visibilities.#{visibility}_long")], ' - ') }, required: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_privacy')
|
||||||
|
|
||||||
.fields-group.fields-row__column.fields-row__column-6
|
.fields-group.fields-row__column.fields-row__column-6
|
||||||
= ff.input :default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: lambda { |locale| locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_language')
|
= ff.input :default_language, collection: [nil] + filterable_languages, wrapper: :with_label, label_method: ->(locale) { locale.nil? ? I18n.t('statuses.default_language') : native_locale_name(locale) }, required: false, include_blank: false, hint: false, label: I18n.t('simple_form.labels.defaults.setting_default_language')
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= ff.input :default_sensitive, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), hint: I18n.t('simple_form.hints.defaults.setting_default_sensitive')
|
= ff.input :default_sensitive, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), hint: I18n.t('simple_form.hints.defaults.setting_default_sensitive')
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
%h4= t 'preferences.public_timelines'
|
%h4= t 'preferences.public_timelines'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :chosen_languages, collection: filterable_languages, wrapper: :with_block_label, include_blank: false, label_method: lambda { |locale| native_locale_name(locale) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
= f.input :chosen_languages, collection: filterable_languages, wrapper: :with_block_label, include_blank: false, label_method: ->(locale) { native_locale_name(locale) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
%span.poll__voted
|
%span.poll__voted
|
||||||
%i.poll__voted__mark.fa.fa-check
|
%i.poll__voted__mark.fa.fa-check
|
||||||
|
|
||||||
%progress{ max: 100, value: percent < 1 ? 1 : percent, 'aria-hidden': 'true' }
|
%progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' }
|
||||||
%span.poll__chart
|
%span.poll__chart
|
||||||
- else
|
- else
|
||||||
%label.poll__option><
|
%label.poll__option><
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
= opengraph 'og:title', "#{display_name(@account)} (#{acct(@account)})"
|
= opengraph 'og:title', "#{display_name(@account)} (#{acct(@account)})"
|
||||||
= opengraph 'og:url', short_account_status_url(@account, @status)
|
= opengraph 'og:url', short_account_status_url(@account, @status)
|
||||||
= opengraph 'og:published_time', @status.created_at.iso8601
|
= opengraph 'og:published_time', @status.created_at.iso8601
|
||||||
= opengraph 'profile:username', acct(@account)[1..-1]
|
= opengraph 'profile:username', acct(@account)[1..]
|
||||||
|
|
||||||
= render 'og_description', activity: @status
|
= render 'og_description', activity: @status
|
||||||
= render 'og_image', activity: @status, account: @account
|
= render 'og_image', activity: @status, account: @account
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :enabled, as: :boolean, wrapper: :with_label, label: t('statuses_cleanup.enabled'), hint: t('statuses_cleanup.enabled_hint')
|
= f.input :enabled, as: :boolean, wrapper: :with_label, label: t('statuses_cleanup.enabled'), hint: t('statuses_cleanup.enabled_hint')
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :min_status_age, wrapper: :with_label, label: t('statuses_cleanup.min_age_label'), collection: AccountStatusesCleanupPolicy::ALLOWED_MIN_STATUS_AGE.map(&:to_i), label_method: lambda { |i| t("statuses_cleanup.min_age.#{i}") }, include_blank: false, hint: false
|
= f.input :min_status_age, wrapper: :with_label, label: t('statuses_cleanup.min_age_label'), collection: AccountStatusesCleanupPolicy::ALLOWED_MIN_STATUS_AGE.map(&:to_i), label_method: ->(i) { t("statuses_cleanup.min_age.#{i}") }, include_blank: false, hint: false
|
||||||
|
|
||||||
.flash-message= t('statuses_cleanup.explanation')
|
.flash-message= t('statuses_cleanup.explanation')
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TagUnmergeWorker
|
||||||
|
include Sidekiq::Worker
|
||||||
|
include DatabaseHelper
|
||||||
|
|
||||||
|
sidekiq_options queue: 'pull'
|
||||||
|
|
||||||
|
def perform(from_tag_id, into_account_id)
|
||||||
|
with_primary do
|
||||||
|
@from_tag = Tag.find(from_tag_id)
|
||||||
|
@into_account = Account.find(into_account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
with_read_replica do
|
||||||
|
FeedManager.instance.unmerge_tag_from_home(@from_tag, @into_account)
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,37 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Api::V1::PollsController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:scopes) { 'read:statuses' }
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
|
||||||
|
|
||||||
before { allow(controller).to receive(:doorkeeper_token) { token } }
|
|
||||||
|
|
||||||
describe 'GET #show' do
|
|
||||||
let(:poll) { Fabricate(:poll, status: Fabricate(:status, visibility: visibility)) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
get :show, params: { id: poll.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when parent status is public' do
|
|
||||||
let(:visibility) { 'public' }
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when parent status is private' do
|
|
||||||
let(:visibility) { 'private' }
|
|
||||||
|
|
||||||
it 'returns http not found' do
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,113 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe Api::V1::Statuses::BookmarksController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:bookmarks', application: app) }
|
|
||||||
|
|
||||||
context 'with an oauth token' do
|
|
||||||
before do
|
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST #create' do
|
|
||||||
let(:status) { Fabricate(:status, account: user.account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post :create, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with public status' do
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the bookmarked attribute' do
|
|
||||||
expect(user.account.bookmarked?(status)).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:bookmarked]).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with private status of not-followed account' do
|
|
||||||
let(:status) { Fabricate(:status, visibility: :private) }
|
|
||||||
|
|
||||||
it 'returns http not found' do
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST #destroy' do
|
|
||||||
context 'with public status' do
|
|
||||||
let(:status) { Fabricate(:status, account: user.account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
Bookmark.find_or_create_by!(account: user.account, status: status)
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the bookmarked attribute' do
|
|
||||||
expect(user.account.bookmarked?(status)).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:bookmarked]).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with public status when blocked by its author' do
|
|
||||||
let(:status) { Fabricate(:status) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
Bookmark.find_or_create_by!(account: user.account, status: status)
|
|
||||||
status.account.block!(user.account)
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the bookmarked attribute' do
|
|
||||||
expect(user.account.bookmarked?(status)).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:bookmarked]).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with private status that was not bookmarked' do
|
|
||||||
let(:status) { Fabricate(:status, visibility: :private) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http not found' do
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,123 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe Api::V1::Statuses::FavouritesController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:favourites', application: app) }
|
|
||||||
|
|
||||||
context 'with an oauth token' do
|
|
||||||
before do
|
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST #create' do
|
|
||||||
let(:status) { Fabricate(:status, account: user.account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post :create, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with public status' do
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the favourites count' do
|
|
||||||
expect(status.favourites.count).to eq 1
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the favourited attribute' do
|
|
||||||
expect(user.account.favourited?(status)).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:favourites_count]).to eq 1
|
|
||||||
expect(hash_body[:favourited]).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with private status of not-followed account' do
|
|
||||||
let(:status) { Fabricate(:status, visibility: :private) }
|
|
||||||
|
|
||||||
it 'returns http not found' do
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST #destroy' do
|
|
||||||
context 'with public status' do
|
|
||||||
let(:status) { Fabricate(:status, account: user.account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
FavouriteService.new.call(user.account, status)
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the favourites count' do
|
|
||||||
expect(status.favourites.count).to eq 0
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the favourited attribute' do
|
|
||||||
expect(user.account.favourited?(status)).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:favourites_count]).to eq 0
|
|
||||||
expect(hash_body[:favourited]).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with public status when blocked by its author' do
|
|
||||||
let(:status) { Fabricate(:status) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
FavouriteService.new.call(user.account, status)
|
|
||||||
status.account.block!(user.account)
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the favourite attribute' do
|
|
||||||
expect(user.account.favourited?(status)).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:favourited]).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with private status that was not favourited' do
|
|
||||||
let(:status) { Fabricate(:status, visibility: :private) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http not found' do
|
|
||||||
expect(response).to have_http_status(404)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,57 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe Api::V1::Statuses::PinsController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:accounts', application: app) }
|
|
||||||
|
|
||||||
context 'with an oauth token' do
|
|
||||||
before do
|
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST #create' do
|
|
||||||
let(:status) { Fabricate(:status, account: user.account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
post :create, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the pinned attribute' do
|
|
||||||
expect(user.account.pinned?(status)).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'return json with updated attributes' do
|
|
||||||
hash_body = body_as_json
|
|
||||||
|
|
||||||
expect(hash_body[:id]).to eq status.id.to_s
|
|
||||||
expect(hash_body[:pinned]).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'POST #destroy' do
|
|
||||||
let(:status) { Fabricate(:status, account: user.account) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
Fabricate(:status_pin, status: status, account: user.account)
|
|
||||||
post :destroy, params: { status_id: status.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates the pinned attribute' do
|
|
||||||
expect(user.account.pinned?(status)).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,44 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe Api::V1::Timelines::HomeController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with a user context' do
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:statuses') }
|
|
||||||
|
|
||||||
describe 'GET #show' do
|
|
||||||
before do
|
|
||||||
follow = Fabricate(:follow, account: user.account)
|
|
||||||
PostStatusService.new.call(follow.target_account, text: 'New status for user home timeline.')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns http success' do
|
|
||||||
get :show
|
|
||||||
|
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
expect(response.headers['Link'].links.size).to eq(2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'without a user context' do
|
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read') }
|
|
||||||
|
|
||||||
describe 'GET #show' do
|
|
||||||
it 'returns http unprocessable entity' do
|
|
||||||
get :show
|
|
||||||
|
|
||||||
expect(response).to have_http_status(422)
|
|
||||||
expect(response.headers['Link']).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -2,27 +2,34 @@
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Api::V1::Emails::ConfirmationsController do
|
RSpec.describe 'Confirmations' do
|
||||||
let(:confirmed_at) { nil }
|
let(:confirmed_at) { nil }
|
||||||
let(:user) { Fabricate(:user, confirmed_at: confirmed_at) }
|
let(:user) { Fabricate(:user, confirmed_at: confirmed_at) }
|
||||||
let(:app) { Fabricate(:application) }
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes, application: app) }
|
let(:scopes) { 'read:accounts write:accounts' }
|
||||||
let(:scopes) { 'write' }
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/emails/confirmations' do
|
||||||
|
subject do
|
||||||
|
post '/api/v1/emails/confirmations', headers: headers, params: params
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read read:accounts'
|
||||||
|
|
||||||
describe '#create' do
|
|
||||||
context 'with an oauth token' do
|
context 'with an oauth token' do
|
||||||
before do
|
context 'when user was created by a different application' do
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
let(:user) { Fabricate(:user, confirmed_at: confirmed_at, created_by_application: Fabricate(:application)) }
|
||||||
end
|
|
||||||
|
|
||||||
context 'when from a random app' do
|
|
||||||
it 'returns http forbidden' do
|
it 'returns http forbidden' do
|
||||||
post :create
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when from an app that created the account' do
|
context 'when user was created by the same application' do
|
||||||
before do
|
before do
|
||||||
user.update(created_by_application: token.application)
|
user.update(created_by_application: token.application)
|
||||||
end
|
end
|
||||||
|
@ -31,55 +38,79 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do
|
||||||
let(:confirmed_at) { Time.now.utc }
|
let(:confirmed_at) { Time.now.utc }
|
||||||
|
|
||||||
it 'returns http forbidden' do
|
it 'returns http forbidden' do
|
||||||
post :create
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with user changed e-mail and has not confirmed it' do
|
context 'when user changed e-mail and has not confirmed it' do
|
||||||
before do
|
before do
|
||||||
user.update(email: 'foo@bar.com')
|
user.update(email: 'foo@bar.com')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
post :create
|
subject
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the account is unconfirmed' do
|
context 'when the account is unconfirmed' do
|
||||||
it 'returns http success' do
|
it 'returns http success' do
|
||||||
post :create
|
subject
|
||||||
expect(response).to have_http_status(:success)
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with email param' do
|
||||||
|
let(:params) { { email: 'foo@bar.com' } }
|
||||||
|
|
||||||
|
it "updates the user's e-mail address", :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.reload.unconfirmed_email).to eq('foo@bar.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid email param' do
|
||||||
|
let(:params) { { email: 'invalid' } }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without an oauth token' do
|
context 'without an oauth token' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
it 'returns http unauthorized' do
|
it 'returns http unauthorized' do
|
||||||
post :create
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(401)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#check' do
|
describe 'GET /api/v1/emails/check_confirmation' do
|
||||||
let(:scopes) { 'read' }
|
subject do
|
||||||
|
get '/api/v1/emails/check_confirmation', headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write'
|
||||||
|
|
||||||
context 'with an oauth token' do
|
context 'with an oauth token' do
|
||||||
before do
|
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the account is not confirmed' do
|
context 'when the account is not confirmed' do
|
||||||
it 'returns http success' do
|
it 'returns the confirmation status successfully', :aggregate_failures do
|
||||||
get :check
|
subject
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false' do
|
expect(response).to have_http_status(200)
|
||||||
get :check
|
|
||||||
expect(body_as_json).to be false
|
expect(body_as_json).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -87,31 +118,27 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do
|
||||||
context 'when the account is confirmed' do
|
context 'when the account is confirmed' do
|
||||||
let(:confirmed_at) { Time.now.utc }
|
let(:confirmed_at) { Time.now.utc }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns the confirmation status successfully', :aggregate_failures do
|
||||||
get :check
|
subject
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true' do
|
expect(response).to have_http_status(200)
|
||||||
get :check
|
|
||||||
expect(body_as_json).to be true
|
expect(body_as_json).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an authentication cookie' do
|
context 'with an authentication cookie' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in user, scope: :user
|
sign_in user, scope: :user
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the account is not confirmed' do
|
context 'when the account is not confirmed' do
|
||||||
it 'returns http success' do
|
it 'returns the confirmation status successfully', :aggregate_failures do
|
||||||
get :check
|
subject
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns false' do
|
expect(response).to have_http_status(200)
|
||||||
get :check
|
|
||||||
expect(body_as_json).to be false
|
expect(body_as_json).to be false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -119,21 +146,20 @@ RSpec.describe Api::V1::Emails::ConfirmationsController do
|
||||||
context 'when the account is confirmed' do
|
context 'when the account is confirmed' do
|
||||||
let(:confirmed_at) { Time.now.utc }
|
let(:confirmed_at) { Time.now.utc }
|
||||||
|
|
||||||
it 'returns http success' do
|
it 'returns the confirmation status successfully', :aggregate_failures do
|
||||||
get :check
|
subject
|
||||||
expect(response).to have_http_status(200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true' do
|
expect(response).to have_http_status(200)
|
||||||
get :check
|
|
||||||
expect(body_as_json).to be true
|
expect(body_as_json).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without an oauth token and an authentication cookie' do
|
context 'without an oauth token and an authentication cookie' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
it 'returns http unauthorized' do
|
it 'returns http unauthorized' do
|
||||||
get :check
|
subject
|
||||||
|
|
||||||
expect(response).to have_http_status(401)
|
expect(response).to have_http_status(401)
|
||||||
end
|
end
|
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Polls' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:scopes) { 'read:statuses' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/polls/:id' do
|
||||||
|
subject do
|
||||||
|
get "/api/v1/polls/#{poll.id}", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:poll) { Fabricate(:poll, status: Fabricate(:status, visibility: visibility)) }
|
||||||
|
let(:visibility) { 'public' }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write write:statuses'
|
||||||
|
|
||||||
|
context 'when parent status is public' do
|
||||||
|
it 'returns the poll data successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(
|
||||||
|
id: poll.id.to_s,
|
||||||
|
voted: false,
|
||||||
|
voters_count: poll.voters_count,
|
||||||
|
votes_count: poll.votes_count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when parent status is private' do
|
||||||
|
let(:visibility) { 'private' }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,155 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Bookmarks' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:scopes) { 'write:bookmarks' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/bookmark' do
|
||||||
|
subject do
|
||||||
|
post "/api/v1/statuses/#{status.id}/bookmark", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read'
|
||||||
|
|
||||||
|
context 'with public status' do
|
||||||
|
it 'bookmarks the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.bookmarked?(status)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, bookmarked: true)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status of not-followed account' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status of followed account' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(status.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'bookmarks the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.bookmarked?(status)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status does not exist' do
|
||||||
|
it 'returns http not found' do
|
||||||
|
post '/api/v1/statuses/-1/bookmark', headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an authorization header' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it 'returns http unauthorized' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/unbookmark' do
|
||||||
|
subject do
|
||||||
|
post "/api/v1/statuses/#{status.id}/unbookmark", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read'
|
||||||
|
|
||||||
|
context 'with public status' do
|
||||||
|
context 'when the status was previously bookmarked' do
|
||||||
|
before do
|
||||||
|
Bookmark.find_or_create_by!(account: user.account, status: status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unbookmarks the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.bookmarked?(status)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, bookmarked: false)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requesting user was blocked by the status author' do
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Bookmark.find_or_create_by!(account: user.account, status: status)
|
||||||
|
status.account.block!(user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unbookmarks the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.bookmarked?(status)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, bookmarked: false)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status is not bookmarked' do
|
||||||
|
it 'returns http success' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status that was not bookmarked' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,143 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Favourites' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:scopes) { 'write:favourites' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/favourite' do
|
||||||
|
subject do
|
||||||
|
post "/api/v1/statuses/#{status.id}/favourite", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read read:favourites'
|
||||||
|
|
||||||
|
context 'with public status' do
|
||||||
|
it 'favourites the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.favourited?(status)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, favourites_count: 1, favourited: true)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status of not-followed account' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status of followed account' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(status.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'favourites the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.favourited?(status)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an authorization header' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it 'returns http unauthorized' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/unfavourite' do
|
||||||
|
subject do
|
||||||
|
post "/api/v1/statuses/#{status.id}/unfavourite", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read read:favourites'
|
||||||
|
|
||||||
|
context 'with public status' do
|
||||||
|
before do
|
||||||
|
FavouriteService.new.call(user.account, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unfavourites the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.favourited?(status)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, favourites_count: 0, favourited: false)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requesting user was blocked by the status author' do
|
||||||
|
before do
|
||||||
|
FavouriteService.new.call(user.account, status)
|
||||||
|
status.account.block!(user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unfavourites the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.favourited?(status)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, favourites_count: 0, favourited: false)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when status is not favourited' do
|
||||||
|
it 'returns http success' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private status that was not favourited' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,131 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'Pins' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:scopes) { 'write:accounts' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/pin' do
|
||||||
|
subject do
|
||||||
|
post "/api/v1/statuses/#{status.id}/pin", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status, account: user.account) }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'read read:accounts'
|
||||||
|
|
||||||
|
context 'when the status is public' do
|
||||||
|
it 'pins the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.pinned?(status)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, pinned: true)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status is private' do
|
||||||
|
let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
|
||||||
|
|
||||||
|
it 'pins the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.pinned?(status)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status belongs to somebody else' do
|
||||||
|
let(:status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status does not exist' do
|
||||||
|
it 'returns http not found' do
|
||||||
|
post '/api/v1/statuses/-1/pin', headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an authorization header' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it 'returns http unauthorized' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/statuses/:status_id/unpin' do
|
||||||
|
subject do
|
||||||
|
post "/api/v1/statuses/#{status.id}/unpin", headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:status) { Fabricate(:status, account: user.account) }
|
||||||
|
|
||||||
|
context 'when the status is pinned' do
|
||||||
|
before do
|
||||||
|
Fabricate(:status_pin, status: status, account: user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'unpins the status successfully', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(user.account.pinned?(status)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return json with updated attributes' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json).to match(
|
||||||
|
a_hash_including(id: status.id.to_s, pinned: false)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status is not pinned' do
|
||||||
|
it 'returns http success' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the status does not exist' do
|
||||||
|
it 'returns http not found' do
|
||||||
|
post '/api/v1/statuses/-1/unpin', headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an authorization header' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it 'returns http unauthorized' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,101 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'Home' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:scopes) { 'read:statuses' }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/timelines/home' do
|
||||||
|
subject do
|
||||||
|
get '/api/v1/timelines/home', headers: headers, params: params
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
it_behaves_like 'forbidden for wrong scope', 'write write:statuses'
|
||||||
|
|
||||||
|
context 'when the timeline is available' do
|
||||||
|
let(:home_statuses) { bob.statuses + ana.statuses }
|
||||||
|
let!(:bob) { Fabricate(:account) }
|
||||||
|
let!(:tim) { Fabricate(:account) }
|
||||||
|
let!(:ana) { Fabricate(:account) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(bob)
|
||||||
|
user.account.follow!(ana)
|
||||||
|
PostStatusService.new.call(bob, text: 'New toot from bob.')
|
||||||
|
PostStatusService.new.call(tim, text: 'New toot from tim.')
|
||||||
|
PostStatusService.new.call(ana, text: 'New toot from ana.')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the statuses of followed users' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json.pluck(:id)).to match_array(home_statuses.map { |status| status.id.to_s })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with limit param' do
|
||||||
|
let(:params) { { limit: 1 } }
|
||||||
|
|
||||||
|
it 'returns only the requested number of statuses' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(body_as_json.size).to eq(params[:limit])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the correct pagination headers', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
headers = response.headers['Link']
|
||||||
|
|
||||||
|
expect(headers.find_link(%w(rel prev)).href).to eq(api_v1_timelines_home_url(limit: 1, min_id: ana.statuses.first.id.to_s))
|
||||||
|
expect(headers.find_link(%w(rel next)).href).to eq(api_v1_timelines_home_url(limit: 1, max_id: ana.statuses.first.id.to_s))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the timeline is regenerating' do
|
||||||
|
let(:timeline) { instance_double(HomeFeed, regenerating?: true, get: []) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(HomeFeed).to receive(:new).and_return(timeline)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http partial content' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(206)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without an authorization header' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
it 'returns http unauthorized' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a user context' do
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: scopes) }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
expect(response.headers['Link']).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -34,11 +34,11 @@ RSpec.describe BatchedRemoveStatusService, type: :service do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes statuses from author\'s home feed' do
|
it 'removes statuses from author\'s home feed' do
|
||||||
expect(HomeFeed.new(alice).get(10)).to_not include([status_alice_hello.id, status_alice_other.id])
|
expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status_alice_hello.id, status_alice_other.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes statuses from local follower\'s home feed' do
|
it 'removes statuses from local follower\'s home feed' do
|
||||||
expect(HomeFeed.new(jeff).get(10)).to_not include([status_alice_hello.id, status_alice_other.id])
|
expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status_alice_hello.id, status_alice_other.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'notifies streaming API of followers' do
|
it 'notifies streaming API of followers' do
|
||||||
|
|
|
@ -28,12 +28,12 @@ RSpec.describe RemoveStatusService, type: :service do
|
||||||
|
|
||||||
it 'removes status from author\'s home feed' do
|
it 'removes status from author\'s home feed' do
|
||||||
subject.call(@status)
|
subject.call(@status)
|
||||||
expect(HomeFeed.new(alice).get(10)).to_not include(@status.id)
|
expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(@status.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes status from local follower\'s home feed' do
|
it 'removes status from local follower\'s home feed' do
|
||||||
subject.call(@status)
|
subject.call(@status)
|
||||||
expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id)
|
expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(@status.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sends Delete activity to followers' do
|
it 'sends Delete activity to followers' do
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe TagUnmergeWorker do
|
||||||
|
subject { described_class.new }
|
||||||
|
|
||||||
|
describe 'perform' do
|
||||||
|
let(:follower) { Fabricate(:account) }
|
||||||
|
let(:followed) { Fabricate(:account) }
|
||||||
|
let(:followed_tag) { Fabricate(:tag) }
|
||||||
|
let(:unchanged_followed_tag) { Fabricate(:tag) }
|
||||||
|
let(:status_from_followed) { Fabricate(:status, created_at: 2.hours.ago, account: followed) }
|
||||||
|
let(:tagged_status) { Fabricate(:status, created_at: 1.hour.ago) }
|
||||||
|
let(:unchanged_tagged_status) { Fabricate(:status) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
tagged_status.tags << followed_tag
|
||||||
|
unchanged_tagged_status.tags << followed_tag
|
||||||
|
unchanged_tagged_status.tags << unchanged_followed_tag
|
||||||
|
|
||||||
|
tag_follow = TagFollow.create_with(rate_limit: false).find_or_create_by!(tag: followed_tag, account: follower)
|
||||||
|
TagFollow.create_with(rate_limit: false).find_or_create_by!(tag: unchanged_followed_tag, account: follower)
|
||||||
|
|
||||||
|
FeedManager.instance.push_to_home(follower, status_from_followed, update: false)
|
||||||
|
FeedManager.instance.push_to_home(follower, tagged_status, update: false)
|
||||||
|
FeedManager.instance.push_to_home(follower, unchanged_tagged_status, update: false)
|
||||||
|
|
||||||
|
tag_follow.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes the expected status from the feed' do
|
||||||
|
expect { subject.perform(followed_tag.id, follower.id) }
|
||||||
|
.to change { HomeFeed.new(follower).get(10).pluck(:id) }
|
||||||
|
.from([unchanged_tagged_status.id, tagged_status.id, status_from_followed.id])
|
||||||
|
.to([unchanged_tagged_status.id, status_from_followed.id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
22
yarn.lock
22
yarn.lock
|
@ -1279,17 +1279,17 @@
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6"
|
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6"
|
||||||
integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==
|
integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==
|
||||||
|
|
||||||
"@floating-ui/core@^1.3.0":
|
"@floating-ui/core@^1.3.1":
|
||||||
version "1.3.0"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.0.tgz#113bc85fa102cf890ae801668f43ee265c547a09"
|
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.1.tgz#4d795b649cc3b1cbb760d191c80dcb4353c9a366"
|
||||||
integrity sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ==
|
integrity sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==
|
||||||
|
|
||||||
"@floating-ui/dom@^1.0.1":
|
"@floating-ui/dom@^1.0.1":
|
||||||
version "1.3.0"
|
version "1.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.3.0.tgz#69456f2164fc3d33eb40837686eaf71537235ac9"
|
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.4.5.tgz#336dfb9870c98b471ff5802002982e489b8bd1c5"
|
||||||
integrity sha512-qIAwejE3r6NeA107u4ELDKkH8+VtgRKdXqtSPaKflL2S2V+doyN+Wt9s5oHKXPDo4E8TaVXaHT3+6BbagH31xw==
|
integrity sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/core" "^1.3.0"
|
"@floating-ui/core" "^1.3.1"
|
||||||
|
|
||||||
"@formatjs/cli@^6.1.1":
|
"@formatjs/cli@^6.1.1":
|
||||||
version "6.1.3"
|
version "6.1.3"
|
||||||
|
@ -9701,9 +9701,9 @@ react-router@^4.3.1:
|
||||||
warning "^4.0.1"
|
warning "^4.0.1"
|
||||||
|
|
||||||
react-select@*, react-select@^5.7.3:
|
react-select@*, react-select@^5.7.3:
|
||||||
version "5.7.3"
|
version "5.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.3.tgz#fa0dc9a23cad6ff3871ad3829f6083a4b54961a2"
|
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.4.tgz#d8cad96e7bc9d6c8e2709bdda8f4363c5dd7ea7d"
|
||||||
integrity sha512-z8i3NCuFFWL3w27xq92rBkVI2onT0jzIIPe480HlBjXJ3b5o6Q+Clp4ydyeKrj9DZZ3lrjawwLC5NGl0FSvUDg==
|
integrity sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.0"
|
"@babel/runtime" "^7.12.0"
|
||||||
"@emotion/cache" "^11.4.0"
|
"@emotion/cache" "^11.4.0"
|
||||||
|
|
Loading…
Reference in New Issue