this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial work on the sandbox

Rough but works

+922 -1
+3 -1
src/app.jsx
··· 43 43 import Mentions from './pages/mentions'; 44 44 import Notifications from './pages/notifications'; 45 45 import Public from './pages/public'; 46 + import Sandbox from './pages/sandbox'; 46 47 import ScheduledPosts from './pages/scheduled-posts'; 47 48 import Search from './pages/search'; 48 49 import StatusRoute from './pages/status-route'; ··· 497 498 const location = useLocation(); 498 499 const nonRootLocation = useMemo(() => { 499 500 const { pathname } = location; 500 - return !/^\/(login|welcome)/i.test(pathname); 501 + return !/^\/(login|welcome|_sandbox)/i.test(pathname); 501 502 }, [location]); 502 503 503 504 return ( ··· 505 506 <Route path="/" element={<Root isLoggedIn={isLoggedIn} />} /> 506 507 <Route path="/login" element={<Login />} /> 507 508 <Route path="/welcome" element={<Welcome />} /> 509 + <Route path="/_sandbox" element={<Sandbox />} /> 508 510 </Routes> 509 511 ); 510 512 });
+180
src/pages/sandbox.css
··· 1 + #sandbox { 2 + display: grid; 3 + grid-template-rows: auto 1fr 1fr; 4 + width: 100%; 5 + height: 100svh; 6 + background-color: var(--bg-faded-color); 7 + 8 + @media (min-width: 40em) { 9 + grid-template-rows: auto 1fr; 10 + grid-template-columns: 1fr min(40%, 320px); 11 + } 12 + 13 + header { 14 + display: flex; 15 + align-items: center; 16 + grid-column: 1 / -1; 17 + 18 + h1 { 19 + font-size: 1em; 20 + font-weight: normal; 21 + text-transform: uppercase; 22 + margin: 0; 23 + padding: 0; 24 + color: var(--text-insignificant-color); 25 + } 26 + } 27 + 28 + + #compose-button, 29 + ~ #shortcuts { 30 + display: none; 31 + } 32 + 33 + .sandbox-preview { 34 + position: relative; 35 + padding: 16px; 36 + background-color: var(--bg-color); 37 + /* No need for preview container transition */ 38 + /* chess board background, not rotated */ 39 + background-image: 40 + linear-gradient(var(--bg-faded-color) 2px, transparent 2px), 41 + linear-gradient(90deg, var(--bg-faded-color) 2px, transparent 2px), 42 + linear-gradient(var(--bg-faded-color) 1px, transparent 1px), 43 + linear-gradient(90deg, var(--bg-faded-color) 1px, transparent 1px); 44 + background-size: 45 + 50px 50px, 46 + 50px 50px, 47 + 10px 10px, 48 + 10px 10px; 49 + background-position: 50 + -2px -2px, 51 + -2px -2px, 52 + -1px -1px, 53 + -1px -1px; 54 + overflow: auto; 55 + box-shadow: 0 0 0 1px var(--outline-color); 56 + display: flex; 57 + align-items: safe center; 58 + justify-content: center; 59 + 60 + > .status, 61 + > * > .status { 62 + min-width: 320px; 63 + max-width: 40em; 64 + background-color: var(--bg-color); 65 + border-radius: 8px; 66 + box-shadow: 67 + 0 4px 16px var(--drop-shadow-color), 68 + 0 8px 32px -4px var(--drop-shadow-color); 69 + view-transition-name: status; 70 + 71 + .meta { 72 + view-transition-name: status-meta; 73 + } 74 + 75 + .avatar { 76 + view-transition-name: status-avatar; 77 + } 78 + 79 + .content-container { 80 + view-transition-name: status-content-container; 81 + } 82 + 83 + .media-container { 84 + view-transition-name: status-media; 85 + } 86 + 87 + .poll { 88 + view-transition-name: status-poll; 89 + } 90 + 91 + .status-badge { 92 + view-transition-name: status-badge; 93 + } 94 + 95 + .actions { 96 + view-transition-name: status-actions; 97 + } 98 + } 99 + } 100 + 101 + .sandbox-toggles { 102 + padding: 16px; 103 + font-size: 0.8em; 104 + overflow: auto; 105 + background-color: var(--bg-blur-color); 106 + box-shadow: 0 0 0 1px var(--outline-color); 107 + 108 + h2 { 109 + margin-top: 0; 110 + padding-top: 0; 111 + color: var(--text-insignificant-color); 112 + font-size: 1em; 113 + text-transform: uppercase; 114 + } 115 + 116 + h3 { 117 + color: var(--text-insignificant-color); 118 + font-size: 1em; 119 + } 120 + 121 + > ul { 122 + display: flex; 123 + flex-direction: column; 124 + row-gap: 8px; 125 + 126 + li:has(> label) ul { 127 + padding-inline-start: 24px; 128 + } 129 + } 130 + 131 + ul { 132 + margin: 0; 133 + padding: 0; 134 + list-style: none; 135 + 136 + ul:not(:has(ul)) { 137 + display: flex; 138 + flex-wrap: wrap; 139 + column-gap: 4px; 140 + 141 + li { 142 + padding-inline-start: 0; 143 + } 144 + } 145 + } 146 + li { 147 + margin: 0; 148 + padding: 0; 149 + list-style: none; 150 + } 151 + 152 + label { 153 + cursor: pointer; 154 + border-radius: 8px; 155 + 156 + &:hover { 157 + background-color: var(--link-bg-color); 158 + } 159 + } 160 + 161 + label, 162 + b, 163 + i { 164 + display: flex; 165 + padding: 4px; 166 + gap: 4px; 167 + align-items: center; 168 + } 169 + 170 + input[type='number'] { 171 + width: 3em; 172 + text-align: end; 173 + } 174 + 175 + input[type='radio' i], 176 + input[type='checkbox' i] { 177 + margin: 0; 178 + } 179 + } 180 + }
+739
src/pages/sandbox.jsx
··· 1 + import './sandbox.css'; 2 + 3 + import { useEffect, useState } from 'preact/hooks'; 4 + 5 + import Status from '../components/status'; 6 + import { getPreferences } from '../utils/api'; 7 + import FilterContext from '../utils/filter-context'; 8 + import store from '../utils/store'; 9 + 10 + function hashID(obj) { 11 + if (!obj) return ''; 12 + if (typeof obj !== 'object') return String(obj); 13 + return Object.entries(obj) 14 + .map(([k, v]) => 15 + typeof v === 'object' && !Array.isArray(v) 16 + ? `${k}:${hashID(v)}` 17 + : `${k}:${v}`, 18 + ) 19 + .join('|'); 20 + } 21 + 22 + const MOCK_STATUS = ({ toggles = {} } = {}) => { 23 + console.log('toggles', toggles); 24 + const { 25 + loading, 26 + mediaFirst, 27 + contentType, 28 + contentFormat, 29 + spoiler, 30 + spoilerType, 31 + mediaCount, 32 + pollCount, 33 + pollMultiple, 34 + pollExpired, 35 + size, 36 + filters, 37 + userPreferences, 38 + } = toggles; 39 + 40 + const shortContent = 'This is a test status with short text content.'; 41 + const longContent = `<p>This is a test status with long text content. It contains multiple paragraphs and spans several lines to demonstrate how longer content appears.</p> 42 + 43 + <p>Second paragraph goes here with more sample text. The Status component will render this appropriately based on the current size setting.</p> 44 + 45 + <p>Third paragraph adds even more content to ensure we have a properly long post that might get truncated depending on the view settings.</p>`; 46 + const linksContent = `<p>This is a test status with links. Check out <a href="https://example.com">this website</a> and <a href="https://google.com">Google</a>. Links should be clickable and properly styled.</p>`; 47 + const hashtagsContent = `<p>This is a test status with hashtags. <a href="https://example.social/tags/coding" class="hashtag" rel="tag">#coding</a> <a href="https://example.social/tags/webdev" class="hashtag" rel="tag">#webdev</a> <a href="https://example.social/tags/javascript" class="hashtag" rel="tag">#javascript</a> <a href="https://example.social/tags/reactjs" class="hashtag" rel="tag">#reactjs</a> <a href="https://example.social/tags/preact" class="hashtag" rel="tag">#preact</a></p><p>Hashtags should be formatted and clickable.</p>`; 48 + const mentionsContent = `<p>This is a test status with mentions. Hello <a href="https://example.social/@cheeaun" class="u-url mention">@cheeaun</a> and <a href="https://example.social/@test" class="u-url mention">@test</a>! What do you think about this <a href="https://example.social/@another_user" class="u-url mention">@another_user</a>?</p><p>Mentions should be highlighted and clickable.</p>`; 49 + 50 + const base = { 51 + // Random ID to un-memoize Status 52 + id: hashID(toggles), 53 + account: { 54 + username: 'test', 55 + name: 'Test', 56 + // avatar: 'https://picsum.photos/seed/avatar/200', 57 + avatar: '/logo-192.png', 58 + acct: 'test@localhost', 59 + url: 'https://test.localhost', 60 + }, 61 + content: 62 + contentFormat === 'text' 63 + ? contentType === 'long' 64 + ? longContent 65 + : contentType === 'links' 66 + ? linksContent 67 + : contentType === 'hashtags' 68 + ? hashtagsContent 69 + : contentType === 'mentions' 70 + ? mentionsContent 71 + : shortContent 72 + : '', 73 + visibility: 'public', 74 + createdAt: new Date().toISOString(), 75 + reblogsCount: 0, 76 + favouritesCount: 0, 77 + repliesCount: 5, 78 + emojis: [], 79 + mentions: [], 80 + tags: [], 81 + mediaAttachments: [], 82 + }; 83 + 84 + // Add media if selected 85 + if (mediaCount > 0) { 86 + base.mediaAttachments = Array(parseInt(mediaCount, 10)) 87 + .fill(0) 88 + .map((_, i) => ({ 89 + id: `media-${i}`, 90 + type: 'image', 91 + url: `https://picsum.photos/seed/media-${i}/600/400`, 92 + previewUrl: `https://picsum.photos/seed/media-${i}/300/200`, 93 + description: 94 + i % 2 === 0 ? `Sample image description for media ${i + 1}` : '', 95 + meta: { 96 + original: { 97 + width: 600, 98 + height: 400, 99 + }, 100 + small: { 101 + width: 600, 102 + height: 400, 103 + }, 104 + }, 105 + })); 106 + } 107 + 108 + // Add poll if selected 109 + if (pollCount > 0) { 110 + base.poll = { 111 + id: 'poll-1', 112 + options: Array(parseInt(pollCount, 10)) 113 + .fill(0) 114 + .map((_, i) => ({ 115 + title: `Option ${i + 1}`, 116 + votesCount: Math.floor(Math.random() * 100), 117 + })), 118 + // Set expiration date in the past if poll is expired, otherwise in the future 119 + expiresAt: pollExpired 120 + ? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // 24 hours ago 121 + : new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours from now 122 + expired: pollExpired, 123 + multiple: pollMultiple, 124 + // Use votersCount for multiple-choice polls, votesCount for single-choice polls 125 + votesCount: 150, 126 + votersCount: pollMultiple ? 100 : undefined, 127 + voted: false, 128 + }; 129 + } 130 + 131 + // Add spoiler if selected 132 + if (spoiler) { 133 + base.sensitive = true; 134 + base.spoilerText = 'Content warning: test spoiler'; 135 + 136 + if (spoilerType === 'mediaOnly') { 137 + // For media-only spoiler, remove spoilerText but keep sensitive true 138 + base.spoilerText = ''; 139 + } 140 + } 141 + 142 + // Add mentions and tags if needed 143 + if (contentType === 'mentions') { 144 + base.mentions = [ 145 + { 146 + id: '1', 147 + username: 'cheeaun', 148 + url: 'https://example.social/@cheeaun', 149 + acct: 'cheeaun', 150 + }, 151 + { 152 + id: '2', 153 + username: 'test', 154 + url: 'https://example.social/@test', 155 + acct: 'test', 156 + }, 157 + { 158 + id: '3', 159 + username: 'another_user', 160 + url: 'https://example.social/@another_user', 161 + acct: 'another_user', 162 + }, 163 + ]; 164 + } 165 + 166 + if (contentType === 'hashtags') { 167 + base.tags = [ 168 + { 169 + name: 'coding', 170 + url: 'https://example.social/tags/coding', 171 + }, 172 + { 173 + name: 'webdev', 174 + url: 'https://example.social/tags/webdev', 175 + }, 176 + { 177 + name: 'javascript', 178 + url: 'https://example.social/tags/javascript', 179 + }, 180 + { 181 + name: 'reactjs', 182 + url: 'https://example.social/tags/reactjs', 183 + }, 184 + { 185 + name: 'preact', 186 + url: 'https://example.social/tags/preact', 187 + }, 188 + ]; 189 + } 190 + 191 + // Add any relevant filtered flags based on filter settings 192 + if (filters && filters.some((f) => f)) { 193 + base.filtered = filters 194 + .map((enabled, i) => { 195 + if (!enabled) return null; 196 + const filterTypes = ['hide', 'blur', 'warn']; 197 + return { 198 + filter: { 199 + id: `filter-${i}`, 200 + title: `Sample ${filterTypes[i]} filter`, 201 + context: ['home', 'public', 'thread', 'account'], 202 + filterAction: filterTypes[i], 203 + }, 204 + keywordMatches: [], 205 + statusMatches: [], 206 + }; 207 + }) 208 + .filter(Boolean); 209 + } 210 + 211 + console.log('Final base', base); 212 + return base; 213 + }; 214 + 215 + export default function Sandbox() { 216 + // Consolidated state for all toggles 217 + const [toggleState, setToggleState] = useState({ 218 + loading: false, 219 + mediaFirst: false, 220 + hasContent: true, 221 + contentType: 'short', 222 + hasSpoiler: false, 223 + spoilerType: 'all', 224 + mediaCount: '0', 225 + pollCount: '0', 226 + pollMultiple: false, 227 + pollExpired: false, 228 + size: 'medium', 229 + filters: [false, false, false], // hide, blur, warn 230 + mediaPreference: 'default', 231 + expandWarnings: false, 232 + }); 233 + 234 + // Update function with view transitions 235 + const updateToggles = (updates) => { 236 + // Check for browser support 237 + if (!document.startViewTransition) { 238 + setToggleState((prev) => ({ ...prev, ...updates })); 239 + return; 240 + } 241 + 242 + // Use view transition API 243 + document.startViewTransition(() => { 244 + setToggleState((prev) => ({ ...prev, ...updates })); 245 + }); 246 + }; 247 + 248 + // Set up preference stubbing 249 + useEffect(() => { 250 + console.log('User preference updated:', { 251 + mediaPreference: toggleState.mediaPreference, 252 + expandWarnings: toggleState.expandWarnings, 253 + }); 254 + 255 + // Create a backup of the original method 256 + const originalGet = store.account.get; 257 + 258 + // Stub the store.account.get method to return our custom preferences 259 + store.account.get = (key) => { 260 + if (key === 'preferences') { 261 + console.log('Preferences requested, returning:', { 262 + 'reading:expand:media': toggleState.mediaPreference, 263 + 'reading:expand:spoilers': toggleState.expandWarnings, 264 + }); 265 + return { 266 + 'reading:expand:media': toggleState.mediaPreference, 267 + 'reading:expand:spoilers': toggleState.expandWarnings, 268 + }; 269 + } 270 + return originalGet.call(store.account, key); 271 + }; 272 + 273 + // Clear the getPreferences cache to ensure our new preferences are used 274 + getPreferences.clear(); 275 + 276 + // Restore the original method when the component unmounts 277 + return () => { 278 + store.account.get = originalGet; 279 + getPreferences.clear(); 280 + }; 281 + }, [toggleState.mediaPreference, toggleState.expandWarnings]); 282 + 283 + // Generate status with current toggle values 284 + const mockStatus = MOCK_STATUS({ 285 + toggles: { 286 + loading: toggleState.loading, 287 + mediaFirst: toggleState.mediaFirst, 288 + contentFormat: toggleState.hasContent ? 'text' : null, 289 + contentType: toggleState.contentType, 290 + spoiler: toggleState.hasSpoiler, 291 + spoilerType: toggleState.spoilerType, 292 + mediaCount: toggleState.mediaCount, 293 + pollCount: toggleState.pollCount, 294 + pollMultiple: toggleState.pollMultiple, 295 + pollExpired: toggleState.pollExpired, 296 + size: toggleState.size, 297 + filters: toggleState.filters, 298 + }, 299 + }); 300 + 301 + // Handler for filter checkboxes 302 + const handleFilterChange = (index) => { 303 + const newFilters = [...toggleState.filters]; 304 + newFilters[index] = !newFilters[index]; 305 + updateToggles({ filters: newFilters }); 306 + }; 307 + 308 + return ( 309 + <main id="sandbox"> 310 + <header> 311 + <a href="#/" class="button plain4"> 312 + × 313 + </a> 314 + <h1>Sandbox</h1> 315 + </header> 316 + <div class="sandbox-preview"> 317 + <FilterContext.Provider value={'home'}> 318 + {toggleState.loading ? ( 319 + <Status 320 + skeleton 321 + mediaFirst={toggleState.mediaFirst} 322 + key={`skeleton-${toggleState.mediaFirst}`} 323 + /> 324 + ) : ( 325 + <Status 326 + status={mockStatus} 327 + mediaFirst={toggleState.mediaFirst} 328 + size={ 329 + toggleState.size === 'small' 330 + ? 's' 331 + : toggleState.size === 'medium' 332 + ? 'm' 333 + : 'l' 334 + } 335 + allowFilters={true} 336 + // Add a key that changes when preferences change to force re-render 337 + key={`status-${toggleState.mediaPreference}-${toggleState.expandWarnings}-${mockStatus.id}`} 338 + /> 339 + )} 340 + </FilterContext.Provider> 341 + </div> 342 + <form class="sandbox-toggles" onSubmit={(e) => e.preventDefault()}> 343 + <h2>Toggles</h2> 344 + <ul> 345 + <li> 346 + <b>Miscellaneous</b> 347 + <ul> 348 + <li> 349 + <label> 350 + <input 351 + type="checkbox" 352 + checked={toggleState.loading} 353 + onChange={() => 354 + updateToggles({ loading: !toggleState.loading }) 355 + } 356 + /> 357 + <span>Loading</span> 358 + </label> 359 + </li> 360 + <li> 361 + <label> 362 + <input 363 + type="checkbox" 364 + checked={toggleState.mediaFirst} 365 + onChange={() => 366 + updateToggles({ mediaFirst: !toggleState.mediaFirst }) 367 + } 368 + /> 369 + <span>Media first</span> 370 + </label> 371 + </li> 372 + </ul> 373 + </li> 374 + <li> 375 + <b>Content</b> 376 + <ul> 377 + <li> 378 + <label> 379 + <input 380 + type="checkbox" 381 + checked={toggleState.hasContent} 382 + onChange={() => { 383 + // Create the update object 384 + const updates = { hasContent: !toggleState.hasContent }; 385 + 386 + // If turning off text and no media, then add media 387 + if ( 388 + toggleState.hasContent && 389 + parseInt(toggleState.mediaCount) === 0 390 + ) { 391 + updates.mediaCount = '1'; 392 + } 393 + 394 + // Apply all updates in one transition 395 + updateToggles(updates); 396 + }} 397 + disabled={parseInt(toggleState.mediaCount) === 0} 398 + /> 399 + <span>Text</span> 400 + </label> 401 + <ul> 402 + <li> 403 + <label> 404 + <input 405 + type="radio" 406 + name="contentType" 407 + checked={toggleState.contentType === 'short'} 408 + onChange={() => updateToggles({ contentType: 'short' })} 409 + disabled={!toggleState.hasContent} 410 + /> 411 + <span>Short</span> 412 + </label> 413 + </li> 414 + <li> 415 + <label> 416 + <input 417 + type="radio" 418 + name="contentType" 419 + checked={toggleState.contentType === 'long'} 420 + onChange={() => updateToggles({ contentType: 'long' })} 421 + disabled={!toggleState.hasContent} 422 + /> 423 + <span>Long</span> 424 + </label> 425 + </li> 426 + <li> 427 + <label> 428 + <input 429 + type="radio" 430 + name="contentType" 431 + checked={toggleState.contentType === 'links'} 432 + onChange={() => updateToggles({ contentType: 'links' })} 433 + disabled={!toggleState.hasContent} 434 + /> 435 + <span>With links</span> 436 + </label> 437 + </li> 438 + <li> 439 + <label> 440 + <input 441 + type="radio" 442 + name="contentType" 443 + checked={toggleState.contentType === 'hashtags'} 444 + onChange={() => 445 + updateToggles({ contentType: 'hashtags' }) 446 + } 447 + disabled={!toggleState.hasContent} 448 + /> 449 + <span>With hashtags</span> 450 + </label> 451 + </li> 452 + <li> 453 + <label> 454 + <input 455 + type="radio" 456 + name="contentType" 457 + checked={toggleState.contentType === 'mentions'} 458 + onChange={() => 459 + updateToggles({ contentType: 'mentions' }) 460 + } 461 + disabled={!toggleState.hasContent} 462 + /> 463 + <span>With mentions</span> 464 + </label> 465 + </li> 466 + </ul> 467 + </li> 468 + <li> 469 + <label> 470 + <input 471 + type="checkbox" 472 + checked={toggleState.hasSpoiler} 473 + onChange={() => 474 + updateToggles({ hasSpoiler: !toggleState.hasSpoiler }) 475 + } 476 + /> 477 + <span>Content warning</span> 478 + </label> 479 + <ul> 480 + <li> 481 + <label> 482 + <input 483 + type="radio" 484 + name="spoilerType" 485 + checked={toggleState.spoilerType === 'all'} 486 + onChange={() => updateToggles({ spoilerType: 'all' })} 487 + /> 488 + <span>Whole content</span> 489 + </label> 490 + </li> 491 + <li> 492 + <label> 493 + <input 494 + type="radio" 495 + name="spoilerType" 496 + checked={toggleState.spoilerType === 'mediaOnly'} 497 + onChange={() => 498 + updateToggles({ spoilerType: 'mediaOnly' }) 499 + } 500 + /> 501 + <span>Media only</span> 502 + </label> 503 + </li> 504 + </ul> 505 + </li> 506 + <li> 507 + <label> 508 + <input 509 + type="checkbox" 510 + checked={parseInt(toggleState.mediaCount) > 0} 511 + onChange={(e) => { 512 + const newHasMedia = e.target.checked; 513 + const updates = { 514 + mediaCount: newHasMedia ? '1' : '0', 515 + }; 516 + 517 + // If removing media and no text content, enable text content 518 + if (!newHasMedia && !toggleState.hasContent) { 519 + updates.hasContent = true; 520 + } 521 + 522 + updateToggles(updates); 523 + }} 524 + /> 525 + <span>Media</span> 526 + <input 527 + type="number" 528 + min="1" 529 + value={ 530 + toggleState.mediaCount === '0' 531 + ? '1' 532 + : toggleState.mediaCount 533 + } 534 + step="1" 535 + onChange={(e) => 536 + updateToggles({ mediaCount: e.target.value }) 537 + } 538 + disabled={parseInt(toggleState.mediaCount) === 0} 539 + /> 540 + </label> 541 + </li> 542 + <li> 543 + <label> 544 + <input 545 + type="checkbox" 546 + checked={parseInt(toggleState.pollCount) > 0} 547 + onChange={(e) => { 548 + const updates = { 549 + pollCount: e.target.checked ? '2' : '0', 550 + }; 551 + 552 + // Reset multiple to false when disabling poll 553 + if (!e.target.checked) { 554 + updates.pollMultiple = false; 555 + } 556 + 557 + updateToggles(updates); 558 + }} 559 + /> 560 + <span>Poll</span> 561 + <input 562 + type="number" 563 + min="2" 564 + value={toggleState.pollCount} 565 + step="1" 566 + onChange={(e) => 567 + updateToggles({ pollCount: e.target.value }) 568 + } 569 + disabled={parseInt(toggleState.pollCount) === 0} 570 + /> 571 + <label> 572 + <input 573 + type="checkbox" 574 + checked={toggleState.pollMultiple} 575 + onChange={() => 576 + updateToggles({ 577 + pollMultiple: !toggleState.pollMultiple, 578 + }) 579 + } 580 + disabled={parseInt(toggleState.pollCount) === 0} 581 + /> 582 + <span>Multiple</span> 583 + </label> 584 + <label> 585 + <input 586 + type="checkbox" 587 + checked={toggleState.pollExpired} 588 + onChange={() => 589 + updateToggles({ pollExpired: !toggleState.pollExpired }) 590 + } 591 + disabled={parseInt(toggleState.pollCount) === 0} 592 + /> 593 + <span>Expired</span> 594 + </label> 595 + </label> 596 + </li> 597 + </ul> 598 + </li> 599 + <li> 600 + <b>Size</b> 601 + <ul> 602 + <li> 603 + <label> 604 + <input 605 + type="radio" 606 + name="size" 607 + checked={toggleState.size === 'small'} 608 + onChange={() => updateToggles({ size: 'small' })} 609 + /> 610 + <span>Small</span> 611 + </label> 612 + </li> 613 + <li> 614 + <label> 615 + <input 616 + type="radio" 617 + name="size" 618 + checked={toggleState.size === 'medium'} 619 + onChange={() => updateToggles({ size: 'medium' })} 620 + /> 621 + <span>Medium</span> 622 + </label> 623 + </li> 624 + <li> 625 + <label> 626 + <input 627 + type="radio" 628 + name="size" 629 + checked={toggleState.size === 'large'} 630 + onChange={() => updateToggles({ size: 'large' })} 631 + /> 632 + <span>Large</span> 633 + </label> 634 + </li> 635 + </ul> 636 + </li> 637 + <li> 638 + <b>Filters</b> 639 + <ul> 640 + <li> 641 + <label> 642 + <input 643 + type="checkbox" 644 + checked={toggleState.filters[0]} 645 + onChange={() => handleFilterChange(0)} 646 + /> 647 + <span>Hide</span> 648 + </label> 649 + </li> 650 + <li> 651 + <label> 652 + <input 653 + type="checkbox" 654 + checked={toggleState.filters[1]} 655 + onChange={() => handleFilterChange(1)} 656 + /> 657 + <span>Blur</span> 658 + </label> 659 + </li> 660 + <li> 661 + <label> 662 + <input 663 + type="checkbox" 664 + checked={toggleState.filters[2]} 665 + onChange={() => handleFilterChange(2)} 666 + /> 667 + <span>Warn</span> 668 + </label> 669 + </li> 670 + </ul> 671 + </li> 672 + <li> 673 + <h3>User preferences for sensitive content</h3> 674 + <ul> 675 + <li> 676 + <b>Media display</b> 677 + <ul> 678 + <li> 679 + <label> 680 + <input 681 + type="radio" 682 + name="mediaPreference" 683 + checked={toggleState.mediaPreference === 'default'} 684 + onChange={() => 685 + updateToggles({ mediaPreference: 'default' }) 686 + } 687 + /> 688 + <span>Hide media marked as sensitive</span> 689 + </label> 690 + </li> 691 + <li> 692 + <label> 693 + <input 694 + type="radio" 695 + name="mediaPreference" 696 + checked={toggleState.mediaPreference === 'show_all'} 697 + onChange={() => 698 + updateToggles({ mediaPreference: 'show_all' }) 699 + } 700 + /> 701 + <span>Always show media</span> 702 + </label> 703 + </li> 704 + <li> 705 + <label> 706 + <input 707 + type="radio" 708 + name="mediaPreference" 709 + checked={toggleState.mediaPreference === 'hide_all'} 710 + onChange={() => 711 + updateToggles({ mediaPreference: 'hide_all' }) 712 + } 713 + /> 714 + <span>Always hide media</span> 715 + </label> 716 + </li> 717 + </ul> 718 + </li> 719 + <li> 720 + <label> 721 + <input 722 + type="checkbox" 723 + checked={toggleState.expandWarnings} 724 + onChange={() => 725 + updateToggles({ 726 + expandWarnings: !toggleState.expandWarnings, 727 + }) 728 + } 729 + />{' '} 730 + <span>Always expand posts marked with content warnings</span> 731 + </label> 732 + </li> 733 + </ul> 734 + </li> 735 + </ul> 736 + </form> 737 + </main> 738 + ); 739 + }