Merge commit 'd13d7b4566d551d027830f7dfc987ba3a8e2dd4f' into glitch-soc/merge-upstream

Conflicts are too numerous to list, but they are all caused by upstream's
theming changes and all have to do with the differences between upstream and
glitch-soc in the theming systems.

They were all manually resolved by adapting the relevant code.
This commit is contained in:
Claire
2026-02-11 13:32:18 +01:00
41 changed files with 372 additions and 243 deletions

View File

@@ -21,9 +21,7 @@ import { reducerWithInitialState } from '@/mastodon/reducers';
import { defaultMiddleware } from '@/mastodon/store/store';
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
// If you want to run the dark theme during development,
// you can change the below to `/application.scss`
import '../app/javascript/styles/mastodon-light.scss';
import '../app/javascript/styles/application.scss';
import './styles.css';
import { modes } from './modes';

View File

@@ -343,8 +343,9 @@ GEM
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.8.2)
irb (1.16.0)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jd-paperclip-azure (3.0.0)

View File

@@ -11,18 +11,12 @@ class ApplicationController < ActionController::Base
include CacheConcern
include PreloadingConcern
include DomainControlHelper
include ThemingConcern
include DatabaseHelper
include AuthorizedFetchHelper
include SelfDestructHelper
helper_method :current_account
helper_method :current_session
helper_method :current_flavour
helper_method :current_skin
helper_method :current_theme
helper_method :color_scheme
helper_method :contrast
helper_method :single_user_mode?
helper_method :use_seamless_external_login?
helper_method :sso_account_settings
@@ -176,25 +170,6 @@ class ApplicationController < ActionController::Base
@current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present?
end
def color_scheme
current = current_user&.setting_color_scheme
return current if current && current != 'auto'
return 'dark' if current_skin.include?('default') || current_skin.include?('contrast')
return 'light' if current_skin.include?('light')
'auto'
end
def contrast
current = current_user&.setting_contrast
return current if current && current != 'auto'
return 'high' if current_skin.include?('contrast')
'auto'
end
def respond_with_error(code)
respond_to do |format|
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }

View File

@@ -1,24 +0,0 @@
# frozen_string_literal: true
module ThemingConcern
extend ActiveSupport::Concern
private
def current_flavour
@current_flavour ||= [current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) }
end
def current_skin
@current_skin ||= begin
skins = Themes.instance.skins_for(current_flavour)
[current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) }
end
end
def current_theme
# NOTE: this is slightly different from upstream, as it's a derived value used
# for the sole purpose of pointing to the appropriate stylesheet pack
[current_flavour, current_skin]
end
end

View File

@@ -89,12 +89,6 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)"
end
def page_color_scheme
return content_for(:force_color_scheme) if content_for(:force_color_scheme)
color_scheme
end
def label_for_scope(scope)
safe_join [
tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }),

View File

@@ -20,9 +20,6 @@ module ThemeHelper
def theme_style_tags(flavour_and_skin)
flavour, theme = flavour_and_skin
# TODO: get rid of that when we retire the themes and perform the settings migration
theme = 'default' if %w(mastodon-light contrast system).include?(theme)
vite_stylesheet_tag "skins/#{flavour}/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous'
end
@@ -51,6 +48,33 @@ module ThemeHelper
)
end
def current_flavour
[current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) }
end
def current_skin
skins = Themes.instance.skins_for(current_flavour)
[current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) }
end
def current_theme
# NOTE: this is slightly different from upstream, as it's a derived value used
# for the sole purpose of pointing to the appropriate stylesheet pack
[current_flavour, current_skin]
end
def color_scheme
current_user&.setting_color_scheme || 'auto'
end
def contrast
current_user&.setting_contrast || 'auto'
end
def page_color_scheme
content_for(:force_color_scheme).presence || color_scheme
end
private
def active_custom_stylesheet

View File

