this repo has no description
0
fork

Configure Feed

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

Merge pull request #311 from cheeaun/main

Update from main

authored by

Chee Aun and committed by
GitHub
11407d0f 84b63682

+320 -75
+4 -2
.github/workflows/prodtag.yml
··· 20 20 with: 21 21 node-version: 18 22 22 - run: npm ci && npm run build 23 - - run: cd dist && zip -r ../phanpy-dist.zip . && cd .. 23 + - run: cd dist && zip -r ../phanpy-dist.zip . && tar -czf ../phanpy-dist.tar.gz . && cd .. 24 24 - id: tag_name 25 25 run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD) 26 26 - uses: softprops/action-gh-release@v1 27 27 with: 28 28 tag_name: ${{ steps.tag_name.outputs.tag_name }} 29 29 generate_release_notes: true 30 - files: phanpy-dist.zip 30 + files: | 31 + phanpy-dist.zip 32 + phanpy-dist.tar.gz
+2 -1
.gitignore
··· 26 26 # Custom 27 27 .env.dev 28 28 src/data/instances-full.json 29 - phanpy-dist.zip 29 + phanpy-dist.zip 30 + phanpy-dist.tar.gz
+1
README.md
··· 172 172 - [Litterbox](https://litterbox.koyu.space/) 173 173 - [Statuzer](https://statuzer.com/) 174 174 - [Tusked](https://tusked.app/) 175 + - [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone) 175 176 - [More...](https://github.com/hueyy/awesome-mastodon/#clients) 176 177 177 178 ## 💁‍♂️ Notice to all other social media client developers
+29 -29
package-lock.json
··· 8 8 "name": "phanpy", 9 9 "version": "0.1.0", 10 10 "dependencies": { 11 - "@formatjs/intl-localematcher": "~0.4.2", 12 - "@formkit/auto-animate": "~0.8.0", 11 + "@formatjs/intl-localematcher": "~0.5.0", 12 + "@formkit/auto-animate": "~0.8.1", 13 13 "@github/text-expander-element": "~2.5.0", 14 14 "@iconify-icons/mingcute": "~1.2.9", 15 15 "@justinribeiro/lite-youtube": "~1.5.0", ··· 36 36 "swiped-events": "~1.1.7", 37 37 "toastify-js": "~1.12.0", 38 38 "uid": "~2.0.2", 39 - "use-debounce": "~9.0.4", 39 + "use-debounce": "~10.0.0", 40 40 "use-long-press": "~3.2.0", 41 41 "use-resize-observer": "~9.1.0", 42 42 "valtio": "1.9.0" ··· 51 51 "vite": "~4.5.0", 52 52 "vite-plugin-generate-file": "~0.0.4", 53 53 "vite-plugin-html-config": "~1.0.11", 54 - "vite-plugin-pwa": "~0.16.6", 54 + "vite-plugin-pwa": "~0.16.7", 55 55 "vite-plugin-remove-console": "~2.1.1", 56 56 "workbox-cacheable-response": "~7.0.0", 57 57 "workbox-expiration": "~7.0.0", ··· 3082 3082 } 3083 3083 }, 3084 3084 "node_modules/@formatjs/intl-localematcher": { 3085 - "version": "0.4.2", 3086 - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", 3087 - "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", 3085 + "version": "0.5.0", 3086 + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.0.tgz", 3087 + "integrity": "sha512-K1Xpg/8oyfCMxisJQa/fILoeoeyndcM0wcN8QiNG/uM5OAe1BcO1+2yd0gIboDI2tRJEsUi/sSBEYPbgkIdq4A==", 3088 3088 "dependencies": { 3089 3089 "tslib": "^2.4.0" 3090 3090 } 3091 3091 }, 3092 3092 "node_modules/@formkit/auto-animate": { 3093 - "version": "0.8.0", 3094 - "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz", 3095 - "integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw==" 3093 + "version": "0.8.1", 3094 + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz", 3095 + "integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig==" 3096 3096 }, 3097 3097 "node_modules/@github/combobox-nav": { 3098 3098 "version": "2.1.5", ··· 7232 7232 } 7233 7233 }, 7234 7234 "node_modules/use-debounce": { 7235 - "version": "9.0.4", 7236 - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz", 7237 - "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==", 7235 + "version": "10.0.0", 7236 + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz", 7237 + "integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==", 7238 7238 "engines": { 7239 - "node": ">= 10.0.0" 7239 + "node": ">= 16.0.0" 7240 7240 }, 7241 7241 "peerDependencies": { 7242 7242 "react": ">=16.8.0" ··· 7446 7446 } 7447 7447 }, 7448 7448 "node_modules/vite-plugin-pwa": { 7449 - "version": "0.16.6", 7450 - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz", 7451 - "integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==", 7449 + "version": "0.16.7", 7450 + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz", 7451 + "integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==", 7452 7452 "dev": true, 7453 7453 "dependencies": { 7454 7454 "debug": "^4.3.4", ··· 9602 9602 "optional": true 9603 9603 }, 9604 9604 "@formatjs/intl-localematcher": { 9605 - "version": "0.4.2", 9606 - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", 9607 - "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", 9605 + "version": "0.5.0", 9606 + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.0.tgz", 9607 + "integrity": "sha512-K1Xpg/8oyfCMxisJQa/fILoeoeyndcM0wcN8QiNG/uM5OAe1BcO1+2yd0gIboDI2tRJEsUi/sSBEYPbgkIdq4A==", 9608 9608 "requires": { 9609 9609 "tslib": "^2.4.0" 9610 9610 } 9611 9611 }, 9612 9612 "@formkit/auto-animate": { 9613 - "version": "0.8.0", 9614 - "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz", 9615 - "integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw==" 9613 + "version": "0.8.1", 9614 + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz", 9615 + "integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig==" 9616 9616 }, 9617 9617 "@github/combobox-nav": { 9618 9618 "version": "2.1.5", ··· 12457 12457 } 12458 12458 }, 12459 12459 "use-debounce": { 12460 - "version": "9.0.4", 12461 - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz", 12462 - "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==", 12460 + "version": "10.0.0", 12461 + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz", 12462 + "integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==", 12463 12463 "requires": {} 12464 12464 }, 12465 12465 "use-long-press": { ··· 12580 12580 "requires": {} 12581 12581 }, 12582 12582 "vite-plugin-pwa": { 12583 - "version": "0.16.6", 12584 - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz", 12585 - "integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==", 12583 + "version": "0.16.7", 12584 + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz", 12585 + "integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==", 12586 12586 "dev": true, 12587 12587 "requires": { 12588 12588 "debug": "^4.3.4",
+4 -4
package.json
··· 10 10 "sourcemap": "npx source-map-explorer dist/assets/*.js" 11 11 }, 12 12 "dependencies": { 13 - "@formatjs/intl-localematcher": "~0.4.2", 14 - "@formkit/auto-animate": "~0.8.0", 13 + "@formatjs/intl-localematcher": "~0.5.0", 14 + "@formkit/auto-animate": "~0.8.1", 15 15 "@github/text-expander-element": "~2.5.0", 16 16 "@iconify-icons/mingcute": "~1.2.9", 17 17 "@justinribeiro/lite-youtube": "~1.5.0", ··· 38 38 "swiped-events": "~1.1.7", 39 39 "toastify-js": "~1.12.0", 40 40 "uid": "~2.0.2", 41 - "use-debounce": "~9.0.4", 41 + "use-debounce": "~10.0.0", 42 42 "use-long-press": "~3.2.0", 43 43 "use-resize-observer": "~9.1.0", 44 44 "valtio": "1.9.0" ··· 53 53 "vite": "~4.5.0", 54 54 "vite-plugin-generate-file": "~0.0.4", 55 55 "vite-plugin-html-config": "~1.0.11", 56 - "vite-plugin-pwa": "~0.16.6", 56 + "vite-plugin-pwa": "~0.16.7", 57 57 "vite-plugin-remove-console": "~2.1.1", 58 58 "workbox-cacheable-response": "~7.0.0", 59 59 "workbox-expiration": "~7.0.0",
+1
src/app.css
··· 1135 1135 touch-action: pan-x; 1136 1136 user-select: none; 1137 1137 width: 100%; 1138 + gap: 16px; 1138 1139 } 1139 1140 .carousel::-webkit-scrollbar { 1140 1141 display: none;
+6 -1
src/components/account-block.jsx
··· 13 13 skeleton, 14 14 account, 15 15 avatarSize = 'xl', 16 + useAvatarStatic = false, 16 17 instance, 17 18 external, 18 19 internal, ··· 81 82 } 82 83 }} 83 84 > 84 - <Avatar url={avatar} size={avatarSize} squircle={bot} /> 85 + <Avatar 86 + url={useAvatarStatic ? avatarStatic : avatar || avatarStatic} 87 + size={avatarSize} 88 + squircle={bot} 89 + /> 85 90 <span class="account-block-content"> 86 91 {!hideDisplayName && ( 87 92 <>
+90 -9
src/components/compose.css
··· 137 137 border-color: transparent; 138 138 139 139 &.compose-field { 140 - @media (width < 30em) { 141 - margin-inline: calc(-1 * var(--form-padding-inline)); 142 - width: 100vw !important; 143 - max-width: 100vw; 144 - border-radius: 0; 145 - border: 0; 146 - } 147 - 148 140 @media (min-width: 40em) { 149 141 max-height: 65vh; 150 142 } ··· 212 204 left: -100vw !important; 213 205 } 214 206 #compose-container .toolbar-button select { 215 - background-color: inherit; 207 + background-color: transparent; 216 208 border: 0; 217 209 padding: 0 0 0 8px; 218 210 margin: 0; 219 211 appearance: none; 212 + line-height: 1em; 220 213 } 221 214 #compose-container .toolbar-button:not(.show-field) select { 222 215 right: 0; ··· 619 612 #custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img { 620 613 transform: scale(1.5); 621 614 } 615 + 616 + .compose-field-container { 617 + display: grid !important; 618 + 619 + @media (width < 30em) { 620 + margin-inline: calc(-1 * var(--form-padding-inline)); 621 + width: 100vw !important; 622 + max-width: 100vw; 623 + 624 + .compose-field { 625 + border-radius: 0; 626 + outline-offset: -2px; 627 + } 628 + } 629 + 630 + &.debug { 631 + grid-template-columns: 1fr 1fr; 632 + } 633 + 634 + > * { 635 + grid-area: 1 / 1 / 2 / 2; 636 + } 637 + 638 + .compose-highlight { 639 + user-drag: none; 640 + user-select: none; 641 + pointer-events: none; 642 + touch-action: none; 643 + padding: 8px; 644 + color: transparent; 645 + background-color: transparent; 646 + border: 2px solid transparent; 647 + line-height: 1.4; 648 + overflow: auto; 649 + unicode-bidi: plaintext; 650 + -webkit-rtl-ordering: logical; 651 + rtl-ordering: logical; 652 + overflow-wrap: break-word; 653 + white-space: pre-wrap; 654 + min-height: 5em; 655 + max-height: 50vh; 656 + 657 + /* Follow textarea styles */ 658 + @media (min-width: 40em) { 659 + max-height: 65vh; 660 + } 661 + 662 + mark { 663 + color: inherit; 664 + } 665 + 666 + .compose-highlight-url, 667 + .compose-highlight-hashtag { 668 + background-color: transparent; 669 + text-decoration: underline; 670 + text-decoration-color: var(--link-faded-color); 671 + text-decoration-thickness: 2px; 672 + text-underline-offset: 2px; 673 + } 674 + .compose-highlight-mention, 675 + .compose-highlight-emoji-shortcode, 676 + .compose-highlight-exceeded { 677 + mix-blend-mode: multiply; 678 + border-radius: 4px; 679 + box-shadow: 0 0 0 1px; 680 + } 681 + .compose-highlight-mention { 682 + background-color: var(--orange-light-bg-color); 683 + box-shadow-color: var(--orange-light-bg-color); 684 + } 685 + .compose-highlight-emoji-shortcode { 686 + background-color: var(--bg-faded-color); 687 + box-shadow-color: var(--bg-faded-color); 688 + } 689 + .compose-highlight-exceeded { 690 + background-color: var(--red-bg-color); 691 + box-shadow-color: var(--red-bg-color); 692 + } 693 + 694 + @media (prefers-color-scheme: dark) { 695 + .compose-highlight-mention, 696 + .compose-highlight-emoji-shortcode, 697 + .compose-highlight-exceeded { 698 + mix-blend-mode: screen; 699 + } 700 + } 701 + } 702 + }
+95 -4
src/components/compose.jsx
··· 7 7 import { useHotkeys } from 'react-hotkeys-hook'; 8 8 import stringLength from 'string-length'; 9 9 import { uid } from 'uid/single'; 10 - import { useDebouncedCallback } from 'use-debounce'; 10 + import { useDebouncedCallback, useThrottledCallback } from 'use-debounce'; 11 11 import { useSnapshot } from 'valtio'; 12 12 13 13 import supportedLanguages from '../data/status-supported-languages'; ··· 102 102 return inputText 103 103 .replace(urlRegexObj, urlPlaceholder) 104 104 .replace(usernameRegex, '$1@$3'); 105 + } 106 + 107 + // https://github.com/mastodon/mastodon/blob/c03bd2a238741a012aa4b98dc4902d6cf948ab63/app/models/account.rb#L69 108 + const USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i; 109 + const MENTION_RE = new RegExp( 110 + `(?<![=\\/\\w])@((${USERNAME_RE.source})(?:@[\\w.-]+[\\w]+)?)`, 111 + 'ig', 112 + ); 113 + 114 + // AI-generated, all other regexes are too complicated 115 + const HASHTAG_RE = new RegExp( 116 + `(?<![=\\/\\w])#([a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)(?![\\/\\w])`, 117 + 'ig', 118 + ); 119 + 120 + // https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31 121 + const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'; 122 + const SCAN_RE = new RegExp( 123 + `(?<=[^A-Za-z0-9_:\\n]|^):(${SHORTCODE_RE_FRAGMENT}):(?=[^A-Za-z0-9_:]|$)`, 124 + 'g', 125 + ); 126 + 127 + function highlightText(text, { maxCharacters = Infinity }) { 128 + // Accept text string, return formatted HTML string 129 + let html = text; 130 + // Exceeded characters limit 131 + const { composerCharacterCount } = states; 132 + let leftoverHTML = ''; 133 + if (composerCharacterCount > maxCharacters) { 134 + const leftoverCount = composerCharacterCount - maxCharacters; 135 + // Highlight exceeded characters 136 + leftoverHTML = 137 + '<mark class="compose-highlight-exceeded">' + 138 + html.slice(-leftoverCount) + 139 + '</mark>'; 140 + html = html.slice(0, -leftoverCount); 141 + } 142 + 143 + html = html 144 + .replace(urlRegexObj, '$2<mark class="compose-highlight-url">$3</mark>') // URLs 145 + .replace(MENTION_RE, '<mark class="compose-highlight-mention">$&</mark>') // Mentions 146 + .replace(HASHTAG_RE, '<mark class="compose-highlight-hashtag">#$1</mark>') // Hashtags 147 + .replace( 148 + SCAN_RE, 149 + '<mark class="compose-highlight-emoji-shortcode">$&</mark>', 150 + ); // Emoji shortcodes 151 + 152 + return html + leftoverHTML; 105 153 } 106 154 107 155 function Compose({ ··· 565 613 account={currentAccountInfo} 566 614 accountInstance={currentAccount.instanceURL} 567 615 hideDisplayName 616 + useAvatarStatic 568 617 /> 569 618 )} 570 619 {!standalone ? ( ··· 1221 1270 // NOTE: This check is needed because the offsetHeight return 50000 (really large number) on first render 1222 1271 // No idea why it does that, will re-investigate in far future 1223 1272 const offset = offsetHeight - clientHeight; 1224 - textarea.style.height = value ? scrollHeight + offset + 'px' : null; 1273 + const height = value ? scrollHeight + offset + 'px' : null; 1274 + textarea.style.height = height; 1225 1275 } 1226 1276 } 1227 1277 ··· 1387 1437 handleCommited = (e) => { 1388 1438 const { input } = e.detail; 1389 1439 setText(input.value); 1440 + // fire input event 1441 + if (ref.current) { 1442 + const event = new Event('input', { bubbles: true }); 1443 + ref.current.dispatchEvent(event); 1444 + } 1390 1445 }; 1391 1446 1392 1447 textExpanderRef.current.addEventListener( ··· 1413 1468 }; 1414 1469 }, []); 1415 1470 1471 + useEffect(() => { 1472 + // Resize observer for textarea 1473 + const textarea = ref.current; 1474 + if (!textarea) return; 1475 + const resizeObserver = new ResizeObserver(() => { 1476 + // Get height of textarea, set height to textExpander 1477 + const { height } = textarea.getBoundingClientRect(); 1478 + textExpanderRef.current.style.height = height + 'px'; 1479 + }); 1480 + resizeObserver.observe(textarea); 1481 + }, []); 1482 + 1483 + const composeHighlightRef = useRef(); 1484 + const throttleHighlightText = useThrottledCallback((text) => { 1485 + composeHighlightRef.current.innerHTML = 1486 + highlightText(text, { 1487 + maxCharacters, 1488 + }) + '\n'; 1489 + // Newline to prevent multiple line breaks at the end from being collapsed, no idea why 1490 + }, 500); 1491 + 1416 1492 return ( 1417 - <text-expander ref={textExpanderRef} keys="@ # :"> 1493 + <text-expander 1494 + ref={textExpanderRef} 1495 + keys="@ # :" 1496 + class="compose-field-container" 1497 + > 1418 1498 <textarea 1419 1499 class="compose-field" 1420 1500 autoCapitalize="sentences" ··· 1466 1546 }} 1467 1547 onInput={(e) => { 1468 1548 const { target } = e; 1469 - setText(target.value); 1549 + const text = target.value; 1550 + setText(text); 1470 1551 autoResizeTextarea(target); 1471 1552 props.onInput?.(e); 1553 + throttleHighlightText(text); 1472 1554 }} 1473 1555 style={{ 1474 1556 width: '100%', 1475 1557 height: '4em', 1476 1558 // '--text-weight': (1 + charCount / 140).toFixed(1) || 1, 1477 1559 }} 1560 + onScroll={(e) => { 1561 + const { scrollTop } = e.target; 1562 + composeHighlightRef.current.scrollTop = scrollTop; 1563 + }} 1564 + /> 1565 + <div 1566 + ref={composeHighlightRef} 1567 + class="compose-highlight" 1568 + aria-hidden="true" 1478 1569 /> 1479 1570 </text-expander> 1480 1571 );
+63 -16
src/components/media-modal.jsx
··· 1 1 import { Menu } from '@szhsin/react-menu'; 2 2 import { getBlurHashAverageColor } from 'fast-blurhash'; 3 - import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; 3 + import { 4 + useEffect, 5 + useLayoutEffect, 6 + useMemo, 7 + useRef, 8 + useState, 9 + } from 'preact/hooks'; 4 10 import { useHotkeys } from 'react-hotkeys-hook'; 5 11 6 12 import { oklab2rgb, rgb2oklab } from '../utils/color-utils'; ··· 102 108 return () => clearTimeout(timer); 103 109 }, []); 104 110 111 + const mediaAccentColors = useMemo(() => { 112 + return mediaAttachments?.map((media) => { 113 + const { blurhash } = media; 114 + if (blurhash) { 115 + const averageColor = getBlurHashAverageColor(blurhash); 116 + const labAverageColor = rgb2oklab(averageColor); 117 + return oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]); 118 + } 119 + return null; 120 + }); 121 + }, [mediaAttachments]); 122 + const mediaAccentGradient = useMemo(() => { 123 + const gap = 5; 124 + const range = 100 / mediaAccentColors.length; 125 + return ( 126 + mediaAccentColors 127 + ?.map((color, i) => { 128 + const start = i * range + gap; 129 + const end = (i + 1) * range - gap; 130 + if (color) { 131 + return ` 132 + rgba(${color?.join(',')}, 0.4) ${start}%, 133 + rgba(${color?.join(',')}, 0.4) ${end}% 134 + `; 135 + } 136 + 137 + return ` 138 + transparent ${start}%, 139 + transparent ${end}% 140 + `; 141 + }) 142 + ?.join(', ') || 'transparent' 143 + ); 144 + }, [mediaAccentColors]); 145 + 105 146 return ( 106 147 <div 107 148 class={`media-modal-container media-modal-count-${mediaAttachments?.length}`} ··· 120 161 onClose(); 121 162 } 122 163 }} 164 + style={ 165 + mediaAttachments.length > 1 166 + ? { 167 + backgroundAttachment: 'local', 168 + backgroundImage: `linear-gradient( 169 + to right, ${mediaAccentGradient})`, 170 + } 171 + : {} 172 + } 123 173 > 124 174 {mediaAttachments?.map((media, i) => { 125 - const { blurhash } = media; 126 - let accentColor; 127 - if (blurhash) { 128 - const averageColor = getBlurHashAverageColor(blurhash); 129 - const labAverageColor = rgb2oklab(averageColor); 130 - accentColor = oklab2rgb([ 131 - 0.6, 132 - labAverageColor[1], 133 - labAverageColor[2], 134 - ]); 135 - } 175 + const accentColor = 176 + mediaAttachments.length === 1 ? mediaAccentColors[i] : null; 136 177 return ( 137 178 <div 138 179 class="carousel-item" 139 - style={{ 140 - '--accent-color': `rgb(${accentColor?.join(',')})`, 141 - '--accent-alpha-color': `rgba(${accentColor?.join(',')}, 0.4)`, 142 - }} 180 + style={ 181 + accentColor 182 + ? { 183 + '--accent-color': `rgb(${accentColor?.join(',')})`, 184 + '--accent-alpha-color': `rgba(${accentColor?.join( 185 + ',', 186 + )}, 0.4)`, 187 + } 188 + : {} 189 + } 143 190 tabindex="0" 144 191 key={media.id} 145 192 ref={i === currentIndex ? carouselFocusItem : null}
+19 -1
src/components/timeline.jsx
··· 65 65 try { 66 66 let { done, value } = await fetchItems(firstLoad); 67 67 if (Array.isArray(value)) { 68 + // Avoid grouping for pinned posts 69 + const [pinnedPosts, otherPosts] = value.reduce( 70 + (acc, item) => { 71 + if (item._pinned) { 72 + acc[0].push(item); 73 + } else { 74 + acc[1].push(item); 75 + } 76 + return acc; 77 + }, 78 + [[], []], 79 + ); 80 + value = otherPosts; 68 81 if (allowGrouping) { 69 82 if (boostsCarousel) { 70 83 value = groupBoosts(value); 71 84 } 72 85 value = groupContext(value); 86 + } 87 + if (pinnedPosts.length) { 88 + value = pinnedPosts.concat(value); 73 89 } 74 90 console.log(value); 75 91 if (firstLoad) { ··· 282 298 // checkForUpdates interval 283 299 useInterval( 284 300 loadOrCheckUpdates, 285 - visible && !showNew ? checkForUpdatesInterval : null, 301 + visible && !showNew 302 + ? checkForUpdatesInterval * (nearReachStart ? 1 : 2) 303 + : null, 286 304 ); 287 305 288 306 const hiddenUI = scrollDirection === 'end' && !nearReachStart;
+6
src/index.css
··· 18 18 --purple-color: blueviolet; 19 19 --green-color: darkgreen; 20 20 --orange-color: darkorange; 21 + --orange-light-bg-color: color-mix( 22 + in srgb, 23 + var(--orange-color) 20%, 24 + transparent 25 + ); 21 26 --red-color: orangered; 27 + --red-bg-color: color-mix(in lch, var(--red-color) 40%, transparent); 22 28 --bg-color: #fff; 23 29 --bg-faded-color: #f0f2f5; 24 30 --bg-blur-color: #fff9;
-8
src/pages/welcome.css
··· 33 33 max-height: 800px; 34 34 display: flex; 35 35 flex-direction: column; 36 - 37 - a { 38 - color: inherit; 39 - 40 - &:hover { 41 - color: var(--link-text-color); 42 - } 43 - } 44 36 } 45 37 46 38 h1 {