mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-28 17:50:01 +01:00
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:
@@ -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';
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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] }
|
||||
|
||||
@@ -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
|
||||
@@ -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) }),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
64
app/javascript/mastodon/hooks/useStorage.ts
Normal file
64
app/javascript/mastodon/hooks/useStorage.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@use '@/flavours/glitch/styles/contrast';
|
||||
@@ -1,12 +0,0 @@
|
||||
en:
|
||||
skins:
|
||||
glitch:
|
||||
contrast: High contrast
|
||||
cs:
|
||||
skins:
|
||||
glitch:
|
||||
contrast: Vysoký kontrast
|
||||
es:
|
||||
skins:
|
||||
glitch:
|
||||
contrast: Alto contraste
|
||||
@@ -1 +0,0 @@
|
||||
@use '@/flavours/glitch/styles/mastodon-light';
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
@use '@/styles/contrast';
|
||||
@@ -1,12 +0,0 @@
|
||||
en:
|
||||
skins:
|
||||
vanilla:
|
||||
contrast: High contrast
|
||||
cs:
|
||||
skins:
|
||||
vanilla:
|
||||
contrast: Vysoký kontrast
|
||||
es:
|
||||
skins:
|
||||
vanilla:
|
||||
contrast: Alto contraste
|
||||
@@ -1 +0,0 @@
|
||||
@use '@/styles/mastodon-light';
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
@use 'common';
|
||||
@@ -1 +0,0 @@
|
||||
@use 'common';
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
app/workers/purge_custom_emoji_worker.rb
Normal file
15
app/workers/purge_custom_emoji_worker.rb
Normal 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
19
db/migrate/20260209142402_migrate_default_theme_setting.rb
Normal file
19
db/migrate/20260209142402_migrate_default_theme_setting.rb
Normal 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
|
||||
31
db/migrate/20260209143308_migrate_user_theme.rb
Normal file
31
db/migrate/20260209143308_migrate_user_theme.rb
Normal 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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
33
spec/workers/purge_custom_emoji_worker_spec.rb
Normal file
33
spec/workers/purge_custom_emoji_worker_spec.rb
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user