@@ -1,28 +0,0 @@
import { useCallback } from 'react';
import { useSearchParam } from '@/mastodon/hooks/useSearchParam';
export function useFilters() {
const [boosts, setBoosts] = useSearchParam('boosts');
const [replies, setReplies] = useSearchParam('replies');
const handleSetBoosts = useCallback(
(value: boolean) => {
setBoosts(value ? '1' : null);
},
[setBoosts],
);
const handleSetReplies = useCallback(
(value: boolean) => {
setReplies(value ? '1' : null);
},
[setReplies],
);
return {
boosts: boosts === '1',
replies: replies === '1',
setBoosts: handleSetBoosts,
setReplies: handleSetReplies,
};
}

View File

@@ -0,0 +1,97 @@
import type { FC, ReactNode } from 'react';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { useStorage } from '@/mastodon/hooks/useStorage';
interface AccountTimelineContextValue {
accountId: string;
boosts: boolean;
replies: boolean;
showAllPinned: boolean;
setBoosts: (value: boolean) => void;
setReplies: (value: boolean) => void;
onShowAllPinned: () => void;
}
const AccountTimelineContext =
createContext<AccountTimelineContextValue | null>(null);
export const AccountTimelineProvider: FC<{
accountId: string;
children: ReactNode;
}> = ({ accountId, children }) => {
const { getItem, setItem } = useStorage({
type: 'session',
prefix: `filters-${accountId}:`,
});
const [boosts, setBoosts] = useState(
() => (getItem('boosts') === '0' ? false : true), // Default to enabled.
);
const [replies, setReplies] = useState(() =>
getItem('replies') === '1' ? true : false,
);
const handleSetBoosts = useCallback(
(value: boolean) => {
setBoosts(value);
setItem('boosts', value ? '1' : '0');
},
[setBoosts, setItem],
);
const handleSetReplies = useCallback(
(value: boolean) => {
setReplies(value);
setItem('replies', value ? '1' : '0');
},
[setReplies, setItem],
);
const [showAllPinned, setShowAllPinned] = useState(false);
const handleShowAllPinned = useCallback(() => {
setShowAllPinned(true);
}, []);
// Memoize the context value to avoid unnecessary re-renders.
const value = useMemo(
() => ({
accountId,
boosts,
replies,
showAllPinned,
setBoosts: handleSetBoosts,
setReplies: handleSetReplies,
onShowAllPinned: handleShowAllPinned,
}),
[
accountId,
boosts,
handleSetBoosts,
handleSetReplies,
handleShowAllPinned,
replies,
showAllPinned,
],
);
return (
<AccountTimelineContext.Provider value={value}>
{children}
</AccountTimelineContext.Provider>
);
};
export function useAccountContext() {
const values = useContext(AccountTimelineContext);
if (!values) {
throw new Error(
'useAccountFilters must be used within an AccountTimelineProvider',
);
}
return values;
}

View File

