Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"vapidPublicKey": "BCnS4SbHjeOaqVFW4wjt5xDt_pYIL62qMzKePfYF9fl9PQU14RieIaObh7nLR_9dQf4sykZa-CTrcjkgMIE1mcg",
"webPushAppID": "moe.sable.app.sygnal"
},
"settingsLinkBaseUrl": "https://app.sable.moe",

"themeCatalogBaseUrl": "https://raw.githubusercontent.com/SableClient/themes/main/",
"themeCatalogApprovedHostPrefixes": ["https://raw.githubusercontent.com/SableClient/themes/"],

"slidingSync": {
"enabled": true
Expand Down
32 changes: 29 additions & 3 deletions src/app/components/RenderMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ import {
UnsupportedContent,
VideoContent,
} from './message';
import { UrlPreviewCard, UrlPreviewHolder, ClientPreview, youtubeUrl } from './url-preview';
import {
UrlPreviewCard,
UrlPreviewHolder,
ClientPreview,
ThemePreviewUrlCard,
TweakPreviewUrlCard,
youtubeUrl,
} from './url-preview';
import { isHttpsFullSableCssUrl } from '../theme/previewUrls';
import { Image, MediaControl, PersistedVolumeVideo } from './media';
import { ImageViewer } from './image-viewer';
import { PdfViewer } from './Pdf-viewer';
Expand Down Expand Up @@ -85,6 +93,7 @@ function RenderMessageContentInternal({

const [autoplayGifs] = useSetting(settingsAtom, 'autoplayGifs');
const [captionPosition] = useSetting(settingsAtom, 'captionPosition');
const [themeChatAny] = useSetting(settingsAtom, 'themeChatPreviewAnyUrl');
const [multiplePreviews] = useSetting(settingsAtom, 'multiplePreviews');
const settingsLinkBaseUrl = useSettingsLinkBaseUrl();
const captionPositionMap = {
Expand Down Expand Up @@ -115,6 +124,15 @@ function RenderMessageContentInternal({
);
if (filteredUrls.length === 0) return undefined;

const themePreviewUrls = themeChatAny
? filteredUrls.filter((u) => /\.preview\.sable\.css(\?|#|$)/i.test(u))
: [];
const themeToRender = themePreviewUrls.filter((u) => /^https:\/\//i.test(u));

const tweakCandidateUrls = themeChatAny
? filteredUrls.filter((u) => isHttpsFullSableCssUrl(u))
: [];

const analyzed = filteredUrls.map((url) => ({
url,
type: getMediaType(url),
Expand All @@ -124,8 +142,16 @@ function RenderMessageContentInternal({
const toRender = multiplePreviews ? previewCandidates : [previewCandidates[0]!];
return (
<UrlPreviewHolder>
{themeToRender.map((url) => (
<ThemePreviewUrlCard key={`theme:${url}`} url={url} />
))}
{tweakCandidateUrls.map((url) => (
<TweakPreviewUrlCard key={`tweak:${url}`} url={url} />
))}
{toRender.map((item) => {
const { url, type } = item;
if (themeToRender.includes(url)) return null;
if (tweakCandidateUrls.includes(url)) return null;
if (type) {
return <UrlPreviewCard urlPreview key={url} url={url} ts={ts} mediaType={type} />;
}
Expand All @@ -140,7 +166,7 @@ function RenderMessageContentInternal({
</UrlPreviewHolder>
);
},
[multiplePreviews, settingsLinkBaseUrl, clientUrlPreview, urlPreview, ts]
[multiplePreviews, themeChatAny, settingsLinkBaseUrl, clientUrlPreview, urlPreview, ts]
);
const renderBundledPreviews = useCallback(
(bundles: IPreviewUrlResponse[]) => (
Expand All @@ -157,7 +183,7 @@ function RenderMessageContentInternal({
),
[urlPreview]
);
const messageUrlsPreview = urlPreview ? renderUrlsPreview : undefined;
const messageUrlsPreview = urlPreview || themeChatAny ? renderUrlsPreview : undefined;
const messageBundlePreview = bundledPreview ? renderBundledPreviews : undefined;

const renderCaption = () => {
Expand Down
163 changes: 163 additions & 0 deletions src/app/components/theme/ThemeMigrationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { useCallback, useMemo, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Button,
config,
Dialog,
Header,
Icon,
IconButton,
Icons,
Overlay,
OverlayBackdrop,
OverlayCenter,
Text,
} from 'folds';
import { useStore } from 'jotai/react';

import { useOptionalClientConfig } from '$hooks/useClientConfig';
import { useSetting } from '$state/hooks/settings';
import { trimTrailingSlash } from '$utils/common';
import { defaultSettings, settingsAtom } from '$state/settings';
import { stopPropagation } from '$utils/keyboard';

import { usePatchSettings } from '$features/settings/cosmetics/themeSettingsPatch';
import { DEFAULT_THEME_CATALOG_BASE } from '../../theme/catalogDefaults';
import { needsLegacyThemeMigration } from '../../theme/legacyToCatalogMap';
import { runLegacyThemeMigration } from '../../theme/migrateLegacyThemes';

export function ThemeMigrationBanner() {
const store = useStore();
const [themeMigrationDismissed] = useSetting(settingsAtom, 'themeMigrationDismissed');
const [themeId] = useSetting(settingsAtom, 'themeId');
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
const patchSettings = usePatchSettings();
const clientConfig = useOptionalClientConfig();
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);

const visible = useMemo(
() =>
needsLegacyThemeMigration({
...defaultSettings,
themeMigrationDismissed: themeMigrationDismissed ?? false,
themeId,
lightThemeId,
darkThemeId,
}),
[themeMigrationDismissed, themeId, lightThemeId, darkThemeId]
);

const catalogBase = trimTrailingSlash(
clientConfig?.themeCatalogBaseUrl?.trim() || DEFAULT_THEME_CATALOG_BASE
);

const dismiss = useCallback(() => {
patchSettings({ themeMigrationDismissed: true });
}, [patchSettings]);

const dismissSafe = useCallback(() => {
if (busy) return;
dismiss();
}, [busy, dismiss]);

const migrate = useCallback(async () => {
setError(null);
setBusy(true);
try {
const current = store.get(settingsAtom);
const result = await runLegacyThemeMigration(current, catalogBase);
if (!result.ok) {
setError(result.error);
return;
}
patchSettings(result.partial);
} finally {
setBusy(false);
}
}, [catalogBase, patchSettings, store]);

if (!visible) return null;

return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: dismissSafe,
clickOutsideDeactivates: false,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface" aria-labelledby="theme-migration-title">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text id="theme-migration-title" size="H4">
Update your theme selection
</Text>
</Box>
<IconButton
size="300"
variant="Secondary"
fill="Soft"
outlined
radii="300"
onClick={dismissSafe}
disabled={busy}
aria-label="Close"
>
<Icon src={Icons.Cross} size="100" />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text priority="400">
Older bundled color themes are no longer included in the app. Migrate to the same
looks from the official catalog (downloaded and cached on this device), or dismiss
this reminder.
</Text>
{error && (
<Text size="T300" priority="400" style={{ color: 'var(--sable-error)' }}>
{error}
</Text>
)}
<Box direction="Column" gap="200">
<Button
variant="Primary"
fill="Soft"
outlined
size="300"
radii="300"
onClick={migrate}
disabled={busy}
>
<Text size="B400">{busy ? 'Migrating…' : 'Migrate'}</Text>
</Button>
<Button
variant="Secondary"
fill="Soft"
outlined
size="300"
radii="300"
onClick={dismissSafe}
disabled={busy}
>
<Text size="B400">Not now</Text>
</Button>
</Box>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
Loading
Loading