Compare commits
30 commits
c6824b9b0d
...
c23e51d277
Author | SHA1 | Date | |
---|---|---|---|
|
c23e51d277 | ||
|
49cdba2652 | ||
|
2f94cb34f6 | ||
|
b7a79c8fdd | ||
|
2f0d04eca4 | ||
|
daabd85273 | ||
|
c84ad73d0d | ||
|
3295b1ab96 | ||
|
5d7b67a410 | ||
|
4e85f92f4b | ||
|
9d80647b11 | ||
|
24a481b782 | ||
|
97cce8a828 | ||
|
3c31c56306 | ||
|
92f4371041 | ||
|
a9d0100087 | ||
|
3fbe11295f | ||
|
98f018913d | ||
|
60ca577f9b | ||
|
1d0d02f39b | ||
|
fbd448c152 | ||
|
038b2b2e6b | ||
|
169aa2d3d3 | ||
|
9a9667d824 | ||
|
afd9d2cf97 | ||
|
b9c287b29e | ||
|
436277c6b4 | ||
|
4f28d3cc6d | ||
|
46415b87a6 | ||
|
913d923877 |
|
@ -104,7 +104,7 @@ Prerequisites: Node.js 18+
|
|||
- `npm run build` - Build for production
|
||||
- `npm run preview` - Preview the production build
|
||||
- `npm run fetch-instances` - Fetch instances list from [instances.social](https://instances.social/), save it to `src/data/instances.json`
|
||||
- requires `.env.dev` file with `INSTANCES_SOCIAL_SECRET_TOKEN` variable set
|
||||
- requires `.env.local` file with `INSTANCES_SOCIAL_SECRET_TOKEN` variable set
|
||||
- `npm run sourcemap` - Run `source-map-explorer` on the production build
|
||||
|
||||
## Tech stack
|
||||
|
@ -193,7 +193,7 @@ See documentation for [lingva-translate](https://github.com/thedaviddelta/lingva
|
|||
|
||||
These are self-hosted by other wonderful folks.
|
||||
|
||||
- [ferengi.one](https://ferengi.one/) by [@david@collantes.social](https://collantes.social/@david)
|
||||
- [ferengi.one](https://m.ferengi.one/) by [@david@collantes.social](https://collantes.social/@david)
|
||||
- [phanpy.blaede.family](https://phanpy.blaede.family/) by [@cassidy@blaede.family](https://mastodon.blaede.family/@cassidy)
|
||||
- [phanpy.mstdn.mx](https://phanpy.mstdn.mx/) by [@maop@mstdn.mx](https://mstdn.mx/@maop)
|
||||
- [phanpy.vmst.io](https://phanpy.vmst.io/) by [@vmstan@vmst.io](https://vmst.io/@vmstan)
|
||||
|
|
75
package-lock.json
generated
75
package-lock.json
generated
|
@ -27,12 +27,12 @@
|
|||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
"preact": "~10.19.3",
|
||||
"react-hotkeys-hook": "~4.4.4",
|
||||
"react-intersection-observer": "~9.5.3",
|
||||
"preact": "~10.19.5",
|
||||
"react-hotkeys-hook": "~4.5.0",
|
||||
"react-intersection-observer": "~9.8.0",
|
||||
"react-quick-pinch-zoom": "~5.1.0",
|
||||
"react-router-dom": "6.6.2",
|
||||
"runes2": "~1.1.3",
|
||||
"runes2": "~1.1.4",
|
||||
"string-length": "5.0.1",
|
||||
"swiped-events": "~1.1.9",
|
||||
"toastify-js": "~1.12.0",
|
||||
|
@ -45,14 +45,14 @@
|
|||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.8.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.33",
|
||||
"postcss-dark-theme-class": "~1.1.0",
|
||||
"postcss": "~8.4.35",
|
||||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.3.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.0.12",
|
||||
"vite": "~5.1.3",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.17.5",
|
||||
"vite-plugin-pwa": "~0.18.2",
|
||||
"vite-plugin-remove-console": "~2.2.0",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
@ -5933,9 +5933,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.33",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
|
||||
"integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
|
||||
"version": "8.4.35",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
|
||||
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6157,9 +6157,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-dark-theme-class": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-dark-theme-class/-/postcss-dark-theme-class-1.1.0.tgz",
|
||||
"integrity": "sha512-3Njz7Ux1YuS+DDb00rKL4JbcyePEwBi2DPzHiK3Rw8HHaXwLhBSAILtyY09FjRPXj8KRyqOxIEVw4+xdO6aFdg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-dark-theme-class/-/postcss-dark-theme-class-1.2.1.tgz",
|
||||
"integrity": "sha512-EzQMGOcYnE0eMBjfgB+xnamlZ8O02Aiojyg/iv84cpRUdLKZW8ankZWxWUhhIid1OF7yl4G3BeYfE+7CGY2tdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -6672,9 +6672,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.19.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz",
|
||||
"integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==",
|
||||
"version": "10.19.5",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz",
|
||||
"integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
|
@ -6796,21 +6796,26 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-hotkeys-hook": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.4.tgz",
|
||||
"integrity": "sha512-wzZmqb/Obr0ds9Myc1sIFPJ52GA/Eeg/vXBWV0HA1LvHlVAW5Va3KB0q6EZNlNSHQWscWZ2K8+6w0GYSie2o7A==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz",
|
||||
"integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.1",
|
||||
"react-dom": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-intersection-observer": {
|
||||
"version": "9.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.3.tgz",
|
||||
"integrity": "sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ==",
|
||||
"license": "MIT",
|
||||
"version": "9.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.8.0.tgz",
|
||||
"integrity": "sha512-wXHvMQUsTagh3X0Z6jDtGkIXc3VVCd2tjDRYR9kII3GKrZr0XF0xtpfdamo2n8BSF+zzfeeBVOTjxZWpBp9X0g==",
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
|
@ -7081,9 +7086,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/runes2": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.3.tgz",
|
||||
"integrity": "sha512-sJ/0iVFLne4f2S7cMB1OckBtC9lqkzP5a/wPnDIkbrWzgUsJ+JMQv6y7hk76U7zvbua+je5GltfpsZazUhG05w=="
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz",
|
||||
"integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g=="
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
|
@ -7773,13 +7778,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
|
||||
"integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
|
||||
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss": "^8.4.35",
|
||||
"rollup": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -7854,9 +7859,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.17.5.tgz",
|
||||
"integrity": "sha512-UxRNPiJBzh4tqU/vc8G2TxmrUTzT6BqvSzhszLk62uKsf+npXdvLxGDz9C675f4BJi6MbD2tPnJhi5txlMzxbQ==",
|
||||
"version": "0.18.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.18.2.tgz",
|
||||
"integrity": "sha512-LVFHHLcRLkP7y5xwAqMmtWQhSw34V2+vk59c18fumejiQPUBar+Au1AnOcVr96hlEWLHXI6BM31QOHq+Rey4EA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
|
|
18
package.json
18
package.json
|
@ -6,7 +6,7 @@
|
|||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"fetch-instances": "env $(cat .env.dev | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
|
||||
"fetch-instances": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js",
|
||||
"sourcemap": "npx source-map-explorer dist/assets/*.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -29,12 +29,12 @@
|
|||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
"preact": "~10.19.3",
|
||||
"react-hotkeys-hook": "~4.4.4",
|
||||
"react-intersection-observer": "~9.5.3",
|
||||
"preact": "~10.19.5",
|
||||
"react-hotkeys-hook": "~4.5.0",
|
||||
"react-intersection-observer": "~9.8.0",
|
||||
"react-quick-pinch-zoom": "~5.1.0",
|
||||
"react-router-dom": "6.6.2",
|
||||
"runes2": "~1.1.3",
|
||||
"runes2": "~1.1.4",
|
||||
"string-length": "5.0.1",
|
||||
"swiped-events": "~1.1.9",
|
||||
"toastify-js": "~1.12.0",
|
||||
|
@ -47,14 +47,14 @@
|
|||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.8.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.33",
|
||||
"postcss-dark-theme-class": "~1.1.0",
|
||||
"postcss": "~8.4.35",
|
||||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.3.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.0.12",
|
||||
"vite": "~5.1.3",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.17.5",
|
||||
"vite-plugin-pwa": "~0.18.2",
|
||||
"vite-plugin-remove-console": "~2.2.0",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
|
59
src/app.css
59
src/app.css
|
@ -915,7 +915,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
text-decoration-line: none;
|
||||
color: inherit;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease-out;
|
||||
transition: background-color 0.1s ease-out;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
animation: appear 0.2s ease-out;
|
||||
-webkit-touch-callout: none;
|
||||
|
@ -928,9 +928,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
-webkit-touch-callout: none;
|
||||
}
|
||||
}
|
||||
:is(.status-link, .status-focus):is(:focus, .is-active) {
|
||||
background-color: var(--link-bg-hover-color);
|
||||
outline-offset: -2px;
|
||||
@media not (pointer: coarse) {
|
||||
:is(.status-link, .status-focus):is(:focus, .is-active) {
|
||||
background-color: var(--link-bg-hover-color);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.status-link:hover {
|
||||
|
@ -1803,7 +1805,7 @@ body > .szh-menu-container {
|
|||
align-items: center;
|
||||
line-height: 1.1;
|
||||
padding: 8px 16px !important;
|
||||
transition: all 0.1s ease-in-out;
|
||||
/* transition: all 0.1s ease-in-out; */
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -1850,6 +1852,7 @@ body > .szh-menu-container {
|
|||
}
|
||||
.szh-menu__divider {
|
||||
background-color: var(--divider-color);
|
||||
margin-block: 4px;
|
||||
}
|
||||
.szh-menu .szh-menu__item .menu-grow {
|
||||
flex-grow: 1;
|
||||
|
@ -1926,6 +1929,52 @@ body > .szh-menu-container {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.szh-menu {
|
||||
.menu-control-group-horizontal {
|
||||
display: grid;
|
||||
/* auto columns */
|
||||
grid-template-columns: repeat(auto-fit, minmax(44px, 1fr));
|
||||
|
||||
.szh-menu__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px !important;
|
||||
gap: 2px;
|
||||
|
||||
.icon {
|
||||
opacity: 1;
|
||||
|
||||
+ span {
|
||||
font-size: 80%;
|
||||
/* line-height: 1.2; */
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
text-overflow: clip;
|
||||
mask-image: linear-gradient(to left, transparent, black 16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-control-group-horizontal:first-child,
|
||||
li[aria-hidden='true'] + .menu-control-group-horizontal {
|
||||
margin-top: -8px;
|
||||
margin-bottom: -4px;
|
||||
|
||||
.szh-menu__item {
|
||||
padding-block: 12px !important;
|
||||
}
|
||||
|
||||
> [class^='szh-menu']:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
}
|
||||
> [class^='szh-menu']:last-child {
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.szh-menu
|
||||
.szh-menu__item--type-checkbox:not(.szh-menu__item--disabled):not(
|
||||
.szh-menu__item--hover
|
||||
|
|
|
@ -100,4 +100,5 @@ export const ICONS = {
|
|||
building: () => import('@iconify-icons/mingcute/building-5-line'),
|
||||
history: () => import('@iconify-icons/mingcute/history-2-line'),
|
||||
document: () => import('@iconify-icons/mingcute/document-line'),
|
||||
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'),
|
||||
};
|
||||
|
|
|
@ -917,6 +917,7 @@ function RelatedActions({
|
|||
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
||||
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
||||
const [showPrivateNoteModal, setShowPrivateNoteModal] = useState(false);
|
||||
const [lists, setLists] = useState([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -976,6 +977,22 @@ function RelatedActions({
|
|||
<Icon icon="more" size="l" alt="More" />
|
||||
</button>
|
||||
}
|
||||
onMenuChange={(e) => {
|
||||
if (following && e.open) {
|
||||
// Fetch lists that have this account
|
||||
(async () => {
|
||||
try {
|
||||
const lists = await currentMasto.v1.accounts
|
||||
.$select(accountID.current)
|
||||
.lists.list();
|
||||
console.log('fetched account lists', lists);
|
||||
setLists(lists);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentAuthenticated && !isSelf && (
|
||||
<>
|
||||
|
@ -1017,7 +1034,20 @@ function RelatedActions({
|
|||
}}
|
||||
>
|
||||
<Icon icon="list" />
|
||||
<span>Add/remove from Lists</span>
|
||||
{lists.length ? (
|
||||
<>
|
||||
<small class="menu-grow">
|
||||
Add/Remove from Lists
|
||||
<br />
|
||||
<span class="more-insignificant">
|
||||
{lists.map((list) => list.title).join(', ')}
|
||||
</span>
|
||||
</small>
|
||||
<small class="more-insignificant">{lists.length}</small>
|
||||
</>
|
||||
) : (
|
||||
<span>Add/Remove from Lists</span>
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuDivider />
|
||||
|
|
|
@ -273,7 +273,7 @@ function MediaModal({
|
|||
<span>
|
||||
<Menu2
|
||||
overflow="auto"
|
||||
align="center"
|
||||
align="end"
|
||||
position="anchor"
|
||||
gap={4}
|
||||
menuClassName="glass-menu"
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: inline;
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.name-text.show-acct {
|
||||
display: inline-block;
|
||||
|
|
|
@ -357,4 +357,6 @@ function TruncatedLink(props) {
|
|||
return <Link {...props} data-read-more="Read more →" ref={ref} />;
|
||||
}
|
||||
|
||||
export default memo(Notification);
|
||||
export default memo(Notification, (oldProps, newProps) => {
|
||||
return oldProps.notification?.id === newProps.notification?.id;
|
||||
});
|
||||
|
|
|
@ -20,6 +20,16 @@
|
|||
var(--hashtag-faded-color),
|
||||
transparent min(160px, 50%)
|
||||
);
|
||||
|
||||
.timeline-item-container:not(.timeline-item-container-start) & {
|
||||
background: radial-gradient(
|
||||
ellipse at 0 1.2em,
|
||||
var(--hashtag-faded-color),
|
||||
transparent max(160px, 50%)
|
||||
);
|
||||
background-size: 100% 3em;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
.status-reply-to {
|
||||
background: linear-gradient(
|
||||
|
@ -351,6 +361,10 @@
|
|||
background-repeat: no-repeat;
|
||||
background-size: 100% calc(100% - var(--top-padding) / 2);
|
||||
|
||||
&.visibility-direct {
|
||||
background-image: var(--yellow-stripes);
|
||||
}
|
||||
|
||||
> * {
|
||||
opacity: 0.65;
|
||||
transition: opacity 1s ease-out;
|
||||
|
@ -737,7 +751,6 @@
|
|||
|
||||
.timeline-deck .status .content {
|
||||
max-height: 50vh;
|
||||
max-height: 50dvh;
|
||||
overflow: clip;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1826,6 +1839,53 @@ a.card:is(:hover, :focus):visited {
|
|||
animation: bookmarked 1s ease-in-out;
|
||||
}
|
||||
|
||||
/* STATUS MENU */
|
||||
|
||||
.status-menu {
|
||||
.szh-menu__item,
|
||||
.szh-menu__submenu {
|
||||
.icon + span {
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
&.szh-menu__item--hover .icon + span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
&:not(.szh-menu__item--hover) {
|
||||
color: var(--checked-color) !important;
|
||||
}
|
||||
|
||||
.szh-menu__item:not(.szh-menu__item--hover) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.icon + span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&,
|
||||
& .szh-menu__item {
|
||||
box-shadow: inset 0 -2px 0 var(--checked-color),
|
||||
inset 0 -16px 8px -16px var(--checked-color);
|
||||
}
|
||||
&:has(.szh-menu__item) {
|
||||
box-shadow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-reblog.checked {
|
||||
--checked-color: var(--reblog-color);
|
||||
}
|
||||
.menu-favourite.checked {
|
||||
--checked-color: var(--favourite-color);
|
||||
}
|
||||
.menu-bookmark.checked {
|
||||
--checked-color: var(--link-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* ENHANCED CONTENT */
|
||||
|
||||
.status .content pre {
|
||||
|
@ -1853,8 +1913,8 @@ a.card:is(:hover, :focus):visited {
|
|||
.status-actions {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
top: -6px;
|
||||
right: 8px;
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
|
@ -1863,10 +1923,18 @@ a.card:is(:hover, :focus):visited {
|
|||
overflow: clip;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(8px);
|
||||
transform: translate3d(0, 6px, 0);
|
||||
transform-origin: right center;
|
||||
transition: all 0.15s ease-out 0.3s, border-color 0.3s ease-out;
|
||||
|
||||
.timeline.contextual .replies[data-comments-level='4'] & {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
transition: border-color 0.3s ease-out;
|
||||
}
|
||||
|
||||
button.plain {
|
||||
color: var(--text-insignificant-color);
|
||||
backdrop-filter: none;
|
||||
|
@ -1902,17 +1970,22 @@ a.card:is(:hover, :focus):visited {
|
|||
border-color: var(--outline-hover-color);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
.status:hover &:not(:hover),
|
||||
.status:focus &,
|
||||
&.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
@media (pointer: coarse) {
|
||||
.status:has(&):hover {
|
||||
transition: background-color 0.1s ease-out 0.3s;
|
||||
background-color: var(--bg-faded-blur-color);
|
||||
& {
|
||||
border-color: var(--outline-hover-color);
|
||||
}
|
||||
}
|
||||
@media (pointer: fine), (hover: hover) {
|
||||
.status:hover & {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1928,6 +2001,23 @@ a.card:is(:hover, :focus):visited {
|
|||
}
|
||||
}
|
||||
}
|
||||
.timeline.contextual .descendant .status {
|
||||
--bg-gradient: linear-gradient(
|
||||
-140deg,
|
||||
var(--bg-faded-color),
|
||||
transparent 75%
|
||||
);
|
||||
|
||||
&:focus {
|
||||
background-image: var(--bg-gradient);
|
||||
}
|
||||
|
||||
@media (pointer: fine), (hover: hover) {
|
||||
&:hover {
|
||||
background-image: var(--bg-gradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* BADGE */
|
||||
|
||||
|
|
|
@ -106,6 +106,43 @@ function getPostText(status) {
|
|||
);
|
||||
}
|
||||
|
||||
const PostContent = memo(
|
||||
({ post, instance, previewMode }) => {
|
||||
const { content, emojis, language, mentions, url } = post;
|
||||
return (
|
||||
<div
|
||||
lang={language}
|
||||
dir="auto"
|
||||
class="inner-content"
|
||||
onClick={handleContentLinks({
|
||||
mentions,
|
||||
instance,
|
||||
previewMode,
|
||||
statusURL: url,
|
||||
})}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: enhanceContent(content, {
|
||||
emojis,
|
||||
postEnhanceDOM: (dom) => {
|
||||
// Remove target="_blank" from links
|
||||
dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => {
|
||||
if (!/http/i.test(a.innerText.trim())) {
|
||||
a.removeAttribute('target');
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
(oldProps, newProps) => {
|
||||
const { post: oldPost } = oldProps;
|
||||
const { post: newPost } = newProps;
|
||||
return oldPost.content === newPost.content;
|
||||
},
|
||||
);
|
||||
|
||||
function Status({
|
||||
statusID,
|
||||
status,
|
||||
|
@ -645,82 +682,34 @@ function Status({
|
|||
const actionsRef = useRef();
|
||||
const StatusMenuItems = (
|
||||
<>
|
||||
{!isSizeLarge && (
|
||||
{isSizeLarge && (
|
||||
<>
|
||||
<MenuHeader>
|
||||
<span class="ib">
|
||||
<Icon icon={visibilityIconsMap[visibility]} size="s" />{' '}
|
||||
<span>{visibilityText[visibility]}</span>
|
||||
</span>{' '}
|
||||
<span class="ib">
|
||||
{repliesCount > 0 && (
|
||||
<span>
|
||||
<Icon icon="comment2" alt="Replies" size="s" />{' '}
|
||||
<span>{shortenNumber(repliesCount)}</span>
|
||||
</span>
|
||||
)}{' '}
|
||||
{reblogsCount > 0 && (
|
||||
<span>
|
||||
<Icon icon="rocket" alt="Boosts" size="s" />{' '}
|
||||
<span>{shortenNumber(reblogsCount)}</span>
|
||||
</span>
|
||||
)}{' '}
|
||||
{favouritesCount > 0 && (
|
||||
<span>
|
||||
<Icon icon="heart" alt="Likes" size="s" />{' '}
|
||||
<span>{shortenNumber(favouritesCount)}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<br />
|
||||
{createdDateText}
|
||||
</MenuHeader>
|
||||
<MenuLink
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
onStatusLinkClick(e, status);
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showGenericAccounts = {
|
||||
heading: 'Boosted/Liked by…',
|
||||
fetchAccounts: fetchBoostedLikedByAccounts,
|
||||
instance,
|
||||
showReactions: true,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-right" />
|
||||
<span>View post by @{username || acct}</span>
|
||||
</MenuLink>
|
||||
<Icon icon="react" />
|
||||
<span>
|
||||
Boosted/Liked by<span class="more-insignificant">…</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{!!editedAt && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowEdited(id);
|
||||
}}
|
||||
>
|
||||
<Icon icon="history" />
|
||||
<span>
|
||||
Show Edit History
|
||||
<br />
|
||||
<small class="more-insignificant">Edited: {editedDateText}</small>
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{(!isSizeLarge || !!editedAt) && <MenuDivider />}
|
||||
{isSizeLarge && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showGenericAccounts = {
|
||||
heading: 'Boosted/Liked by…',
|
||||
fetchAccounts: fetchBoostedLikedByAccounts,
|
||||
instance,
|
||||
showReactions: true,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="react" />
|
||||
<span>
|
||||
Boosted/Liked by<span class="more-insignificant">…</span>
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isSizeLarge && sameInstance && (
|
||||
<>
|
||||
<div class="menu-horizontal">
|
||||
<div class="menu-control-group-horizontal status-menu">
|
||||
<MenuItem onClick={replyStatus}>
|
||||
<Icon icon="comment" />
|
||||
<span>
|
||||
{repliesCount > 0 ? shortenNumber(repliesCount) : 'Reply'}
|
||||
</span>
|
||||
</MenuItem>
|
||||
<MenuConfirm
|
||||
subMenu
|
||||
confirmLabel={
|
||||
|
@ -729,6 +718,7 @@ function Status({
|
|||
<span>{reblogged ? 'Unboost?' : 'Boost to everyone?'}</span>
|
||||
</>
|
||||
}
|
||||
className={`menu-reblog ${reblogged ? 'checked' : ''}`}
|
||||
menuFooter={
|
||||
mediaNoDesc &&
|
||||
!reblogged && (
|
||||
|
@ -752,13 +742,14 @@ function Status({
|
|||
} catch (e) {}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="rocket"
|
||||
style={{
|
||||
color: reblogged && 'var(--reblog-color)',
|
||||
}}
|
||||
/>
|
||||
<span>{reblogged ? 'Unboost' : 'Boost…'}</span>
|
||||
<Icon icon="rocket" />
|
||||
<span>
|
||||
{reblogsCount > 0
|
||||
? shortenNumber(reblogsCount)
|
||||
: reblogged
|
||||
? 'Unboost'
|
||||
: 'Boost…'}
|
||||
</span>
|
||||
</MenuConfirm>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
|
@ -773,20 +764,16 @@ function Status({
|
|||
}
|
||||
} catch (e) {}
|
||||
}}
|
||||
className={`menu-favourite ${favourited ? 'checked' : ''}`}
|
||||
>
|
||||
<Icon
|
||||
icon="heart"
|
||||
style={{
|
||||
color: favourited && 'var(--favourite-color)',
|
||||
}}
|
||||
/>
|
||||
<span>{favourited ? 'Unlike' : 'Like'}</span>
|
||||
</MenuItem>
|
||||
</div>
|
||||
<div class="menu-horizontal">
|
||||
<MenuItem onClick={replyStatus}>
|
||||
<Icon icon="comment" />
|
||||
<span>Reply</span>
|
||||
<Icon icon="heart" />
|
||||
<span>
|
||||
{favouritesCount > 0
|
||||
? shortenNumber(favouritesCount)
|
||||
: favourited
|
||||
? 'Unlike'
|
||||
: 'Like'}
|
||||
</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
|
@ -801,18 +788,15 @@ function Status({
|
|||
}
|
||||
} catch (e) {}
|
||||
}}
|
||||
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
||||
>
|
||||
<Icon
|
||||
icon="bookmark"
|
||||
style={{
|
||||
color: bookmarked && 'var(--link-color)',
|
||||
}}
|
||||
/>
|
||||
<Icon icon="bookmark" />
|
||||
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(enableTranslate || !language || differentLanguage) && <MenuDivider />}
|
||||
{enableTranslate ? (
|
||||
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||
<MenuItem
|
||||
|
@ -863,7 +847,46 @@ function Status({
|
|||
</div>
|
||||
)
|
||||
)}
|
||||
{((!isSizeLarge && sameInstance) || enableTranslate) && <MenuDivider />}
|
||||
{!isSizeLarge ||
|
||||
((enableTranslate || !language || differentLanguage) && (
|
||||
<MenuDivider />
|
||||
))}
|
||||
{!isSizeLarge && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuLink
|
||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||
onClick={(e) => {
|
||||
onStatusLinkClick(e, status);
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrows-right" />
|
||||
<small>
|
||||
View post by @{username || acct}
|
||||
<br />
|
||||
<span class="more-insignificant">
|
||||
{visibilityText[visibility]} • {createdDateText}
|
||||
</span>
|
||||
</small>
|
||||
</MenuLink>
|
||||
</>
|
||||
)}
|
||||
{!!editedAt && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowEdited(id);
|
||||
}}
|
||||
>
|
||||
<Icon icon="history" />
|
||||
<small>
|
||||
Show Edit History
|
||||
<br />
|
||||
<span class="more-insignificant">Edited: {editedDateText}</span>
|
||||
</small>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuItem href={url} target="_blank">
|
||||
<Icon icon="external" />
|
||||
<small class="menu-double-lines">{nicePostURL(url)}</small>
|
||||
|
@ -1419,9 +1442,10 @@ function Status({
|
|||
anchorRef: {
|
||||
current: e.currentTarget,
|
||||
},
|
||||
align: 'end',
|
||||
direction: 'bottom',
|
||||
gap: 4,
|
||||
align: 'start',
|
||||
direction: 'left',
|
||||
gap: 0,
|
||||
shift: -8,
|
||||
});
|
||||
setIsContextMenuOpen('actions-bar');
|
||||
}}
|
||||
|
@ -1680,59 +1704,10 @@ function Status({
|
|||
ref={contentRef}
|
||||
data-read-more={readMoreText}
|
||||
>
|
||||
<div
|
||||
lang={language}
|
||||
dir="auto"
|
||||
class="inner-content"
|
||||
onClick={handleContentLinks({
|
||||
mentions,
|
||||
instance,
|
||||
previewMode,
|
||||
statusURL: url,
|
||||
})}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: enhanceContent(content, {
|
||||
emojis,
|
||||
postEnhanceDOM: (dom) => {
|
||||
// Remove target="_blank" from links
|
||||
dom
|
||||
.querySelectorAll('a.u-url[target="_blank"]')
|
||||
.forEach((a) => {
|
||||
if (!/http/i.test(a.innerText.trim())) {
|
||||
a.removeAttribute('target');
|
||||
}
|
||||
});
|
||||
// if (previewMode) return;
|
||||
// Unfurl Mastodon links
|
||||
// Array.from(
|
||||
// dom.querySelectorAll(
|
||||
// 'a[href]:not(.u-url):not(.mention):not(.hashtag)',
|
||||
// ),
|
||||
// )
|
||||
// .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;
|
||||
// a.removeAttribute('target');
|
||||
// if (!sKey) return;
|
||||
// if (!Array.isArray(states.statusQuotes[sKey])) {
|
||||
// states.statusQuotes[sKey] = [];
|
||||
// }
|
||||
// if (!states.statusQuotes[sKey][i]) {
|
||||
// states.statusQuotes[sKey].splice(i, 0, result);
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// });
|
||||
},
|
||||
}),
|
||||
}}
|
||||
<PostContent
|
||||
post={status}
|
||||
instance={instance}
|
||||
previewMode={previewMode}
|
||||
/>
|
||||
<QuoteStatuses id={id} instance={instance} level={quoted} />
|
||||
</div>
|
||||
|
@ -1870,10 +1845,11 @@ function Status({
|
|||
<span class="status-deleted-tag">Deleted</span>
|
||||
) : (
|
||||
<>
|
||||
<Icon
|
||||
{/* <Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={visibilityText[visibility]}
|
||||
/>{' '}
|
||||
/> */}
|
||||
<span>{visibilityText[visibility]}</span> •{' '}
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<time
|
||||
class="created"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { memo } from 'preact/compat';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
@ -243,9 +244,9 @@ function Timeline({
|
|||
({
|
||||
scrollDirection,
|
||||
nearReachStart,
|
||||
nearReachEnd,
|
||||
// nearReachEnd,
|
||||
reachStart,
|
||||
reachEnd,
|
||||
// reachEnd,
|
||||
}) => {
|
||||
// setHiddenUI(scrollDirection === 'end' && !nearReachEnd);
|
||||
if (headerRef.current) {
|
||||
|
@ -516,190 +517,205 @@ function Timeline({
|
|||
);
|
||||
}
|
||||
|
||||
function TimelineItem({
|
||||
status,
|
||||
instance,
|
||||
useItemID,
|
||||
// allowFilters,
|
||||
filterContext,
|
||||
view,
|
||||
showFollowedTags,
|
||||
showReplyParent,
|
||||
}) {
|
||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||
if (_pinned) useItemID = false;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
let title = '';
|
||||
if (type === 'boosts') {
|
||||
title = `${items.length} Boosts`;
|
||||
} else if (type === 'pinned') {
|
||||
title = 'Pinned posts';
|
||||
}
|
||||
const isCarousel = type === 'boosts' || type === 'pinned';
|
||||
if (items) {
|
||||
const fItems = filteredItems(items, filterContext);
|
||||
if (isCarousel) {
|
||||
// Here, we don't hide filtered posts, but we sort them last
|
||||
fItems.sort((a, b) => {
|
||||
// if (a._filtered && !b._filtered) {
|
||||
// return 1;
|
||||
// }
|
||||
// if (!a._filtered && b._filtered) {
|
||||
// return -1;
|
||||
// }
|
||||
const aFiltered = isFiltered(a.filtered, filterContext);
|
||||
const bFiltered = isFiltered(b.filtered, filterContext);
|
||||
if (aFiltered && !bFiltered) {
|
||||
return 1;
|
||||
}
|
||||
if (!aFiltered && bFiltered) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
const TimelineItem = memo(
|
||||
({
|
||||
status,
|
||||
instance,
|
||||
useItemID,
|
||||
// allowFilters,
|
||||
filterContext,
|
||||
view,
|
||||
showFollowedTags,
|
||||
showReplyParent,
|
||||
}) => {
|
||||
console.debug('RENDER TimelineItem', status.id);
|
||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||
if (_pinned) useItemID = false;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
let title = '';
|
||||
if (type === 'boosts') {
|
||||
title = `${items.length} Boosts`;
|
||||
} else if (type === 'pinned') {
|
||||
title = 'Pinned posts';
|
||||
}
|
||||
const isCarousel = type === 'boosts' || type === 'pinned';
|
||||
if (items) {
|
||||
const fItems = filteredItems(items, filterContext);
|
||||
if (isCarousel) {
|
||||
// Here, we don't hide filtered posts, but we sort them last
|
||||
fItems.sort((a, b) => {
|
||||
// if (a._filtered && !b._filtered) {
|
||||
// return 1;
|
||||
// }
|
||||
// if (!a._filtered && b._filtered) {
|
||||
// return -1;
|
||||
// }
|
||||
const aFiltered = isFiltered(a.filtered, filterContext);
|
||||
const bFiltered = isFiltered(b.filtered, filterContext);
|
||||
if (aFiltered && !bFiltered) {
|
||||
return 1;
|
||||
}
|
||||
if (!aFiltered && bFiltered) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return (
|
||||
<li key={`timeline-${statusID}`} class="timeline-item-carousel">
|
||||
<StatusCarousel title={title} class={`${type}-carousel`}>
|
||||
{fItems.map((item) => {
|
||||
const { id: statusID, reblog, _pinned } = item;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
if (_pinned) useItemID = false;
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<Link
|
||||
class="status-carousel-link timeline-item-alt"
|
||||
to={url}
|
||||
>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
enableCommentHint
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
enableCommentHint
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StatusCarousel>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const manyItems = fItems.length > 3;
|
||||
return fItems.map((item, i) => {
|
||||
const { id: statusID, _differentAuthor } = item;
|
||||
const url = instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`;
|
||||
const isMiddle = i > 0 && i < fItems.length - 1;
|
||||
const isSpoiler = item.sensitive && !!item.spoilerText;
|
||||
const showCompact =
|
||||
(!_differentAuthor && isSpoiler && i > 0) ||
|
||||
(manyItems &&
|
||||
isMiddle &&
|
||||
(type === 'thread' ||
|
||||
(type === 'conversation' &&
|
||||
!_differentAuthor &&
|
||||
!fItems[i - 1]._differentAuthor &&
|
||||
!fItems[i + 1]._differentAuthor)));
|
||||
const isStart = i === 0;
|
||||
const isEnd = i === fItems.length - 1;
|
||||
return (
|
||||
<li
|
||||
key={`timeline-${statusID}`}
|
||||
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
||||
isStart ? 'start' : isEnd ? 'end' : 'middle'
|
||||
} ${_differentAuthor ? 'timeline-item-diff-author' : ''}`}
|
||||
>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{showCompact ? (
|
||||
<TimelineStatusCompact status={item} instance={instance} />
|
||||
) : useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
enableCommentHint={isEnd}
|
||||
showFollowedTags={showFollowedTags}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
enableCommentHint={isEnd}
|
||||
showFollowedTags={showFollowedTags}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<li key={`timeline-${statusID}`} class="timeline-item-carousel">
|
||||
<StatusCarousel title={title} class={`${type}-carousel`}>
|
||||
{fItems.map((item) => {
|
||||
const { id: statusID, reblog, _pinned } = item;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
if (_pinned) useItemID = false;
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<Link class="status-carousel-link timeline-item-alt" to={url}>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
enableCommentHint
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
size="s"
|
||||
contentTextWeight
|
||||
enableCommentHint
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StatusCarousel>
|
||||
</li>
|
||||
}
|
||||
|
||||
const itemKey = `timeline-${statusID + _pinned}`;
|
||||
|
||||
if (view === 'media') {
|
||||
return useItemID ? (
|
||||
<MediaPost
|
||||
class="timeline-item"
|
||||
parent="li"
|
||||
key={itemKey}
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<MediaPost
|
||||
class="timeline-item"
|
||||
parent="li"
|
||||
key={itemKey}
|
||||
status={status}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const manyItems = fItems.length > 3;
|
||||
return fItems.map((item, i) => {
|
||||
const { id: statusID, _differentAuthor } = item;
|
||||
const url = instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`;
|
||||
const isMiddle = i > 0 && i < fItems.length - 1;
|
||||
const isSpoiler = item.sensitive && !!item.spoilerText;
|
||||
const showCompact =
|
||||
(!_differentAuthor && isSpoiler && i > 0) ||
|
||||
(manyItems &&
|
||||
isMiddle &&
|
||||
(type === 'thread' ||
|
||||
(type === 'conversation' &&
|
||||
!_differentAuthor &&
|
||||
!fItems[i - 1]._differentAuthor &&
|
||||
!fItems[i + 1]._differentAuthor)));
|
||||
const isStart = i === 0;
|
||||
const isEnd = i === fItems.length - 1;
|
||||
return (
|
||||
<li
|
||||
key={`timeline-${statusID}`}
|
||||
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
||||
isStart ? 'start' : isEnd ? 'end' : 'middle'
|
||||
} ${_differentAuthor ? 'timeline-item-diff-author' : ''}`}
|
||||
>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{showCompact ? (
|
||||
<TimelineStatusCompact status={item} instance={instance} />
|
||||
) : useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
enableCommentHint={isEnd}
|
||||
showFollowedTags={showFollowedTags}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={item}
|
||||
instance={instance}
|
||||
enableCommentHint={isEnd}
|
||||
showFollowedTags={showFollowedTags}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const itemKey = `timeline-${statusID + _pinned}`;
|
||||
|
||||
if (view === 'media') {
|
||||
return useItemID ? (
|
||||
<MediaPost
|
||||
class="timeline-item"
|
||||
parent="li"
|
||||
key={itemKey}
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<MediaPost
|
||||
class="timeline-item"
|
||||
parent="li"
|
||||
key={itemKey}
|
||||
status={status}
|
||||
instance={instance}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
return (
|
||||
<li key={itemKey}>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
enableCommentHint
|
||||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={status}
|
||||
instance={instance}
|
||||
enableCommentHint
|
||||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={itemKey}>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
enableCommentHint
|
||||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
) : (
|
||||
<Status
|
||||
status={status}
|
||||
instance={instance}
|
||||
enableCommentHint
|
||||
showFollowedTags={showFollowedTags}
|
||||
showReplyParent={showReplyParent}
|
||||
// allowFilters={allowFilters}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
},
|
||||
(oldProps, newProps) => {
|
||||
const oldID = (oldProps.status?.id || '').toString();
|
||||
const newID = (newProps.status?.id || '').toString();
|
||||
return (
|
||||
oldID === newID &&
|
||||
oldProps.instance === newProps.instance &&
|
||||
oldProps.view === newProps.view
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function StatusCarousel({ title, class: className, children }) {
|
||||
const carouselRef = useRef();
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,7 +23,7 @@ import states from '../utils/states';
|
|||
import store from '../utils/store';
|
||||
|
||||
const DEFAULT_TEXT_SIZE = 16;
|
||||
const TEXT_SIZES = [15, 16, 17, 18, 19, 20];
|
||||
const TEXT_SIZES = [14, 15, 16, 17, 18, 19, 20];
|
||||
const {
|
||||
PHANPY_WEBSITE: WEBSITE,
|
||||
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
||||
|
|
|
@ -17,15 +17,22 @@ export default function useScrollFn(
|
|||
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 [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 scrollDirection = useRef(null);
|
||||
|
||||
const onScroll = useThrottledCallback(() => {
|
||||
// let scrollDirection = null;
|
||||
let reachStart = false;
|
||||
let reachEnd = false;
|
||||
let nearReachStart = false;
|
||||
let nearReachEnd = false;
|
||||
|
||||
const scrollableElement = scrollableRef.current;
|
||||
const {
|
||||
scrollTop,
|
||||
|
@ -60,18 +67,33 @@ export default function useScrollFn(
|
|||
? scrollThresholdEnd
|
||||
: scrollThresholdStart)
|
||||
) {
|
||||
setScrollDirection(
|
||||
previousScrollStart.current < scrollStart ? 'end' : 'start',
|
||||
);
|
||||
// setScrollDirection(
|
||||
// previousScrollStart.current < scrollStart ? 'end' : 'start',
|
||||
// );
|
||||
scrollDirection.current =
|
||||
previousScrollStart.current < scrollStart ? 'end' : 'start';
|
||||
previousScrollStart.current = scrollStart;
|
||||
}
|
||||
|
||||
setReachStart(scrollStart <= 0);
|
||||
setReachEnd(scrollStart + clientDimension >= scrollDimension);
|
||||
setNearReachStart(scrollStart <= distanceFromStartPx);
|
||||
setNearReachEnd(
|
||||
scrollStart + clientDimension >= scrollDimension - distanceFromEndPx,
|
||||
);
|
||||
// setReachStart(scrollStart <= 0);
|
||||
// setReachEnd(scrollStart + clientDimension >= scrollDimension);
|
||||
// setNearReachStart(scrollStart <= distanceFromStartPx);
|
||||
// setNearReachEnd(
|
||||
// scrollStart + clientDimension >= scrollDimension - distanceFromEndPx,
|
||||
// );
|
||||
reachStart = scrollStart <= 0;
|
||||
reachEnd = scrollStart + clientDimension >= scrollDimension;
|
||||
nearReachStart = scrollStart <= distanceFromStartPx;
|
||||
nearReachEnd =
|
||||
scrollStart + clientDimension >= scrollDimension - distanceFromEndPx;
|
||||
|
||||
callback({
|
||||
scrollDirection: scrollDirection.current,
|
||||
reachStart,
|
||||
reachEnd,
|
||||
nearReachStart,
|
||||
nearReachEnd,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -88,25 +110,26 @@ export default function useScrollFn(
|
|||
distanceFromEnd,
|
||||
scrollThresholdStart,
|
||||
scrollThresholdEnd,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
callback({
|
||||
scrollDirection,
|
||||
reachStart,
|
||||
reachEnd,
|
||||
nearReachStart,
|
||||
nearReachEnd,
|
||||
});
|
||||
}, [
|
||||
scrollDirection,
|
||||
reachStart,
|
||||
reachEnd,
|
||||
nearReachStart,
|
||||
nearReachEnd,
|
||||
...deps,
|
||||
]);
|
||||
|
||||
// useEffect(() => {
|
||||
// callback({
|
||||
// scrollDirection,
|
||||
// reachStart,
|
||||
// reachEnd,
|
||||
// nearReachStart,
|
||||
// nearReachEnd,
|
||||
// });
|
||||
// }, [
|
||||
// scrollDirection,
|
||||
// reachStart,
|
||||
// reachEnd,
|
||||
// nearReachStart,
|
||||
// nearReachEnd,
|
||||
// ...deps,
|
||||
// ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (init && scrollableRef.current) {
|
||||
queueMicrotask(() => {
|
||||
|
|
|
@ -46,6 +46,9 @@ export default defineConfig({
|
|||
server: {
|
||||
host: true,
|
||||
},
|
||||
css: {
|
||||
preprocessorMaxWorkers: 1,
|
||||
},
|
||||
plugins: [
|
||||
preact(),
|
||||
splitVendorChunkPlugin(),
|
||||
|
|
Loading…
Reference in a new issue