@@ -13,8 +13,7 @@ import { useOverflowButton } from '@/mastodon/hooks/useOverflow';
import { selectAccountFeaturedTags } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { useFilters } from '../hooks/useFilters';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
@@ -83,7 +82,7 @@ export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
function useTagNavigate() {
// Get current account, tag, and filters.
const { acct, tagged } = useParams<{ acct: string; tagged?: string }>();
const { boosts, replies } = useFilters();
const { boosts, replies } = useAccountContext();
const history = useAppHistory();

View File

@@ -12,8 +12,8 @@ import { Icon } from '@/mastodon/components/icon';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import { AccountTabs } from '../components/tabs';
import { useFilters } from '../hooks/useFilters';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
export const AccountFilters: FC = () => {
@@ -42,7 +42,7 @@ const FilterDropdown: FC = () => {
setOpen(false);
}, []);
const { boosts, replies, setBoosts, setReplies } = useFilters();
const { boosts, replies, setBoosts, setReplies } = useAccountContext();
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
const { name, checked } = event.target;
@@ -101,7 +101,6 @@ const FilterDropdown: FC = () => {
<Overlay
show={open}
target={buttonRef}
flip
placement='bottom-start'
rootClose
onHide={handleHide}

View File

@@ -25,12 +25,11 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountHeader } from '../components/account_header';
import { LimitedAccountHint } from '../components/limited_account_hint';
import { useFilters } from '../hooks/useFilters';
import { AccountTimelineProvider, useAccountContext } from './context';
import { FeaturedTags } from './featured_tags';
import { AccountFilters } from './filters';
import {
PinnedStatusProvider,
renderPinnedStatusHeader,
usePinnedStatusIds,
} from './pinned_statuses';
@@ -56,13 +55,13 @@ const AccountTimelineV2: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
// Add this key to remount the timeline when accountId changes.
return (
<PinnedStatusProvider>
<AccountTimelineProvider accountId={accountId}>
<InnerTimeline
accountId={accountId}
key={accountId}
multiColumn={multiColumn}
/>
</PinnedStatusProvider>
</AccountTimelineProvider>
);
};
@@ -71,7 +70,7 @@ const InnerTimeline: FC<{ accountId: string; multiColumn: boolean }> = ({
multiColumn,
}) => {
const { tagged } = useParams<{ tagged?: string }>();
const { boosts, replies } = useFilters();
const { boosts, replies } = useAccountContext();
const key = timelineKey({
type: 'account',
userId: accountId,

View File

@@ -1,12 +1,5 @@
import type { FC, ReactNode } from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import type { FC } from 'react';
import { useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -28,42 +21,9 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '../common';
import { PinnedBadge } from '../components/badges';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
const PinnedStatusContext = createContext<{
showAllPinned: boolean;
onShowAllPinned: () => void;
}>({
showAllPinned: false,
onShowAllPinned: () => {
throw new Error('No onShowAllPinned provided');
},
});
export const PinnedStatusProvider: FC<{ children: ReactNode }> = ({
children,
}) => {
const [showAllPinned, setShowAllPinned] = useState(false);
const handleShowAllPinned = useCallback(() => {
setShowAllPinned(true);
}, []);
// Memoize so the context doesn't change every render.
const value = useMemo(
() => ({
showAllPinned,
onShowAllPinned: handleShowAllPinned,
}),
[handleShowAllPinned, showAllPinned],
);
return (
<PinnedStatusContext.Provider value={value}>
{children}
</PinnedStatusContext.Provider>
);
};
export function usePinnedStatusIds({
accountId,
tagged,
@@ -89,7 +49,7 @@ export function usePinnedStatusIds({
selectTimelineByKey(state, pinnedKey),
);
const { showAllPinned } = useContext(PinnedStatusContext);
const { showAllPinned } = useAccountContext();
const pinnedTimelineItems = pinnedTimeline?.items; // Make a const to avoid the React Compiler complaining.
const pinnedStatusIds = useMemo(() => {
@@ -125,7 +85,7 @@ export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
};
export const PinnedShowAllButton: FC = () => {
const { onShowAllPinned } = useContext(PinnedStatusContext);
const { onShowAllPinned } = useAccountContext();
if (!isRedesignEnabled()) {
return null;

View File

@@ -0,0 +1,64 @@
import { useCallback, useMemo } from 'react';
export function useStorage({
type = 'local',
prefix = '',
}: { type?: 'local' | 'session'; prefix?: string } = {}) {
const storageType = type === 'local' ? 'localStorage' : 'sessionStorage';
const isAvailable = useMemo(
() => storageAvailable(storageType),
[storageType],
);
const getItem = useCallback(
(key: string) => {
if (!isAvailable) {
return null;
}
try {
return window[storageType].getItem(prefix ? `${prefix};${key}` : key);
} catch {
return null;
}
},
[isAvailable, storageType, prefix],
);
const setItem = useCallback(
(key: string, value: string) => {
if (!isAvailable) {
return;
}
try {
window[storageType].setItem(prefix ? `${prefix};${key}` : key, value);
} catch {}
},
[isAvailable, storageType, prefix],
);
return {
isAvailable,
getItem,
setItem,
};
}
// Tests the storage availability for the given type. Taken from MDN:
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
export function storageAvailable(type: 'localStorage' | 'sessionStorage') {
let storage;
try {
storage = window[type];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return (
e instanceof DOMException &&
e.name === 'QuotaExceededError' &&
// acknowledge QuotaExceededError only if there's something already stored
storage &&
storage.length !== 0
);
}
}

View File

@@ -1 +0,0 @@
@use '@/flavours/glitch/styles/contrast';

View File

@@ -1,12 +0,0 @@
en:
skins:
glitch:
contrast: High contrast
cs:
skins:
glitch:
contrast: Vysoký kontrast
es:
skins:
glitch:
contrast: Alto contraste

View File

@@ -1 +0,0 @@
@use '@/flavours/glitch/styles/mastodon-light';

View File

@@ -1,12 +0,0 @@
en:
skins:
glitch:
mastodon-light: Mastodon (light)
cs:
skins:
glitch:
mastodon-light: Mastodon (světlý)
es:
skins:
glitch:
mastodon-light: Mastodon (claro)

View File

@@ -1 +0,0 @@
@use '@/styles/contrast';

View File

@@ -1,12 +0,0 @@
en:
skins:
vanilla:
contrast: High contrast
cs:
skins:
vanilla:
contrast: Vysoký kontrast
es:
skins:
vanilla:
contrast: Alto contraste

View File

@@ -1 +0,0 @@
@use '@/styles/mastodon-light';

View File

@@ -1,12 +0,0 @@
en:
skins:
vanilla:
mastodon-light: Mastodon (light)
cs:
skins:
vanilla:
mastodon-light: Mastodon (světlý)
es:
skins:
glitch:
mastodon-light: Mastodon (claro)

View File

@@ -1 +0,0 @@
@use 'common';

View File

@@ -1 +0,0 @@
@use 'common';

View File

@@ -18,6 +18,8 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService
private
def process_items(items)
return if items.nil?
names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT)
tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) }
normalized_names = tags.keys

View File

@@ -29,7 +29,12 @@ class BlockDomainService < BaseService
suspend_accounts!
end
DomainClearMediaWorker.perform_async(domain_block.id) if domain_block.reject_media?
if domain_block.suspend?
# Account images and attachments are already handled by `suspend_accounts!`
PurgeCustomEmojiWorker.perform_async(blocked_domain)
elsif domain_block.reject_media?
DomainClearMediaWorker.perform_async(domain_block.id)
end
end
def silence_accounts!

View File

@@ -27,7 +27,7 @@ class CreateCollectionService
@accounts_to_add.each do |account_to_add|
raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@account, account_to_add).feature?
@collection.collection_items.build(account: account_to_add)
@collection.collection_items.build(account: account_to_add, state: :accepted)
end
end

View File

@@ -3,8 +3,6 @@
class DateOfBirthValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors.add(attribute, :below_limit) if value.present? && value.to_date > min_age.ago
rescue Date::Error
record.errors.add(attribute, :invalid)
end
private

View File

@@ -15,7 +15,7 @@
= vite_client_tag
= vite_react_refresh_tag
= vite_polyfills_tag
= theme_style_tags current_theme
= theme_style_tags ['glitch', 'default']
= vite_preload_file_tag "mastodon/locales/#{I18n.locale}.json" # TODO: fix preload for flavour
= render_initial_state
= flavoured_vite_typescript_tag 'embed.tsx', integrity: true, crossorigin: 'anonymous'

View File

@@ -21,8 +21,8 @@
selected: current_user.time_zone || Time.zone.tzinfo.name,
wrapper: :with_label
- if Mastodon::Feature.new_theme_options_enabled?
.fields-group
.fields-group
= f.simple_fields_for :settings, current_user.settings do |ff|
.input.horizontal-options
= ff.input :'web.color_scheme',
as: :radio_buttons,

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
class PurgeCustomEmojiWorker
include Sidekiq::IterableJob
def build_enumerator(domain, cursor:)
return if domain.blank?
active_record_batches_enumerator(CustomEmoji.by_domain_and_subdomains(domain), cursor:)
end
def each_iteration(custom_emojis, _domain)
AttachmentBatch.new(CustomEmoji, custom_emojis).delete
end
end

View File

@@ -2022,10 +2022,7 @@ en:
review_link: Review terms of service
title: The terms of service of %{domain} are changing
themes:
contrast: Mastodon (High contrast)
default: Mastodon (Dark)
mastodon-light: Mastodon (Light)
system: Automatic (use system theme)
default: Mastodon
time:
formats:
default: "%b %d, %Y, %H:%M"

View File

@@ -20,7 +20,7 @@ defaults: &defaults
preview_sensitive_media: false
noindex: false
flavour: 'glitch'
skin: 'system'
skin: 'default'
trends: true
trendable_by_default: false
trending_status_cw: true

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
class MigrateDefaultThemeSetting < ActiveRecord::Migration[8.0]
class Setting < ApplicationRecord; end
def up
Setting.reset_column_information
setting = Setting.find_by(var: 'theme')
return unless setting.present? && setting.attributes['value'].present?
theme = YAML.safe_load(setting.attributes['value'], permitted_classes: [ActiveSupport::HashWithIndifferentAccess, Symbol])
return unless %w(mastodon-light contrast system).include?(theme)
setting.update_column('value', "--- default\n")
end
def down; end
end

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
class MigrateUserTheme < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
# Dummy classes, to make migration possible across version changes
class User < ApplicationRecord; end
def up
User.where.not(settings: nil).find_each do |user|
settings = Oj.load(user.attributes_before_type_cast['settings'])
next if settings.nil? || settings['theme'].blank? || %w(system default mastodon-light contrast).exclude?(settings['theme'])
case settings['theme']
when 'default'
settings['web.color_scheme'] = 'dark'
settings['web.contrast'] = 'auto'
when 'contrast'
settings['web.color_scheme'] = 'dark'
settings['web.contrast'] = 'high'
when 'mastodon-light'
settings['web.color_scheme'] = 'light'
settings['web.contrast'] = 'auto'
end
settings['theme'] = 'default'
user.update_column('settings', Oj.dump(settings))
end
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_01_27_141820) do
ActiveRecord::Schema[8.0].define(version: 2026_02_09_143308) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"

View File

@@ -154,6 +154,11 @@ namespace :tests do
exit(1)
end
unless Setting.theme == 'default'
puts 'Default theme setting not migrated as expected'
exit(1)
end
puts 'No errors found. Database state is consistent with a successful migration process.'
end
@@ -177,7 +182,8 @@ namespace :tests do
(id, thing_type, thing_id, var, value, created_at, updated_at)
VALUES
(7, NULL, NULL, 'timeline_preview', E'--- false\n', now(), now()),
(8, NULL, NULL, 'trends_as_landing_page', E'--- false\n', now(), now());
(8, NULL, NULL, 'trends_as_landing_page', E'--- false\n', now(), now()),
(9, NULL, NULL, 'theme', E'--- system', now(), now());
/* Doorkeeper records
While the `read:me` scope was technically not valid in 3.3.0,

View File

@@ -6,35 +6,13 @@ RSpec.describe ThemeHelper do
describe 'theme_style_tags' do
let(:result) { helper.theme_style_tags(theme) }
context 'when using "system" theme' do
let(:theme) { ['glitch', 'system'] }
it 'returns the default theme' do
expect(html_links.first.attributes.symbolize_keys)
.to include(
href: have_attributes(value: match(/contrast/))
)
end
end
context 'when using "default" theme' do
let(:theme) { ['glitch', 'default'] }
it 'returns the default stylesheet' do
expect(html_links.last.attributes.symbolize_keys)
.to include(
href: have_attributes(value: match(/contrast/))
)
end
end
context 'when using other theme' do
let(:theme) { ['glitch', 'contrast'] }
it 'returns the theme stylesheet without color scheme information' do
expect(html_links.first.attributes.symbolize_keys)
.to include(
href: have_attributes(value: match(/contrast/))
href: have_attributes(value: match(/default/))
)
end
end
@@ -122,6 +100,48 @@ RSpec.describe ThemeHelper do
end
end
describe '#current_theme' do
subject { helper.current_theme }
context 'when user is not signed in' do
context 'when theme was not changed in settings' do
it { is_expected.to eq(['glitch', 'default']) }
end
end
context 'when user is signed in' do
before { allow(helper).to receive(:current_user).and_return(current_user) }
let(:current_user) { Fabricate :user }
context 'when user did not set theme' do
it { is_expected.to eq(['glitch', 'default']) }
end
context 'when user set theme' do
before { current_user.settings.update(skin: 'alternate', noindex: false) }
context 'when theme is not valid' do
it { is_expected.to eq(['glitch', 'default']) }
end
end
end
end
describe '#page_color_scheme' do
subject { helper.page_color_scheme }
context 'when force_color_scheme is present' do
before { helper.content_for(:force_color_scheme) { 'value' } }
it { is_expected.to eq('value') }
end
context 'when force_color_scheme is absent' do
it { is_expected.to eq('auto') }
end
end
private
def html_links

View File

@@ -13,8 +13,6 @@ RSpec.describe 'Settings preferences appearance page' do
expect(page)
.to have_private_cache_control
# TODO: glitch-soc's option is elsewhere
# select 'contrast', from: theme_selection_field
check confirm_reblog_field
uncheck confirm_delete_field

View File

@@ -10,7 +10,7 @@ RSpec.describe DateOfBirthValidator do
context 'with an invalid date' do
let(:invalid_date) { '76.830.10' }
it { is_expected.to_not allow_values(invalid_date).for(:date_of_birth) }
it { is_expected.to_not allow_values(invalid_date).for(:date_of_birth).with_message(:blank) }
end
context 'with a date below the age limit' do

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe PurgeCustomEmojiWorker do
let(:worker) { described_class.new }
let(:domain) { 'evil' }
before do
Fabricate(:custom_emoji)
Fabricate(:custom_emoji, domain: 'example.com')
Fabricate.times(5, :custom_emoji, domain: domain)
end
describe '#perform' do
context 'when domain is nil' do
it 'does not delete emojis' do
expect { worker.perform(nil) }
.to_not(change(CustomEmoji, :count))
end
end
context 'when passing a domain' do
it 'deletes emojis from this domain only' do
expect { worker.perform(domain) }
.to change { CustomEmoji.where(domain: domain).count }.to(0)
.and not_change { CustomEmoji.local.count }
.and(not_change { CustomEmoji.where(domain: 'example.com').count })
end
end
end
end

View File

@@ -376,6 +376,7 @@ const startServer = async () => {
req.scopes = result.rows[0].scopes.split(' ');
req.accountId = result.rows[0].account_id;
req.chosenLanguages = result.rows[0].chosen_languages;
req.permissions = result.rows[0].permissions;
return {
accessTokenId: result.rows[0].id,
@@ -601,13 +602,13 @@ const startServer = async () => {
/**
* @param {string} kind
* @param {ResolvedAccount} account
* @param {Request} req
* @returns {Promise.<{ localAccess: boolean, remoteAccess: boolean }>}
*/
const getFeedAccessSettings = async (kind, account) => {
const getFeedAccessSettings = async (kind, req) => {
const access = { localAccess: true, remoteAccess: true };
if (account.permissions & PERMISSION_VIEW_FEEDS) {
if (req.permissions & PERMISSION_VIEW_FEEDS) {
return access;
}