mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-10 16:08:52 +01:00
New experiment: Boosts Carousel™️
This commit is contained in:
parent
62e88e4b78
commit
e2139399ee
7 changed files with 361 additions and 91 deletions
116
src/app.css
116
src/app.css
|
@ -75,7 +75,7 @@ a.mention span {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deck header {
|
.deck > header {
|
||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -93,25 +93,25 @@ a.mention span {
|
||||||
transition: transform 0.5s ease-in-out;
|
transition: transform 0.5s ease-in-out;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.deck header[hidden] {
|
.deck > header[hidden] {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.deck header > .header-side:last-of-type {
|
.deck > header > .header-side:last-of-type {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
}
|
}
|
||||||
.deck header :is(button, .button).plain {
|
.deck > header :is(button, .button).plain {
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
}
|
}
|
||||||
.deck header h1 {
|
.deck > header h1 {
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.deck header h1:first-child {
|
.deck > header h1:first-child {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
@ -368,11 +368,109 @@ a.mention span {
|
||||||
filter: brightness(0.95);
|
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 {
|
.ui-state {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
text-align: center;
|
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 {
|
.deck-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -867,7 +965,7 @@ meter.donut:is(.danger, .explode):after {
|
||||||
border: 0;
|
border: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.timeline-deck header {
|
.timeline-deck > header {
|
||||||
min-height: 6em;
|
min-height: 6em;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
background-color: var(--bg-faded-blur-color);
|
background-color: var(--bg-faded-blur-color);
|
||||||
|
@ -884,10 +982,10 @@ meter.donut:is(.danger, .explode):after {
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.deck header h1 {
|
.deck > header h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
.timeline-deck .timeline:not(.flat) li {
|
.timeline-deck .timeline:not(.flat) > li {
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
|
|
|
@ -43,6 +43,7 @@ const ICONS = {
|
||||||
popin: ['mingcute:external-link-line', '180deg'],
|
popin: ['mingcute:external-link-line', '180deg'],
|
||||||
plus: 'mingcute:add-circle-line',
|
plus: 'mingcute:add-circle-line',
|
||||||
'chevron-left': 'mingcute:left-line',
|
'chevron-left': 'mingcute:left-line',
|
||||||
|
'chevron-right': 'mingcute:right-line',
|
||||||
reply: ['mingcute:share-forward-line', '180deg', 'horizontal'],
|
reply: ['mingcute:share-forward-line', '180deg', 'horizontal'],
|
||||||
thread: 'mingcute:route-line',
|
thread: 'mingcute:route-line',
|
||||||
group: 'mingcute:group-line',
|
group: 'mingcute:group-line',
|
||||||
|
|
|
@ -47,6 +47,55 @@ function Home({ hidden }) {
|
||||||
reply: !!status.inReplyToAccountId,
|
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) {
|
if (firstLoad) {
|
||||||
states.home = homeValues;
|
states.home = homeValues;
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,36 +133,35 @@ function Home({ hidden }) {
|
||||||
useHotkeys('j', () => {
|
useHotkeys('j', () => {
|
||||||
// focus on next status after active status
|
// focus on next status after active status
|
||||||
// Traverses .timeline li .status-link, focus on .status-link
|
// 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 activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||||
|
const allStatusLinks = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
),
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
activeStatus &&
|
activeStatus &&
|
||||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||||
activeStatusRect.bottom > 0
|
activeStatusRect.bottom > 0
|
||||||
) {
|
) {
|
||||||
const nextStatus = activeStatus.parentElement.nextElementSibling;
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||||
|
const nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||||
if (nextStatus) {
|
if (nextStatus) {
|
||||||
const statusLink = nextStatus.querySelector('.status-link');
|
nextStatus.focus();
|
||||||
if (statusLink) {
|
nextStatus.scrollIntoViewIfNeeded?.();
|
||||||
statusLink.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
const statusLinks = document.querySelectorAll(
|
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||||
'.timeline li .status-link',
|
|
||||||
);
|
|
||||||
let topmostStatusLink;
|
|
||||||
for (const statusLink of statusLinks) {
|
|
||||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||||
if (statusLinkRect.top >= 44) {
|
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||||
// 44 is the magic number for header height, not real
|
});
|
||||||
topmostStatusLink = statusLink;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (topmostStatusLink) {
|
if (topmostStatusLink) {
|
||||||
topmostStatusLink.focus();
|
topmostStatusLink.focus();
|
||||||
|
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -121,67 +169,68 @@ function Home({ hidden }) {
|
||||||
useHotkeys('k', () => {
|
useHotkeys('k', () => {
|
||||||
// focus on previous status after active status
|
// focus on previous status after active status
|
||||||
// Traverses .timeline li .status-link, focus on .status-link
|
// 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 activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||||
|
const allStatusLinks = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
),
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
activeStatus &&
|
activeStatus &&
|
||||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||||
activeStatusRect.bottom > 0
|
activeStatusRect.bottom > 0
|
||||||
) {
|
) {
|
||||||
const prevStatus = activeStatus.parentElement.previousElementSibling;
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||||
|
const prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||||
if (prevStatus) {
|
if (prevStatus) {
|
||||||
const statusLink = prevStatus.querySelector('.status-link');
|
prevStatus.focus();
|
||||||
if (statusLink) {
|
prevStatus.scrollIntoViewIfNeeded?.();
|
||||||
statusLink.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If active status is not in viewport, get the topmost status-link in viewport
|
// If active status is not in viewport, get the topmost status-link in viewport
|
||||||
const statusLinks = document.querySelectorAll(
|
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||||
'.timeline li .status-link',
|
|
||||||
);
|
|
||||||
let topmostStatusLink;
|
|
||||||
for (const statusLink of statusLinks) {
|
|
||||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||||
if (statusLinkRect.top >= 44) {
|
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||||
// 44 is the magic number for header height, not real
|
});
|
||||||
topmostStatusLink = statusLink;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (topmostStatusLink) {
|
if (topmostStatusLink) {
|
||||||
topmostStatusLink.focus();
|
topmostStatusLink.focus();
|
||||||
|
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotkeys(['enter', 'o'], () => {
|
useHotkeys(['enter', 'o'], () => {
|
||||||
// open active status
|
// open active status
|
||||||
const activeStatus = document.activeElement.closest('.status-link');
|
const activeStatus = document.activeElement.closest(
|
||||||
|
'.status-link, .status-boost-link',
|
||||||
|
);
|
||||||
if (activeStatus) {
|
if (activeStatus) {
|
||||||
activeStatus.click();
|
activeStatus.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { scrollDirection, reachTop, nearReachTop, nearReachBottom } =
|
const { scrollDirection, reachStart, nearReachStart, nearReachEnd } =
|
||||||
useScroll({
|
useScroll({
|
||||||
scrollableElement: scrollableRef.current,
|
scrollableElement: scrollableRef.current,
|
||||||
distanceFromTop: 0.1,
|
distanceFromStart: 0.1,
|
||||||
distanceFromBottom: 0.15,
|
distanceFromEnd: 0.15,
|
||||||
scrollThresholdUp: 44,
|
scrollThresholdStart: 44,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nearReachBottom && showMore) {
|
if (nearReachEnd && showMore) {
|
||||||
loadStatuses();
|
loadStatuses();
|
||||||
}
|
}
|
||||||
}, [nearReachBottom]);
|
}, [nearReachEnd]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (reachTop) {
|
if (reachStart) {
|
||||||
loadStatuses(true);
|
loadStatuses(true);
|
||||||
}
|
}
|
||||||
}, [reachTop]);
|
}, [reachStart]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -196,6 +245,10 @@ function Home({ hidden }) {
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const snapHome = snapStates.settings.boostsCarousel
|
||||||
|
? snapStates.specialHome
|
||||||
|
: snapStates.home;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="home-page"
|
id="home-page"
|
||||||
|
@ -205,7 +258,7 @@ function Home({ hidden }) {
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
hidden={scrollDirection === 'down' && !nearReachTop}
|
hidden={scrollDirection === 'down' && !nearReachStart}
|
||||||
type="button"
|
type="button"
|
||||||
id="compose-button"
|
id="compose-button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -224,7 +277,7 @@ function Home({ hidden }) {
|
||||||
</button>
|
</button>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
<header
|
<header
|
||||||
hidden={scrollDirection === 'down' && !nearReachTop}
|
hidden={scrollDirection === 'down' && !nearReachStart}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}}
|
}}
|
||||||
|
@ -263,8 +316,8 @@ function Home({ hidden }) {
|
||||||
</header>
|
</header>
|
||||||
{snapStates.homeNew.length > 0 &&
|
{snapStates.homeNew.length > 0 &&
|
||||||
scrollDirection === 'up' &&
|
scrollDirection === 'up' &&
|
||||||
!nearReachTop &&
|
!nearReachStart &&
|
||||||
!nearReachBottom && (
|
!nearReachEnd && (
|
||||||
<button
|
<button
|
||||||
class="updates-button"
|
class="updates-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -285,11 +338,18 @@ function Home({ hidden }) {
|
||||||
<Icon icon="arrow-up" /> New posts
|
<Icon icon="arrow-up" /> New posts
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{snapStates.home.length ? (
|
{snapHome.length ? (
|
||||||
<>
|
<>
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
{snapStates.home.map(({ id: statusID, reblog }) => {
|
{snapHome.map(({ id: statusID, reblog, boosts }) => {
|
||||||
const actualStatusID = reblog || statusID;
|
const actualStatusID = reblog || statusID;
|
||||||
|
if (boosts) {
|
||||||
|
return (
|
||||||
|
<li key={statusID}>
|
||||||
|
<BoostsCarousel boosts={boosts} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<li key={statusID}>
|
<li key={statusID}>
|
||||||
<Link
|
<Link
|
||||||
|
@ -367,4 +427,61 @@ function Home({ hidden }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BoostsCarousel({ boosts }) {
|
||||||
|
const carouselRef = useRef();
|
||||||
|
const { reachStart, reachEnd } = useScroll({
|
||||||
|
scrollableElement: carouselRef.current,
|
||||||
|
direction: 'horizontal',
|
||||||
|
});
|
||||||
|
console.log({ reachStart, reachEnd });
|
||||||
|
return (
|
||||||
|
<div class="boost-carousel">
|
||||||
|
<header>
|
||||||
|
<h3>{boosts.length} Boosts</h3>
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain2"
|
||||||
|
disabled={reachStart}
|
||||||
|
onClick={() => {
|
||||||
|
carouselRef.current?.scrollBy({
|
||||||
|
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-left" />
|
||||||
|
</button>{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="small plain2"
|
||||||
|
disabled={reachEnd}
|
||||||
|
onClick={() => {
|
||||||
|
carouselRef.current?.scrollBy({
|
||||||
|
left: Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<ul ref={carouselRef}>
|
||||||
|
{boosts.map((boost) => {
|
||||||
|
const { id: statusID, reblog } = boost;
|
||||||
|
const actualStatusID = reblog || statusID;
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<a class="status-boost-link" href={`#/s/${actualStatusID}`}>
|
||||||
|
<Status statusID={statusID} size="s" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(Home);
|
export default memo(Home);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import './settings.css';
|
import './settings.css';
|
||||||
|
|
||||||
import { useRef, useState } from 'preact/hooks';
|
import { useRef, useState } from 'preact/hooks';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Avatar from '../components/avatar';
|
import Avatar from '../components/avatar';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
|
@ -16,6 +17,7 @@ import store from '../utils/store';
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Settings({ onClose }) {
|
function Settings({ onClose }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
// Accounts
|
// Accounts
|
||||||
const accounts = store.local.getJSON('accounts');
|
const accounts = store.local.getJSON('accounts');
|
||||||
const currentAccount = store.session.get('currentAccount');
|
const currentAccount = store.session.get('currentAccount');
|
||||||
|
@ -184,6 +186,17 @@ function Settings({ onClose }) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.boostsCarousel}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.boostsCarousel = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Boosts carousel (experimental)
|
||||||
|
</label>
|
||||||
<h2>Hidden features</h2>
|
<h2>Hidden features</h2>
|
||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -295,9 +295,9 @@ function StatusPage({ id }) {
|
||||||
location.hash = closeLink;
|
location.hash = closeLink;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nearReachTop } = useScroll({
|
const { nearReachStart } = useScroll({
|
||||||
scrollableElement: scrollableRef.current,
|
scrollableElement: scrollableRef.current,
|
||||||
distanceFromTop: 0.1,
|
distanceFromStart: 0.1,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -367,7 +367,7 @@ function StatusPage({ id }) {
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
hidden={!ancestors.length || nearReachTop}
|
hidden={!ancestors.length || nearReachStart}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up" />
|
<Icon icon="arrow-up" />
|
||||||
<Icon icon="comment" />{' '}
|
<Icon icon="comment" />{' '}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { proxy } from 'valtio';
|
import { proxy, subscribe } from 'valtio';
|
||||||
|
|
||||||
|
import store from './store';
|
||||||
|
|
||||||
const states = proxy({
|
const states = proxy({
|
||||||
history: [],
|
history: [],
|
||||||
statuses: {},
|
statuses: {},
|
||||||
statusThreadNumber: {},
|
statusThreadNumber: {},
|
||||||
home: [],
|
home: [],
|
||||||
|
specialHome: [],
|
||||||
homeNew: [],
|
homeNew: [],
|
||||||
homeLastFetchTime: null,
|
homeLastFetchTime: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
|
@ -20,9 +23,19 @@ const states = proxy({
|
||||||
showAccount: false,
|
showAccount: false,
|
||||||
showDrafts: false,
|
showDrafts: false,
|
||||||
composeCharacterCount: 0,
|
composeCharacterCount: 0,
|
||||||
|
settings: {
|
||||||
|
boostsCarousel: store.local.get('settings:boostsCarousel') === '1' || true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
export default states;
|
export default states;
|
||||||
|
|
||||||
|
subscribe(states.settings, () => {
|
||||||
|
store.local.set(
|
||||||
|
'settings:boostsCarousel',
|
||||||
|
states.settings.boostsCarousel ? '1' : '0',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export function saveStatus(status, opts) {
|
export function saveStatus(status, opts) {
|
||||||
const { override, skipThreading } = Object.assign(
|
const { override, skipThreading } = Object.assign(
|
||||||
{ override: true, skipThreading: false },
|
{ override: true, skipThreading: false },
|
||||||
|
|
|
@ -1,55 +1,83 @@
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function useScroll({
|
export default function useScroll({
|
||||||
scrollableElement = window,
|
scrollableElement,
|
||||||
distanceFromTop = 0,
|
distanceFromStart = 0,
|
||||||
distanceFromBottom = 0,
|
distanceFromEnd = 0,
|
||||||
scrollThresholdUp = 10,
|
scrollThresholdStart = 10,
|
||||||
scrollThresholdDown = 10,
|
scrollThresholdEnd = 10,
|
||||||
|
direction = 'vertical',
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const [scrollDirection, setScrollDirection] = useState(null);
|
const [scrollDirection, setScrollDirection] = useState(null);
|
||||||
const [reachTop, setReachTop] = useState(false);
|
const [reachStart, setReachStart] = useState(false);
|
||||||
const [nearReachTop, setNearReachTop] = useState(false);
|
const [reachEnd, setReachEnd] = useState(false);
|
||||||
const [nearReachBottom, setNearReachBottom] = useState(false);
|
const [nearReachStart, setNearReachStart] = useState(false);
|
||||||
|
const [nearReachEnd, setNearReachEnd] = useState(false);
|
||||||
|
const isVertical = direction === 'vertical';
|
||||||
|
|
||||||
|
if (!scrollableElement) {
|
||||||
|
console.warn('Scrollable element is not defined');
|
||||||
|
scrollableElement = window;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let previousScrollTop = scrollableElement.scrollTop;
|
let previousScrollStart = isVertical
|
||||||
|
? scrollableElement.scrollTop
|
||||||
|
: scrollableElement.scrollLeft;
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
|
const {
|
||||||
const scrollDistance = Math.abs(scrollTop - previousScrollTop);
|
scrollTop,
|
||||||
const distanceFromTopPx =
|
scrollLeft,
|
||||||
scrollHeight * Math.min(1, Math.max(0, distanceFromTop));
|
scrollHeight,
|
||||||
const distanceFromBottomPx =
|
scrollWidth,
|
||||||
scrollHeight * Math.min(1, Math.max(0, distanceFromBottom));
|
clientHeight,
|
||||||
|
clientWidth,
|
||||||
|
} = scrollableElement;
|
||||||
|
const scrollStart = isVertical ? scrollTop : scrollLeft;
|
||||||
|
const scrollDimension = isVertical ? scrollHeight : scrollWidth;
|
||||||
|
const clientDimension = isVertical ? clientHeight : clientWidth;
|
||||||
|
const scrollDistance = Math.abs(scrollStart - previousScrollStart);
|
||||||
|
const distanceFromStartPx =
|
||||||
|
scrollDimension * Math.min(1, Math.max(0, distanceFromStart));
|
||||||
|
const distanceFromEndPx =
|
||||||
|
scrollDimension * Math.min(1, Math.max(0, distanceFromEnd));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
scrollDistance >=
|
scrollDistance >=
|
||||||
(previousScrollTop < scrollTop
|
(previousScrollStart < scrollStart
|
||||||
? scrollThresholdDown
|
? scrollThresholdEnd
|
||||||
: scrollThresholdUp)
|
: scrollThresholdStart)
|
||||||
) {
|
) {
|
||||||
setScrollDirection(previousScrollTop < scrollTop ? 'down' : 'up');
|
setScrollDirection(previousScrollStart < scrollStart ? 'end' : 'start');
|
||||||
previousScrollTop = scrollTop;
|
previousScrollStart = scrollStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
setReachTop(scrollTop === 0);
|
setReachStart(scrollStart === 0);
|
||||||
setNearReachTop(scrollTop <= distanceFromTopPx);
|
setReachEnd(scrollStart + clientDimension >= scrollDimension);
|
||||||
setNearReachBottom(
|
setNearReachStart(scrollStart <= distanceFromStartPx);
|
||||||
scrollTop + clientHeight >= scrollHeight - distanceFromBottomPx,
|
setNearReachEnd(
|
||||||
|
scrollStart + clientDimension >= scrollDimension - distanceFromEndPx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollableElement.addEventListener('scroll', onScroll, { passive: true });
|
scrollableElement.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
scrollableElement.dispatchEvent(new Event('scroll'));
|
||||||
|
|
||||||
return () => scrollableElement.removeEventListener('scroll', onScroll);
|
return () => scrollableElement.removeEventListener('scroll', onScroll);
|
||||||
}, [
|
}, [
|
||||||
scrollableElement,
|
scrollableElement,
|
||||||
distanceFromTop,
|
distanceFromStart,
|
||||||
distanceFromBottom,
|
distanceFromEnd,
|
||||||
scrollThresholdUp,
|
scrollThresholdStart,
|
||||||
scrollThresholdDown,
|
scrollThresholdEnd,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { scrollDirection, reachTop, nearReachTop, nearReachBottom };
|
return {
|
||||||
|
scrollDirection,
|
||||||
|
reachStart,
|
||||||
|
reachEnd,
|
||||||
|
nearReachStart,
|
||||||
|
nearReachEnd,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue