Merge pull request #143 from mkljczk/config-migration
pl-fe: remove immutable usage from compose reducer
This commit is contained in:
commit
d3d4bc1674
41 changed files with 390 additions and 418 deletions
|
@ -87,7 +87,6 @@
|
|||
"fuzzysort": "^3.1.0",
|
||||
"graphemesplit": "^2.4.4",
|
||||
"html-react-parser": "^5.1.18",
|
||||
"immutable": "^4.3.7",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
"intl-pluralrules": "^2.0.1",
|
||||
|
|
|
@ -318,7 +318,7 @@ const needsDescriptions = (state: RootState, composeId: string) => {
|
|||
const media = state.compose[composeId]!.media_attachments;
|
||||
const missingDescriptionModal = useSettingsStore.getState().settings.missingDescriptionModal;
|
||||
|
||||
const hasMissing = media.filter(item => !item.description).size > 0;
|
||||
const hasMissing = media.filter(item => !item.description).length > 0;
|
||||
|
||||
return missingDescriptionModal && hasMissing;
|
||||
};
|
||||
|
@ -357,7 +357,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
return;
|
||||
}
|
||||
|
||||
if ((!status || !status.length) && media.size === 0) {
|
||||
if ((!status || !status.length) && media.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -392,7 +392,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
status,
|
||||
in_reply_to_id: compose.in_reply_to || undefined,
|
||||
quote_id: compose.quote || undefined,
|
||||
media_ids: media.map(item => item.id).toArray(),
|
||||
media_ids: media.map(item => item.id),
|
||||
sensitive: compose.sensitive,
|
||||
spoiler_text: compose.spoiler_text,
|
||||
visibility: compose.privacy,
|
||||
|
@ -471,7 +471,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
|||
const progress = new Array(files.length).fill(0);
|
||||
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
|
||||
|
||||
const mediaCount = media ? media.size : 0;
|
||||
const mediaCount = media ? media.length : 0;
|
||||
|
||||
if (files.length + mediaCount > attachmentLimit) {
|
||||
toast.error(messages.uploadErrorLimit);
|
||||
|
@ -726,7 +726,7 @@ const insertIntoTagHistory = (composeId: string, recognizedTags: Array<Tag>, tex
|
|||
.map(tag => tag.name);
|
||||
const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
|
||||
|
||||
names.push(...intersectedOldHistory.toJS());
|
||||
names.push(...intersectedOldHistory);
|
||||
|
||||
const newHistory = names.slice(0, 1000);
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { getHost } from 'pl-fe/actions/instance';
|
||||
import { normalizePlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import KVStore from 'pl-fe/storage/kv-store';
|
||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||
|
||||
|
@ -17,10 +18,8 @@ const PLFE_CONFIG_REMEMBER_SUCCESS = 'PLFE_CONFIG_REMEMBER_SUCCESS' as const;
|
|||
|
||||
const getPlFeConfig = createSelector([
|
||||
(state: RootState) => state.plfe,
|
||||
], (plfe) => {
|
||||
// Do some additional normalization with the state
|
||||
return normalizePlFeConfig(plfe);
|
||||
});
|
||||
// Do some additional normalization with the state
|
||||
], (plfe) => v.parse(plFeConfigSchema, plfe));
|
||||
|
||||
const rememberPlFeConfig = (host: string | null) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
|
|
|
@ -28,7 +28,7 @@ const processTimelineUpdate = (timeline: string, status: BaseStatus) =>
|
|||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const me = getState().me;
|
||||
const ownStatus = status.account?.id === me;
|
||||
const hasPendingStatuses = !getState().pending_statuses.isEmpty();
|
||||
const hasPendingStatuses = !!getState().pending_statuses.length;
|
||||
|
||||
const columnSettings = useSettingsStore.getState().settings.timelines[timeline];
|
||||
const shouldSkipQueue = shouldFilter({
|
||||
|
|
|
@ -22,7 +22,7 @@ const checkComposeContent = (compose?: Compose) =>
|
|||
!!compose && [
|
||||
compose.editorState && compose.editorState.length > 0,
|
||||
compose.spoiler_text.length > 0,
|
||||
compose.media_attachments.size > 0,
|
||||
compose.media_attachments.length > 0,
|
||||
compose.poll !== null,
|
||||
].some(check => check === true);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ const Navlinks: React.FC<INavlinks> = ({ type }) => {
|
|||
return (
|
||||
<footer className='relative mx-auto mt-auto max-w-7xl py-8'>
|
||||
<div className='flex flex-wrap justify-center'>
|
||||
{navlinks.get(type)?.map((link, idx) => {
|
||||
{navlinks[type]?.map((link, idx) => {
|
||||
const url = link.url;
|
||||
const isExternal = url.startsWith('http');
|
||||
const Comp = (isExternal ? 'a' : Link) as 'a';
|
||||
|
@ -26,7 +26,7 @@ const Navlinks: React.FC<INavlinks> = ({ type }) => {
|
|||
<div key={idx} className='px-5 py-2'>
|
||||
<Comp {...compProps} className='text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<Text tag='span' theme='inherit' size='sm'>
|
||||
{(link.getIn(['titleLocales', locale]) || link.get('title')) as string}
|
||||
{link.titleLocales[locale] || link.title}
|
||||
</Text>
|
||||
</Comp>
|
||||
</div>
|
||||
|
|
|
@ -158,20 +158,20 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
|||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<SiteErrorBoundaryLink href={links.get('status')!}>
|
||||
{links.status && (
|
||||
<SiteErrorBoundaryLink href={links.status}>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
||||
{links.get('help') && (
|
||||
<SiteErrorBoundaryLink href={links.get('help')!}>
|
||||
{links.help && (
|
||||
<SiteErrorBoundaryLink href={links.help}>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
||||
{links.get('support') && (
|
||||
<SiteErrorBoundaryLink href={links.get('support')!}>
|
||||
{links.support && (
|
||||
<SiteErrorBoundaryLink href={links.support}>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
|
|
@ -24,9 +24,9 @@ const AboutPage: React.FC = () => {
|
|||
|
||||
const { aboutPages } = plFeConfig;
|
||||
|
||||
const page = aboutPages.get(slug || 'about');
|
||||
const defaultLocale = page?.get('default') as string | undefined;
|
||||
const pageLocales = page?.get('locales', []) as string[];
|
||||
const page = aboutPages[slug || 'about'];
|
||||
const defaultLocale = page?.defaultLocale;
|
||||
const pageLocales = page?.locales || [];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLocale = Boolean(page && locale !== defaultLocale && pageLocales.includes(locale));
|
||||
|
|
|
@ -72,7 +72,7 @@ const CaptchaField: React.FC<ICaptchaField> = ({
|
|||
};
|
||||
}, [idempotencyKey]);
|
||||
|
||||
switch (captcha.get('type')) {
|
||||
switch (captcha.type) {
|
||||
case 'native':
|
||||
return (
|
||||
<div>
|
||||
|
|
|
@ -96,7 +96,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
const hasPoll = !!compose.poll;
|
||||
const isEditing = compose.id !== null;
|
||||
const anyMedia = compose.media_attachments.size > 0;
|
||||
const anyMedia = compose.media_attachments.length > 0;
|
||||
|
||||
const [composeFocused, setComposeFocused] = useState(false);
|
||||
|
||||
|
@ -189,7 +189,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
</HStack>
|
||||
), [features, id, anyMedia]);
|
||||
|
||||
const showModifiers = !condensed && (compose.media_attachments.size || compose.is_uploading || compose.poll?.options.length || compose.schedule);
|
||||
const showModifiers = !condensed && (compose.media_attachments.length || compose.is_uploading || compose.poll?.options.length || compose.schedule);
|
||||
|
||||
const composeModifiers = showModifiers && (
|
||||
<Stack space={4} className='font-[inherit] text-sm text-gray-900'>
|
||||
|
|
|
@ -90,7 +90,7 @@ const Option: React.FC<IOption> = ({
|
|||
maxLength={maxChars}
|
||||
value={title}
|
||||
onChange={handleOptionTitleChange}
|
||||
suggestions={suggestions.toArray()}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSuggestionSelected}
|
||||
|
|
|
@ -37,7 +37,7 @@ const SpoilerInput: React.FC<ISpoilerInput> = ({
|
|||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={handleChangeSpoilerText}
|
||||
suggestions={suggestions.toArray()}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSuggestionSelected}
|
||||
|
|
|
@ -38,13 +38,13 @@ const UploadForm: React.FC<IUploadForm> = ({ composeId, onSubmit }) => {
|
|||
dragOverItem.current = null;
|
||||
}, [dragItem, dragOverItem]);
|
||||
|
||||
if (!isUploading && mediaIds.isEmpty()) return null;
|
||||
if (!isUploading && !mediaIds.length) return null;
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden'>
|
||||
<UploadProgress composeId={composeId} />
|
||||
|
||||
<HStack wrap className={clsx('overflow-hidden', mediaIds.size !== 0 && 'm-[-5px]')}>
|
||||
<HStack wrap className={clsx('overflow-hidden', mediaIds.length > 0 && 'm-[-5px]')}>
|
||||
{mediaIds.map((id: string) => (
|
||||
<Upload
|
||||
id={id}
|
||||
|
|
|
@ -303,7 +303,7 @@ const AutosuggestPlugin = ({
|
|||
};
|
||||
|
||||
const onSelectSuggestion = (index: number) => {
|
||||
const suggestion = suggestions.get(index) as AutoSuggestion;
|
||||
const suggestion = suggestions[index];
|
||||
|
||||
editor.update(() => {
|
||||
dispatch((dispatch, getState) => {
|
||||
|
@ -446,11 +446,11 @@ const AutosuggestPlugin = ({
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (suggestions && suggestions.size > 0) setSuggestionsHidden(false);
|
||||
if (suggestions && suggestions.length > 0) setSuggestionsHidden(false);
|
||||
}, [suggestions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolution !== null && !suggestionsHidden && !suggestions.isEmpty()) {
|
||||
if (resolution !== null && !suggestionsHidden && suggestions.length) {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
|
@ -462,7 +462,7 @@ const AutosuggestPlugin = ({
|
|||
|
||||
return () => document.removeEventListener('click', handleClick);
|
||||
}
|
||||
}, [resolution, suggestionsHidden, suggestions.isEmpty()]);
|
||||
}, [resolution, suggestionsHidden, !suggestions.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolution === null) return;
|
||||
|
@ -472,8 +472,8 @@ const AutosuggestPlugin = ({
|
|||
KEY_ARROW_UP_COMMAND,
|
||||
(payload) => {
|
||||
const event = payload;
|
||||
if (suggestions !== null && suggestions.size && selectedSuggestion !== null) {
|
||||
const newSelectedSuggestion = selectedSuggestion !== 0 ? selectedSuggestion - 1 : suggestions.size - 1;
|
||||
if (suggestions !== null && suggestions.length && selectedSuggestion !== null) {
|
||||
const newSelectedSuggestion = selectedSuggestion !== 0 ? selectedSuggestion - 1 : suggestions.length - 1;
|
||||
setSelectedSuggestion(newSelectedSuggestion);
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
@ -486,8 +486,8 @@ const AutosuggestPlugin = ({
|
|||
KEY_ARROW_DOWN_COMMAND,
|
||||
(payload) => {
|
||||
const event = payload;
|
||||
if (suggestions !== null && suggestions.size && selectedSuggestion !== null) {
|
||||
const newSelectedSuggestion = selectedSuggestion !== suggestions.size - 1 ? selectedSuggestion + 1 : 0;
|
||||
if (suggestions !== null && suggestions.length && selectedSuggestion !== null) {
|
||||
const newSelectedSuggestion = selectedSuggestion !== suggestions.length - 1 ? selectedSuggestion + 1 : 0;
|
||||
setSelectedSuggestion(newSelectedSuggestion);
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
@ -543,8 +543,8 @@ const AutosuggestPlugin = ({
|
|||
<div
|
||||
className={clsx({
|
||||
'scroll-smooth snap-y snap-always will-change-scroll mt-6 overflow-y-auto max-h-56 relative w-max z-[1000] shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||
hidden: suggestionsHidden || suggestions.isEmpty(),
|
||||
block: !suggestionsHidden && !suggestions.isEmpty(),
|
||||
hidden: suggestionsHidden || !suggestions.length,
|
||||
block: !suggestionsHidden && suggestions.length,
|
||||
})}
|
||||
>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
|
|
|
@ -22,9 +22,9 @@ const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Ele
|
|||
const history = useHistory();
|
||||
const instance = useInstance();
|
||||
|
||||
const addresses = usePlFeConfig().get('cryptoAddresses');
|
||||
const addresses = usePlFeConfig().cryptoAddresses;
|
||||
|
||||
if (limit === 0 || addresses.size === 0) {
|
||||
if (limit === 0 || addresses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Ele
|
|||
<Widget
|
||||
title={<FormattedMessage id='crypto_donate_panel.heading' defaultMessage='Donate Cryptocurrency' />}
|
||||
onActionClick={handleAction}
|
||||
actionTitle={intl.formatMessage(messages.actionTitle, { count: addresses.size })}
|
||||
actionTitle={intl.formatMessage(messages.actionTitle, { count: addresses.length })}
|
||||
>
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -11,7 +11,7 @@ interface ISiteWallet {
|
|||
|
||||
const SiteWallet: React.FC<ISiteWallet> = ({ limit }): JSX.Element => {
|
||||
const { cryptoAddresses } = usePlFeConfig();
|
||||
const addresses = typeof limit === 'number' ? cryptoAddresses.take(limit) : cryptoAddresses;
|
||||
const addresses = typeof limit === 'number' ? cryptoAddresses.slice(0, limit) : cryptoAddresses;
|
||||
|
||||
return (
|
||||
<Stack space={4}>
|
||||
|
|
|
@ -5,7 +5,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Input from 'pl-fe/components/ui/input';
|
||||
|
||||
import type { StreamfieldComponent } from 'pl-fe/components/ui/streamfield';
|
||||
import type { CryptoAddress } from 'pl-fe/types/pl-fe';
|
||||
import type { CryptoAddress } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
const messages = defineMessages({
|
||||
ticker: { id: 'plfe_config.crypto_address.meta_fields.ticker_placeholder', defaultMessage: 'Ticker' },
|
||||
|
@ -17,7 +17,7 @@ const CryptoAddressInput: StreamfieldComponent<CryptoAddress> = ({ value, onChan
|
|||
const intl = useIntl();
|
||||
|
||||
const handleChange = (key: 'ticker' | 'address' | 'note'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||
onChange(value.set(key, e.currentTarget.value));
|
||||
onChange({ ...value, [key]: e.currentTarget.value });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,7 +5,7 @@ import HStack from 'pl-fe/components/ui/hstack';
|
|||
import Input from 'pl-fe/components/ui/input';
|
||||
|
||||
import type { StreamfieldComponent } from 'pl-fe/components/ui/streamfield';
|
||||
import type { FooterItem } from 'pl-fe/types/pl-fe';
|
||||
import type { FooterItem } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'plfe_config.home_footer.meta_fields.label_placeholder', defaultMessage: 'Label' },
|
||||
|
@ -16,7 +16,7 @@ const PromoPanelInput: StreamfieldComponent<FooterItem> = ({ value, onChange })
|
|||
const intl = useIntl();
|
||||
|
||||
const handleChange = (key: 'title' | 'url'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||
onChange(value.set(key, e.currentTarget.value));
|
||||
onChange({ ...value, [key]: e.currentTarget.value });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,7 +7,7 @@ import Input from 'pl-fe/components/ui/input';
|
|||
import IconPicker from './icon-picker';
|
||||
|
||||
import type { StreamfieldComponent } from 'pl-fe/components/ui/streamfield';
|
||||
import type { PromoPanelItem } from 'pl-fe/types/pl-fe';
|
||||
import type { PromoPanelItem } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
const messages = defineMessages({
|
||||
icon: { id: 'plfe_config.promo_panel.meta_fields.icon_placeholder', defaultMessage: 'Icon' },
|
||||
|
@ -19,11 +19,11 @@ const PromoPanelInput: StreamfieldComponent<PromoPanelItem> = ({ value, onChange
|
|||
const intl = useIntl();
|
||||
|
||||
const handleIconChange = (icon: string) => {
|
||||
onChange(value.set('icon', icon));
|
||||
onChange({ ...value, icon });
|
||||
};
|
||||
|
||||
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||
onChange(value.set(key, e.currentTarget.value));
|
||||
onChange({ ...value, [key]: e.currentTarget.value });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import BackgroundShapes from 'pl-fe/features/ui/components/background-shapes';
|
||||
import { useSystemTheme } from 'pl-fe/hooks/use-system-theme';
|
||||
import { normalizePlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { useSettingsStore } from 'pl-fe/stores/settings';
|
||||
import { generateThemeCss } from 'pl-fe/utils/theme';
|
||||
|
||||
|
@ -15,7 +16,7 @@ interface ISitePreview {
|
|||
|
||||
/** Renders a preview of the website's style with the configuration applied. */
|
||||
const SitePreview: React.FC<ISitePreview> = ({ plFe }) => {
|
||||
const plFeConfig = useMemo(() => normalizePlFeConfig(plFe), [plFe]);
|
||||
const plFeConfig = useMemo(() => v.parse(plFeConfigSchema, plFe), [plFe]);
|
||||
const { defaultSettings } = useSettingsStore();
|
||||
|
||||
const userTheme = defaultSettings.themeMode;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { create } from 'mutative';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { updatePlFeConfig } from 'pl-fe/actions/admin';
|
||||
import { uploadMedia } from 'pl-fe/actions/media';
|
||||
|
@ -21,7 +22,7 @@ import ThemeSelector from 'pl-fe/features/ui/components/theme-selector';
|
|||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { normalizePlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { cryptoAddressSchema, footerItemSchema, plFeConfigSchema, promoPanelItemSchema, type PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import CryptoAddressInput from './components/crypto-address-input';
|
||||
|
@ -34,13 +35,11 @@ const messages = defineMessages({
|
|||
saved: { id: 'plfe_config.saved', defaultMessage: 'pl-fe config saved!' },
|
||||
copyrightFooterLabel: { id: 'plfe_config.copyright_footer.meta_fields.label_placeholder', defaultMessage: 'Copyright footer' },
|
||||
cryptoDonatePanelLimitLabel: { id: 'plfe_config.crypto_donate_panel_limit.meta_fields.limit_placeholder', defaultMessage: 'Number of items to display in the crypto homepage widget' },
|
||||
customCssLabel: { id: 'plfe_config.custom_css.meta_fields.url_placeholder', defaultMessage: 'URL' },
|
||||
rawJSONLabel: { id: 'plfe_config.raw_json_label', defaultMessage: 'Advanced: Edit raw JSON data' },
|
||||
rawJSONHint: { id: 'plfe_config.raw_json_hint', defaultMessage: 'Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click "Save" to apply your changes.' },
|
||||
rawJSONInvalid: { id: 'plfe_config.raw_json_invalid', defaultMessage: 'is invalid' },
|
||||
displayFqnLabel: { id: 'plfe_config.display_fqn_label', defaultMessage: 'Display domain (eg @user@domain) for local accounts.' },
|
||||
greentextLabel: { id: 'plfe_config.greentext_label', defaultMessage: 'Enable greentext support' },
|
||||
promoPanelIconsLink: { id: 'plfe_config.hints.promo_panel_icons.link', defaultMessage: 'pl-fe Icons List' },
|
||||
authenticatedProfileLabel: { id: 'plfe_config.authenticated_profile_label', defaultMessage: 'Profiles require authentication' },
|
||||
authenticatedProfileHint: { id: 'plfe_config.authenticated_profile_hint', defaultMessage: 'Users must be logged-in to view replies and media on user profiles.' },
|
||||
displayCtaLabel: { id: 'plfe_config.cta_label', defaultMessage: 'Display call to action panels if not authenticated' },
|
||||
|
@ -56,18 +55,11 @@ const messages = defineMessages({
|
|||
sentryDsnHint: { id: 'plfe_config.sentry_dsn_hint', defaultMessage: 'DSN URL for error reporting. Works with Sentry and GlitchTip.' },
|
||||
});
|
||||
|
||||
type ValueGetter<T = Element> = (e: React.ChangeEvent<T>) => any;
|
||||
type Template = ImmutableMap<string, any>;
|
||||
type ConfigPath = Array<string | number>;
|
||||
type ValueGetter<T1 = Element, T2 = any> = (e: React.ChangeEvent<T1>) => T2;
|
||||
type StreamItemConfigPath = ['promoPanel', 'items'] | ['navlinks', 'homeFooter'] | ['cryptoAddresses'];
|
||||
type ThemeChangeHandler = (theme: string) => void;
|
||||
|
||||
const templates: Record<string, Template> = {
|
||||
promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }),
|
||||
footerItem: ImmutableMap({ title: '', url: '' }),
|
||||
cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }),
|
||||
};
|
||||
|
||||
const PlFeConfig: React.FC = () => {
|
||||
const PlFeConfigEditor: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -76,26 +68,25 @@ const PlFeConfig: React.FC = () => {
|
|||
const initialData = useAppSelector(state => state.plfe);
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(initialData);
|
||||
const [data, setData] = useState(v.parse(plFeConfigSchema, initialData));
|
||||
const [jsonEditorExpanded, setJsonEditorExpanded] = useState(false);
|
||||
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(initialData, null, 2));
|
||||
const [jsonValid, setJsonValid] = useState(true);
|
||||
|
||||
const plFe = useMemo(() => normalizePlFeConfig(data), [data]);
|
||||
const plFe = useMemo(() => v.parse(plFeConfigSchema, data), [data]);
|
||||
|
||||
const setConfig = (path: ConfigPath, value: any) => {
|
||||
const newData = data.setIn(path, value);
|
||||
const setConfig = (newData: PlFeConfig) => {
|
||||
setData(newData);
|
||||
setJsonValid(true);
|
||||
};
|
||||
|
||||
const putConfig = (newData: any) => {
|
||||
const putConfig = (newData: PlFeConfig) => {
|
||||
setData(newData);
|
||||
setJsonValid(true);
|
||||
};
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (e) => {
|
||||
dispatch(updatePlFeConfig(data.toJS())).then(() => {
|
||||
dispatch(updatePlFeConfig(data)).then(() => {
|
||||
setLoading(false);
|
||||
toast.success(intl.formatMessage(messages.saved));
|
||||
}).catch(() => {
|
||||
|
@ -105,15 +96,20 @@ const PlFeConfig: React.FC = () => {
|
|||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleChange = (path: ConfigPath, getValue: ValueGetter<any>): React.ChangeEventHandler => e => {
|
||||
setConfig(path, getValue(e));
|
||||
const handleChange = (path: keyof PlFeConfig, getValue: ValueGetter<any, PlFeConfig[typeof path]>): React.ChangeEventHandler => e => {
|
||||
const newData: PlFeConfig = { ...data, [path]: getValue(e) };
|
||||
setConfig(newData);
|
||||
};
|
||||
|
||||
const handleThemeChange = (path: ConfigPath): ThemeChangeHandler => theme => {
|
||||
setConfig(path, theme);
|
||||
const handleThemeChange: ThemeChangeHandler = (theme) => {
|
||||
const newData = create(data, (draft) => {
|
||||
if (!draft.defaultSettings) draft.defaultSettings = {};
|
||||
draft.defaultSettings.themeMode = theme;
|
||||
});
|
||||
setConfig(newData);
|
||||
};
|
||||
|
||||
const handleFileChange = (path: ConfigPath): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||
const handleFileChange = (path: keyof PlFeConfig): React.ChangeEventHandler<HTMLInputElement> => e => {
|
||||
const file = e.target.files?.item(0);
|
||||
|
||||
if (file) {
|
||||
|
@ -123,19 +119,40 @@ const PlFeConfig: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleStreamItemChange = (path: ConfigPath) => (values: any[]) => {
|
||||
setConfig(path, ImmutableList(values));
|
||||
const handleStreamItemChange = (path: StreamItemConfigPath) => (values: any[]) => {
|
||||
const newData = create(data, (draft) => {
|
||||
if (path[0] === 'cryptoAddresses') {
|
||||
draft.cryptoAddresses = values;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
draft[path[0]][path[1]] = values;
|
||||
}
|
||||
});
|
||||
setConfig(newData);
|
||||
};
|
||||
|
||||
const addStreamItem = (path: ConfigPath, template: Template) => () => {
|
||||
let items = data;
|
||||
path.forEach(key => items = items?.[key] || []);
|
||||
setConfig(path, items.push(template));
|
||||
const addStreamItem = <T, >(path: StreamItemConfigPath, schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) => () => {
|
||||
const newData = create(data, (draft) => {
|
||||
if (path[0] === 'cryptoAddresses') {
|
||||
draft.cryptoAddresses.push(v.parse(cryptoAddressSchema, {}));
|
||||
} else {
|
||||
// @ts-ignore
|
||||
draft[path[0]][path[1]].push(v.parse(schema, {}));
|
||||
}
|
||||
});
|
||||
setConfig(newData);
|
||||
};
|
||||
|
||||
const deleteStreamItem = (path: ConfigPath) => (i: number) => {
|
||||
const newData = data.deleteIn([...path, i]);
|
||||
setData(newData);
|
||||
const deleteStreamItem = (path: StreamItemConfigPath) => (i: number) => {
|
||||
const newData = create(data, (draft) => {
|
||||
if (path[0] === 'cryptoAddresses') {
|
||||
draft.cryptoAddresses = draft.cryptoAddresses.filter((_, index) => index !== i);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
draft[path[0]][path[1]] = draft[path[0]][path[1]].filter((_, index) => index !== i);
|
||||
}
|
||||
});
|
||||
setConfig(newData);
|
||||
};
|
||||
|
||||
const handleEditJSON: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
|
||||
|
@ -145,7 +162,7 @@ const PlFeConfig: React.FC = () => {
|
|||
const toggleJSONEditor = (expanded: boolean) => setJsonEditorExpanded(expanded);
|
||||
|
||||
useEffect(() => {
|
||||
putConfig(initialData);
|
||||
putConfig(v.parse(plFeConfigSchema, initialData));
|
||||
}, [initialData]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -154,7 +171,7 @@ const PlFeConfig: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data = fromJS(JSON.parse(rawJSON));
|
||||
const data = v.parse(plFeConfigSchema, JSON.parse(rawJSON));
|
||||
putConfig(data);
|
||||
} catch {
|
||||
setJsonValid(false);
|
||||
|
@ -172,7 +189,7 @@ const PlFeConfig: React.FC = () => {
|
|||
hintText={<FormattedMessage id='plfe_config.hints.logo' defaultMessage='SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio' />}
|
||||
>
|
||||
<FileInput
|
||||
onChange={handleFileChange(['logo'])}
|
||||
onChange={handleFileChange('logo')}
|
||||
accept='image/svg+xml,image/png'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -184,8 +201,8 @@ const PlFeConfig: React.FC = () => {
|
|||
<List>
|
||||
<ListItem label={<FormattedMessage id='plfe_config.fields.theme_label' defaultMessage='Default theme' />}>
|
||||
<ThemeSelector
|
||||
value={plFe.defaultSettings.get('themeMode')}
|
||||
onChange={handleThemeChange(['defaultSettings', 'themeMode'])}
|
||||
value={plFe.defaultSettings?.themeMode}
|
||||
onChange={handleThemeChange}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
|
@ -203,14 +220,14 @@ const PlFeConfig: React.FC = () => {
|
|||
<ListItem label={intl.formatMessage(messages.displayFqnLabel)}>
|
||||
<Toggle
|
||||
checked={plFe.displayFqn === true}
|
||||
onChange={handleChange(['displayFqn'], (e) => e.target.checked)}
|
||||
onChange={handleChange('displayFqn', (e) => e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={intl.formatMessage(messages.greentextLabel)}>
|
||||
<Toggle
|
||||
checked={plFe.greentext === true}
|
||||
onChange={handleChange(['greentext'], (e) => e.target.checked)}
|
||||
onChange={handleChange('greentext', (e) => e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
|
@ -220,7 +237,7 @@ const PlFeConfig: React.FC = () => {
|
|||
>
|
||||
<Toggle
|
||||
checked={plFe.feedInjection === true}
|
||||
onChange={handleChange(['feedInjection'], (e) => e.target.checked)}
|
||||
onChange={handleChange('feedInjection', (e) => e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
|
@ -230,14 +247,14 @@ const PlFeConfig: React.FC = () => {
|
|||
>
|
||||
<Toggle
|
||||
checked={plFe.mediaPreview === true}
|
||||
onChange={handleChange(['mediaPreview'], (e) => e.target.checked)}
|
||||
onChange={handleChange('mediaPreview', (e) => e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem label={intl.formatMessage(messages.displayCtaLabel)}>
|
||||
<Toggle
|
||||
checked={plFe.displayCta === true}
|
||||
onChange={handleChange(['displayCta'], (e) => e.target.checked)}
|
||||
onChange={handleChange('displayCta', (e) => e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
|
@ -247,7 +264,7 @@ const PlFeConfig: React.FC = () => {
|
|||
>
|
||||
<Toggle
|
||||
checked={plFe.authenticatedProfile === true}
|
||||
onChange={handleChange(['authenticatedProfile'], (e) => e.target.checked)}
|
||||
onChange={handleChange('authenticatedProfile', (e) => e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
|
@ -259,7 +276,7 @@ const PlFeConfig: React.FC = () => {
|
|||
type='text'
|
||||
placeholder='/timeline/local'
|
||||
value={String(data.redirectRootNoLogin || '')}
|
||||
onChange={handleChange(['redirectRootNoLogin'], (e) => e.target.value)}
|
||||
onChange={handleChange('redirectRootNoLogin', (e) => e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
|
@ -271,7 +288,7 @@ const PlFeConfig: React.FC = () => {
|
|||
type='text'
|
||||
placeholder='https://01234abcdef@glitch.tip.tld/5678'
|
||||
value={String(data.sentryDsn || '')}
|
||||
onChange={handleChange(['sentryDsn'], (e) => e.target.value)}
|
||||
onChange={handleChange('sentryDsn', (e) => e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
@ -284,9 +301,9 @@ const PlFeConfig: React.FC = () => {
|
|||
label={<FormattedMessage id='plfe_config.fields.promo_panel_fields_label' defaultMessage='Promo panel items' />}
|
||||
hint={<FormattedMessage id='plfe_config.hints.promo_panel_fields' defaultMessage='You can have custom defined links displayed on the right panel of the timelines page.' />}
|
||||
component={PromoPanelInput}
|
||||
values={plFe.promoPanel.items.toArray()}
|
||||
values={plFe.promoPanel.items}
|
||||
onChange={handleStreamItemChange(['promoPanel', 'items'])}
|
||||
onAddItem={addStreamItem(['promoPanel', 'items'], templates.promoPanel)}
|
||||
onAddItem={addStreamItem(['promoPanel', 'items'], promoPanelItemSchema)}
|
||||
onRemoveItem={deleteStreamItem(['promoPanel', 'items'])}
|
||||
draggable
|
||||
/>
|
||||
|
@ -295,9 +312,9 @@ const PlFeConfig: React.FC = () => {
|
|||
label={<FormattedMessage id='plfe_config.fields.home_footer_fields_label' defaultMessage='Home footer items' />}
|
||||
hint={<FormattedMessage id='plfe_config.hints.home_footer_fields' defaultMessage='You can have custom defined links displayed on the footer of your static pages' />}
|
||||
component={FooterLinkInput}
|
||||
values={plFe.navlinks.get('homeFooter')?.toArray() || []}
|
||||
values={plFe.navlinks.homeFooter || []}
|
||||
onChange={handleStreamItemChange(['navlinks', 'homeFooter'])}
|
||||
onAddItem={addStreamItem(['navlinks', 'homeFooter'], templates.footerItem)}
|
||||
onAddItem={addStreamItem(['navlinks', 'homeFooter'], footerItemSchema)}
|
||||
onRemoveItem={deleteStreamItem(['navlinks', 'homeFooter'])}
|
||||
draggable
|
||||
/>
|
||||
|
@ -307,7 +324,7 @@ const PlFeConfig: React.FC = () => {
|
|||
type='text'
|
||||
placeholder={intl.formatMessage(messages.copyrightFooterLabel)}
|
||||
value={plFe.copyright}
|
||||
onChange={handleChange(['copyright'], (e) => e.target.value)}
|
||||
onChange={handleChange('copyright', (e) => e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -322,7 +339,7 @@ const PlFeConfig: React.FC = () => {
|
|||
type='text'
|
||||
placeholder={intl.formatMessage(messages.tileServerLabel)}
|
||||
value={plFe.tileServer}
|
||||
onChange={handleChange(['tileServer'], (e) => e.target.value)}
|
||||
onChange={handleChange('tileServer', (e) => e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -331,7 +348,7 @@ const PlFeConfig: React.FC = () => {
|
|||
type='text'
|
||||
placeholder={intl.formatMessage(messages.tileServerAttributionLabel)}
|
||||
value={plFe.tileServerAttribution}
|
||||
onChange={handleChange(['tileServerAttribution'], (e) => e.target.value)}
|
||||
onChange={handleChange('tileServerAttribution', (e) => e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
|
@ -345,9 +362,9 @@ const PlFeConfig: React.FC = () => {
|
|||
label={<FormattedMessage id='plfe_config.fields.crypto_addresses_label' defaultMessage='Cryptocurrency addresses' />}
|
||||
hint={<FormattedMessage id='plfe_config.hints.crypto_addresses' defaultMessage='Add cryptocurrency addresses so users of your site can donate to you. Order matters, and you must use lowercase ticker values.' />}
|
||||
component={CryptoAddressInput}
|
||||
values={plFe.cryptoAddresses.toArray()}
|
||||
values={plFe.cryptoAddresses}
|
||||
onChange={handleStreamItemChange(['cryptoAddresses'])}
|
||||
onAddItem={addStreamItem(['cryptoAddresses'], templates.cryptoAddress)}
|
||||
onAddItem={addStreamItem(['cryptoAddresses'], cryptoAddressSchema)}
|
||||
onRemoveItem={deleteStreamItem(['cryptoAddresses'])}
|
||||
draggable
|
||||
/>
|
||||
|
@ -358,8 +375,8 @@ const PlFeConfig: React.FC = () => {
|
|||
min={0}
|
||||
pattern='[0-9]+'
|
||||
placeholder={intl.formatMessage(messages.cryptoDonatePanelLimitLabel)}
|
||||
value={plFe.cryptoDonatePanel.get('limit')}
|
||||
onChange={handleChange(['cryptoDonatePanel', 'limit'], (e) => Number(e.target.value))}
|
||||
value={plFe.cryptoDonatePanel.limit}
|
||||
onChange={handleChange('cryptoDonatePanel', (e) => ({ limit: Number(e.target.value) }))}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -396,4 +413,4 @@ const PlFeConfig: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export { PlFeConfig as default };
|
||||
export { PlFeConfigEditor as default };
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { updatePlFeConfig } from 'pl-fe/actions/admin';
|
||||
import { getHost } from 'pl-fe/actions/instance';
|
||||
|
@ -14,7 +15,7 @@ import ColorPicker from 'pl-fe/features/pl-fe-config/components/color-picker';
|
|||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { normalizePlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import toast from 'pl-fe/toast';
|
||||
import { download } from 'pl-fe/utils/download';
|
||||
|
||||
|
@ -53,13 +54,15 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
|
|||
const host = useAppSelector(state => getHost(state));
|
||||
const rawConfig = useAppSelector(state => state.plfe);
|
||||
|
||||
const [colors, setColors] = useState(plFe.colors.toJS() as any);
|
||||
const [colors, setColors] = useState(plFe.colors);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [resetKey, setResetKey] = useState(crypto.randomUUID());
|
||||
|
||||
const fileInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const updateColors = (key: string) => (newColors: ColorGroup) => {
|
||||
if (typeof colors[key] === 'string') return;
|
||||
|
||||
setColors({
|
||||
...colors,
|
||||
[key]: {
|
||||
|
@ -82,16 +85,16 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
|
|||
};
|
||||
|
||||
const resetTheme = () => {
|
||||
setTheme(plFe.colors.toJS() as any);
|
||||
setTheme(plFe.colors);
|
||||
};
|
||||
|
||||
const updateTheme = async () => {
|
||||
const params = rawConfig.set('colors', colors).toJS();
|
||||
const params = { ...rawConfig, colors };
|
||||
await dispatch(updatePlFeConfig(params));
|
||||
};
|
||||
|
||||
const restoreDefaultTheme = () => {
|
||||
const colors = normalizePlFeConfig({ brandColor: '#d80482' }).colors.toJS();
|
||||
const colors = v.parse(plFeConfigSchema, { brandColor: '#d80482' }).colors;
|
||||
setTheme(colors);
|
||||
};
|
||||
|
||||
|
@ -110,7 +113,7 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
|
|||
if (file) {
|
||||
const text = await file.text();
|
||||
const json = JSON.parse(text);
|
||||
const colors = normalizePlFeConfig({ colors: json }).colors.toJS();
|
||||
const colors = v.parse(plFeConfigSchema, { colors: json }).colors;
|
||||
|
||||
setTheme(colors);
|
||||
toast.success(intl.formatMessage(messages.importSuccess));
|
||||
|
@ -243,13 +246,13 @@ const ThemeEditor: React.FC<IThemeEditor> = () => {
|
|||
|
||||
interface IPaletteListItem {
|
||||
label: React.ReactNode;
|
||||
palette: ColorGroup;
|
||||
palette: ColorGroup | string;
|
||||
onChange: (palette: ColorGroup) => void;
|
||||
resetKey?: string;
|
||||
}
|
||||
|
||||
/** Palette editor inside a ListItem. */
|
||||
const PaletteListItem: React.FC<IPaletteListItem> = ({ label, palette, onChange, resetKey }) => (
|
||||
const PaletteListItem: React.FC<IPaletteListItem> = ({ label, palette, onChange, resetKey }) => typeof palette === 'string' ? null : (
|
||||
<ListItem label={<div className='w-20'>{label}</div>}>
|
||||
<Palette palette={palette} onChange={onChange} resetKey={resetKey} />
|
||||
</ListItem>
|
||||
|
@ -257,12 +260,14 @@ const PaletteListItem: React.FC<IPaletteListItem> = ({ label, palette, onChange,
|
|||
|
||||
interface IColorListItem {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
value: string | Record<string, string>;
|
||||
onChange: (hex: string) => void;
|
||||
}
|
||||
|
||||
/** Single-color picker. */
|
||||
const ColorListItem: React.FC<IColorListItem> = ({ label, value, onChange }) => {
|
||||
if (typeof value !== 'string') return null;
|
||||
|
||||
const handleChange: ColorChangeHandler = (color, _e) => {
|
||||
onChange(color.hex);
|
||||
};
|
||||
|
|
|
@ -29,7 +29,7 @@ const renderTermsOfServiceLink = (href: string) => (
|
|||
|
||||
const ConfirmationStep: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const links = useAppSelector((state) => getPlFeConfig(state).get('links') as any);
|
||||
const links = useAppSelector((state) => getPlFeConfig(state).links);
|
||||
|
||||
const entity = intl.formatMessage(messages.accountEntity);
|
||||
|
||||
|
@ -42,8 +42,8 @@ const ConfirmationStep: React.FC = () => {
|
|||
<Text>
|
||||
{intl.formatMessage(messages.content, {
|
||||
entity,
|
||||
link: links.get('termsOfService') ?
|
||||
renderTermsOfServiceLink(links.get('termsOfService')) :
|
||||
link: links.termsOfService ?
|
||||
renderTermsOfServiceLink(links.termsOfService) :
|
||||
termsOfServiceText,
|
||||
})}
|
||||
</Text>
|
||||
|
|
|
@ -13,9 +13,9 @@ const PromoPanel: React.FC = () => {
|
|||
const { promoPanel } = usePlFeConfig();
|
||||
const { locale } = useSettings();
|
||||
|
||||
const promoItems = promoPanel.get('items');
|
||||
const promoItems = promoPanel.items;
|
||||
|
||||
if (!promoItems || promoItems.isEmpty()) return null;
|
||||
if (!promoItems || !promoItems.length) return null;
|
||||
|
||||
return (
|
||||
<Widget title={instance.title}>
|
||||
|
@ -27,7 +27,7 @@ const PromoPanel: React.FC = () => {
|
|||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<ForkAwesomeIcon id={item.icon} className='flex-none text-lg' fixedWidth />
|
||||
<span>{item.textLocales.get(locale) || item.text}</span>
|
||||
<span>{item.textLocales[locale] || item.text}</span>
|
||||
</HStack>
|
||||
}
|
||||
size='sm'
|
||||
|
|
|
@ -47,7 +47,7 @@ const PendingStatusMedia: React.FC<IPendingStatusMedia> = ({ status }) => {
|
|||
|
||||
const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, muted, variant = 'rounded' }) => {
|
||||
const status = useAppSelector((state) => {
|
||||
const pendingStatus = state.pending_statuses.get(idempotencyKey);
|
||||
const pendingStatus = state.pending_statuses[idempotencyKey];
|
||||
return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null;
|
||||
});
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
const standalone = useAppSelector(isStandalone);
|
||||
|
||||
const { authenticatedProfile, cryptoAddresses } = usePlFeConfig();
|
||||
const hasCrypto = cryptoAddresses.size > 0;
|
||||
const hasCrypto = cryptoAddresses.length > 0;
|
||||
|
||||
// NOTE: Mastodon and Pleroma route some basenames to the backend.
|
||||
// When adding new routes, use a basename that does NOT conflict
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { create } from 'mutative';
|
||||
import { statusSchema } from 'pl-api';
|
||||
import * as v from 'valibot';
|
||||
|
||||
|
@ -12,17 +12,18 @@ const getAccount = makeGetAccount();
|
|||
|
||||
const buildMentions = (pendingStatus: PendingStatus) => {
|
||||
if (pendingStatus.in_reply_to_id) {
|
||||
return ImmutableList(pendingStatus.to || []).map(acct => ImmutableMap({ acct }));
|
||||
return (pendingStatus.to || []).map(acct => ({ acct }));
|
||||
} else {
|
||||
return ImmutableList();
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildPoll = (pendingStatus: PendingStatus) => {
|
||||
if (pendingStatus.hasIn(['poll', 'options'])) {
|
||||
return pendingStatus.poll!.update('options', (options: ImmutableMap<string, any>) =>
|
||||
options.map((title: string) => ImmutableMap({ title })),
|
||||
);
|
||||
if (pendingStatus.poll?.options) {
|
||||
return create(pendingStatus.poll, (draft) => {
|
||||
// @ts-ignore
|
||||
draft.options = draft.options.map((title) => ({ title }));
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -39,7 +40,7 @@ const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotency
|
|||
id: `末pending-${idempotencyKey}`,
|
||||
in_reply_to_account_id: state.statuses[inReplyToId || '']?.account_id || null,
|
||||
in_reply_to_id: inReplyToId,
|
||||
media_attachments: (pendingStatus.media_ids || ImmutableList()).map((id: string) => ({ id })),
|
||||
media_attachments: (pendingStatus.media_ids || []).map((id: string) => ({ id })),
|
||||
mentions: buildMentions(pendingStatus),
|
||||
poll: buildPoll(pendingStatus),
|
||||
quote: pendingStatus.quote_id ? state.statuses[pendingStatus.quote_id] : null,
|
||||
|
|
|
@ -2,9 +2,7 @@ import { getPlFeConfig } from 'pl-fe/actions/pl-fe';
|
|||
|
||||
import { useAppSelector } from './use-app-selector';
|
||||
|
||||
import type { PlFeConfig } from 'pl-fe/types/pl-fe';
|
||||
|
||||
/** Get the pl-fe config from the store */
|
||||
const usePlFeConfig = (): PlFeConfig => useAppSelector((state) => getPlFeConfig(state));
|
||||
const usePlFeConfig = () => useAppSelector((state) => getPlFeConfig(state));
|
||||
|
||||
export { usePlFeConfig };
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useLocale } from 'pl-fe/hooks/use-locale';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
import { useTheme } from 'pl-fe/hooks/use-theme';
|
||||
import { normalizePlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import { startSentry } from 'pl-fe/sentry';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { generateThemeCss } from 'pl-fe/utils/theme';
|
||||
|
@ -21,7 +22,7 @@ const PlFeHead = () => {
|
|||
|
||||
const withModals = useModalsStore().modals.length > 0;
|
||||
|
||||
const themeCss = generateThemeCss(demo ? normalizePlFeConfig({ brandColor: '#d80482' }) : plFeConfig);
|
||||
const themeCss = generateThemeCss(demo ? v.parse(plFeConfigSchema, { brandColor: '#d80482' }) : plFeConfig);
|
||||
const dsn = plFeConfig.sentryDsn;
|
||||
|
||||
const bodyClass = clsx('h-full bg-white text-base antialiased black:bg-black dark:bg-gray-800', {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { configureMockStore } from '@jedmao/redux-mock-store';
|
|||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, RenderOptions } from '@testing-library/react';
|
||||
import { renderHook, RenderHookOptions } from '@testing-library/react-hooks';
|
||||
import { merge } from 'immutable';
|
||||
import React, { FC, ReactElement } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
@ -37,7 +36,7 @@ const TestApp: FC<any> = ({ children, storeProps, routerProps = {} }) => {
|
|||
if (storeProps && typeof storeProps.getState !== 'undefined') { // storeProps is a store
|
||||
store = storeProps;
|
||||
} else if (storeProps) { // storeProps is state
|
||||
appState = merge(rootState, storeProps);
|
||||
appState = { ...rootState, ...storeProps };
|
||||
store = createTestStore(appState);
|
||||
} else {
|
||||
store = createTestStore(appState);
|
||||
|
|
|
@ -44,8 +44,8 @@ const HomeLayout: React.FC<IHomeLayout> = ({ children }) => {
|
|||
const composeBlock = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const hasCrypto = typeof plFeConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string';
|
||||
const cryptoLimit = plFeConfig.cryptoDonatePanel.get('limit', 0);
|
||||
const hasCrypto = typeof plFeConfig.cryptoAddresses[0]?.ticker === 'string';
|
||||
const cryptoLimit = plFeConfig.cryptoDonatePanel.limit;
|
||||
|
||||
const { isDragging, isDraggedOver } = useDraggedFiles(composeBlock, (files) => {
|
||||
dispatch(uploadCompose(composeId, files, intl));
|
||||
|
|
|
@ -1,23 +1,12 @@
|
|||
import {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { normalizeUsername } from 'pl-fe/utils/input';
|
||||
import { coerceObject, filteredArray } from 'pl-fe/schemas/utils';
|
||||
import { toTailwind } from 'pl-fe/utils/tailwind';
|
||||
import { generateAccent } from 'pl-fe/utils/theme';
|
||||
|
||||
import type {
|
||||
PromoPanelItem,
|
||||
FooterItem,
|
||||
CryptoAddress,
|
||||
} from 'pl-fe/types/pl-fe';
|
||||
|
||||
const DEFAULT_COLORS = ImmutableMap<string, any>({
|
||||
success: ImmutableMap({
|
||||
const DEFAULT_COLORS = {
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
|
@ -28,8 +17,8 @@ const DEFAULT_COLORS = ImmutableMap<string, any>({
|
|||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
}),
|
||||
danger: ImmutableMap({
|
||||
},
|
||||
danger: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
|
@ -40,189 +29,142 @@ const DEFAULT_COLORS = ImmutableMap<string, any>({
|
|||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
}),
|
||||
},
|
||||
'greentext': '#789922',
|
||||
};
|
||||
|
||||
const promoPanelItemSchema = coerceObject({
|
||||
icon: v.fallback(v.string(), ''),
|
||||
text: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.string(), ''),
|
||||
textLocales: v.fallback(v.record(v.string(), v.string()), {}),
|
||||
});
|
||||
|
||||
const PromoPanelItemRecord = ImmutableRecord({
|
||||
icon: '',
|
||||
text: '',
|
||||
url: '',
|
||||
textLocales: ImmutableMap<string, string>(),
|
||||
type PromoPanelItem = v.InferOutput<typeof promoPanelItemSchema>;
|
||||
|
||||
const promoPanelSchema = coerceObject({
|
||||
items: filteredArray(promoPanelItemSchema),
|
||||
});
|
||||
|
||||
const PromoPanelRecord = ImmutableRecord({
|
||||
items: ImmutableList<PromoPanelItem>(),
|
||||
type PromoPanel = v.InferOutput<typeof promoPanelSchema>;
|
||||
|
||||
const footerItemSchema = coerceObject({
|
||||
title: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.string(), ''),
|
||||
titleLocales: v.fallback(v.record(v.string(), v.string()), {}),
|
||||
});
|
||||
|
||||
const FooterItemRecord = ImmutableRecord({
|
||||
title: '',
|
||||
url: '',
|
||||
});
|
||||
type FooterItem = v.InferOutput<typeof footerItemSchema>;
|
||||
|
||||
const CryptoAddressRecord = ImmutableRecord({
|
||||
address: '',
|
||||
note: '',
|
||||
ticker: '',
|
||||
});
|
||||
const cryptoAddressSchema = v.pipe(coerceObject({
|
||||
address: v.fallback(v.string(), ''),
|
||||
note: v.fallback(v.string(), ''),
|
||||
ticker: v.fallback(v.string(), ''),
|
||||
}), v.transform((address) => {
|
||||
address.ticker = trimStart(address.ticker, '$').toLowerCase();
|
||||
return address;
|
||||
}));
|
||||
|
||||
const PlFeConfigRecord = ImmutableRecord({
|
||||
appleAppId: null,
|
||||
authProvider: '',
|
||||
logo: '',
|
||||
logoDarkMode: null,
|
||||
banner: '',
|
||||
brandColor: '', // Empty
|
||||
accentColor: '',
|
||||
colors: ImmutableMap(),
|
||||
copyright: `♥${new Date().getFullYear()}. Copying is an act of love. Please copy and share.`,
|
||||
customCss: ImmutableList<string>(),
|
||||
defaultSettings: ImmutableMap<string, any>(),
|
||||
extensions: ImmutableMap(),
|
||||
gdpr: false,
|
||||
gdprUrl: '',
|
||||
greentext: false,
|
||||
promoPanel: PromoPanelRecord(),
|
||||
navlinks: ImmutableMap({
|
||||
homeFooter: ImmutableList<FooterItem>(),
|
||||
type CryptoAddress = v.InferOutput<typeof cryptoAddressSchema>;
|
||||
|
||||
const plFeConfigSchema = v.pipe(coerceObject({
|
||||
appleAppId: v.fallback(v.nullable(v.string()), null),
|
||||
logo: v.fallback(v.string(), ''),
|
||||
logoDarkMode: v.fallback(v.nullable(v.string()), null),
|
||||
brandColor: v.fallback(v.string(), ''),
|
||||
accentColor: v.fallback(v.string(), ''),
|
||||
colors: v.any(),
|
||||
copyright: v.fallback(v.string(), `♥${new Date().getFullYear()}. Copying is an act of love. Please copy and share.`),
|
||||
defaultSettings: v.fallback(v.record(v.string(), v.any()), {}),
|
||||
gdpr: v.fallback(v.boolean(), false),
|
||||
gdprUrl: v.fallback(v.string(), ''),
|
||||
greentext: v.fallback(v.boolean(), false),
|
||||
promoPanel: promoPanelSchema,
|
||||
navlinks: v.fallback(v.record(v.string(), filteredArray(footerItemSchema)), {}),
|
||||
verifiedIcon: v.fallback(v.string(), ''),
|
||||
displayFqn: v.fallback(v.boolean(), true),
|
||||
cryptoAddresses: filteredArray(cryptoAddressSchema),
|
||||
cryptoDonatePanel: coerceObject({
|
||||
limit: v.fallback(v.number(), 1),
|
||||
}),
|
||||
verifiedIcon: '',
|
||||
displayFqn: true,
|
||||
cryptoAddresses: ImmutableList<CryptoAddress>(),
|
||||
cryptoDonatePanel: ImmutableMap({
|
||||
limit: 1,
|
||||
}),
|
||||
aboutPages: ImmutableMap<string, ImmutableMap<string, unknown>>(),
|
||||
authenticatedProfile: false,
|
||||
linkFooterMessage: '',
|
||||
links: ImmutableMap<string, string>(),
|
||||
displayCta: false,
|
||||
aboutPages: v.fallback(v.record(v.string(), coerceObject({
|
||||
defaultLocale: v.fallback(v.string(), ''), // v.fallback(v.optional(v.string()), undefined),
|
||||
locales: filteredArray(v.string()),
|
||||
})), {}),
|
||||
authenticatedProfile: v.fallback(v.boolean(), false),
|
||||
linkFooterMessage: v.fallback(v.string(), ''),
|
||||
links: v.fallback(v.record(v.string(), v.string()), {}),
|
||||
displayCta: v.fallback(v.boolean(), false),
|
||||
/** Whether to inject suggested profiles into the Home feed. */
|
||||
feedInjection: true,
|
||||
tileServer: '',
|
||||
tileServerAttribution: '',
|
||||
redirectRootNoLogin: '',
|
||||
feedInjection: v.fallback(v.boolean(), true),
|
||||
tileServer: v.fallback(v.string(), ''),
|
||||
tileServerAttribution: v.fallback(v.string(), ''),
|
||||
redirectRootNoLogin: v.fallback(v.pipe(v.string(), v.transform((url: string) => {
|
||||
if (!url) return '';
|
||||
|
||||
try {
|
||||
// Basically just get the pathname with a leading slash.
|
||||
const normalized = new URL(url, 'http://a').pathname;
|
||||
|
||||
if (normalized !== '/') {
|
||||
return normalized;
|
||||
} else {
|
||||
// Prevent infinite redirect(?)
|
||||
return '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('You have configured an invalid redirect in pl-fe Config.');
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
})), ''),
|
||||
/**
|
||||
* Whether to use the preview URL for media thumbnails.
|
||||
* On some platforms this can be too blurry without additional configuration.
|
||||
*/
|
||||
mediaPreview: false,
|
||||
sentryDsn: undefined as string | undefined,
|
||||
}, 'PlFeConfig');
|
||||
mediaPreview: v.fallback(v.boolean(), false),
|
||||
sentryDsn: v.fallback(v.optional(v.string()), undefined),
|
||||
}), v.transform((config) => {
|
||||
const brandColor: string = config.brandColor || config.colors?.primary?.['500'] || '';
|
||||
const accentColor: string = config.accentColor || config.colors?.accent?.['500'] || '' || generateAccent(brandColor);
|
||||
|
||||
type PlFeConfigMap = ImmutableMap<string, any>;
|
||||
const colors = {
|
||||
...config.colors,
|
||||
...Object.fromEntries(Object.entries(DEFAULT_COLORS).map(([key, value]) => [key, typeof value === 'string' ? value : { ...value, ...config.colors?.[key] }])),
|
||||
};
|
||||
|
||||
const normalizeCryptoAddress = (address: unknown): CryptoAddress =>
|
||||
CryptoAddressRecord(ImmutableMap(fromJS(address))).update('ticker', ticker =>
|
||||
trimStart(ticker, '$').toLowerCase(),
|
||||
);
|
||||
|
||||
const normalizeCryptoAddresses = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const addresses = ImmutableList(plFeConfig.get('cryptoAddresses'));
|
||||
return plFeConfig.set('cryptoAddresses', addresses.map(normalizeCryptoAddress));
|
||||
};
|
||||
|
||||
const normalizeBrandColor = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const brandColor = plFeConfig.get('brandColor') || plFeConfig.getIn(['colors', 'primary', '500']) || '';
|
||||
return plFeConfig.set('brandColor', brandColor);
|
||||
};
|
||||
|
||||
const normalizeAccentColor = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const brandColor = plFeConfig.get('brandColor');
|
||||
|
||||
const accentColor = plFeConfig.get('accentColor')
|
||||
|| plFeConfig.getIn(['colors', 'accent', '500'])
|
||||
|| (brandColor ? generateAccent(brandColor) : '');
|
||||
|
||||
return plFeConfig.set('accentColor', accentColor);
|
||||
};
|
||||
|
||||
const normalizeColors = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const colors = DEFAULT_COLORS.mergeDeep(plFeConfig.get('colors'));
|
||||
return toTailwind(plFeConfig.set('colors', colors));
|
||||
};
|
||||
|
||||
const maybeAddMissingColors = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const colors = plFeConfig.get('colors');
|
||||
|
||||
const missing = ImmutableMap({
|
||||
'gradient-start': colors.getIn(['primary', '500']),
|
||||
'gradient-end': colors.getIn(['accent', '500']),
|
||||
'accent-blue': colors.getIn(['primary', '600']),
|
||||
const normalizedColors = toTailwind({
|
||||
brandColor,
|
||||
accentColor,
|
||||
colors,
|
||||
});
|
||||
|
||||
return plFeConfig.set('colors', missing.mergeDeep(colors));
|
||||
};
|
||||
return {
|
||||
...config,
|
||||
brandColor,
|
||||
accentColor,
|
||||
colors: {
|
||||
// @ts-ignore
|
||||
'gradient-start': normalizedColors.primary?.['500'],
|
||||
// @ts-ignore
|
||||
'gradient-end': normalizedColors.accent?.['500'],
|
||||
// @ts-ignore
|
||||
'accent-blue': normalizedColors.primary?.['600'],
|
||||
...normalizedColors,
|
||||
} as typeof normalizedColors,
|
||||
};
|
||||
}));
|
||||
|
||||
const normalizePromoPanel = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const promoPanel = PromoPanelRecord(plFeConfig.get('promoPanel'));
|
||||
const items = promoPanel.items.map(PromoPanelItemRecord);
|
||||
return plFeConfig.set('promoPanel', promoPanel.set('items', items));
|
||||
};
|
||||
|
||||
const normalizeFooterLinks = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const path = ['navlinks', 'homeFooter'];
|
||||
const items = (plFeConfig.getIn(path, ImmutableList()) as ImmutableList<any>).map(FooterItemRecord);
|
||||
return plFeConfig.setIn(path, items);
|
||||
};
|
||||
|
||||
/** Single user mode is now managed by `redirectRootNoLogin`. */
|
||||
const upgradeSingleUserMode = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const singleUserMode = plFeConfig.get('singleUserMode') as boolean | undefined;
|
||||
const singleUserModeProfile = plFeConfig.get('singleUserModeProfile') as string | undefined;
|
||||
const redirectRootNoLogin = plFeConfig.get('redirectRootNoLogin') as string | undefined;
|
||||
|
||||
if (!redirectRootNoLogin && singleUserMode && singleUserModeProfile) {
|
||||
return plFeConfig
|
||||
.set('redirectRootNoLogin', `/@${normalizeUsername(singleUserModeProfile)}`)
|
||||
.deleteAll(['singleUserMode', 'singleUserModeProfile']);
|
||||
} else {
|
||||
return plFeConfig
|
||||
.deleteAll(['singleUserMode', 'singleUserModeProfile']);
|
||||
}
|
||||
};
|
||||
|
||||
/** Ensure a valid path is used. */
|
||||
const normalizeRedirectRootNoLogin = (plFeConfig: PlFeConfigMap): PlFeConfigMap => {
|
||||
const redirectRootNoLogin = plFeConfig.get('redirectRootNoLogin');
|
||||
|
||||
if (!redirectRootNoLogin) return plFeConfig;
|
||||
|
||||
try {
|
||||
// Basically just get the pathname with a leading slash.
|
||||
const normalized = new URL(redirectRootNoLogin, 'http://a').pathname;
|
||||
|
||||
if (normalized !== '/') {
|
||||
return plFeConfig.set('redirectRootNoLogin', normalized);
|
||||
} else {
|
||||
// Prevent infinite redirect(?)
|
||||
return plFeConfig.delete('redirectRootNoLogin');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('You have configured an invalid redirect in pl-fe Config.');
|
||||
console.error(e);
|
||||
return plFeConfig.delete('redirectRootNoLogin');
|
||||
}
|
||||
};
|
||||
|
||||
const normalizePlFeConfig = (plFeConfig: Record<string, any>) => PlFeConfigRecord(
|
||||
ImmutableMap(fromJS(plFeConfig)).withMutations(plFeConfig => {
|
||||
normalizeBrandColor(plFeConfig);
|
||||
normalizeAccentColor(plFeConfig);
|
||||
normalizeColors(plFeConfig);
|
||||
normalizePromoPanel(plFeConfig);
|
||||
normalizeFooterLinks(plFeConfig);
|
||||
maybeAddMissingColors(plFeConfig);
|
||||
normalizeCryptoAddresses(plFeConfig);
|
||||
upgradeSingleUserMode(plFeConfig);
|
||||
normalizeRedirectRootNoLogin(plFeConfig);
|
||||
}),
|
||||
);
|
||||
type PlFeConfig = v.InferOutput<typeof plFeConfigSchema>;
|
||||
|
||||
export {
|
||||
PromoPanelItemRecord,
|
||||
FooterItemRecord,
|
||||
CryptoAddressRecord,
|
||||
PlFeConfigRecord,
|
||||
normalizePlFeConfig,
|
||||
promoPanelItemSchema,
|
||||
footerItemSchema,
|
||||
cryptoAddressSchema,
|
||||
plFeConfigSchema,
|
||||
type PromoPanelItem,
|
||||
type PromoPanel,
|
||||
type FooterItem,
|
||||
type CryptoAddress,
|
||||
type PlFeConfig,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import trim from 'lodash/trim';
|
||||
import { create, Draft } from 'mutative';
|
||||
import { applicationSchema, PlApiClient, tokenSchema, type CredentialAccount, type CredentialApplication, type Token } from 'pl-api';
|
||||
|
@ -6,6 +5,7 @@ import * as v from 'valibot';
|
|||
|
||||
import { MASTODON_PRELOAD_IMPORT, type PreloadAction } from 'pl-fe/actions/preload';
|
||||
import * as BuildConfig from 'pl-fe/build-config';
|
||||
import { coerceObject } from 'pl-fe/schemas/utils';
|
||||
import KVStore from 'pl-fe/storage/kv-store';
|
||||
import { validId, isURL, parseBaseURL } from 'pl-fe/utils/auth';
|
||||
|
||||
|
@ -27,6 +27,16 @@ import type { AnyAction } from 'redux';
|
|||
|
||||
const backendUrl = (isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : '');
|
||||
|
||||
const mastodonPreloadSchema = coerceObject({
|
||||
meta: coerceObject({
|
||||
access_token: v.string(),
|
||||
me: v.string(),
|
||||
}),
|
||||
accounts: v.record(v.string(), v.object({
|
||||
url: v.string(),
|
||||
})),
|
||||
});
|
||||
|
||||
const authUserSchema = v.object({
|
||||
access_token: v.string(),
|
||||
id: v.string(),
|
||||
|
@ -229,10 +239,11 @@ const deleteUser = (state: State | Draft<State>, account: Pick<AccountEntity, 'u
|
|||
maybeShiftMe(state);
|
||||
};
|
||||
|
||||
const importMastodonPreload = (state: State | Draft<State>, data: ImmutableMap<string, any>) => {
|
||||
const accountId = data.getIn(['meta', 'me']) as string;
|
||||
const accountUrl = data.getIn(['accounts', accountId, 'url']) as string;
|
||||
const accessToken = data.getIn(['meta', 'access_token']) as string;
|
||||
const importMastodonPreload = (state: State | Draft<State>, data: Record<string, any>) => {
|
||||
const parsedData = v.parse(mastodonPreloadSchema, data);
|
||||
const accountId = parsedData.meta.me;
|
||||
const accountUrl = parsedData.accounts[accountId]?.url;
|
||||
const accessToken = parsedData.meta.access_token;
|
||||
|
||||
if (validId(accessToken) && validId(accountId) && isURL(accountUrl)) {
|
||||
state.tokens[accessToken] = v.parse(tokenSchema, {
|
||||
|
@ -335,7 +346,7 @@ const reducer = (state: State, action: AnyAction | AuthAction | MeAction | Prelo
|
|||
});
|
||||
case MASTODON_PRELOAD_IMPORT:
|
||||
return updateState(state, (draft) => {
|
||||
importMastodonPreload(draft, fromJS<ImmutableMap<string, any>>(action.data));
|
||||
importMastodonPreload(draft, action.data);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import { create } from 'mutative';
|
||||
import { Instance, PLEROMA, type CredentialAccount, type MediaAttachment, type Tag } from 'pl-api';
|
||||
import { PLEROMA, type CredentialAccount, type Instance, type MediaAttachment, type Tag } from 'pl-api';
|
||||
|
||||
import { INSTANCE_FETCH_SUCCESS, InstanceAction } from 'pl-fe/actions/instance';
|
||||
import { isNativeEmoji } from 'pl-fe/features/emoji';
|
||||
import { isNativeEmoji, type Emoji } from 'pl-fe/features/emoji';
|
||||
import { tagHistory } from 'pl-fe/settings';
|
||||
import { hasIntegerMediaIds } from 'pl-fe/utils/status';
|
||||
|
||||
|
@ -69,11 +68,9 @@ import { FE_NAME } from '../actions/settings';
|
|||
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
|
||||
import { unescapeHTML } from '../utils/html';
|
||||
|
||||
import type { Emoji } from 'pl-fe/features/emoji';
|
||||
import type { Language } from 'pl-fe/features/preferences';
|
||||
import type { Account } from 'pl-fe/normalizers/account';
|
||||
import type { Status } from 'pl-fe/normalizers/status';
|
||||
import type { APIEntity } from 'pl-fe/types/entities';
|
||||
|
||||
const getResetFileKey = () => Math.floor((Math.random() * 0x10000));
|
||||
|
||||
|
@ -109,7 +106,7 @@ interface Compose {
|
|||
is_composing: boolean;
|
||||
is_submitting: boolean;
|
||||
is_uploading: boolean;
|
||||
media_attachments: ImmutableList<MediaAttachment>;
|
||||
media_attachments: Array<MediaAttachment>;
|
||||
poll: ComposePoll | null;
|
||||
privacy: string;
|
||||
progress: number;
|
||||
|
@ -119,9 +116,9 @@ interface Compose {
|
|||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
spoilerTextMap: Partial<Record<Language, string>>;
|
||||
suggestions: ImmutableList<string>;
|
||||
suggestions: Array<string> | Array<Emoji>;
|
||||
suggestion_token: string | null;
|
||||
tagHistory: ImmutableList<string>;
|
||||
tagHistory: Array<string>;
|
||||
text: string;
|
||||
textMap: Partial<Record<Language, string>>;
|
||||
to: Array<string>;
|
||||
|
@ -149,7 +146,7 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
|||
is_composing: false,
|
||||
is_submitting: false,
|
||||
is_uploading: false,
|
||||
media_attachments: ImmutableList<MediaAttachment>(),
|
||||
media_attachments: [],
|
||||
poll: null,
|
||||
privacy: 'public',
|
||||
progress: 0,
|
||||
|
@ -159,9 +156,9 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
|||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
spoilerTextMap: {},
|
||||
suggestions: ImmutableList<string>(),
|
||||
suggestions: [],
|
||||
suggestion_token: null,
|
||||
tagHistory: ImmutableList<string>(),
|
||||
tagHistory: [],
|
||||
text: '',
|
||||
textMap: {},
|
||||
to: [],
|
||||
|
@ -204,7 +201,7 @@ const statusToMentionsAccountIdsArray = (status: Pick<Status, 'mentions' | 'acco
|
|||
};
|
||||
|
||||
const appendMedia = (compose: Compose, media: MediaAttachment, defaultSensitive?: boolean) => {
|
||||
const prevSize = compose.media_attachments.size;
|
||||
const prevSize = compose.media_attachments.length;
|
||||
|
||||
compose.media_attachments.push(media);
|
||||
compose.is_uploading = false;
|
||||
|
@ -217,7 +214,7 @@ const appendMedia = (compose: Compose, media: MediaAttachment, defaultSensitive?
|
|||
};
|
||||
|
||||
const removeMedia = (compose: Compose, mediaId: string) => {
|
||||
const prevSize = compose.media_attachments.size;
|
||||
const prevSize = compose.media_attachments.length;
|
||||
|
||||
compose.media_attachments = compose.media_attachments.filter(item => item.id !== mediaId);
|
||||
compose.idempotencyKey = crypto.randomUUID();
|
||||
|
@ -235,17 +232,17 @@ const insertSuggestion = (compose: Compose, position: number, token: string | nu
|
|||
compose.poll.options[path[2]] = updateText(compose.poll.options[path[2]]);
|
||||
}
|
||||
compose.suggestion_token = null;
|
||||
compose.suggestions = ImmutableList();
|
||||
compose.suggestions = [];
|
||||
compose.idempotencyKey = crypto.randomUUID();
|
||||
};
|
||||
|
||||
const updateSuggestionTags = (compose: Compose, token: string, tags: Tag[]) => {
|
||||
const prefix = token.slice(1);
|
||||
|
||||
compose.suggestions = ImmutableList(tags
|
||||
compose.suggestions = tags
|
||||
.filter((tag) => tag.name.toLowerCase().startsWith(prefix.toLowerCase()))
|
||||
.slice(0, 4)
|
||||
.map((tag) => '#' + tag.name));
|
||||
.map((tag) => '#' + tag.name);
|
||||
compose.suggestion_token = token;
|
||||
};
|
||||
|
||||
|
@ -299,7 +296,7 @@ const importAccount = (compose: Compose, account: CredentialAccount) => {
|
|||
|
||||
if (settings.defaultPrivacy) compose.privacy = settings.defaultPrivacy;
|
||||
if (settings.defaultContentType) compose.content_type = settings.defaultContentType;
|
||||
compose.tagHistory = ImmutableList(tagHistory.get(account.id));
|
||||
compose.tagHistory = tagHistory.get(account.id);
|
||||
};
|
||||
|
||||
// const updateSetting = (compose: Compose, path: string[], value: string) => {
|
||||
|
@ -493,12 +490,12 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
|||
});
|
||||
case COMPOSE_SUGGESTIONS_CLEAR:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.suggestions = compose.suggestions.clear();
|
||||
compose.suggestions = [];
|
||||
compose.suggestion_token = null;
|
||||
});
|
||||
case COMPOSE_SUGGESTIONS_READY:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.suggestions = ImmutableList(action.accounts ? action.accounts.map((item: APIEntity) => item.id) : action.emojis);
|
||||
compose.suggestions = action.accounts ? action.accounts.map((item) => item.id) : action.emojis || [];
|
||||
compose.suggestion_token = action.token;
|
||||
});
|
||||
case COMPOSE_SUGGESTION_SELECT:
|
||||
|
@ -507,7 +504,7 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
|||
return updateCompose(state, action.composeId, compose => updateSuggestionTags(compose, action.token, action.tags));
|
||||
case COMPOSE_TAG_HISTORY_UPDATE:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.tagHistory = ImmutableList(action.tags) as ImmutableList<string>;
|
||||
compose.tagHistory = action.tags;
|
||||
});
|
||||
case TIMELINE_DELETE:
|
||||
return updateCompose(state, 'compose-modal', compose => {
|
||||
|
@ -550,9 +547,9 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
|||
compose.group_id = action.status.group_id;
|
||||
|
||||
if (action.v?.software === PLEROMA && action.withRedraft && hasIntegerMediaIds(action.status)) {
|
||||
compose.media_attachments = ImmutableList();
|
||||
compose.media_attachments = [];
|
||||
} else {
|
||||
compose.media_attachments = ImmutableList(action.status.media_attachments);
|
||||
compose.media_attachments = action.status.media_attachments;
|
||||
}
|
||||
|
||||
if (action.status.spoiler_text.length > 0) {
|
||||
|
@ -661,10 +658,10 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
|||
case COMPOSE_CHANGE_MEDIA_ORDER:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
const indexA = compose.media_attachments.findIndex(x => x.id === action.a);
|
||||
const moveItem = compose.media_attachments.get(indexA)!;
|
||||
const indexB = compose.media_attachments.findIndex(x => x.id === action.b);
|
||||
|
||||
return compose.media_attachments.splice(indexA, 1).splice(indexB, 0, moveItem);
|
||||
const item = compose.media_attachments.splice(indexA, 1)[0];
|
||||
compose.media_attachments.splice(indexB, 0, item);
|
||||
});
|
||||
case COMPOSE_ADD_SUGGESTED_QUOTE:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
import { create } from 'mutative';
|
||||
import { CreateStatusParams } from 'pl-api';
|
||||
|
||||
import {
|
||||
STATUS_CREATE_FAIL,
|
||||
|
@ -9,36 +10,53 @@ import {
|
|||
import type { StatusVisibility } from 'pl-fe/normalizers/status';
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const PendingStatusRecord = ImmutableRecord({
|
||||
interface PendingStatus {
|
||||
content_type: string;
|
||||
in_reply_to_id: string | null;
|
||||
media_ids: Array<string> | null;
|
||||
quote_id: string | null;
|
||||
poll: Exclude<CreateStatusParams['poll'], undefined> | null;
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
status: string;
|
||||
to: Array<string> | null;
|
||||
visibility: StatusVisibility;
|
||||
}
|
||||
|
||||
const newPendingStatus = (props: Partial<PendingStatus> = {}): PendingStatus => ({
|
||||
content_type: '',
|
||||
in_reply_to_id: null as string | null,
|
||||
media_ids: null as ImmutableList<string> | null,
|
||||
quote_id: null as string | null,
|
||||
poll: null as ImmutableMap<string, any> | null,
|
||||
in_reply_to_id: null,
|
||||
media_ids: null,
|
||||
quote_id: null,
|
||||
poll: null,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
status: '',
|
||||
to: null as ImmutableList<string> | null,
|
||||
visibility: 'public' as StatusVisibility,
|
||||
to: null,
|
||||
visibility: 'public',
|
||||
...props,
|
||||
});
|
||||
|
||||
type PendingStatus = ReturnType<typeof PendingStatusRecord>;
|
||||
type State = ImmutableMap<string, PendingStatus>;
|
||||
type State = Record<string, PendingStatus>;
|
||||
|
||||
const initialState: State = ImmutableMap();
|
||||
const initialState: State = {};
|
||||
|
||||
const importStatus = (state: State, params: ImmutableMap<string, any>, idempotencyKey: string) =>
|
||||
state.set(idempotencyKey, PendingStatusRecord(params));
|
||||
const importStatus = (state: State, params: Record<string, any>, idempotencyKey: string) => {
|
||||
state[idempotencyKey] = newPendingStatus(params);
|
||||
};
|
||||
|
||||
const deleteStatus = (state: State, idempotencyKey: string) => state.delete(idempotencyKey);
|
||||
const deleteStatus = (state: State, idempotencyKey: string) => {
|
||||
delete state[idempotencyKey];
|
||||
};
|
||||
|
||||
const pending_statuses = (state = initialState, action: AnyAction) => {
|
||||
const pending_statuses = (state = initialState, action: AnyAction): State => {
|
||||
switch (action.type) {
|
||||
case STATUS_CREATE_REQUEST:
|
||||
return action.editing ? state : importStatus(state, ImmutableMap(fromJS(action.params)), action.idempotencyKey);
|
||||
if (action.editing) return state;
|
||||
return create(state, (draft) => importStatus(draft, action.params, action.idempotencyKey));
|
||||
case STATUS_CREATE_FAIL:
|
||||
case STATUS_CREATE_SUCCESS:
|
||||
return deleteStatus(state, action.idempotencyKey);
|
||||
return create(state, (draft) => deleteStatus(draft, action.idempotencyKey));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -10,10 +10,11 @@ import {
|
|||
} from '../actions/pl-fe';
|
||||
|
||||
import type { PleromaConfig } from 'pl-api';
|
||||
import type { PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
const initialState: Record<string, any> = {};
|
||||
const initialState: Partial<PlFeConfig> = {};
|
||||
|
||||
const fallbackState = {
|
||||
const fallbackState: Partial<PlFeConfig> = {
|
||||
brandColor: '#d80482',
|
||||
};
|
||||
|
||||
|
@ -46,12 +47,12 @@ const persistPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const importPlFeConfig = (plFeConfig: Record<string, any>, host: string) => {
|
||||
const importPlFeConfig = (plFeConfig: PlFeConfig, host: string) => {
|
||||
persistPlFeConfig(plFeConfig, host);
|
||||
return plFeConfig;
|
||||
};
|
||||
|
||||
const plfe = (state = initialState, action: Record<string, any>): Record<string, any> => {
|
||||
const plfe = (state = initialState, action: Record<string, any>): Partial<PlFeConfig> => {
|
||||
switch (action.type) {
|
||||
case PLEROMA_PRELOAD_IMPORT:
|
||||
return preloadImport(state, action);
|
||||
|
|
|
@ -6,7 +6,7 @@ type TailwindColorObject = {
|
|||
};
|
||||
|
||||
type TailwindColorPalette = {
|
||||
[key: string]: TailwindColorObject | string;
|
||||
[key: string]: TailwindColorObject | string | null;
|
||||
}
|
||||
|
||||
export type { Rgb, Hsl, TailwindColorObject, TailwindColorPalette };
|
||||
|
|
|
@ -1,21 +1,3 @@
|
|||
import {
|
||||
PromoPanelItemRecord,
|
||||
FooterItemRecord,
|
||||
CryptoAddressRecord,
|
||||
PlFeConfigRecord,
|
||||
} from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
|
||||
type Me = string | null | false;
|
||||
|
||||
type PromoPanelItem = ReturnType<typeof PromoPanelItemRecord>;
|
||||
type FooterItem = ReturnType<typeof FooterItemRecord>;
|
||||
type CryptoAddress = ReturnType<typeof CryptoAddressRecord>;
|
||||
type PlFeConfig = ReturnType<typeof PlFeConfigRecord>;
|
||||
|
||||
export {
|
||||
Me,
|
||||
PromoPanelItem,
|
||||
FooterItem,
|
||||
CryptoAddress,
|
||||
PlFeConfig,
|
||||
};
|
||||
export { Me };
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import tintify from 'pl-fe/utils/colors';
|
||||
import { generateAccent, generateNeutral } from 'pl-fe/utils/theme';
|
||||
|
||||
import type { TailwindColorPalette } from 'pl-fe/types/colors';
|
||||
|
||||
type PlFeConfig = ImmutableMap<string, any>;
|
||||
type PlFeColors = ImmutableMap<string, any>;
|
||||
type PlFeColors = Record<string, Record<string, string>>;
|
||||
|
||||
/** Check if the value is a valid hex color */
|
||||
const isHex = (value: any): boolean => /^#([0-9A-F]{3}){1,2}$/i.test(value);
|
||||
|
||||
/** Expand hex colors into tints */
|
||||
const expandPalette = (palette: TailwindColorPalette): TailwindColorPalette => {
|
||||
const expandPalette = (palette: TailwindColorPalette): TailwindColorPalette =>
|
||||
// Generate palette only for present colors
|
||||
return Object.entries(palette).reduce((result: TailwindColorPalette, colorData) => {
|
||||
Object.entries(palette).reduce((result: TailwindColorPalette, colorData) => {
|
||||
const [colorName, color] = colorData;
|
||||
|
||||
// Conditionally handle hex color and Tailwind color object
|
||||
|
@ -26,32 +23,36 @@ const expandPalette = (palette: TailwindColorPalette): TailwindColorPalette => {
|
|||
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
// Generate accent color only if brandColor is present
|
||||
const maybeGenerateAccentColor = (brandColor: any): string | null =>
|
||||
const maybeGenerateAccentColor = (brandColor: string): string | null =>
|
||||
isHex(brandColor) ? generateAccent(brandColor) : null;
|
||||
|
||||
/** Build a color object from legacy colors */
|
||||
const fromLegacyColors = (plFeConfig: PlFeConfig): TailwindColorPalette => {
|
||||
const brandColor = plFeConfig.get('brandColor');
|
||||
const accentColor = plFeConfig.get('accentColor');
|
||||
const accent = isHex(accentColor) ? accentColor : maybeGenerateAccentColor(brandColor);
|
||||
const fromLegacyColors = ({ brandColor, accentColor }: {
|
||||
brandColor: string;
|
||||
accentColor: string | null;
|
||||
}): TailwindColorPalette => {
|
||||
const accent = typeof accentColor === 'string' && isHex(accentColor) ? accentColor : maybeGenerateAccentColor(brandColor);
|
||||
|
||||
return expandPalette({
|
||||
primary: isHex(brandColor) ? brandColor : null,
|
||||
secondary: accent,
|
||||
accent,
|
||||
gray: (isHex(brandColor) ? generateNeutral(brandColor) : null) as any,
|
||||
gray: (isHex(brandColor) ? generateNeutral(brandColor) : null),
|
||||
});
|
||||
};
|
||||
|
||||
/** Convert pl-fe Config into Tailwind colors */
|
||||
const toTailwind = (plFeConfig: PlFeConfig): PlFeConfig => {
|
||||
const colors: PlFeColors = ImmutableMap(plFeConfig.get('colors'));
|
||||
const legacyColors = ImmutableMap(fromJS(fromLegacyColors(plFeConfig))) as PlFeColors;
|
||||
const toTailwind = (config: {
|
||||
brandColor: string;
|
||||
accentColor: string | null;
|
||||
colors: Record<string, Record<string, string>>;
|
||||
}): Record<string, Record<string, string> | string> => {
|
||||
const colors: PlFeColors = config.colors;
|
||||
const legacyColors = fromLegacyColors(config);
|
||||
|
||||
return plFeConfig.set('colors', legacyColors.mergeDeep(colors));
|
||||
return Object.fromEntries(Object.entries(legacyColors).map(([key, value]) => [key, typeof value === 'string' ? colors[key] || value : { ...value, ...colors[key] }]));
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { hexToRgb } from './colors';
|
||||
|
||||
import type { PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config';
|
||||
import type { Rgb, Hsl, TailwindColorPalette, TailwindColorObject } from 'pl-fe/types/colors';
|
||||
import type { PlFeConfig } from 'pl-fe/types/pl-fe';
|
||||
|
||||
// Taken from chromatism.js
|
||||
// https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js
|
||||
|
@ -111,7 +111,7 @@ const colorsToCss = (colors: TailwindColorPalette): string => {
|
|||
};
|
||||
|
||||
const generateThemeCss = (plFeConfig: PlFeConfig): string =>
|
||||
colorsToCss(plFeConfig.colors.toJS() as TailwindColorPalette);
|
||||
colorsToCss(plFeConfig.colors);
|
||||
|
||||
const hexToHsl = (hex: string): Hsl | null => {
|
||||
const rgb = hexToRgb(hex);
|
||||
|
|
|
@ -5826,7 +5826,7 @@ immer@^10.0.3:
|
|||
resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
|
||||
integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
|
||||
|
||||
immutable@^4.0.0, immutable@^4.3.7:
|
||||
immutable@^4.0.0:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||
|
|
Loading…
Reference in a new issue