diff --git a/src/app.jsx b/src/app.jsx
index 0ca54ab1..c2f42cc1 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -43,6 +43,7 @@ import Login from './pages/login';
import Mentions from './pages/mentions';
import Notifications from './pages/notifications';
import Public from './pages/public';
+import Sandbox from './pages/sandbox';
import ScheduledPosts from './pages/scheduled-posts';
import Search from './pages/search';
import StatusRoute from './pages/status-route';
@@ -497,7 +498,7 @@ const PrimaryRoutes = memo(({ isLoggedIn }) => {
const location = useLocation();
const nonRootLocation = useMemo(() => {
const { pathname } = location;
- return !/^\/(login|welcome)/i.test(pathname);
+ return !/^\/(login|welcome|_sandbox)/i.test(pathname);
}, [location]);
return (
@@ -505,6 +506,7 @@ const PrimaryRoutes = memo(({ isLoggedIn }) => {
This is a test status with long text content. It contains multiple paragraphs and spans several lines to demonstrate how longer content appears.
+ +Second paragraph goes here with more sample text. The Status component will render this appropriately based on the current size setting.
+ +Third paragraph adds even more content to ensure we have a properly long post that might get truncated depending on the view settings.
`; + const linksContent = `This is a test status with links. Check out this website and Google. Links should be clickable and properly styled.
`; + const hashtagsContent = `This is a test status with hashtags. #coding #webdev #javascript #reactjs #preact
Hashtags should be formatted and clickable.
`; + const mentionsContent = `This is a test status with mentions. Hello @cheeaun and @test! What do you think about this @another_user?
Mentions should be highlighted and clickable.
`; + + const base = { + // Random ID to un-memoize Status + id: hashID(toggles), + account: { + username: 'test', + name: 'Test', + // avatar: 'https://picsum.photos/seed/avatar/200', + avatar: '/logo-192.png', + acct: 'test@localhost', + url: 'https://test.localhost', + }, + content: + contentFormat === 'text' + ? contentType === 'long' + ? longContent + : contentType === 'links' + ? linksContent + : contentType === 'hashtags' + ? hashtagsContent + : contentType === 'mentions' + ? mentionsContent + : shortContent + : '', + visibility: 'public', + createdAt: new Date().toISOString(), + reblogsCount: 0, + favouritesCount: 0, + repliesCount: 5, + emojis: [], + mentions: [], + tags: [], + mediaAttachments: [], + }; + + // Add media if selected + if (mediaCount > 0) { + base.mediaAttachments = Array(parseInt(mediaCount, 10)) + .fill(0) + .map((_, i) => ({ + id: `media-${i}`, + type: 'image', + url: `https://picsum.photos/seed/media-${i}/600/400`, + previewUrl: `https://picsum.photos/seed/media-${i}/300/200`, + description: + i % 2 === 0 ? `Sample image description for media ${i + 1}` : '', + meta: { + original: { + width: 600, + height: 400, + }, + small: { + width: 600, + height: 400, + }, + }, + })); + } + + // Add poll if selected + if (pollCount > 0) { + base.poll = { + id: 'poll-1', + options: Array(parseInt(pollCount, 10)) + .fill(0) + .map((_, i) => ({ + title: `Option ${i + 1}`, + votesCount: Math.floor(Math.random() * 100), + })), + // Set expiration date in the past if poll is expired, otherwise in the future + expiresAt: pollExpired + ? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // 24 hours ago + : new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours from now + expired: pollExpired, + multiple: pollMultiple, + // Use votersCount for multiple-choice polls, votesCount for single-choice polls + votesCount: 150, + votersCount: pollMultiple ? 100 : undefined, + voted: false, + }; + } + + // Add spoiler if selected + if (spoiler) { + base.sensitive = true; + base.spoilerText = 'Content warning: test spoiler'; + + if (spoilerType === 'mediaOnly') { + // For media-only spoiler, remove spoilerText but keep sensitive true + base.spoilerText = ''; + } + } + + // Add mentions and tags if needed + if (contentType === 'mentions') { + base.mentions = [ + { + id: '1', + username: 'cheeaun', + url: 'https://example.social/@cheeaun', + acct: 'cheeaun', + }, + { + id: '2', + username: 'test', + url: 'https://example.social/@test', + acct: 'test', + }, + { + id: '3', + username: 'another_user', + url: 'https://example.social/@another_user', + acct: 'another_user', + }, + ]; + } + + if (contentType === 'hashtags') { + base.tags = [ + { + name: 'coding', + url: 'https://example.social/tags/coding', + }, + { + name: 'webdev', + url: 'https://example.social/tags/webdev', + }, + { + name: 'javascript', + url: 'https://example.social/tags/javascript', + }, + { + name: 'reactjs', + url: 'https://example.social/tags/reactjs', + }, + { + name: 'preact', + url: 'https://example.social/tags/preact', + }, + ]; + } + + // Add any relevant filtered flags based on filter settings + if (filters && filters.some((f) => f)) { + base.filtered = filters + .map((enabled, i) => { + if (!enabled) return null; + const filterTypes = ['hide', 'blur', 'warn']; + return { + filter: { + id: `filter-${i}`, + title: `Sample ${filterTypes[i]} filter`, + context: ['home', 'public', 'thread', 'account'], + filterAction: filterTypes[i], + }, + keywordMatches: [], + statusMatches: [], + }; + }) + .filter(Boolean); + } + + console.log('Final base', base); + return base; +}; + +export default function Sandbox() { + // Consolidated state for all toggles + const [toggleState, setToggleState] = useState({ + loading: false, + mediaFirst: false, + hasContent: true, + contentType: 'short', + hasSpoiler: false, + spoilerType: 'all', + mediaCount: '0', + pollCount: '0', + pollMultiple: false, + pollExpired: false, + size: 'medium', + filters: [false, false, false], // hide, blur, warn + mediaPreference: 'default', + expandWarnings: false, + }); + + // Update function with view transitions + const updateToggles = (updates) => { + // Check for browser support + if (!document.startViewTransition) { + setToggleState((prev) => ({ ...prev, ...updates })); + return; + } + + // Use view transition API + document.startViewTransition(() => { + setToggleState((prev) => ({ ...prev, ...updates })); + }); + }; + + // Set up preference stubbing + useEffect(() => { + console.log('User preference updated:', { + mediaPreference: toggleState.mediaPreference, + expandWarnings: toggleState.expandWarnings, + }); + + // Create a backup of the original method + const originalGet = store.account.get; + + // Stub the store.account.get method to return our custom preferences + store.account.get = (key) => { + if (key === 'preferences') { + console.log('Preferences requested, returning:', { + 'reading:expand:media': toggleState.mediaPreference, + 'reading:expand:spoilers': toggleState.expandWarnings, + }); + return { + 'reading:expand:media': toggleState.mediaPreference, + 'reading:expand:spoilers': toggleState.expandWarnings, + }; + } + return originalGet.call(store.account, key); + }; + + // Clear the getPreferences cache to ensure our new preferences are used + getPreferences.clear(); + + // Restore the original method when the component unmounts + return () => { + store.account.get = originalGet; + getPreferences.clear(); + }; + }, [toggleState.mediaPreference, toggleState.expandWarnings]); + + // Generate status with current toggle values + const mockStatus = MOCK_STATUS({ + toggles: { + loading: toggleState.loading, + mediaFirst: toggleState.mediaFirst, + contentFormat: toggleState.hasContent ? 'text' : null, + contentType: toggleState.contentType, + spoiler: toggleState.hasSpoiler, + spoilerType: toggleState.spoilerType, + mediaCount: toggleState.mediaCount, + pollCount: toggleState.pollCount, + pollMultiple: toggleState.pollMultiple, + pollExpired: toggleState.pollExpired, + size: toggleState.size, + filters: toggleState.filters, + }, + }); + + // Handler for filter checkboxes + const handleFilterChange = (index) => { + const newFilters = [...toggleState.filters]; + newFilters[index] = !newFilters[index]; + updateToggles({ filters: newFilters }); + }; + + return ( +