diff --git a/src/app.css b/src/app.css index 109e7df3..c5e88541 100644 --- a/src/app.css +++ b/src/app.css @@ -75,7 +75,7 @@ a.mention span { overscroll-behavior: contain; } -.deck header { +.deck > header { min-height: 3em; position: sticky; top: 0; @@ -93,25 +93,25 @@ a.mention span { transition: transform 0.5s ease-in-out; user-select: none; } -.deck header[hidden] { +.deck > header[hidden] { transform: translateY(-100%); pointer-events: none; user-select: none; } -.deck header > .header-side:last-of-type { +.deck > header > .header-side:last-of-type { text-align: right; grid-column: 3; } -.deck header :is(button, .button).plain { +.deck > header :is(button, .button).plain { backdrop-filter: none; } -.deck header h1 { +.deck > header h1 { margin: 0 8px; padding: 0; font-size: 1.2em; text-align: center; } -.deck header h1:first-child { +.deck > header h1:first-child { text-align: left; padding-left: 8px; } @@ -368,11 +368,109 @@ a.mention span { filter: brightness(0.95); } +.boost-carousel { + background: linear-gradient( + to bottom right, + var(--reblog-faded-color), + transparent 60% + ); + position: relative; +} +.boost-carousel:after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background-image: radial-gradient( + ellipse 50% 32px at bottom center, + var(--reblog-faded-color), + transparent + ), + linear-gradient(to top, var(--bg-color), transparent 64px); + background-repeat: no-repeat; + background-position: bottom center; +} +.boost-carousel .status-reblog { + background-image: none; +} +.boost-carousel header { + padding: 8px 16px 0; + display: flex; + justify-content: space-between; + align-items: center; +} +.boost-carousel h3 { + margin: 0; + padding: 0; + font-size: 14px; + text-transform: uppercase; + color: var(--reblog-color); + text-shadow: 0 1px var(--bg-color); +} +.boost-carousel ul { + display: flex; + overflow-x: auto; + overflow-y: hidden; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + margin: 0; + padding: 8px 16px; + gap: 16px; + align-items: flex-start; + counter-reset: index; +} +.boost-carousel ul > li { + scroll-snap-align: center; + scroll-snap-stop: always; + flex-shrink: 0; + display: flex; + width: 100%; + max-width: min(320px, calc(100% - 16px)); + list-style: none; + margin: 0; + padding: 0; + max-height: 70vh; + max-height: 70dvh; + counter-increment: index; + position: relative; +} +.boost-carousel ul > li:before { + content: counter(index); + position: absolute; + right: 0; + font-size: 10px; + color: var(--reblog-color); + padding: 8px; + opacity: 0.5; +} + .ui-state { padding: 16px; text-align: center; } +.status-boost-link { + display: block; + width: 100%; + text-decoration-line: none; + color: inherit; + user-select: none; + transition: background-color 0.2s ease-out; + -webkit-tap-highlight-color: transparent; + animation: appear 0.2s ease-out; + border: 1px solid var(--outline-color); + background-color: var(--bg-blur-color); + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px var(--bg-color); +} +.status-boost-link:is(:hover, :focus) { + background-color: var(--link-bg-hover-color); +} +.status-boost-link:active:not(:has(:is(.media, button):active)) { + filter: brightness(0.95); +} + .deck-backdrop { position: fixed; top: 0; @@ -867,7 +965,7 @@ meter.donut:is(.danger, .explode):after { border: 0; background-color: transparent; } - .timeline-deck header { + .timeline-deck > header { min-height: 6em; border-bottom: 0; background-color: var(--bg-faded-blur-color); @@ -884,10 +982,10 @@ meter.donut:is(.danger, .explode):after { transparent ); } - .deck header h1 { + .deck > header h1 { font-size: 1.5em; } - .timeline-deck .timeline:not(.flat) li { + .timeline-deck .timeline:not(.flat) > li { border: 1px solid var(--divider-color); margin: 16px 0; background-color: var(--bg-color); diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 5925f5e2..ffd39a69 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -43,6 +43,7 @@ const ICONS = { popin: ['mingcute:external-link-line', '180deg'], plus: 'mingcute:add-circle-line', 'chevron-left': 'mingcute:left-line', + 'chevron-right': 'mingcute:right-line', reply: ['mingcute:share-forward-line', '180deg', 'horizontal'], thread: 'mingcute:route-line', group: 'mingcute:group-line', diff --git a/src/pages/home.jsx b/src/pages/home.jsx index 99896610..d4a79d35 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -47,6 +47,55 @@ function Home({ hidden }) { reply: !!status.inReplyToAccountId, }; }); + + { + // BOOSTS CAROUSEL + let specialHome = []; + let boostStash = []; + for (let i = 0; i < homeValues.length; i++) { + const status = homeValues[i]; + if (status.reblog) { + boostStash.push(status); + } else { + specialHome.push(status); + } + } + // if boostStash is more than quarter of homeValues + if (boostStash.length > homeValues.length / 4) { + // if boostStash is more than 3 quarter of homeValues + const boostStashID = boostStash.map((status) => status.id); + if (boostStash.length > (homeValues.length * 3) / 4) { + // insert boost array at the end of specialHome list + specialHome = [ + ...specialHome, + { id: boostStashID, boosts: boostStash }, + ]; + } else { + // insert boosts array in the middle of specialHome list + const half = Math.floor(specialHome.length / 2); + specialHome = [ + ...specialHome.slice(0, half), + { + id: boostStashID, + boosts: boostStash, + }, + ...specialHome.slice(half), + ]; + } + } else { + // Untouched, this is fine + specialHome = homeValues; + } + console.log({ + specialHome, + }); + if (firstLoad) { + states.specialHome = specialHome; + } else { + states.specialHome.push(...specialHome); + } + } + if (firstLoad) { states.home = homeValues; } else { @@ -84,36 +133,35 @@ function Home({ hidden }) { useHotkeys('j', () => { // focus on next status after active status // Traverses .timeline li .status-link, focus on .status-link - const activeStatus = document.activeElement.closest('.status-link'); + const activeStatus = document.activeElement.closest( + '.status-link, .status-boost-link', + ); const activeStatusRect = activeStatus?.getBoundingClientRect(); + const allStatusLinks = Array.from( + scrollableRef.current.querySelectorAll( + '.status-link, .status-boost-link', + ), + ); if ( activeStatus && activeStatusRect.top < scrollableRef.current.clientHeight && activeStatusRect.bottom > 0 ) { - const nextStatus = activeStatus.parentElement.nextElementSibling; + const activeStatusIndex = allStatusLinks.indexOf(activeStatus); + const nextStatus = allStatusLinks[activeStatusIndex + 1]; if (nextStatus) { - const statusLink = nextStatus.querySelector('.status-link'); - if (statusLink) { - statusLink.focus(); - } + nextStatus.focus(); + nextStatus.scrollIntoViewIfNeeded?.(); } } else { // If active status is not in viewport, get the topmost status-link in viewport - const statusLinks = document.querySelectorAll( - '.timeline li .status-link', - ); - let topmostStatusLink; - for (const statusLink of statusLinks) { + const topmostStatusLink = allStatusLinks.find((statusLink) => { const statusLinkRect = statusLink.getBoundingClientRect(); - if (statusLinkRect.top >= 44) { - // 44 is the magic number for header height, not real - topmostStatusLink = statusLink; - break; - } - } + return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real + }); if (topmostStatusLink) { topmostStatusLink.focus(); + topmostStatusLink.scrollIntoViewIfNeeded?.(); } } }); @@ -121,67 +169,68 @@ function Home({ hidden }) { useHotkeys('k', () => { // focus on previous status after active status // Traverses .timeline li .status-link, focus on .status-link - const activeStatus = document.activeElement.closest('.status-link'); + const activeStatus = document.activeElement.closest( + '.status-link, .status-boost-link', + ); const activeStatusRect = activeStatus?.getBoundingClientRect(); + const allStatusLinks = Array.from( + scrollableRef.current.querySelectorAll( + '.status-link, .status-boost-link', + ), + ); if ( activeStatus && activeStatusRect.top < scrollableRef.current.clientHeight && activeStatusRect.bottom > 0 ) { - const prevStatus = activeStatus.parentElement.previousElementSibling; + const activeStatusIndex = allStatusLinks.indexOf(activeStatus); + const prevStatus = allStatusLinks[activeStatusIndex - 1]; if (prevStatus) { - const statusLink = prevStatus.querySelector('.status-link'); - if (statusLink) { - statusLink.focus(); - } + prevStatus.focus(); + prevStatus.scrollIntoViewIfNeeded?.(); } } else { // If active status is not in viewport, get the topmost status-link in viewport - const statusLinks = document.querySelectorAll( - '.timeline li .status-link', - ); - let topmostStatusLink; - for (const statusLink of statusLinks) { + const topmostStatusLink = allStatusLinks.find((statusLink) => { const statusLinkRect = statusLink.getBoundingClientRect(); - if (statusLinkRect.top >= 44) { - // 44 is the magic number for header height, not real - topmostStatusLink = statusLink; - break; - } - } + return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real + }); if (topmostStatusLink) { topmostStatusLink.focus(); + topmostStatusLink.scrollIntoViewIfNeeded?.(); } } }); useHotkeys(['enter', 'o'], () => { // open active status - const activeStatus = document.activeElement.closest('.status-link'); + const activeStatus = document.activeElement.closest( + '.status-link, .status-boost-link', + ); if (activeStatus) { activeStatus.click(); } }); - const { scrollDirection, reachTop, nearReachTop, nearReachBottom } = + const { scrollDirection, reachStart, nearReachStart, nearReachEnd } = useScroll({ scrollableElement: scrollableRef.current, - distanceFromTop: 0.1, - distanceFromBottom: 0.15, - scrollThresholdUp: 44, + distanceFromStart: 0.1, + distanceFromEnd: 0.15, + scrollThresholdStart: 44, }); useEffect(() => { - if (nearReachBottom && showMore) { + if (nearReachEnd && showMore) { loadStatuses(); } - }, [nearReachBottom]); + }, [nearReachEnd]); useEffect(() => { - if (reachTop) { + if (reachStart) { loadStatuses(true); } - }, [reachTop]); + }, [reachStart]); useEffect(() => { (async () => { @@ -196,6 +245,10 @@ function Home({ hidden }) { })(); }, []); + const snapHome = snapStates.settings.boostsCarousel + ? snapStates.specialHome + : snapStates.home; + return (