Compare commits
50 commits
452cc7704b
...
e83f18430c
Author | SHA1 | Date | |
---|---|---|---|
|
e83f18430c | ||
|
96387c8abb | ||
|
35958d429d | ||
|
99b0b7c096 | ||
|
e44ac16396 | ||
|
147a12cbcb | ||
|
16e2ac9bce | ||
|
1574be2b35 | ||
|
7223baaaad | ||
|
9cffd429b0 | ||
|
9a5d749b8d | ||
|
e43f2283dd | ||
|
be5fcc35ac | ||
|
54314de976 | ||
|
bc2886f7e2 | ||
|
2bc1b8387e | ||
|
3989b218d0 | ||
|
a8331375ba | ||
|
6919975c6d | ||
|
c0987209a8 | ||
|
d25c2df392 | ||
|
848433365d | ||
|
3d4ebb8abe | ||
|
72dc4cc81b | ||
|
92c0a8b4f0 | ||
|
1adcca5666 | ||
|
b4d4c61128 | ||
|
764125e6b9 | ||
|
098df0ad2c | ||
|
e41e49884f | ||
|
852f7090f6 | ||
|
d54511aa10 | ||
|
d8ceb03d74 | ||
|
df393ae959 | ||
|
0ebbc5b34e | ||
|
cf52e0776e | ||
|
b168707c14 | ||
|
d2fb86036c | ||
|
62c8a51307 | ||
|
f056d7407a | ||
|
c3e40297e0 | ||
|
d6099df51b | ||
|
096bc69584 | ||
|
32d32b72f4 | ||
|
796b365fd8 | ||
|
bd38122f1b | ||
|
d7d838ebf8 | ||
|
de3787209e | ||
|
6500be2782 | ||
|
2240380f68 |
|
@ -199,6 +199,7 @@ These are self-hosted by other wonderful folks.
|
||||||
- [phanpy.vmst.io](https://phanpy.vmst.io/) by [@vmstan@vmst.io](https://vmst.io/@vmstan)
|
- [phanpy.vmst.io](https://phanpy.vmst.io/) by [@vmstan@vmst.io](https://vmst.io/@vmstan)
|
||||||
- [phanpy.gotosocial.social](https://phanpy.gotosocial.social/) by [@admin@gotosocial.social](https://gotosocial.social/@admin)
|
- [phanpy.gotosocial.social](https://phanpy.gotosocial.social/) by [@admin@gotosocial.social](https://gotosocial.social/@admin)
|
||||||
- [phanpy.bauxite.tech](https://phanpy.bauxite.tech) by [@b4ux1t3@hachyderm.io](https://hachyderm.io/@b4ux1t3)
|
- [phanpy.bauxite.tech](https://phanpy.bauxite.tech) by [@b4ux1t3@hachyderm.io](https://hachyderm.io/@b4ux1t3)
|
||||||
|
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
|
||||||
|
|
||||||
> Note: Add yours by creating a pull request.
|
> Note: Add yours by creating a pull request.
|
||||||
|
|
||||||
|
|
|
@ -31,14 +31,16 @@
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
data-theme-setting="auto"
|
data-theme-setting="auto"
|
||||||
content="#fff"
|
content="#fff"
|
||||||
data-content-temp="#ffff"
|
data-content="#fff"
|
||||||
|
data-content-temp="#fffa"
|
||||||
media="(prefers-color-scheme: light)"
|
media="(prefers-color-scheme: light)"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
data-theme-setting="auto"
|
data-theme-setting="auto"
|
||||||
content="#242526"
|
content="#242526"
|
||||||
data-content-temp="#242526ff"
|
data-content="#242526"
|
||||||
|
data-content-temp="#242526aa"
|
||||||
media="(prefers-color-scheme: dark)"
|
media="(prefers-color-scheme: dark)"
|
||||||
/>
|
/>
|
||||||
<meta name="google" content="notranslate" />
|
<meta name="google" content="notranslate" />
|
||||||
|
|
18
package-lock.json
generated
18
package-lock.json
generated
|
@ -19,7 +19,7 @@
|
||||||
"dayjs": "~1.11.10",
|
"dayjs": "~1.11.10",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-equals": "~5.0.1",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "~1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
|
@ -4405,13 +4405,16 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
"version": "3.0.3",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
||||||
"integrity": "sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg==",
|
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
|
||||||
"license": "MIT"
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
|
@ -5570,6 +5573,11 @@
|
||||||
"micro-memoize": "^4.1.2"
|
"micro-memoize": "^4.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moize/node_modules/fast-equals": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg=="
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"dayjs": "~1.11.10",
|
"dayjs": "~1.11.10",
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-deep-equal": "~3.1.3",
|
"fast-equals": "~5.0.1",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "~1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
|
|
158
src/app.css
158
src/app.css
|
@ -86,7 +86,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
:is(#home-page, #welcome, #columns, #loader-root):has(~ .deck-container) {
|
:is(#home-page, #welcome, #columns, #loader-root):has(~ .deck-container) {
|
||||||
display: block;
|
/* display: block; */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -144,9 +144,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.deck > header .header-grid {
|
.deck > header .header-grid {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-color);
|
||||||
|
/* background-color: var(--bg-blur-color);
|
||||||
background-image: linear-gradient(to bottom, var(--bg-color), transparent);
|
background-image: linear-gradient(to bottom, var(--bg-color), transparent);
|
||||||
backdrop-filter: saturate(180%) blur(20px);
|
backdrop-filter: saturate(180%) blur(20px); */
|
||||||
border-bottom: var(--hairline-width) solid var(--divider-color);
|
border-bottom: var(--hairline-width) solid var(--divider-color);
|
||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -546,9 +547,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > .replies-summary {
|
.timeline.contextual > li .replies > .replies-summary {
|
||||||
padding: 8px;
|
--summary-padding: 8px;
|
||||||
|
padding: var(--summary-padding);
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
@ -558,12 +560,67 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
box-shadow: 0 0 0 2px var(--bg-color);
|
box-shadow: 0 0 0 2px var(--bg-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
white-space: nowrap;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: calc(44px + 8px);
|
||||||
|
|
||||||
b {
|
b {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin: 0 0 0 -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.replies-counts {
|
||||||
|
/* flex-grow: 1; */
|
||||||
|
|
||||||
|
> * {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.replies-summary-chevron {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replies-parent-link {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
align-self: stretch;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--summary-padding) calc(var(--summary-padding) * 2);
|
||||||
|
transform: translateX(100%);
|
||||||
|
margin: calc(-1 * var(--summary-padding)) calc(-1 * var(--summary-padding))
|
||||||
|
calc(-1 * var(--summary-padding)) 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--link-bg-color);
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
color: var(--link-text-color);
|
||||||
|
box-shadow: inset 0 0 0 2px var(--link-faded-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--link-faded-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > .replies-summary::-webkit-details-marker {
|
.timeline.contextual > li .replies > .replies-summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -571,14 +628,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.timeline.contextual > li .replies > .replies-summary > * {
|
.timeline.contextual > li .replies > .replies-summary > * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies > .replies-summary .avatars {
|
.timeline.contextual
|
||||||
margin-right: 8px;
|
> li
|
||||||
|
.replies
|
||||||
> *:not(:first-child) {
|
> .replies-summary
|
||||||
margin: 0 0 0 -4px;
|
.timeline.contextual
|
||||||
}
|
> li
|
||||||
}
|
.replies
|
||||||
.timeline.contextual > li .replies > .replies-summary:active,
|
> .replies-summary:active,
|
||||||
.timeline.contextual > li .replies[open] > .replies-summary {
|
.timeline.contextual > li .replies[open] > .replies-summary {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: var(--comment-line-color);
|
background-color: var(--comment-line-color);
|
||||||
|
@ -590,6 +647,18 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies[open] > .replies-summary {
|
.timeline.contextual > li .replies[open] > .replies-summary {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.replies-summary-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline.contextual > li .replies .replies-summary[hidden] {
|
.timeline.contextual > li .replies .replies-summary[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -686,6 +755,22 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline.contextual > li .replies {
|
||||||
|
> ul > li:only-child {
|
||||||
|
> .replies {
|
||||||
|
> ul > li:only-child {
|
||||||
|
margin-left: calc(-1 * var(--line-margin-end));
|
||||||
|
background-position: calc(16px) 0;
|
||||||
|
background-size: 100% calc(20px + 8px);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-deck.compact .status {
|
.timeline-deck.compact .status {
|
||||||
max-height: max(25vh, 160px);
|
max-height: max(25vh, 160px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -746,6 +831,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
background-size: 100% 16px;
|
background-size: 100% 16px;
|
||||||
|
|
||||||
|
&:has(.status-pre-meta) {
|
||||||
|
/* 20px = icon of the pre-meta */
|
||||||
|
background-size: 100% calc(16px + 20px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline:not(.flat)
|
.timeline:not(.flat)
|
||||||
> li:is(.timeline-item-container-middle, .timeline-item-container-end)
|
> li:is(.timeline-item-container-middle, .timeline-item-container-end)
|
||||||
|
@ -877,17 +967,20 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
}
|
}
|
||||||
.status-carousel > ul {
|
.status-carousel > ul {
|
||||||
|
--carousel-gap: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: clip;
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
gap: 16px;
|
gap: var(--carousel-gap);
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
counter-reset: index;
|
counter-reset: index;
|
||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
|
max-height: 65vh;
|
||||||
|
max-height: 65dvh;
|
||||||
}
|
}
|
||||||
.status-carousel > ul > li {
|
.status-carousel > ul > li {
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
|
@ -899,14 +992,23 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-height: 65vh;
|
/* max-height: 65vh;
|
||||||
max-height: 65dvh;
|
max-height: 65dvh; */
|
||||||
counter-increment: index;
|
counter-increment: index;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.status-carousel > ul > li:is(:empty, :has(> a:empty)) {
|
.status-carousel > ul > li:is(:empty, :has(> a:empty)) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.status-carousel .status-carousel-beacon {
|
||||||
|
margin-right: calc(-1 * var(--carousel-gap));
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
~ .status-carousel-beacon {
|
||||||
|
margin-left: calc(-1 * var(--carousel-gap));
|
||||||
|
}
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
Assume that browsers that do support inline-size property also support container queries.
|
Assume that browsers that do support inline-size property also support container queries.
|
||||||
https://www.smashingmagazine.com/2021/05/css-container-queries-use-cases-migration-strategies/#progressive-enhancement-polyfills
|
https://www.smashingmagazine.com/2021/05/css-container-queries-use-cases-migration-strategies/#progressive-enhancement-polyfills
|
||||||
|
@ -925,6 +1027,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
}
|
}
|
||||||
.status-carousel .content-container .content:only-child {
|
.status-carousel .content-container .content:only-child {
|
||||||
font-size: calc(100% + 25% * max(2 - var(--content-text-weight), 0));
|
font-size: calc(100% + 25% * max(2 - var(--content-text-weight), 0));
|
||||||
|
|
||||||
|
&:has(.status-card) {
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* .status-carousel
|
/* .status-carousel
|
||||||
.content-container[data-content-text-weight='1']
|
.content-container[data-content-text-weight='1']
|
||||||
|
@ -1092,6 +1198,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
header[hidden] & {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -100%) scale(0.9);
|
||||||
|
pointer-events: none;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.updates-button .icon {
|
.updates-button .icon {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -1398,7 +1512,7 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
right: max(16px, env(safe-area-inset-right));
|
right: max(16px, env(safe-area-inset-right));
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: var(--button-bg-blur-color);
|
background-color: var(--button-bg-blur-color);
|
||||||
backdrop-filter: blur(16px);
|
/* backdrop-filter: blur(16px); */
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
box-shadow: 0 3px 8px -1px var(--drop-shadow-color),
|
||||||
0 10px 36px -4px var(--button-bg-blur-color);
|
0 10px 36px -4px var(--button-bg-blur-color);
|
||||||
|
@ -1611,7 +1725,7 @@ body > .szh-menu-container {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 3px 16px -3px var(--drop-shadow-color);
|
box-shadow: 0 3px 16px -3px var(--drop-shadow-color);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
animation: appear-smooth 0.15s ease-in-out;
|
/* animation: appear-smooth 0.15s ease-in-out; */
|
||||||
width: 16em;
|
width: 16em;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
/* overflow: hidden; */
|
/* overflow: hidden; */
|
||||||
|
@ -2146,6 +2260,9 @@ ul.link-list li a .icon {
|
||||||
.header-grid {
|
.header-grid {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
}
|
}
|
||||||
|
#columns > .deck-container > .timeline-deck {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
#columns .header-grid input {
|
#columns .header-grid input {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -2447,6 +2564,7 @@ ul.link-list li a .icon {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-faded-blur-color);
|
background-color: var(--bg-faded-blur-color);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
min-height: 4em;
|
min-height: 4em;
|
||||||
|
|
|
@ -224,7 +224,7 @@ if (isIOS) {
|
||||||
`meta[name="theme-color"][media*="${colorScheme}"]`,
|
`meta[name="theme-color"][media*="${colorScheme}"]`,
|
||||||
);
|
);
|
||||||
if ($meta) {
|
if ($meta) {
|
||||||
const color = $meta.content;
|
const color = $meta.dataset.content;
|
||||||
const tempColor = $meta.dataset.contentTemp;
|
const tempColor = $meta.dataset.contentTemp;
|
||||||
$meta.content = tempColor || '';
|
$meta.content = tempColor || '';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import './compose.css';
|
||||||
|
|
||||||
import '@github/text-expander-element';
|
import '@github/text-expander-element';
|
||||||
import { MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import equal from 'fast-deep-equal';
|
import { deepEqual } from 'fast-equals';
|
||||||
import { forwardRef } from 'preact/compat';
|
import { forwardRef } from 'preact/compat';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
@ -502,7 +502,10 @@ function Compose({
|
||||||
mediaAttachments,
|
mediaAttachments,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (!equal(backgroundDraft, prevBackgroundDraft.current) && !canClose()) {
|
if (
|
||||||
|
!deepEqual(backgroundDraft, prevBackgroundDraft.current) &&
|
||||||
|
!canClose()
|
||||||
|
) {
|
||||||
console.debug('not equal', backgroundDraft, prevBackgroundDraft.current);
|
console.debug('not equal', backgroundDraft, prevBackgroundDraft.current);
|
||||||
db.drafts
|
db.drafts
|
||||||
.set(key, {
|
.set(key, {
|
||||||
|
@ -1838,10 +1841,17 @@ function MediaAttachment({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
}).then((r) => r.json());
|
}).then((r) => r.json());
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
setDescription(response.description);
|
setDescription(response.description);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showToast('Failed to generate description');
|
showToast(
|
||||||
|
`Failed to generate description${
|
||||||
|
e?.message ? `: ${e.message}` : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
toastRef.current?.hideToast?.();
|
toastRef.current?.hideToast?.();
|
||||||
|
|
27
src/components/embed-modal.css
Normal file
27
src/components/embed-modal.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.embed-modal-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.top-controls {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
pointer-events: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/components/embed-modal.jsx
Normal file
28
src/components/embed-modal.jsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import './embed-modal.css';
|
||||||
|
|
||||||
|
import Icon from './icon';
|
||||||
|
|
||||||
|
function EmbedModal({ html, url, onClose = () => {} }) {
|
||||||
|
return (
|
||||||
|
<div class="embed-modal-container">
|
||||||
|
<div class="top-controls">
|
||||||
|
<button type="button" class="light" onClick={() => onClose()}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
{url && (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="button plain"
|
||||||
|
>
|
||||||
|
<span>Open link</span> <Icon icon="external" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="embed-content" dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmbedModal;
|
|
@ -1,3 +1,4 @@
|
||||||
|
import moize from 'moize';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
const SIZES = {
|
const SIZES = {
|
||||||
|
@ -110,6 +111,29 @@ export const ICONS = {
|
||||||
|
|
||||||
const ICONDATA = {};
|
const ICONDATA = {};
|
||||||
|
|
||||||
|
// Memoize the dangerouslySetInnerHTML of the SVGs
|
||||||
|
const SVGICon = moize(
|
||||||
|
function ({ size, width, height, body, rotate, flip }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: body }}
|
||||||
|
style={{
|
||||||
|
transform: `${rotate ? `rotate(${rotate})` : ''} ${
|
||||||
|
flip ? `scaleX(-1)` : ''
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isShallowEqual: true,
|
||||||
|
maxSize: Object.keys(ICONS).length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function Icon({
|
function Icon({
|
||||||
icon,
|
icon,
|
||||||
size = 'm',
|
size = 'm',
|
||||||
|
@ -122,6 +146,11 @@ function Icon({
|
||||||
|
|
||||||
const iconSize = SIZES[size];
|
const iconSize = SIZES[size];
|
||||||
let iconBlock = ICONS[icon];
|
let iconBlock = ICONS[icon];
|
||||||
|
if (!iconBlock) {
|
||||||
|
console.warn(`Icon ${icon} not found`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let rotate, flip;
|
let rotate, flip;
|
||||||
if (Array.isArray(iconBlock)) {
|
if (Array.isArray(iconBlock)) {
|
||||||
[iconBlock, rotate, flip] = iconBlock;
|
[iconBlock, rotate, flip] = iconBlock;
|
||||||
|
@ -150,16 +179,24 @@ function Icon({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{iconData && (
|
{iconData && (
|
||||||
<svg
|
// <svg
|
||||||
width={iconSize}
|
// width={iconSize}
|
||||||
height={iconSize}
|
// height={iconSize}
|
||||||
viewBox={`0 0 ${iconData.width} ${iconData.height}`}
|
// viewBox={`0 0 ${iconData.width} ${iconData.height}`}
|
||||||
dangerouslySetInnerHTML={{ __html: iconData.body }}
|
// dangerouslySetInnerHTML={{ __html: iconData.body }}
|
||||||
style={{
|
// style={{
|
||||||
transform: `${rotate ? `rotate(${rotate})` : ''} ${
|
// transform: `${rotate ? `rotate(${rotate})` : ''} ${
|
||||||
flip ? `scaleX(-1)` : ''
|
// flip ? `scaleX(-1)` : ''
|
||||||
}`,
|
// }`,
|
||||||
}}
|
// }}
|
||||||
|
// />
|
||||||
|
<SVGICon
|
||||||
|
size={iconSize}
|
||||||
|
width={iconData.width}
|
||||||
|
height={iconData.height}
|
||||||
|
body={iconData.body}
|
||||||
|
rotate={rotate}
|
||||||
|
flip={flip}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
mix-blend-mode: luminosity;
|
/* mix-blend-mode: luminosity; */
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
line-clamp: 3;
|
line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
|
|
@ -358,7 +358,7 @@ function Media({
|
||||||
<Parent
|
<Parent
|
||||||
class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${
|
class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${
|
||||||
autoGIFAnimate ? 'media-contain' : ''
|
autoGIFAnimate ? 'media-contain' : ''
|
||||||
}`}
|
} ${hoverAnimate ? 'media-hover-animate' : ''}`}
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
data-formatted-duration={
|
data-formatted-duration={
|
||||||
!showOriginal ? formattedDuration : undefined
|
!showOriginal ? formattedDuration : undefined
|
||||||
|
|
|
@ -10,6 +10,7 @@ import states from '../utils/states';
|
||||||
import AccountSheet from './account-sheet';
|
import AccountSheet from './account-sheet';
|
||||||
import Compose from './compose';
|
import Compose from './compose';
|
||||||
import Drafts from './drafts';
|
import Drafts from './drafts';
|
||||||
|
import EmbedModal from './embed-modal';
|
||||||
import GenericAccounts from './generic-accounts';
|
import GenericAccounts from './generic-accounts';
|
||||||
import MediaAltModal from './media-alt-modal';
|
import MediaAltModal from './media-alt-modal';
|
||||||
import MediaModal from './media-modal';
|
import MediaModal from './media-modal';
|
||||||
|
@ -200,6 +201,21 @@ export default function Modals() {
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{!!snapStates.showEmbedModal && (
|
||||||
|
<Modal
|
||||||
|
onClose={() => {
|
||||||
|
states.showEmbedModal = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmbedModal
|
||||||
|
html={snapStates.showEmbedModal.html}
|
||||||
|
url={snapStates.showEmbedModal.url}
|
||||||
|
onClose={() => {
|
||||||
|
states.showEmbedModal = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,22 @@ import Link from './link';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import Notification from './notification';
|
import Notification from './notification';
|
||||||
|
|
||||||
|
{
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
console.log('👂👂👂 Listen to message');
|
||||||
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||||
|
console.log('💥💥💥 Message event', event);
|
||||||
|
const { type, id, accessToken } = event?.data || {};
|
||||||
|
if (type === 'notification') {
|
||||||
|
states.routeNotification = {
|
||||||
|
id,
|
||||||
|
accessToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(function NotificationService() {
|
export default memo(function NotificationService() {
|
||||||
if (!('serviceWorker' in navigator)) return null;
|
if (!('serviceWorker' in navigator)) return null;
|
||||||
|
|
||||||
|
@ -82,25 +98,25 @@ export default memo(function NotificationService() {
|
||||||
})();
|
})();
|
||||||
}, [id, accessToken]);
|
}, [id, accessToken]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
// useLayoutEffect(() => {
|
||||||
// Listen to message from service worker
|
// // Listen to message from service worker
|
||||||
const handleMessage = (event) => {
|
// const handleMessage = (event) => {
|
||||||
console.log('💥💥💥 Message event', event);
|
// console.log('💥💥💥 Message event', event);
|
||||||
const { type, id, accessToken } = event?.data || {};
|
// const { type, id, accessToken } = event?.data || {};
|
||||||
if (type === 'notification') {
|
// if (type === 'notification') {
|
||||||
states.routeNotification = {
|
// states.routeNotification = {
|
||||||
id,
|
// id,
|
||||||
accessToken,
|
// accessToken,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
console.log('👂👂👂 Listen to message');
|
// console.log('👂👂👂 Listen to message');
|
||||||
navigator.serviceWorker.addEventListener('message', handleMessage);
|
// navigator.serviceWorker.addEventListener('message', handleMessage);
|
||||||
return () => {
|
// return () => {
|
||||||
console.log('👂👂👂 Remove listen to message');
|
// console.log('👂👂👂 Remove listen to message');
|
||||||
navigator.serviceWorker.removeEventListener('message', handleMessage);
|
// navigator.serviceWorker.removeEventListener('message', handleMessage);
|
||||||
};
|
// };
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (navigator?.clearAppBadge) {
|
if (navigator?.clearAppBadge) {
|
||||||
|
|
|
@ -172,7 +172,9 @@ export const SHORTCUTS_META = {
|
||||||
id: 'search',
|
id: 'search',
|
||||||
title: ({ query }) => (query ? `"${query}"` : 'Search'),
|
title: ({ query }) => (query ? `"${query}"` : 'Search'),
|
||||||
path: ({ query }) =>
|
path: ({ query }) =>
|
||||||
query ? `/search?q=${query}&type=statuses` : '/search',
|
query
|
||||||
|
? `/search?q=${encodeURIComponent(query)}&type=statuses`
|
||||||
|
: '/search',
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
excludeViewMode: ({ query }) => (!query ? ['multi-column'] : []),
|
excludeViewMode: ({ query }) => (!query ? ['multi-column'] : []),
|
||||||
},
|
},
|
||||||
|
|
|
@ -59,8 +59,9 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-color);
|
||||||
backdrop-filter: blur(16px) saturate(3);
|
/* background-color: var(--bg-blur-color);
|
||||||
|
backdrop-filter: blur(16px) saturate(3); */
|
||||||
border-top: var(--hairline-width) solid var(--outline-color);
|
border-top: var(--hairline-width) solid var(--outline-color);
|
||||||
box-shadow: 0 -8px 16px -8px var(--drop-shadow-color);
|
box-shadow: 0 -8px 16px -8px var(--drop-shadow-color);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -165,6 +166,7 @@ shortcuts .tab-bar[hidden] {
|
||||||
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) 0
|
||||||
env(safe-area-inset-left);
|
env(safe-area-inset-left);
|
||||||
background-color: var(--bg-faded-blur-color);
|
background-color: var(--bg-faded-blur-color);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-bottom: var(--hairline-width) solid var(--bg-faded-color);
|
border-bottom: var(--hairline-width) solid var(--bg-faded-color);
|
||||||
|
|
|
@ -206,11 +206,11 @@
|
||||||
.status-card:not(.status-carousel .status)
|
.status-card:not(.status-carousel .status)
|
||||||
:is(.content, .poll, .media-container) {
|
:is(.content, .poll, .media-container) {
|
||||||
max-height: 160px !important;
|
max-height: 160px !important;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
.status.small:not(.status-carousel .status)
|
.status.small:not(.status-carousel .status, .status.large .status)
|
||||||
.status-card
|
.status-card
|
||||||
:is(.content, .poll, .media-container) {
|
:is(.content, .poll, .media-container:not(.media-gt2)) {
|
||||||
max-height: 80px !important;
|
max-height: 80px !important;
|
||||||
}
|
}
|
||||||
.status.large .status-card :is(.content, .poll, .media-container) {
|
.status.large .status-card :is(.content, .poll, .media-container) {
|
||||||
|
@ -730,6 +730,9 @@
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
text-wrap: pretty;
|
text-wrap: pretty;
|
||||||
}
|
}
|
||||||
|
.status-card .content p {
|
||||||
|
margin-block: min(0.25em, 4px);
|
||||||
|
}
|
||||||
.status .content p:first-child {
|
.status .content p:first-child {
|
||||||
margin-block-start: 0;
|
margin-block-start: 0;
|
||||||
}
|
}
|
||||||
|
@ -767,6 +770,9 @@
|
||||||
.status .content ul {
|
.status .content ul {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
.status .content :is(strong, b) {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.status .content .invisible {
|
.status .content .invisible {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -937,6 +943,10 @@
|
||||||
}
|
}
|
||||||
.status.large .media-container.media-eq1 {
|
.status.large .media-container.media-eq1 {
|
||||||
max-height: min(var(--height), 60vh);
|
max-height: min(var(--height), 60vh);
|
||||||
|
|
||||||
|
.media-gif.media-contain {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.status.large
|
.status.large
|
||||||
.media-container:not(.status-card .media-container).media-eq1
|
.media-container:not(.status-card .media-container).media-eq1
|
||||||
|
@ -952,6 +962,11 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
}
|
}
|
||||||
|
.status.status-card .media-container.media-eq1 .media {
|
||||||
|
max-height: 160px;
|
||||||
|
width: auto;
|
||||||
|
max-width: min(var(--width), 100%);
|
||||||
|
}
|
||||||
/* Special media borders */
|
/* Special media borders */
|
||||||
.status .media-container.media-eq2 .media:first-of-type {
|
.status .media-container.media-eq2 .media:first-of-type {
|
||||||
border-radius: var(--media-radius) var(--media-radius-inner)
|
border-radius: var(--media-radius) var(--media-radius-inner)
|
||||||
|
@ -1156,6 +1171,15 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
.status .media-contain video {
|
.status .media-contain video {
|
||||||
object-fit: scale-down !important;
|
object-fit: scale-down !important;
|
||||||
}
|
}
|
||||||
|
.status .media-eq1 .media-hover-animate {
|
||||||
|
transition: border-radius 0.15s ease-out;
|
||||||
|
transition-delay: 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-delay: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
/* .status .media-audio {
|
/* .status .media-audio {
|
||||||
border: 0;
|
border: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
@ -1941,7 +1965,7 @@ a.card:is(:hover, :focus):visited {
|
||||||
color: var(--media-fg-color);
|
color: var(--media-fg-color);
|
||||||
background-color: var(--media-bg-color);
|
background-color: var(--media-bg-color);
|
||||||
border: var(--hairline-width) solid var(--media-outline-color);
|
border: var(--hairline-width) solid var(--media-outline-color);
|
||||||
mix-blend-mode: luminosity;
|
/* mix-blend-mode: luminosity; */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import pThrottle from 'p-throttle';
|
import { shallowEqual } from 'fast-equals';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -20,10 +20,8 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
|
||||||
import { useLongPress } from 'use-long-press';
|
import { useLongPress } from 'use-long-press';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
import { snapshot } from 'valtio/vanilla';
|
|
||||||
|
|
||||||
import AccountBlock from '../components/account-block';
|
import AccountBlock from '../components/account-block';
|
||||||
import EmojiText from '../components/emoji-text';
|
import EmojiText from '../components/emoji-text';
|
||||||
|
@ -54,6 +52,7 @@ import { speak, supportsTTS } from '../utils/speech';
|
||||||
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
||||||
import statusPeek from '../utils/status-peek';
|
import statusPeek from '../utils/status-peek';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import unfurlMastodonLink from '../utils/unfurl-link';
|
||||||
import useTruncated from '../utils/useTruncated';
|
import useTruncated from '../utils/useTruncated';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
|
@ -68,10 +67,6 @@ import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
const SHOW_COMMENT_COUNT_LIMIT = 280;
|
const SHOW_COMMENT_COUNT_LIMIT = 280;
|
||||||
const INLINE_TRANSLATE_LIMIT = 140;
|
const INLINE_TRANSLATE_LIMIT = 140;
|
||||||
const throttle = pThrottle({
|
|
||||||
limit: 1,
|
|
||||||
interval: 1000,
|
|
||||||
});
|
|
||||||
|
|
||||||
function fetchAccount(id, masto) {
|
function fetchAccount(id, masto) {
|
||||||
return masto.v1.accounts.$select(id).fetch();
|
return masto.v1.accounts.$select(id).fetch();
|
||||||
|
@ -1025,7 +1020,7 @@ function Status({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const hotkeysEnabled = !readOnly && !previewMode;
|
const hotkeysEnabled = !readOnly && !previewMode && !quoted;
|
||||||
const rRef = useHotkeys('r, shift+r', replyStatus, {
|
const rRef = useHotkeys('r, shift+r', replyStatus, {
|
||||||
enabled: hotkeysEnabled,
|
enabled: hotkeysEnabled,
|
||||||
});
|
});
|
||||||
|
@ -1587,34 +1582,34 @@ function Status({
|
||||||
a.removeAttribute('target');
|
a.removeAttribute('target');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (previewMode) return;
|
// if (previewMode) return;
|
||||||
// Unfurl Mastodon links
|
// Unfurl Mastodon links
|
||||||
Array.from(
|
// Array.from(
|
||||||
dom.querySelectorAll(
|
// dom.querySelectorAll(
|
||||||
'a[href]:not(.u-url):not(.mention):not(.hashtag)',
|
// 'a[href]:not(.u-url):not(.mention):not(.hashtag)',
|
||||||
),
|
// ),
|
||||||
)
|
// )
|
||||||
.filter((a) => {
|
// .filter((a) => {
|
||||||
const url = a.href;
|
// const url = a.href;
|
||||||
const isPostItself =
|
// const isPostItself =
|
||||||
url === status.url || url === status.uri;
|
// url === status.url || url === status.uri;
|
||||||
return !isPostItself && isMastodonLinkMaybe(url);
|
// return !isPostItself && isMastodonLinkMaybe(url);
|
||||||
})
|
// })
|
||||||
.forEach((a, i) => {
|
// .forEach((a, i) => {
|
||||||
unfurlMastodonLink(currentInstance, a.href).then(
|
// unfurlMastodonLink(currentInstance, a.href).then(
|
||||||
(result) => {
|
// (result) => {
|
||||||
if (!result) return;
|
// if (!result) return;
|
||||||
a.removeAttribute('target');
|
// a.removeAttribute('target');
|
||||||
if (!sKey) return;
|
// if (!sKey) return;
|
||||||
if (!Array.isArray(states.statusQuotes[sKey])) {
|
// if (!Array.isArray(states.statusQuotes[sKey])) {
|
||||||
states.statusQuotes[sKey] = [];
|
// states.statusQuotes[sKey] = [];
|
||||||
}
|
// }
|
||||||
if (!states.statusQuotes[sKey][i]) {
|
// if (!states.statusQuotes[sKey][i]) {
|
||||||
states.statusQuotes[sKey].splice(i, 0, result);
|
// states.statusQuotes[sKey].splice(i, 0, result);
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
|
@ -1989,6 +1984,20 @@ function Card({ card, selfReferential, instance }) {
|
||||||
|
|
||||||
if (snapStates.unfurledLinks[url]) return null;
|
if (snapStates.unfurledLinks[url]) return null;
|
||||||
|
|
||||||
|
const hasIframeHTML = /<iframe/i.test(html);
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (hasIframeHTML) {
|
||||||
|
e.preventDefault();
|
||||||
|
states.showEmbedModal = {
|
||||||
|
html,
|
||||||
|
url: url || embedUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hasIframeHTML],
|
||||||
|
);
|
||||||
|
|
||||||
if (hasText && (image || (type === 'photo' && blurhash))) {
|
if (hasText && (image || (type === 'photo' && blurhash))) {
|
||||||
const domain = new URL(url).hostname
|
const domain = new URL(url).hostname
|
||||||
.replace(/^www\./, '')
|
.replace(/^www\./, '')
|
||||||
|
@ -2021,6 +2030,7 @@ function Card({ card, selfReferential, instance }) {
|
||||||
'--average-color':
|
'--average-color':
|
||||||
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||||
}}
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<div class="card-image">
|
<div class="card-image">
|
||||||
<img
|
<img
|
||||||
|
@ -2059,6 +2069,7 @@ function Card({ card, selfReferential, instance }) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="nofollow noopener noreferrer"
|
rel="nofollow noopener noreferrer"
|
||||||
class="card photo"
|
class="card photo"
|
||||||
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={embedUrl}
|
src={embedUrl}
|
||||||
|
@ -2073,42 +2084,46 @@ function Card({ card, selfReferential, instance }) {
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (type === 'video') {
|
} else {
|
||||||
if (/youtube/i.test(providerName)) {
|
if (type === 'video') {
|
||||||
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
if (/youtube/i.test(providerName)) {
|
||||||
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
||||||
if (videoID) {
|
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
||||||
return <lite-youtube videoid={videoID} nocookie></lite-youtube>;
|
if (videoID) {
|
||||||
|
return <lite-youtube videoid={videoID} nocookie></lite-youtube>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// return (
|
||||||
|
// <div
|
||||||
|
// class="card video"
|
||||||
|
// style={{
|
||||||
|
// aspectRatio: `${width}/${height}`,
|
||||||
|
// }}
|
||||||
|
// dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
if (hasText && !image) {
|
||||||
|
const domain = new URL(url).hostname.replace(/^www\./, '');
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={cardStatusURL || url}
|
||||||
|
target={cardStatusURL ? null : '_blank'}
|
||||||
|
rel="nofollow noopener noreferrer"
|
||||||
|
class={`card link no-image`}
|
||||||
|
lang={language}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div class="meta-container">
|
||||||
|
<p class="meta domain">
|
||||||
|
<Icon icon="link" size="s" /> <span>{domain}</span>
|
||||||
|
</p>
|
||||||
|
<p class="title">{title}</p>
|
||||||
|
<p class="meta">{description || providerName || authorName}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="card video"
|
|
||||||
style={{
|
|
||||||
aspectRatio: `${width}/${height}`,
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (hasText && !image) {
|
|
||||||
const domain = new URL(url).hostname.replace(/^www\./, '');
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={cardStatusURL || url}
|
|
||||||
target={cardStatusURL ? null : '_blank'}
|
|
||||||
rel="nofollow noopener noreferrer"
|
|
||||||
class={`card link no-image`}
|
|
||||||
lang={language}
|
|
||||||
>
|
|
||||||
<div class="meta-container">
|
|
||||||
<p class="meta domain">
|
|
||||||
<Icon icon="link" size="s" /> <span>{domain}</span>
|
|
||||||
</p>
|
|
||||||
<p class="title">{title}</p>
|
|
||||||
<p class="meta">{description || providerName || authorName}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2257,130 +2272,6 @@ export function formatDuration(time) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const denylistDomains = /(twitter|github)\.com/i;
|
|
||||||
const failedUnfurls = {};
|
|
||||||
|
|
||||||
function _unfurlMastodonLink(instance, url) {
|
|
||||||
const snapStates = snapshot(states);
|
|
||||||
if (denylistDomains.test(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (failedUnfurls[url]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const instanceRegex = new RegExp(instance + '/');
|
|
||||||
if (instanceRegex.test(snapStates.unfurledLinks[url]?.url)) {
|
|
||||||
return Promise.resolve(snapStates.unfurledLinks[url]);
|
|
||||||
}
|
|
||||||
console.debug('🦦 Unfurling URL', url);
|
|
||||||
|
|
||||||
let remoteInstanceFetch;
|
|
||||||
let theURL = url;
|
|
||||||
|
|
||||||
// https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123
|
|
||||||
if (/\/\/elk\.[^\/]+\/[^\/]+\.[^\/]+/i.test(theURL)) {
|
|
||||||
theURL = theURL.replace(/elk\.[^\/]+\//i, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://trunks.social/status/domain.com/@stest/123 -> https://domain.com/@stest/123
|
|
||||||
if (/\/\/trunks\.[^\/]+\/status\/[^\/]+\.[^\/]+/i.test(theURL)) {
|
|
||||||
theURL = theURL.replace(/trunks\.[^\/]+\/status\//i, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://phanpy.social/#/domain.com/s/123 -> https://domain.com/statuses/123
|
|
||||||
if (/\/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(theURL)) {
|
|
||||||
const urlAfterHash = theURL.split('/#/')[1];
|
|
||||||
const finalURL = urlAfterHash.replace(/\/s\//i, '/@fakeUsername/');
|
|
||||||
theURL = `https://${finalURL}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let urlObj;
|
|
||||||
try {
|
|
||||||
urlObj = new URL(theURL);
|
|
||||||
} catch (e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const domain = urlObj.hostname;
|
|
||||||
const path = urlObj.pathname;
|
|
||||||
// Regex /:username/:id, where username = @username or @username@domain, id = number
|
|
||||||
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i;
|
|
||||||
const statusMatch = statusRegex.exec(path);
|
|
||||||
if (statusMatch) {
|
|
||||||
const id = statusMatch[3];
|
|
||||||
const { masto } = api({ instance: domain });
|
|
||||||
remoteInstanceFetch = masto.v1.statuses
|
|
||||||
.$select(id)
|
|
||||||
.fetch()
|
|
||||||
.then((status) => {
|
|
||||||
if (status?.id) {
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
instance: domain,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error('No results');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { masto } = api({ instance });
|
|
||||||
const mastoSearchFetch = masto.v2.search
|
|
||||||
.fetch({
|
|
||||||
q: theURL,
|
|
||||||
type: 'statuses',
|
|
||||||
resolve: true,
|
|
||||||
limit: 1,
|
|
||||||
})
|
|
||||||
.then((results) => {
|
|
||||||
if (results.statuses.length > 0) {
|
|
||||||
const status = results.statuses[0];
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
instance,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error('No results');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleFulfill(result) {
|
|
||||||
const { status, instance } = result;
|
|
||||||
const { id } = status;
|
|
||||||
const selfURL = `/${instance}/s/${id}`;
|
|
||||||
console.debug('🦦 Unfurled URL', url, id, selfURL);
|
|
||||||
const data = {
|
|
||||||
id,
|
|
||||||
instance,
|
|
||||||
url: selfURL,
|
|
||||||
};
|
|
||||||
states.unfurledLinks[url] = data;
|
|
||||||
saveStatus(status, instance, {
|
|
||||||
skipThreading: true,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
function handleCatch(e) {
|
|
||||||
failedUnfurls[url] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteInstanceFetch) {
|
|
||||||
// return Promise.any([remoteInstanceFetch, mastoSearchFetch])
|
|
||||||
// .then(handleFulfill)
|
|
||||||
// .catch(handleCatch);
|
|
||||||
// If mastoSearchFetch is fulfilled within 3s, return it, else return remoteInstanceFetch
|
|
||||||
const finalPromise = Promise.race([
|
|
||||||
mastoSearchFetch,
|
|
||||||
new Promise((resolve, reject) => setTimeout(reject, 3000)),
|
|
||||||
]).catch(() => {
|
|
||||||
// If remoteInstanceFetch is fullfilled, return it, else return mastoSearchFetch
|
|
||||||
return remoteInstanceFetch.catch(() => mastoSearchFetch);
|
|
||||||
});
|
|
||||||
return finalPromise.then(handleFulfill).catch(handleCatch);
|
|
||||||
} else {
|
|
||||||
return mastoSearchFetch.then(handleFulfill).catch(handleCatch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nicePostURL(url) {
|
function nicePostURL(url) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
|
@ -2404,8 +2295,6 @@ function nicePostURL(url) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
|
||||||
|
|
||||||
function FilteredStatus({
|
function FilteredStatus({
|
||||||
status,
|
status,
|
||||||
filterInfo,
|
filterInfo,
|
||||||
|
@ -2592,4 +2481,12 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default memo(Status);
|
export default memo(Status, (oldProps, newProps) => {
|
||||||
|
// Shallow equal all props except 'status'
|
||||||
|
// This will be pure static until status ID changes
|
||||||
|
const { status, ...restOldProps } = oldProps;
|
||||||
|
const { status: newStatus, ...restNewProps } = newProps;
|
||||||
|
return (
|
||||||
|
status?.id === newStatus?.id && shallowEqual(restOldProps, restNewProps)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { groupBoosts, groupContext } from '../utils/timeline-utils';
|
||||||
import useInterval from '../utils/useInterval';
|
import useInterval from '../utils/useInterval';
|
||||||
import usePageVisibility from '../utils/usePageVisibility';
|
import usePageVisibility from '../utils/usePageVisibility';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
|
import useScrollFn from '../utils/useScrollFn';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
|
@ -203,17 +204,48 @@ function Timeline({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
scrollDirection,
|
// scrollDirection,
|
||||||
nearReachStart,
|
// nearReachStart,
|
||||||
nearReachEnd,
|
// nearReachEnd,
|
||||||
reachStart,
|
// reachStart,
|
||||||
reachEnd,
|
// reachEnd,
|
||||||
} = useScroll({
|
// } = useScroll({
|
||||||
scrollableRef,
|
// scrollableRef,
|
||||||
distanceFromEnd: 2,
|
// distanceFromEnd: 2,
|
||||||
scrollThresholdStart: 44,
|
// scrollThresholdStart: 44,
|
||||||
});
|
// });
|
||||||
|
const headerRef = useRef();
|
||||||
|
// const [hiddenUI, setHiddenUI] = useState(false);
|
||||||
|
const [nearReachStart, setNearReachStart] = useState(false);
|
||||||
|
useScrollFn(
|
||||||
|
{
|
||||||
|
scrollableRef,
|
||||||
|
distanceFromEnd: 2,
|
||||||
|
scrollThresholdStart: 44,
|
||||||
|
},
|
||||||
|
({
|
||||||
|
scrollDirection,
|
||||||
|
nearReachStart,
|
||||||
|
nearReachEnd,
|
||||||
|
reachStart,
|
||||||
|
reachEnd,
|
||||||
|
}) => {
|
||||||
|
// setHiddenUI(scrollDirection === 'end' && !nearReachEnd);
|
||||||
|
if (headerRef.current) {
|
||||||
|
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||||
|
headerRef.current.hidden = hiddenUI;
|
||||||
|
}
|
||||||
|
setNearReachStart(nearReachStart);
|
||||||
|
if (reachStart) {
|
||||||
|
loadItems(true);
|
||||||
|
}
|
||||||
|
// else if (nearReachEnd || (reachEnd && showMore)) {
|
||||||
|
// loadItems();
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollableRef.current?.scrollTo({ top: 0 });
|
scrollableRef.current?.scrollTo({ top: 0 });
|
||||||
|
@ -223,17 +255,17 @@ function Timeline({
|
||||||
loadItems(true);
|
loadItems(true);
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (reachStart) {
|
// if (reachStart) {
|
||||||
loadItems(true);
|
// loadItems(true);
|
||||||
}
|
// }
|
||||||
}, [reachStart]);
|
// }, [reachStart]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (nearReachEnd || (reachEnd && showMore)) {
|
// if (nearReachEnd || (reachEnd && showMore)) {
|
||||||
loadItems();
|
// loadItems();
|
||||||
}
|
// }
|
||||||
}, [nearReachEnd, showMore]);
|
// }, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
const prevView = useRef(view);
|
const prevView = useRef(view);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -304,7 +336,7 @@ function Timeline({
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
// const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterContext.Provider value={filterContext}>
|
<FilterContext.Provider value={filterContext}>
|
||||||
|
@ -321,7 +353,8 @@ function Timeline({
|
||||||
>
|
>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
<header
|
<header
|
||||||
hidden={hiddenUI}
|
ref={headerRef}
|
||||||
|
// hidden={hiddenUI}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (!e.target.closest('a, button')) {
|
if (!e.target.closest('a, button')) {
|
||||||
scrollableRef.current?.scrollTo({
|
scrollableRef.current?.scrollTo({
|
||||||
|
@ -356,7 +389,7 @@ function Timeline({
|
||||||
</div>
|
</div>
|
||||||
{items.length > 0 &&
|
{items.length > 0 &&
|
||||||
uiState !== 'loading' &&
|
uiState !== 'loading' &&
|
||||||
!hiddenUI &&
|
// !hiddenUI &&
|
||||||
showNew && (
|
showNew && (
|
||||||
<button
|
<button
|
||||||
class="updates-button shiny-pill"
|
class="updates-button shiny-pill"
|
||||||
|
@ -419,6 +452,8 @@ function Timeline({
|
||||||
{uiState === 'default' &&
|
{uiState === 'default' &&
|
||||||
(showMore ? (
|
(showMore ? (
|
||||||
<InView
|
<InView
|
||||||
|
root={scrollableRef.current}
|
||||||
|
rootMargin={`0px 0px ${screen.height * 1.5}px 0px`}
|
||||||
onChange={(inView) => {
|
onChange={(inView) => {
|
||||||
if (inView) {
|
if (inView) {
|
||||||
loadItems();
|
loadItems();
|
||||||
|
@ -657,12 +692,33 @@ function TimelineItem({
|
||||||
|
|
||||||
function StatusCarousel({ title, class: className, children }) {
|
function StatusCarousel({ title, class: className, children }) {
|
||||||
const carouselRef = useRef();
|
const carouselRef = useRef();
|
||||||
const { reachStart, reachEnd, init } = useScroll({
|
// const { reachStart, reachEnd, init } = useScroll({
|
||||||
scrollableRef: carouselRef,
|
// scrollableRef: carouselRef,
|
||||||
direction: 'horizontal',
|
// direction: 'horizontal',
|
||||||
});
|
// });
|
||||||
|
const startButtonRef = useRef();
|
||||||
|
const endButtonRef = useRef();
|
||||||
|
// useScrollFn(
|
||||||
|
// {
|
||||||
|
// scrollableRef: carouselRef,
|
||||||
|
// direction: 'horizontal',
|
||||||
|
// init: true,
|
||||||
|
// },
|
||||||
|
// ({ reachStart, reachEnd }) => {
|
||||||
|
// if (startButtonRef.current) startButtonRef.current.disabled = reachStart;
|
||||||
|
// if (endButtonRef.current) endButtonRef.current.disabled = reachEnd;
|
||||||
|
// },
|
||||||
|
// [],
|
||||||
|
// );
|
||||||
|
// useEffect(() => {
|
||||||
|
// init?.();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
const [render, setRender] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
init?.();
|
setTimeout(() => {
|
||||||
|
setRender(true);
|
||||||
|
}, 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -671,9 +727,10 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<span>
|
<span>
|
||||||
<button
|
<button
|
||||||
|
ref={startButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
class="small plain2"
|
class="small plain2"
|
||||||
disabled={reachStart}
|
// disabled={reachStart}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
carouselRef.current?.scrollBy({
|
carouselRef.current?.scrollBy({
|
||||||
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
left: -Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
@ -684,9 +741,10 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
<Icon icon="chevron-left" />
|
<Icon icon="chevron-left" />
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<button
|
<button
|
||||||
|
ref={endButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
class="small plain2"
|
class="small plain2"
|
||||||
disabled={reachEnd}
|
// disabled={reachEnd}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
carouselRef.current?.scrollBy({
|
carouselRef.current?.scrollBy({
|
||||||
left: Math.min(320, carouselRef.current?.offsetWidth),
|
left: Math.min(320, carouselRef.current?.offsetWidth),
|
||||||
|
@ -698,7 +756,23 @@ function StatusCarousel({ title, class: className, children }) {
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
<ul ref={carouselRef}>{children}</ul>
|
<ul ref={carouselRef}>
|
||||||
|
<InView
|
||||||
|
class="status-carousel-beacon"
|
||||||
|
onChange={(inView) => {
|
||||||
|
if (startButtonRef.current)
|
||||||
|
startButtonRef.current.disabled = inView;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{children[0]}
|
||||||
|
{render && children.slice(1)}
|
||||||
|
<InView
|
||||||
|
class="status-carousel-beacon"
|
||||||
|
onChange={(inView) => {
|
||||||
|
if (endButtonRef.current) endButtonRef.current.disabled = inView;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
--text-color: #1c1e21;
|
--text-color: #1c1e21;
|
||||||
--text-insignificant-color: #1c1e2199;
|
--text-insignificant-color: #1c1e2199;
|
||||||
--link-color: var(--blue-color);
|
--link-color: var(--blue-color);
|
||||||
|
--link-bg-color: #4169e122;
|
||||||
--link-light-color: #4169e199;
|
--link-light-color: #4169e199;
|
||||||
--link-faded-color: #4169e155;
|
--link-faded-color: #4169e155;
|
||||||
--link-bg-hover-color: #f0f2f599;
|
--link-bg-hover-color: #f0f2f599;
|
||||||
|
@ -89,6 +90,7 @@
|
||||||
--media-outline-color: color-mix(in lch, var(--media-fg-color), transparent);
|
--media-outline-color: color-mix(in lch, var(--media-fg-color), transparent);
|
||||||
|
|
||||||
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
||||||
|
--spring-timing-funtion: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-resolution: 2dppx) {
|
@media (min-resolution: 2dppx) {
|
||||||
|
@ -447,6 +449,17 @@ kbd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up-smooth {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes position-object {
|
@keyframes position-object {
|
||||||
0% {
|
0% {
|
||||||
object-position: 50% 50%;
|
object-position: 50% 50%;
|
||||||
|
|
|
@ -377,14 +377,14 @@ function AccountStatuses() {
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
);
|
);
|
||||||
|
const [year, month] = value.split('-');
|
||||||
|
const monthIndex = parseInt(month, 10) - 1;
|
||||||
|
const date = new Date(year, monthIndex);
|
||||||
showToast(
|
showToast(
|
||||||
`Showing posts in ${new Date(value).toLocaleString(
|
`Showing posts in ${date.toLocaleString('default', {
|
||||||
'default',
|
month: 'long',
|
||||||
{
|
year: 'numeric',
|
||||||
month: 'long',
|
})}`,
|
||||||
year: 'numeric',
|
|
||||||
},
|
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -164,7 +164,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
portal
|
portal
|
||||||
setDownOverflow
|
setDownOverflow
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
viewScroll="close"
|
// viewScroll="close"
|
||||||
position="anchor"
|
position="anchor"
|
||||||
menuButton={
|
menuButton={
|
||||||
<button type="button" class="plain">
|
<button type="button" class="plain">
|
||||||
|
|
|
@ -37,16 +37,20 @@ export default function HttpRoute() {
|
||||||
const { masto: currentMasto, instance: currentInstance } = api();
|
const { masto: currentMasto, instance: currentInstance } = api();
|
||||||
const result = await currentMasto.v2.search.fetch({
|
const result = await currentMasto.v2.search.fetch({
|
||||||
q: url,
|
q: url,
|
||||||
type: 'statuses',
|
|
||||||
limit: 1,
|
limit: 1,
|
||||||
resolve: true,
|
resolve: true,
|
||||||
});
|
});
|
||||||
if (result.statuses.length) {
|
if (result.statuses.length) {
|
||||||
const status = result.statuses[0];
|
const status = result.statuses[0];
|
||||||
window.location.hash = `/${currentInstance}/s/${status.id}?view=full`;
|
window.location.hash = `/${currentInstance}/s/${status.id}?view=full`;
|
||||||
} else {
|
} else if (result.accounts.length) {
|
||||||
|
const account = result.accounts[0];
|
||||||
|
window.location.hash = `/${currentInstance}/a/${account.id}`;
|
||||||
|
} else if (statusURL) {
|
||||||
// Fallback to original URL, which will probably show error
|
// Fallback to original URL, which will probably show error
|
||||||
window.location.hash = statusURL + '?view=full';
|
window.location.hash = statusURL + '?view=full';
|
||||||
|
} else {
|
||||||
|
setUIState('error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -115,22 +115,25 @@ ul.link-list.hashtag-list li a {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.search-popover-item:is(:hover, :focus, .focus) {
|
.search-popover-item:is(:hover, :focus, .focus) {
|
||||||
background-color: var(--button-bg-color);
|
background-color: var(--link-bg-color);
|
||||||
color: var(--button-text-color);
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.search-popover-item:is(:focus, .focus) {
|
||||||
|
box-shadow: inset 4px 0 0 0 var(--button-bg-color);
|
||||||
}
|
}
|
||||||
.search-popover-item :is(mark, q) {
|
.search-popover-item :is(mark, q) {
|
||||||
background-color: var(--bg-faded-blur-color);
|
color: var(--text-color);
|
||||||
color: inherit;
|
background-color: var(--link-bg-color);
|
||||||
}
|
}
|
||||||
.search-popover-item:is(:hover, :focus, .focus) :is(mark, q) {
|
.search-popover-item:is(:hover, :focus, .focus) :is(mark, q) {
|
||||||
background-color: var(--button-bg-color);
|
background-color: var(--link-bg-color);
|
||||||
}
|
}
|
||||||
.search-popover:hover .search-popover-item.focus:not(:hover, :focus),
|
.search-popover:hover .search-popover-item.focus:not(:hover, :focus),
|
||||||
.search-popover:hover
|
.search-popover:hover
|
||||||
.search-popover-item.focus:not(:hover, :focus)
|
.search-popover-item.focus:not(:hover, :focus)
|
||||||
:is(mark, q) {
|
:is(mark, q) {
|
||||||
background-color: unset;
|
/* background-color: unset; */
|
||||||
color: unset;
|
/* color: unset; */
|
||||||
}
|
}
|
||||||
.search-popover-item > span {
|
.search-popover-item > span {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -17,7 +17,6 @@ import { api } from '../utils/api';
|
||||||
import { fetchRelationships } from '../utils/relationships';
|
import { fetchRelationships } from '../utils/relationships';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import usePageVisibility from '../utils/usePageVisibility';
|
import usePageVisibility from '../utils/usePageVisibility';
|
||||||
import useScroll from '../utils/useScroll';
|
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const SHORT_LIMIT = 5;
|
const SHORT_LIMIT = 5;
|
||||||
|
@ -151,11 +150,9 @@ function Search({ columnMode, ...props }) {
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { reachStart } = useScroll({
|
|
||||||
scrollableRef,
|
|
||||||
});
|
|
||||||
const lastHiddenTime = useRef();
|
const lastHiddenTime = useRef();
|
||||||
usePageVisibility((visible) => {
|
usePageVisibility((visible) => {
|
||||||
|
const reachStart = scrollableRef.current?.scrollTop === 0;
|
||||||
if (visible && reachStart) {
|
if (visible && reachStart) {
|
||||||
const timeDiff = Date.now() - lastHiddenTime.current;
|
const timeDiff = Date.now() - lastHiddenTime.current;
|
||||||
if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
|
if (!lastHiddenTime.current || timeDiff > 1000 * 3) {
|
||||||
|
|
|
@ -7,15 +7,12 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
.status-deck header h1 .deck-back {
|
.status-deck header h1 .deck-back {
|
||||||
margin-left: -16px;
|
margin-left: -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-deck header.inview h1 {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-heading {
|
.hero-heading {
|
||||||
font-size: var(--text-size);
|
font-size: var(--text-size);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -36,7 +33,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ancestors-indicator:not([hidden]) {
|
.ancestors-indicator:not([hidden]) {
|
||||||
animation: slide-up 0.3s both ease-out 0.3s;
|
animation: slide-up-smooth 0.3s both var(--spring-timing-funtion) 0.3s;
|
||||||
}
|
}
|
||||||
.ancestors-indicator[hidden] {
|
.ancestors-indicator[hidden] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -184,6 +184,15 @@ function StatusPage(params) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatusParent(props) {
|
||||||
|
const { linkable, to, onClick, ...restProps } = props;
|
||||||
|
return linkable ? (
|
||||||
|
<Link class="status-link" to={to} onClick={onClick} {...restProps} />
|
||||||
|
) : (
|
||||||
|
<div class="status-focus" tabIndex={0} {...restProps} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const mediaParam = searchParams.get('media');
|
const mediaParam = searchParams.get('media');
|
||||||
|
@ -429,7 +438,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(initContext, [id, masto]);
|
useEffect(initContext, [id, masto]);
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!statuses.length) return;
|
if (!statuses.length) return;
|
||||||
console.debug('STATUSES', statuses);
|
console.debug('STATUSES', statuses);
|
||||||
const scrollPosition = scrollPositions[id];
|
const scrollPosition = scrollPositions[id];
|
||||||
|
@ -545,7 +554,6 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
const ancestors = statuses.filter((s) => s.ancestor);
|
const ancestors = statuses.filter((s) => s.ancestor);
|
||||||
|
|
||||||
const [heroInView, setHeroInView] = useState(true);
|
const [heroInView, setHeroInView] = useState(true);
|
||||||
const onView = useDebouncedCallback(setHeroInView, 100);
|
|
||||||
const heroPointer = useMemo(() => {
|
const heroPointer = useMemo(() => {
|
||||||
// get top offset of heroStatus
|
// get top offset of heroStatus
|
||||||
if (!heroStatusRef.current || heroInView) return null;
|
if (!heroStatusRef.current || heroInView) return null;
|
||||||
|
@ -652,10 +660,11 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { nearReachStart } = useScroll({
|
const [reachTopPost, setReachTopPost] = useState(false);
|
||||||
scrollableRef,
|
// const { nearReachStart } = useScroll({
|
||||||
distanceFromStartPx: 16,
|
// scrollableRef,
|
||||||
});
|
// distanceFromStartPx: 16,
|
||||||
|
// });
|
||||||
|
|
||||||
const initialPageState = useRef(showMedia ? 'media+status' : 'status');
|
const initialPageState = useRef(showMedia ? 'media+status' : 'status');
|
||||||
|
|
||||||
|
@ -693,7 +702,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}, [mediaStatusID, showMedia]);
|
}, [mediaStatusID, showMedia]);
|
||||||
|
|
||||||
const renderStatus = useCallback(
|
const renderStatus = useCallback(
|
||||||
(status) => {
|
(status, i) => {
|
||||||
const {
|
const {
|
||||||
id: statusID,
|
id: statusID,
|
||||||
ancestor,
|
ancestor,
|
||||||
|
@ -705,24 +714,8 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
weight,
|
weight,
|
||||||
} = status;
|
} = status;
|
||||||
const isHero = statusID === id;
|
const isHero = statusID === id;
|
||||||
// const StatusParent = useCallback(
|
const isLinkable = isThread || ancestor;
|
||||||
// (props) =>
|
|
||||||
// isThread || thread || ancestor ? (
|
|
||||||
// <Link
|
|
||||||
// class="status-link"
|
|
||||||
// to={
|
|
||||||
// instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
|
|
||||||
// }
|
|
||||||
// onClick={() => {
|
|
||||||
// resetScrollPosition(statusID);
|
|
||||||
// }}
|
|
||||||
// {...props}
|
|
||||||
// />
|
|
||||||
// ) : (
|
|
||||||
// <div class="status-focus" tabIndex={0} {...props} />
|
|
||||||
// ),
|
|
||||||
// [isThread, thread],
|
|
||||||
// );
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={statusID}
|
key={statusID}
|
||||||
|
@ -735,7 +728,13 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
<>
|
<>
|
||||||
<InView
|
<InView
|
||||||
threshold={0.1}
|
threshold={0.1}
|
||||||
onChange={onView}
|
onChange={(inView) => {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setHeroInView(inView);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
class="status-focus"
|
class="status-focus"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
|
@ -802,23 +801,52 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// <StatusParent>
|
<StatusParent
|
||||||
<Link
|
linkable={isLinkable}
|
||||||
class="status-link"
|
|
||||||
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetScrollPosition(statusID);
|
resetScrollPosition(statusID);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Status
|
{/* <Link
|
||||||
statusID={statusID}
|
class="status-link"
|
||||||
instance={instance}
|
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
||||||
withinContext
|
onClick={() => {
|
||||||
size={thread || ancestor ? 'm' : 's'}
|
resetScrollPosition(statusID);
|
||||||
enableTranslate
|
}}
|
||||||
onMediaClick={handleMediaClick}
|
> */}
|
||||||
onStatusLinkClick={handleStatusLinkClick}
|
{i === 0 && ancestor ? (
|
||||||
/>
|
<InView
|
||||||
|
threshold={0.5}
|
||||||
|
onChange={(inView) => {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setReachTopPost(inView);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
withinContext
|
||||||
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
|
enableTranslate
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
onStatusLinkClick={handleStatusLinkClick}
|
||||||
|
/>
|
||||||
|
</InView>
|
||||||
|
) : (
|
||||||
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
withinContext
|
||||||
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
|
enableTranslate
|
||||||
|
onMediaClick={handleMediaClick}
|
||||||
|
onStatusLinkClick={handleStatusLinkClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{ancestor && repliesCount > 1 && (
|
{ancestor && repliesCount > 1 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
<Icon icon="comment2" />{' '}
|
<Icon icon="comment2" />{' '}
|
||||||
|
@ -835,8 +863,8 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
{/* </StatusParent> */}
|
</StatusParent>
|
||||||
</Link>
|
// </Link>
|
||||||
)}
|
)}
|
||||||
{descendant && replies?.length > 0 && (
|
{descendant && replies?.length > 0 && (
|
||||||
<SubComments
|
<SubComments
|
||||||
|
@ -846,6 +874,10 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
level={1}
|
level={1}
|
||||||
accWeight={weight}
|
accWeight={weight}
|
||||||
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
|
openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
|
||||||
|
parentLink={{
|
||||||
|
to: instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`,
|
||||||
|
onClick: () => resetScrollPosition(statusID),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{uiState === 'loading' &&
|
{uiState === 'loading' &&
|
||||||
|
@ -901,6 +933,24 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
return STATUS_URL_REGEX.test(states.prevLocation?.pathname);
|
return STATUS_URL_REGEX.test(states.prevLocation?.pathname);
|
||||||
}, [sKey]);
|
}, [sKey]);
|
||||||
|
|
||||||
|
const moreStatusesKeys = useMemo(() => {
|
||||||
|
if (!showMore) return [];
|
||||||
|
const ids = [];
|
||||||
|
function getIDs(status) {
|
||||||
|
ids.push(status.id);
|
||||||
|
if (status.replies) {
|
||||||
|
status.replies.forEach(getIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statuses.slice(limit).forEach(getIDs);
|
||||||
|
return ids.map((id) => statusKey(id, instance));
|
||||||
|
}, [showMore, statuses, limit, instance]);
|
||||||
|
|
||||||
|
const statusesList = useMemo(
|
||||||
|
() => statuses.slice(0, limit).map(renderStatus),
|
||||||
|
[statuses, limit, renderStatus],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
|
@ -922,9 +972,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class={`${heroInView ? 'inview' : ''} ${
|
class={`${uiState === 'loading' ? 'loading' : ''}`}
|
||||||
uiState === 'loading' ? 'loading' : ''
|
|
||||||
}`}
|
|
||||||
onDblClick={(e) => {
|
onDblClick={(e) => {
|
||||||
// reload statuses
|
// reload statuses
|
||||||
states.reloadStatusPage++;
|
states.reloadStatusPage++;
|
||||||
|
@ -998,7 +1046,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
hidden={!ancestors.length || nearReachStart}
|
hidden={!ancestors.length || reachTopPost}
|
||||||
title={`${ancestors.length} posts above ‒ Go to top`}
|
title={`${ancestors.length} posts above ‒ Go to top`}
|
||||||
>
|
>
|
||||||
<Icon icon="arrow-up" />
|
<Icon icon="arrow-up" />
|
||||||
|
@ -1147,7 +1195,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
uiState === 'loading' ? 'loading' : ''
|
uiState === 'loading' ? 'loading' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{statuses.slice(0, limit).map(renderStatus)}
|
{statusesList}
|
||||||
{showMore > 0 && (
|
{showMore > 0 && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
|
@ -1156,10 +1204,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
onClick={() => setLimit((l) => l + LIMIT)}
|
onClick={() => setLimit((l) => l + LIMIT)}
|
||||||
style={{ marginBlockEnd: '6em' }}
|
style={{ marginBlockEnd: '6em' }}
|
||||||
data-state-post-ids={statuses
|
data-state-post-ids={moreStatusesKeys.join(' ')}
|
||||||
.slice(limit)
|
|
||||||
.map((s) => statusKey(s.id, instance))
|
|
||||||
.join(' ')}
|
|
||||||
>
|
>
|
||||||
<div class="ib avatars-bunch">
|
<div class="ib avatars-bunch">
|
||||||
{/* show avatars for first 5 statuses */}
|
{/* show avatars for first 5 statuses */}
|
||||||
|
@ -1218,6 +1263,7 @@ function SubComments({
|
||||||
level,
|
level,
|
||||||
accWeight,
|
accWeight,
|
||||||
openAll,
|
openAll,
|
||||||
|
parentLink,
|
||||||
}) {
|
}) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
@ -1304,34 +1350,49 @@ function SubComments({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<b>
|
<span class="replies-counts">
|
||||||
<span title={replies.length}>{shortenNumber(replies.length)}</span>{' '}
|
<b>
|
||||||
repl
|
<span title={replies.length}>{shortenNumber(replies.length)}</span>{' '}
|
||||||
{replies.length === 1 ? 'y' : 'ies'}
|
repl
|
||||||
</b>
|
{replies.length === 1 ? 'y' : 'ies'}
|
||||||
{!sameCount && totalComments > 1 && (
|
</b>
|
||||||
<>
|
{!sameCount && totalComments > 1 && (
|
||||||
{' '}
|
<>
|
||||||
·{' '}
|
{' '}
|
||||||
<span>
|
·{' '}
|
||||||
<span title={totalComments}>{shortenNumber(totalComments)}</span>{' '}
|
<span>
|
||||||
comment
|
<span title={totalComments}>
|
||||||
{totalComments === 1 ? '' : 's'}
|
{shortenNumber(totalComments)}
|
||||||
</span>
|
</span>{' '}
|
||||||
</>
|
comment
|
||||||
|
{totalComments === 1 ? '' : 's'}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<Icon icon="chevron-down" class="replies-summary-chevron" />
|
||||||
|
{!!parentLink && (
|
||||||
|
<Link
|
||||||
|
class="replies-parent-link"
|
||||||
|
to={parentLink.to}
|
||||||
|
onClick={parentLink.onClick}
|
||||||
|
title="View post with its replies"
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</summary>
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{replies.map((r) => (
|
{replies.map((r) => (
|
||||||
<li key={r.id}>
|
<li key={r.id}>
|
||||||
<Link
|
{/* <Link
|
||||||
class="status-link"
|
class="status-link"
|
||||||
to={instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`}
|
to={instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetScrollPosition(r.id);
|
resetScrollPosition(r.id);
|
||||||
}}
|
}}
|
||||||
>
|
> */}
|
||||||
{/* <div class="status-focus" tabIndex={0}> */}
|
<div class="status-focus" tabIndex={0}>
|
||||||
<Status
|
<Status
|
||||||
statusID={r.id}
|
statusID={r.id}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
|
@ -1348,8 +1409,8 @@ function SubComments({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* </div> */}
|
</div>
|
||||||
</Link>
|
{/* </Link> */}
|
||||||
{r.replies?.length && (
|
{r.replies?.length && (
|
||||||
<SubComments
|
<SubComments
|
||||||
instance={instance}
|
instance={instance}
|
||||||
|
@ -1357,6 +1418,12 @@ function SubComments({
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
accWeight={!open ? r.weight : totalWeight}
|
accWeight={!open ? r.weight : totalWeight}
|
||||||
openAll={openAll}
|
openAll={openAll}
|
||||||
|
parentLink={{
|
||||||
|
to: instance ? `/${instance}/s/${r.id}` : `/s/${r.id}`,
|
||||||
|
onClick: () => {
|
||||||
|
resetScrollPosition(r.id);
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function getInstanceStatusObject(url) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInstanceStatusURL(url) {
|
function getInstanceStatusURL(url) {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
import { deepEqual } from 'fast-equals';
|
||||||
import { proxy, subscribe } from 'valtio';
|
import { proxy, subscribe } from 'valtio';
|
||||||
import { subscribeKey } from 'valtio/utils';
|
import { subscribeKey } from 'valtio/utils';
|
||||||
|
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
|
import isMastodonLinkMaybe from './isMastodonLinkMaybe';
|
||||||
import pmem from './pmem';
|
import pmem from './pmem';
|
||||||
import rateLimit from './ratelimit';
|
import rateLimit from './ratelimit';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import unfurlMastodonLink from './unfurl-link';
|
||||||
|
|
||||||
const states = proxy({
|
const states = proxy({
|
||||||
appVersion: {},
|
appVersion: {},
|
||||||
|
@ -47,6 +50,7 @@ const states = proxy({
|
||||||
showKeyboardShortcutsHelp: false,
|
showKeyboardShortcutsHelp: false,
|
||||||
showGenericAccounts: false,
|
showGenericAccounts: false,
|
||||||
showMediaAlt: false,
|
showMediaAlt: false,
|
||||||
|
showEmbedModal: false,
|
||||||
// Shortcuts
|
// Shortcuts
|
||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
// Settings
|
// Settings
|
||||||
|
@ -148,6 +152,7 @@ export function hideAllModals() {
|
||||||
states.showKeyboardShortcutsHelp = false;
|
states.showKeyboardShortcutsHelp = false;
|
||||||
states.showGenericAccounts = false;
|
states.showGenericAccounts = false;
|
||||||
states.showMediaAlt = false;
|
states.showMediaAlt = false;
|
||||||
|
states.showEmbedModal = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusKey(id, instance) {
|
export function statusKey(id, instance) {
|
||||||
|
@ -168,13 +173,15 @@ export function saveStatus(status, instance, opts) {
|
||||||
opts = instance;
|
opts = instance;
|
||||||
instance = null;
|
instance = null;
|
||||||
}
|
}
|
||||||
const { override, skipThreading } = Object.assign(
|
const {
|
||||||
{ override: true, skipThreading: false },
|
override = true,
|
||||||
opts,
|
skipThreading = false,
|
||||||
);
|
skipUnfurling = false,
|
||||||
|
} = opts || {};
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
const oldStatus = getStatus(status.id, instance);
|
const oldStatus = getStatus(status.id, instance);
|
||||||
if (!override && oldStatus) return;
|
if (!override && oldStatus) return;
|
||||||
|
if (deepEqual(status, oldStatus)) return;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const key = statusKey(status.id, instance);
|
const key = statusKey(status.id, instance);
|
||||||
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
|
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
|
||||||
|
@ -189,12 +196,14 @@ export function saveStatus(status, instance, opts) {
|
||||||
// THREAD TRAVERSER
|
// THREAD TRAVERSER
|
||||||
if (!skipThreading) {
|
if (!skipThreading) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
threadifyStatus(status, instance);
|
threadifyStatus(status.reblog || status, instance);
|
||||||
if (status.reblog) {
|
});
|
||||||
queueMicrotask(() => {
|
}
|
||||||
threadifyStatus(status.reblog, instance);
|
|
||||||
});
|
// UNFURLER
|
||||||
}
|
if (!skipUnfurling) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
unfurlStatus(status.reblog || status, instance);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,6 +249,38 @@ function _threadifyStatus(status, propInstance) {
|
||||||
}
|
}
|
||||||
export const threadifyStatus = rateLimit(_threadifyStatus, 100);
|
export const threadifyStatus = rateLimit(_threadifyStatus, 100);
|
||||||
|
|
||||||
|
const fauxDiv = document.createElement('div');
|
||||||
|
export function unfurlStatus(status, instance) {
|
||||||
|
const { instance: currentInstance } = api();
|
||||||
|
const content = status?.content;
|
||||||
|
const hasLink = /<a/i.test(content);
|
||||||
|
if (hasLink) {
|
||||||
|
const sKey = statusKey(status?.id, instance);
|
||||||
|
fauxDiv.innerHTML = content;
|
||||||
|
const links = fauxDiv.querySelectorAll(
|
||||||
|
'a[href]:not(.u-url):not(.mention):not(.hashtag)',
|
||||||
|
);
|
||||||
|
[...links]
|
||||||
|
.filter((a) => {
|
||||||
|
const url = a.href;
|
||||||
|
const isPostItself = url === status.url || url === status.uri;
|
||||||
|
return !isPostItself && isMastodonLinkMaybe(url);
|
||||||
|
})
|
||||||
|
.forEach((a, i) => {
|
||||||
|
unfurlMastodonLink(currentInstance, a.href).then((result) => {
|
||||||
|
if (!result) return;
|
||||||
|
if (!sKey) return;
|
||||||
|
if (!Array.isArray(states.statusQuotes[sKey])) {
|
||||||
|
states.statusQuotes[sKey] = [];
|
||||||
|
}
|
||||||
|
if (!states.statusQuotes[sKey][i]) {
|
||||||
|
states.statusQuotes[sKey].splice(i, 0, result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchStatus = pmem((statusID, masto) => {
|
const fetchStatus = pmem((statusID, masto) => {
|
||||||
return masto.v1.statuses.$select(statusID).fetch();
|
return masto.v1.statuses.$select(statusID).fetch();
|
||||||
});
|
});
|
||||||
|
|
136
src/utils/unfurl-link.jsx
Normal file
136
src/utils/unfurl-link.jsx
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import pThrottle from 'p-throttle';
|
||||||
|
import { snapshot } from 'valtio/vanilla';
|
||||||
|
|
||||||
|
import { api } from './api';
|
||||||
|
import states, { saveStatus } from './states';
|
||||||
|
|
||||||
|
export const throttle = pThrottle({
|
||||||
|
limit: 1,
|
||||||
|
interval: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const denylistDomains = /(twitter|github)\.com/i;
|
||||||
|
const failedUnfurls = {};
|
||||||
|
function _unfurlMastodonLink(instance, url) {
|
||||||
|
const snapStates = snapshot(states);
|
||||||
|
if (denylistDomains.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failedUnfurls[url]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const instanceRegex = new RegExp(instance + '/');
|
||||||
|
if (instanceRegex.test(snapStates.unfurledLinks[url]?.url)) {
|
||||||
|
return Promise.resolve(snapStates.unfurledLinks[url]);
|
||||||
|
}
|
||||||
|
console.debug('🦦 Unfurling URL', url);
|
||||||
|
|
||||||
|
let remoteInstanceFetch;
|
||||||
|
let theURL = url;
|
||||||
|
|
||||||
|
// https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123
|
||||||
|
if (/\/\/elk\.[^\/]+\/[^\/]+\.[^\/]+/i.test(theURL)) {
|
||||||
|
theURL = theURL.replace(/elk\.[^\/]+\//i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://trunks.social/status/domain.com/@stest/123 -> https://domain.com/@stest/123
|
||||||
|
if (/\/\/trunks\.[^\/]+\/status\/[^\/]+\.[^\/]+/i.test(theURL)) {
|
||||||
|
theURL = theURL.replace(/trunks\.[^\/]+\/status\//i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://phanpy.social/#/domain.com/s/123 -> https://domain.com/statuses/123
|
||||||
|
if (/\/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(theURL)) {
|
||||||
|
const urlAfterHash = theURL.split('/#/')[1];
|
||||||
|
const finalURL = urlAfterHash.replace(/\/s\//i, '/@fakeUsername/');
|
||||||
|
theURL = `https://${finalURL}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlObj;
|
||||||
|
try {
|
||||||
|
urlObj = new URL(theURL);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const domain = urlObj.hostname;
|
||||||
|
const path = urlObj.pathname;
|
||||||
|
// Regex /:username/:id, where username = @username or @username@domain, id = number
|
||||||
|
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i;
|
||||||
|
const statusMatch = statusRegex.exec(path);
|
||||||
|
if (statusMatch) {
|
||||||
|
const id = statusMatch[3];
|
||||||
|
const { masto } = api({ instance: domain });
|
||||||
|
remoteInstanceFetch = masto.v1.statuses
|
||||||
|
.$select(id)
|
||||||
|
.fetch()
|
||||||
|
.then((status) => {
|
||||||
|
if (status?.id) {
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
instance: domain,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('No results');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { masto } = api({ instance });
|
||||||
|
const mastoSearchFetch = masto.v2.search
|
||||||
|
.fetch({
|
||||||
|
q: theURL,
|
||||||
|
type: 'statuses',
|
||||||
|
resolve: true,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
if (results.statuses.length > 0) {
|
||||||
|
const status = results.statuses[0];
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('No results');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleFulfill(result) {
|
||||||
|
const { status, instance } = result;
|
||||||
|
const { id } = status;
|
||||||
|
const selfURL = `/${instance}/s/${id}`;
|
||||||
|
console.debug('🦦 Unfurled URL', url, id, selfURL);
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
instance,
|
||||||
|
url: selfURL,
|
||||||
|
};
|
||||||
|
states.unfurledLinks[url] = data;
|
||||||
|
saveStatus(status, instance, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
function handleCatch(e) {
|
||||||
|
failedUnfurls[url] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteInstanceFetch) {
|
||||||
|
// return Promise.any([remoteInstanceFetch, mastoSearchFetch])
|
||||||
|
// .then(handleFulfill)
|
||||||
|
// .catch(handleCatch);
|
||||||
|
// If mastoSearchFetch is fulfilled within 3s, return it, else return remoteInstanceFetch
|
||||||
|
const finalPromise = Promise.race([
|
||||||
|
mastoSearchFetch,
|
||||||
|
new Promise((resolve, reject) => setTimeout(reject, 3000)),
|
||||||
|
]).catch(() => {
|
||||||
|
// If remoteInstanceFetch is fullfilled, return it, else return mastoSearchFetch
|
||||||
|
return remoteInstanceFetch.catch(() => mastoSearchFetch);
|
||||||
|
});
|
||||||
|
return finalPromise.then(handleFulfill).catch(handleCatch);
|
||||||
|
} else {
|
||||||
|
return mastoSearchFetch.then(handleFulfill).catch(handleCatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
||||||
|
export default unfurlMastodonLink;
|
130
src/utils/useScrollFn.js
Normal file
130
src/utils/useScrollFn.js
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useThrottledCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
export default function useScrollFn(
|
||||||
|
{
|
||||||
|
scrollableRef,
|
||||||
|
distanceFromStart = 1, // ratio of clientHeight/clientWidth
|
||||||
|
distanceFromEnd = 1, // ratio of clientHeight/clientWidth
|
||||||
|
scrollThresholdStart = 10,
|
||||||
|
scrollThresholdEnd = 10,
|
||||||
|
direction = 'vertical',
|
||||||
|
distanceFromStartPx: _distanceFromStartPx,
|
||||||
|
distanceFromEndPx: _distanceFromEndPx,
|
||||||
|
init,
|
||||||
|
} = {},
|
||||||
|
callback,
|
||||||
|
deps,
|
||||||
|
) {
|
||||||
|
if (!callback) return;
|
||||||
|
const [scrollDirection, setScrollDirection] = useState(null);
|
||||||
|
const [reachStart, setReachStart] = useState(false);
|
||||||
|
const [reachEnd, setReachEnd] = useState(false);
|
||||||
|
const [nearReachStart, setNearReachStart] = useState(false);
|
||||||
|
const [nearReachEnd, setNearReachEnd] = useState(false);
|
||||||
|
const isVertical = direction === 'vertical';
|
||||||
|
const previousScrollStart = useRef(null);
|
||||||
|
|
||||||
|
const onScroll = useThrottledCallback(() => {
|
||||||
|
const scrollableElement = scrollableRef.current;
|
||||||
|
const {
|
||||||
|
scrollTop,
|
||||||
|
scrollLeft,
|
||||||
|
scrollHeight,
|
||||||
|
scrollWidth,
|
||||||
|
clientHeight,
|
||||||
|
clientWidth,
|
||||||
|
} = scrollableElement;
|
||||||
|
const scrollStart = isVertical ? scrollTop : scrollLeft;
|
||||||
|
const scrollDimension = isVertical ? scrollHeight : scrollWidth;
|
||||||
|
const clientDimension = isVertical ? clientHeight : clientWidth;
|
||||||
|
const scrollDistance = Math.abs(scrollStart - previousScrollStart.current);
|
||||||
|
const distanceFromStartPx =
|
||||||
|
_distanceFromStartPx ||
|
||||||
|
Math.min(
|
||||||
|
clientDimension * distanceFromStart,
|
||||||
|
scrollDimension,
|
||||||
|
scrollStart,
|
||||||
|
);
|
||||||
|
const distanceFromEndPx =
|
||||||
|
_distanceFromEndPx ||
|
||||||
|
Math.min(
|
||||||
|
clientDimension * distanceFromEnd,
|
||||||
|
scrollDimension,
|
||||||
|
scrollDimension - scrollStart - clientDimension,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
scrollDistance >=
|
||||||
|
(previousScrollStart.current < scrollStart
|
||||||
|
? scrollThresholdEnd
|
||||||
|
: scrollThresholdStart)
|
||||||
|
) {
|
||||||
|
setScrollDirection(
|
||||||
|
previousScrollStart.current < scrollStart ? 'end' : 'start',
|
||||||
|
);
|
||||||
|
previousScrollStart.current = scrollStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReachStart(scrollStart <= 0);
|
||||||
|
setReachEnd(scrollStart + clientDimension >= scrollDimension);
|
||||||
|
setNearReachStart(scrollStart <= distanceFromStartPx);
|
||||||
|
setNearReachEnd(
|
||||||
|
scrollStart + clientDimension >= scrollDimension - distanceFromEndPx,
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const scrollableElement = scrollableRef.current;
|
||||||
|
if (!scrollableElement) return {};
|
||||||
|
previousScrollStart.current =
|
||||||
|
scrollableElement[isVertical ? 'scrollTop' : 'scrollLeft'];
|
||||||
|
|
||||||
|
scrollableElement.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => scrollableElement.removeEventListener('scroll', onScroll);
|
||||||
|
}, [
|
||||||
|
distanceFromStart,
|
||||||
|
distanceFromEnd,
|
||||||
|
scrollThresholdStart,
|
||||||
|
scrollThresholdEnd,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callback({
|
||||||
|
scrollDirection,
|
||||||
|
reachStart,
|
||||||
|
reachEnd,
|
||||||
|
nearReachStart,
|
||||||
|
nearReachEnd,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
scrollDirection,
|
||||||
|
reachStart,
|
||||||
|
reachEnd,
|
||||||
|
nearReachStart,
|
||||||
|
nearReachEnd,
|
||||||
|
...deps,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (init && scrollableRef.current) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
scrollableRef.current.dispatchEvent(new Event('scroll'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [init]);
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// scrollDirection,
|
||||||
|
// reachStart,
|
||||||
|
// reachEnd,
|
||||||
|
// nearReachStart,
|
||||||
|
// nearReachEnd,
|
||||||
|
// init: () => {
|
||||||
|
// if (scrollableRef.current) {
|
||||||
|
// scrollableRef.current.dispatchEvent(new Event('scroll'));
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
}
|
|
@ -9,11 +9,12 @@ import htmlPlugin from 'vite-plugin-html-config';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
import removeConsole from 'vite-plugin-remove-console';
|
import removeConsole from 'vite-plugin-remove-console';
|
||||||
|
|
||||||
|
const allowedEnvPrefixes = ['VITE_', 'PHANPY_'];
|
||||||
const { NODE_ENV } = process.env;
|
const { NODE_ENV } = process.env;
|
||||||
const {
|
const {
|
||||||
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
PHANPY_CLIENT_NAME: CLIENT_NAME,
|
||||||
PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
|
PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
|
||||||
} = loadEnv('production', process.cwd());
|
} = loadEnv('production', process.cwd(), allowedEnvPrefixes);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let commitHash;
|
let commitHash;
|
||||||
|
@ -35,7 +36,7 @@ const rollbarCode = fs.readFileSync(
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: './',
|
||||||
envPrefix: ['VITE_', 'PHANPY_'],
|
envPrefix: allowedEnvPrefixes,
|
||||||
mode: NODE_ENV,
|
mode: NODE_ENV,
|
||||||
define: {
|
define: {
|
||||||
__BUILD_TIME__: JSON.stringify(now),
|
__BUILD_TIME__: JSON.stringify(now),
|
||||||
|
|
Loading…
Reference in a new issue