commit
83f9498b79
110
package-lock.json
generated
110
package-lock.json
generated
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.5.4",
|
||||
"@formatjs/intl-segmenter": "~11.5.5",
|
||||
"@formkit/auto-animate": "~0.8.1",
|
||||
"@github/text-expander-element": "~2.6.1",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
|
@ -20,10 +21,11 @@
|
|||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"fast-equals": "~5.0.1",
|
||||
"html-prettify": "^1.0.7",
|
||||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.6.0",
|
||||
"masto": "~6.6.4",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
|
@ -32,15 +34,14 @@
|
|||
"react-intersection-observer": "~9.8.1",
|
||||
"react-quick-pinch-zoom": "~5.1.0",
|
||||
"react-router-dom": "6.6.2",
|
||||
"runes2": "~1.1.4",
|
||||
"string-length": "5.0.1",
|
||||
"string-length": "6.0.0",
|
||||
"swiped-events": "~1.1.9",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~10.0.0",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.9.0"
|
||||
"valtio": "1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.8.1",
|
||||
|
@ -49,10 +50,10 @@
|
|||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.4.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.1.4",
|
||||
"vite": "~5.1.5",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.19.0",
|
||||
"vite-plugin-pwa": "~0.19.2",
|
||||
"vite-plugin-remove-console": "~2.2.0",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
@ -2923,6 +2924,15 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz",
|
||||
"integrity": "sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "0.5.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz",
|
||||
|
@ -2931,6 +2941,16 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-segmenter": {
|
||||
"version": "11.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-segmenter/-/intl-segmenter-11.5.5.tgz",
|
||||
"integrity": "sha512-mMbJKFGzwYJBcwfL9EfqFje75Ce5WPar5rSi7wWvFtBPFY2Zi1cWIss7FSm2MNNM9l1BycBAsBQuXFt+Hd+0tQ==",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.18.2",
|
||||
"@formatjs/intl-localematcher": "0.5.4",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formkit/auto-animate": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz",
|
||||
|
@ -3966,15 +3986,6 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/char-regex": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz",
|
||||
"integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
@ -4262,6 +4273,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/derive-valtio": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz",
|
||||
"integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==",
|
||||
"peerDependencies": {
|
||||
"valtio": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
|
@ -4964,6 +4983,11 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/html-prettify": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/html-prettify/-/html-prettify-1.0.7.tgz",
|
||||
"integrity": "sha512-99pRsP2PV2DyWnrVibNyad7gNmzCP7AANO8jw7Z9yanWyXH9dPdqdMXGefySplroqCNdk95u7j5TLxfyJ1Cbbg=="
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
|
||||
|
@ -5622,9 +5646,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/masto": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.6.0.tgz",
|
||||
"integrity": "sha512-spXDwI22M7cWaFPBmpEY+05F+9kAE0nLwzUx5YPECKsLEEQ+c+a92yo/WOnocNs/ILr9/OCKjYPyCykL6TVw+w==",
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/masto/-/masto-6.6.4.tgz",
|
||||
"integrity": "sha512-qoq08UAzdRhVNo9eUG+LIxVNx8UWq+ZqyyeoPGYKLsCOdNyxZpkdK7r3M1negyBscfsNzppdMDpuRhmxDOizxw==",
|
||||
"dependencies": {
|
||||
"change-case": "^4.1.2",
|
||||
"events-to-async": "^2.0.1",
|
||||
|
@ -6740,10 +6764,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.4.0.tgz",
|
||||
"integrity": "sha512-FD8KmQUQD6Mfpd0hywCOzcon/dbkFP8XBd9F1ycbKtvVsfv6TsFUKJ2eC0Iz2y+KzlkdT1Z8SY6ZSgm07zOyqg==",
|
||||
"license": "MIT"
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz",
|
||||
"integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.0",
|
||||
|
@ -7103,11 +7126,6 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/runes2": {
|
||||
"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",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
@ -7249,16 +7267,14 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-length": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz",
|
||||
"integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==",
|
||||
"license": "MIT",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz",
|
||||
"integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==",
|
||||
"dependencies": {
|
||||
"char-regex": "^2.0.0",
|
||||
"strip-ansi": "^7.0.1"
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
|
@ -7774,30 +7790,34 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.9.0.tgz",
|
||||
"integrity": "sha512-mQLFsAlKbYascZygFQh6lXuDjU5WHLoeZ8He4HqMnWfasM96V6rDbeFkw1XeG54xycmDonr/Jb4xgviHtuySrA==",
|
||||
"license": "MIT",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz",
|
||||
"integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==",
|
||||
"dependencies": {
|
||||
"proxy-compare": "2.4.0",
|
||||
"derive-valtio": "0.1.0",
|
||||
"proxy-compare": "2.6.0",
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
|
||||
"integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz",
|
||||
"integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
|
@ -7876,9 +7896,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.19.0.tgz",
|
||||
"integrity": "sha512-Unfb4Jk/ka4HELtpMLIPCmGcW4LFT+CL7Ri1/Of1544CVKXS2ftP91kUkNzkzeI1sGpOdVGuxprVLB9NjMoCAA==",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.19.2.tgz",
|
||||
"integrity": "sha512-LSQJFPxCAQYbRuSyc9EbRLRqLpaBA9onIZuQFomfUYjWSgHuQLonahetDlPSC9zsxmkSEhQH8dXZN8yL978h3w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
|
|
13
package.json
13
package.json
|
@ -11,6 +11,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.5.4",
|
||||
"@formatjs/intl-segmenter": "~11.5.5",
|
||||
"@formkit/auto-animate": "~0.8.1",
|
||||
"@github/text-expander-element": "~2.6.1",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
|
@ -22,10 +23,11 @@
|
|||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"fast-equals": "~5.0.1",
|
||||
"html-prettify": "^1.0.7",
|
||||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.6.0",
|
||||
"masto": "~6.6.4",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
|
@ -34,15 +36,14 @@
|
|||
"react-intersection-observer": "~9.8.1",
|
||||
"react-quick-pinch-zoom": "~5.1.0",
|
||||
"react-router-dom": "6.6.2",
|
||||
"runes2": "~1.1.4",
|
||||
"string-length": "5.0.1",
|
||||
"string-length": "6.0.0",
|
||||
"swiped-events": "~1.1.9",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~10.0.0",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.9.0"
|
||||
"valtio": "1.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "~2.8.1",
|
||||
|
@ -51,10 +52,10 @@
|
|||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.4.0",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.1.4",
|
||||
"vite": "~5.1.5",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.19.0",
|
||||
"vite-plugin-pwa": "~0.19.2",
|
||||
"vite-plugin-remove-console": "~2.2.0",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
|
32
public/404.html
Normal file
32
public/404.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
||||
/>
|
||||
<title>Page not found</title>
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<style>
|
||||
body {
|
||||
text-align: center;
|
||||
font-family: ui-rounded, -apple-system, BlinkMacSystemFont, Segoe UI,
|
||||
Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Page not found</h1>
|
||||
<p><a href="/">Go home</a></p>
|
||||
</body>
|
||||
</html>
|
|
@ -33,8 +33,9 @@ const imageRoute = new Route(
|
|||
const isRemote = !sameOrigin;
|
||||
const isImage = request.destination === 'image';
|
||||
const isAvatar = request.url.includes('/avatars/');
|
||||
const isCustomEmoji = request.url.includes('/custom/_emojis');
|
||||
const isEmoji = request.url.includes('/emoji/');
|
||||
return isRemote && isImage && (isAvatar || isEmoji);
|
||||
return isRemote && isImage && (isAvatar || isCustomEmoji || isEmoji);
|
||||
},
|
||||
new CacheFirst({
|
||||
cacheName: 'remote-images',
|
||||
|
|
12
src/app.css
12
src/app.css
|
@ -103,6 +103,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
max-width: 100%;
|
||||
background-color: var(--bg-color);
|
||||
overflow-anchor: auto;
|
||||
|
||||
&.wide {
|
||||
width: 60em;
|
||||
}
|
||||
}
|
||||
.deck.contained {
|
||||
overflow: auto;
|
||||
|
@ -1752,7 +1756,7 @@ body > .szh-menu-container {
|
|||
max-width: 90vw;
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
.szh-menu[aria-label='Submenu'] {
|
||||
.szh-menu[aria-label='Submenu'].menu-blur {
|
||||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
|
||||
|
@ -1920,6 +1924,10 @@ body > .szh-menu-container {
|
|||
).danger {
|
||||
color: var(--red-color);
|
||||
}
|
||||
.szh-menu
|
||||
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
|
||||
background-color: var(--red-color);
|
||||
}
|
||||
.szh-menu
|
||||
.szh-menu__item:not(.szh-menu__item--disabled):not(
|
||||
.szh-menu__item--hover
|
||||
|
@ -2108,7 +2116,7 @@ meter.donut[hidden] {
|
|||
:root .toastify {
|
||||
user-select: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
border-radius: 44px;
|
||||
pointer-events: none;
|
||||
color: var(--button-text-color);
|
||||
text-shadow: 0 calc(var(--hairline-width) * -1) var(--drop-shadow-color);
|
||||
|
|
19
src/app.jsx
19
src/app.jsx
|
@ -1,6 +1,7 @@
|
|||
import './app.css';
|
||||
|
||||
import debounce from 'just-debounce-it';
|
||||
import { lazy, Suspense } from 'preact/compat';
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
|
@ -17,13 +18,14 @@ import ComposeButton from './components/compose-button';
|
|||
import { ICONS } from './components/ICONS';
|
||||
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
||||
import Loader from './components/loader';
|
||||
import Modals from './components/modals';
|
||||
// import Modals from './components/modals';
|
||||
import NotificationService from './components/notification-service';
|
||||
import SearchCommand from './components/search-command';
|
||||
import Shortcuts from './components/shortcuts';
|
||||
import NotFound from './pages/404';
|
||||
import AccountStatuses from './pages/account-statuses';
|
||||
import Bookmarks from './pages/bookmarks';
|
||||
// import Catchup from './pages/catchup';
|
||||
import Favourites from './pages/favourites';
|
||||
import FollowedHashtags from './pages/followed-hashtags';
|
||||
import Following from './pages/following';
|
||||
|
@ -54,6 +56,9 @@ import store from './utils/store';
|
|||
import { getCurrentAccount } from './utils/store-utils';
|
||||
import './utils/toast-alert';
|
||||
|
||||
const Catchup = lazy(() => import('./pages/catchup'));
|
||||
const Modals = lazy(() => import('./components/modals'));
|
||||
|
||||
window.__STATES__ = states;
|
||||
window.__STATES_STATS__ = () => {
|
||||
const keys = [
|
||||
|
@ -381,7 +386,9 @@ function App() {
|
|||
)}
|
||||
{isLoggedIn && <ComposeButton />}
|
||||
{isLoggedIn && <Shortcuts />}
|
||||
<Suspense>
|
||||
<Modals />
|
||||
</Suspense>
|
||||
{isLoggedIn && <NotificationService />}
|
||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
|
||||
|
@ -394,7 +401,7 @@ function PrimaryRoutes({ isLoggedIn, loading }) {
|
|||
const location = useLocation();
|
||||
const nonRootLocation = useMemo(() => {
|
||||
const { pathname } = location;
|
||||
return !/^\/(login|welcome)/.test(pathname);
|
||||
return !/^\/(login|welcome)/i.test(pathname);
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
|
@ -457,6 +464,14 @@ function SecondaryRoutes({ isLoggedIn }) {
|
|||
<Route path=":id" element={<List />} />
|
||||
</Route>
|
||||
<Route path="/ft" element={<FollowedHashtags />} />
|
||||
<Route
|
||||
path="/catchup"
|
||||
element={
|
||||
<Suspense>
|
||||
<Catchup />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
|
||||
|
|
BIN
src/assets/features/catch-up.png
Normal file
BIN
src/assets/features/catch-up.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -8,11 +8,14 @@ body.cloak,
|
|||
.name-text *,
|
||||
.status .content-container,
|
||||
.status .content-container *,
|
||||
.status .content-compact,
|
||||
.status .content-compact > *,
|
||||
.account-container :is(header, main > *:not(.actions)),
|
||||
.account-container :is(header, main > *:not(.actions)) *,
|
||||
.header-double-lines,
|
||||
.account-block {
|
||||
.account-block,
|
||||
.catchup-filters .filter-author *,
|
||||
.post-peek-html *,
|
||||
.post-peek-content > * {
|
||||
text-decoration-thickness: 1.1em;
|
||||
text-decoration-line: line-through;
|
||||
/* text-rendering: optimizeSpeed; */
|
||||
|
@ -20,15 +23,17 @@ body.cloak,
|
|||
}
|
||||
.name-text *,
|
||||
.status .content-container *,
|
||||
.account-container :is(header, main > *:not(.actions)) * {
|
||||
.account-container :is(header, main > *:not(.actions)) *,
|
||||
.post-peek-content > * {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.status :is(img, video, audio),
|
||||
.media-post .media,
|
||||
.avatar,
|
||||
.avatar *,
|
||||
.emoji,
|
||||
.header-banner {
|
||||
.header-banner,
|
||||
.post-peek-media {
|
||||
filter: contrast(0) !important;
|
||||
background-color: #000 !important;
|
||||
}
|
||||
|
@ -37,7 +42,16 @@ body.cloak,
|
|||
/* SPECIAL CASES */
|
||||
|
||||
@supports (display: -webkit-box) {
|
||||
body.cloak .card :is(.title, .meta) {
|
||||
background-color: var(--text-color) !important;
|
||||
:is(body.cloak, .cloak) .card :is(.title, .meta) {
|
||||
background-color: currentColor !important;
|
||||
}
|
||||
}
|
||||
|
||||
body.cloak,
|
||||
.cloak {
|
||||
.media-container figcaption,
|
||||
.media-container figcaption > *,
|
||||
.catchup-filters .filter-author * {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ export const ICONS = {
|
|||
() => import('@iconify-icons/mingcute/forbid-circle-line'),
|
||||
'180deg',
|
||||
],
|
||||
flag: () => import('@iconify-icons/mingcute/flag-4-line'),
|
||||
flag: () => import('@iconify-icons/mingcute/flag-1-line'),
|
||||
time: () => import('@iconify-icons/mingcute/time-line'),
|
||||
refresh: () => import('@iconify-icons/mingcute/refresh-2-line'),
|
||||
emoji2: () => import('@iconify-icons/mingcute/emoji-2-line'),
|
||||
|
@ -98,7 +98,9 @@ export const ICONS = {
|
|||
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
||||
speak: () => import('@iconify-icons/mingcute/radar-line'),
|
||||
building: () => import('@iconify-icons/mingcute/building-5-line'),
|
||||
history: () => import('@iconify-icons/mingcute/history-2-line'),
|
||||
history2: () => import('@iconify-icons/mingcute/history-2-line'),
|
||||
document: () => import('@iconify-icons/mingcute/document-line'),
|
||||
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'),
|
||||
code: () => import('@iconify-icons/mingcute/code-line'),
|
||||
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
|
||||
};
|
||||
|
|
|
@ -9,11 +9,15 @@
|
|||
--original-color: var(--link-color);
|
||||
|
||||
.note {
|
||||
font-size: 95%;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.4;
|
||||
text-wrap: pretty;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
|
|
|
@ -1053,6 +1053,27 @@ function RelatedActions({
|
|||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
const handle = `@${currentInfo?.acct || acct}`;
|
||||
try {
|
||||
navigator.clipboard.writeText(handle);
|
||||
showToast('Handle copied');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast('Unable to copy handle');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="copy" />
|
||||
<small>
|
||||
Copy handle
|
||||
<br />
|
||||
<span class="more-insignificant">
|
||||
@{currentInfo?.acct || acct}
|
||||
</span>
|
||||
</small>
|
||||
</MenuItem>
|
||||
<MenuItem href={url} target="_blank">
|
||||
<Icon icon="external" />
|
||||
<small class="menu-double-lines">{niceAccountURL(url)}</small>
|
||||
|
@ -1124,6 +1145,7 @@ function RelatedActions({
|
|||
</MenuItem>
|
||||
) : (
|
||||
<SubMenu
|
||||
menuClassName="menu-blur"
|
||||
openTrigger="clickOnly"
|
||||
direction="bottom"
|
||||
overflow="auto"
|
||||
|
@ -1238,10 +1260,38 @@ function RelatedActions({
|
|||
</>
|
||||
)}
|
||||
</MenuConfirm>
|
||||
{/* <MenuItem>
|
||||
<MenuItem
|
||||
className="danger"
|
||||
onClick={() => {
|
||||
states.showReportModal = {
|
||||
account: currentInfo || info,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="flag" />
|
||||
<span>Report @{username}…</span>
|
||||
</MenuItem> */}
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{import.meta.env.DEV && currentAuthenticated && isSelf && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
const relationships =
|
||||
await currentMasto.v1.accounts.relationships.fetch({
|
||||
id: [accountID.current],
|
||||
});
|
||||
const { note } = relationships[0] || {};
|
||||
if (note) {
|
||||
alert(note);
|
||||
console.log(note);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="pencil" />
|
||||
<span>See note</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu2>
|
||||
|
@ -1324,7 +1374,6 @@ function RelatedActions({
|
|||
</div>
|
||||
{!!showTranslatedBio && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={() => {
|
||||
setShowTranslatedBio(false);
|
||||
}}
|
||||
|
@ -1338,7 +1387,6 @@ function RelatedActions({
|
|||
)}
|
||||
{!!showAddRemoveLists && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={() => {
|
||||
setShowAddRemoveLists(false);
|
||||
}}
|
||||
|
@ -1351,7 +1399,6 @@ function RelatedActions({
|
|||
)}
|
||||
{!!showPrivateNoteModal && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={() => {
|
||||
setShowPrivateNoteModal(false);
|
||||
}}
|
||||
|
@ -1543,7 +1590,6 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
|||
</main>
|
||||
{showListAddEditModal && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowListAddEditModal(false);
|
||||
|
|
|
@ -6,7 +6,6 @@ import { deepEqual } from 'fast-equals';
|
|||
import { forwardRef } from 'preact/compat';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { substring } from 'runes2';
|
||||
import stringLength from 'string-length';
|
||||
import { uid } from 'uid/single';
|
||||
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
||||
|
@ -131,6 +130,7 @@ const SCAN_RE = new RegExp(
|
|||
'g',
|
||||
);
|
||||
|
||||
const segmenter = new Intl.Segmenter();
|
||||
function highlightText(text, { maxCharacters = Infinity }) {
|
||||
// Accept text string, return formatted HTML string
|
||||
// Escape all HTML special characters
|
||||
|
@ -143,19 +143,25 @@ function highlightText(text, { maxCharacters = Infinity }) {
|
|||
|
||||
// Exceeded characters limit
|
||||
const { composerCharacterCount } = states;
|
||||
let leftoverHTML = '';
|
||||
if (composerCharacterCount > maxCharacters) {
|
||||
// NOTE: runes2 substring considers surrogate pairs
|
||||
// const leftoverCount = composerCharacterCount - maxCharacters;
|
||||
// Highlight exceeded characters
|
||||
leftoverHTML =
|
||||
let withinLimitHTML = '',
|
||||
exceedLimitHTML = '';
|
||||
const htmlSegments = segmenter.segment(html);
|
||||
for (const { segment, index } of htmlSegments) {
|
||||
if (index < maxCharacters) {
|
||||
withinLimitHTML += segment;
|
||||
} else {
|
||||
exceedLimitHTML += segment;
|
||||
}
|
||||
}
|
||||
if (exceedLimitHTML) {
|
||||
exceedLimitHTML =
|
||||
'<mark class="compose-highlight-exceeded">' +
|
||||
// html.slice(-leftoverCount) +
|
||||
substring(html, maxCharacters) +
|
||||
exceedLimitHTML +
|
||||
'</mark>';
|
||||
// html = html.slice(0, -leftoverCount);
|
||||
html = substring(html, 0, maxCharacters);
|
||||
return html + leftoverHTML;
|
||||
}
|
||||
return withinLimitHTML + exceedLimitHTML;
|
||||
}
|
||||
|
||||
return html
|
||||
|
@ -1254,7 +1260,6 @@ function Compose({
|
|||
</div>
|
||||
{showEmoji2Picker && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowEmoji2Picker(false);
|
||||
|
@ -1558,7 +1563,7 @@ const Textarea = forwardRef((props, ref) => {
|
|||
onKeyDown={(e) => {
|
||||
// Get line before cursor position after pressing 'Enter'
|
||||
const { key, target } = e;
|
||||
if (key === 'Enter') {
|
||||
if (key === 'Enter' && !(e.ctrlKey || e.metaKey)) {
|
||||
try {
|
||||
const { value, selectionStart } = target;
|
||||
const textBeforeCursor = value.slice(0, selectionStart);
|
||||
|
@ -1768,7 +1773,6 @@ function MediaAttachment({
|
|||
</div>
|
||||
{showModal && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowModal(false);
|
||||
|
|
|
@ -32,7 +32,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
|||
|
||||
return (
|
||||
!!snapStates.showKeyboardShortcutsHelp && (
|
||||
<Modal class="light" onClose={onClose}>
|
||||
<Modal onClose={onClose}>
|
||||
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
|
||||
<button type="button" class="sheet-close" onClick={onClose}>
|
||||
<Icon icon="x" />
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
text-shadow: 0 1px var(--bg-blur-color);
|
||||
transition: opacity 0.3s ease-out;
|
||||
|
||||
&:not(#columns &) {
|
||||
#trending-page &:not(#columns &) {
|
||||
@media (min-width: 40em) {
|
||||
width: 95vw;
|
||||
max-width: calc(320px * 3.3);
|
||||
|
@ -96,6 +96,7 @@
|
|||
}
|
||||
|
||||
article {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
@ -113,7 +114,6 @@
|
|||
margin: 0 0 -16px;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
|
@ -143,6 +143,7 @@
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:is(:hover, :focus-visible) article {
|
||||
background-position-y: -40px;
|
||||
|
@ -187,5 +188,9 @@
|
|||
overflow: hidden;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ const AltBadge = (props) => {
|
|||
};
|
||||
|
||||
const MEDIA_CAPTION_LIMIT = 140;
|
||||
const MEDIA_CAPTION_LIMIT_LONGER = 280;
|
||||
export const isMediaCaptionLong = mem((caption) =>
|
||||
caption?.length
|
||||
? caption.length > MEDIA_CAPTION_LIMIT ||
|
||||
|
@ -69,6 +70,7 @@ function Media({
|
|||
showOriginal,
|
||||
autoAnimate,
|
||||
showCaption,
|
||||
allowLongerCaption,
|
||||
altIndex,
|
||||
onClick = () => {},
|
||||
}) {
|
||||
|
@ -198,8 +200,15 @@ function Media({
|
|||
};
|
||||
|
||||
const longDesc = isMediaCaptionLong(description);
|
||||
const showInlineDesc =
|
||||
let showInlineDesc =
|
||||
!!showCaption && !showOriginal && !!description && !longDesc;
|
||||
if (
|
||||
allowLongerCaption &&
|
||||
!showInlineDesc &&
|
||||
description?.length <= MEDIA_CAPTION_LIMIT_LONGER
|
||||
) {
|
||||
showInlineDesc = true;
|
||||
}
|
||||
const Figure = !showInlineDesc
|
||||
? Fragment
|
||||
: (props) => {
|
||||
|
|
|
@ -3,11 +3,12 @@ import { FocusableItem } from '@szhsin/react-menu';
|
|||
import Link from './link';
|
||||
|
||||
function MenuLink(props) {
|
||||
const { className, disabled, ...restProps } = props;
|
||||
return (
|
||||
<FocusableItem>
|
||||
<FocusableItem className={className} disabled={disabled}>
|
||||
{({ ref, closeMenu }) => (
|
||||
<Link
|
||||
{...props}
|
||||
{...restProps}
|
||||
ref={ref}
|
||||
onClick={({ detail }) =>
|
||||
closeMenu(detail === 0 ? 'Enter' : undefined)
|
||||
|
|
|
@ -9,17 +9,18 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--backdrop-color);
|
||||
backdrop-filter: blur(24px);
|
||||
animation: appear 0.5s var(--timing-function) both;
|
||||
|
||||
&.solid {
|
||||
background-color: var(--backdrop-solid-color);
|
||||
}
|
||||
#modal-container > div .sheet {
|
||||
|
||||
.sheet {
|
||||
transition: transform 0.3s var(--timing-function);
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
#modal-container > div:has(~ div) .sheet {
|
||||
|
||||
&:has(~ div) .sheet {
|
||||
transform: scale(0.975);
|
||||
}
|
||||
|
||||
#modal-container > .light {
|
||||
backdrop-filter: saturate(0.75);
|
||||
}
|
||||
|
|
|
@ -56,8 +56,19 @@ function Modal({ children, onClose, onClick, class: className }) {
|
|||
}}
|
||||
tabIndex="-1"
|
||||
onFocus={(e) => {
|
||||
try {
|
||||
if (e.target === e.currentTarget) {
|
||||
modalRef.current?.querySelector?.('[tabindex="-1"]')?.focus?.();
|
||||
const focusElement =
|
||||
modalRef.current?.querySelector('[tabindex="-1"]');
|
||||
const isFocusable =
|
||||
!!focusElement &&
|
||||
getComputedStyle(focusElement)?.pointerEvents !== 'none';
|
||||
if (focusElement && isFocusable) {
|
||||
focusElement.focus();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -15,6 +15,7 @@ import GenericAccounts from './generic-accounts';
|
|||
import MediaAltModal from './media-alt-modal';
|
||||
import MediaModal from './media-modal';
|
||||
import Modal from './modal';
|
||||
import ReportModal from './report-modal';
|
||||
import ShortcutsSettings from './shortcuts-settings';
|
||||
|
||||
subscribe(states, (changes) => {
|
||||
|
@ -34,7 +35,7 @@ export default function Modals() {
|
|||
return (
|
||||
<>
|
||||
{!!snapStates.showCompose && (
|
||||
<Modal>
|
||||
<Modal class="solid">
|
||||
<Compose
|
||||
replyToStatus={
|
||||
typeof snapStates.showCompose !== 'boolean'
|
||||
|
@ -108,7 +109,6 @@ export default function Modals() {
|
|||
)}
|
||||
{!!snapStates.showAccount && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={() => {
|
||||
states.showAccount = false;
|
||||
}}
|
||||
|
@ -159,7 +159,6 @@ export default function Modals() {
|
|||
)}
|
||||
{!!snapStates.showShortcutsSettings && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={() => {
|
||||
states.showShortcutsSettings = false;
|
||||
}}
|
||||
|
@ -171,7 +170,6 @@ export default function Modals() {
|
|||
)}
|
||||
{!!snapStates.showGenericAccounts && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={() => {
|
||||
states.showGenericAccounts = false;
|
||||
}}
|
||||
|
@ -187,7 +185,6 @@ export default function Modals() {
|
|||
)}
|
||||
{!!snapStates.showMediaAlt && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClose={(e) => {
|
||||
states.showMediaAlt = false;
|
||||
}}
|
||||
|
@ -203,6 +200,7 @@ export default function Modals() {
|
|||
)}
|
||||
{!!snapStates.showEmbedModal && (
|
||||
<Modal
|
||||
class="solid"
|
||||
onClose={() => {
|
||||
states.showEmbedModal = false;
|
||||
}}
|
||||
|
@ -218,6 +216,21 @@ export default function Modals() {
|
|||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{!!snapStates.showReportModal && (
|
||||
<Modal
|
||||
onClose={() => {
|
||||
states.showReportModal = false;
|
||||
}}
|
||||
>
|
||||
<ReportModal
|
||||
account={snapStates.showReportModal.account}
|
||||
post={snapStates.showReportModal.post}
|
||||
onClose={() => {
|
||||
states.showReportModal = false;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import './nav-menu.css';
|
||||
|
||||
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||
import {
|
||||
ControlledMenu,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
SubMenu,
|
||||
} from '@szhsin/react-menu';
|
||||
import { memo } from 'preact/compat';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
|
@ -130,7 +135,7 @@ function NavMenu(props) {
|
|||
if (Date.now() - buttonClickTS.current < 300) {
|
||||
return;
|
||||
}
|
||||
setMenuState(undefined);
|
||||
// setMenuState(undefined);
|
||||
},
|
||||
}}
|
||||
portal={{
|
||||
|
@ -169,13 +174,17 @@ function NavMenu(props) {
|
|||
<MenuLink to="/">
|
||||
<Icon icon="home" size="l" /> <span>Home</span>
|
||||
</MenuLink>
|
||||
{authenticated && (
|
||||
{authenticated ? (
|
||||
<>
|
||||
{showFollowing && (
|
||||
<MenuLink to="/following">
|
||||
<Icon icon="following" size="l" /> <span>Following</span>
|
||||
</MenuLink>
|
||||
)}
|
||||
<MenuLink to="/catchup">
|
||||
<Icon icon="history2" size="l" />
|
||||
<span>Catch-up</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/mentions">
|
||||
<Icon icon="at" size="l" /> <span>Mentions</span>
|
||||
</MenuLink>
|
||||
|
@ -188,51 +197,37 @@ function NavMenu(props) {
|
|||
</sup>
|
||||
)}
|
||||
</MenuLink>
|
||||
<MenuDivider />
|
||||
<MenuLink to="/l">
|
||||
<Icon icon="list" size="l" /> <span>Lists</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/ft">
|
||||
<Icon icon="hashtag" size="l" /> <span>Followed Hashtags</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/b">
|
||||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/f">
|
||||
<Icon icon="heart" size="l" /> <span>Likes</span>
|
||||
</MenuLink>
|
||||
</>
|
||||
)}
|
||||
<MenuDivider />
|
||||
<MenuLink to={`/search`}>
|
||||
<Icon icon="search" size="l" /> <span>Search</span>
|
||||
</MenuLink>
|
||||
<MenuLink to={`/${instance}/p/l`}>
|
||||
<Icon icon="building" size="l" /> <span>Local</span>
|
||||
</MenuLink>
|
||||
<MenuLink to={`/${instance}/p`}>
|
||||
<Icon icon="earth" size="l" /> <span>Federated</span>
|
||||
</MenuLink>
|
||||
<MenuLink to={`/${instance}/trending`}>
|
||||
<Icon icon="chart" size="l" /> <span>Trending</span>
|
||||
</MenuLink>
|
||||
</section>
|
||||
<section>
|
||||
{authenticated ? (
|
||||
<>
|
||||
<MenuDivider />
|
||||
{currentAccount?.info?.id && (
|
||||
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
|
||||
<Icon icon="user" size="l" /> <span>Profile</span>
|
||||
</MenuLink>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showAccounts = true;
|
||||
}}
|
||||
<MenuLink to="/l">
|
||||
<Icon icon="list" size="l" /> <span>Lists</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/b">
|
||||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||
</MenuLink>
|
||||
<SubMenu
|
||||
overflow="auto"
|
||||
gap={-8}
|
||||
label={
|
||||
<>
|
||||
<Icon icon="more" size="l" />
|
||||
<span class="menu-grow">More…</span>
|
||||
<Icon icon="chevron-right" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
||||
</MenuItem>
|
||||
<MenuLink to="/f">
|
||||
<Icon icon="heart" size="l" /> <span>Likes</span>
|
||||
</MenuLink>
|
||||
<MenuLink to="/ft">
|
||||
<Icon icon="hashtag" size="l" />{' '}
|
||||
<span>Followed Hashtags</span>
|
||||
</MenuLink>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showGenericAccounts = {
|
||||
|
@ -257,7 +252,42 @@ function NavMenu(props) {
|
|||
>
|
||||
<Icon icon="block" size="l" />
|
||||
Blocked users…
|
||||
</MenuItem>{' '}
|
||||
</SubMenu>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showAccounts = true;
|
||||
}}
|
||||
>
|
||||
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuLink to="/login">
|
||||
<Icon icon="user" size="l" /> <span>Log in</span>
|
||||
</MenuLink>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<MenuDivider />
|
||||
<MenuLink to={`/search`}>
|
||||
<Icon icon="search" size="l" /> <span>Search</span>
|
||||
</MenuLink>
|
||||
<MenuLink to={`/${instance}/trending`}>
|
||||
<Icon icon="chart" size="l" /> <span>Trending</span>
|
||||
</MenuLink>
|
||||
<MenuLink to={`/${instance}/p/l`}>
|
||||
<Icon icon="building" size="l" /> <span>Local</span>
|
||||
</MenuLink>
|
||||
<MenuLink to={`/${instance}/p`}>
|
||||
<Icon icon="earth" size="l" /> <span>Federated</span>
|
||||
</MenuLink>
|
||||
{authenticated ? (
|
||||
<>
|
||||
<MenuDivider className="divider-grow" />
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
|
@ -286,9 +316,6 @@ function NavMenu(props) {
|
|||
) : (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuLink to="/login">
|
||||
<Icon icon="user" size="l" /> <span>Log in</span>
|
||||
</MenuLink>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
states.showSettings = true;
|
||||
|
|
|
@ -144,7 +144,6 @@ export default memo(function NotificationService() {
|
|||
const { id, account, notification, sameInstance } = showNotificationSheet;
|
||||
return (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
|
|
196
src/components/report-modal.css
Normal file
196
src/components/report-modal.css
Normal file
|
@ -0,0 +1,196 @@
|
|||
.report-modal-container {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 40em;
|
||||
background-color: var(--bg-color);
|
||||
box-shadow: 0 16px 32px -8px var(--drop-shadow-color);
|
||||
overflow-y: auto;
|
||||
animation: slide-up-smooth 0.3s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
max-height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.top-controls {
|
||||
position: sticky;
|
||||
top: var(--sai-top, 0);
|
||||
z-index: 1;
|
||||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(16px);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
pointer-events: auto;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 16px 16px;
|
||||
/* display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px; */
|
||||
}
|
||||
|
||||
form {
|
||||
/* display: flex; */
|
||||
/* flex-direction: column; */
|
||||
/* gap: 16px; */
|
||||
text-wrap: pretty;
|
||||
|
||||
input {
|
||||
margin-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.report-preview {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
border: 2px dashed var(--red-color);
|
||||
box-shadow: inset 0 0 16px -4px var(--red-bg-color);
|
||||
overflow: auto;
|
||||
max-height: 33vh;
|
||||
|
||||
.status {
|
||||
font-size: 90%;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-drag: none;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.account-block {
|
||||
margin: 16px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-drag: none;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.rubber-stamp {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
margin-top: -48px;
|
||||
animation: rubber-stamp 0.3s ease-in both;
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
color: var(--red-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.5px;
|
||||
font-size: 2em;
|
||||
line-height: 1;
|
||||
padding: 0.1em;
|
||||
border: 0.15em solid var(--red-color);
|
||||
border-radius: 0.3em;
|
||||
background-color: var(--bg-blur-color);
|
||||
text-align: center;
|
||||
/* Noise pattern - https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ */
|
||||
mask-image: repeating-conic-gradient(
|
||||
#000 0 0.01%,
|
||||
rgba(0, 0, 0, 0.45) 0 0.02%
|
||||
);
|
||||
|
||||
small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block: 0.5em;
|
||||
}
|
||||
|
||||
section {
|
||||
label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:has(:checked) {
|
||||
.insignificant {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
> label:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.report-categories {
|
||||
label {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.report-rules {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.report-comment {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin-top: 2em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 8px 0 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex-grow: 1;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2em;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
border-radius: 8px !important;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rubber-stamp {
|
||||
0% {
|
||||
transform: rotate(-20deg) scale(5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-20deg) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
298
src/components/report-modal.jsx
Normal file
298
src/components/report-modal.jsx
Normal file
|
@ -0,0 +1,298 @@
|
|||
import './report-modal.css';
|
||||
|
||||
import { Fragment } from 'preact';
|
||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import showToast from '../utils/show-toast';
|
||||
import { getCurrentInstance } from '../utils/store-utils';
|
||||
|
||||
import AccountBlock from './account-block';
|
||||
import Icon from './icon';
|
||||
import Loader from './loader';
|
||||
import Status from './status';
|
||||
|
||||
// NOTE: `dislike` hidden for now, it's actually not used for reporting
|
||||
// Mastodon shows another screen for unfollowing, muting or blocking instead of reporting
|
||||
|
||||
const CATEGORIES = [, /*'dislike'*/ 'spam', 'legal', 'violation', 'other'];
|
||||
// `violation` will be set if there are `rule_ids[]`
|
||||
|
||||
const CATEGORIES_INFO = {
|
||||
// dislike: {
|
||||
// label: 'Dislike',
|
||||
// description: 'Not something you want to see',
|
||||
// },
|
||||
spam: {
|
||||
label: 'Spam',
|
||||
description: 'Malicious links, fake engagement, or repetitive replies',
|
||||
},
|
||||
legal: {
|
||||
label: 'Illegal',
|
||||
description: "Violates the law of your or the server's country",
|
||||
},
|
||||
violation: {
|
||||
label: 'Server rule violation',
|
||||
description: 'Breaks specific server rules',
|
||||
stampLabel: 'Violation',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
description: "Issue doesn't fit other categories",
|
||||
excludeStamp: true,
|
||||
},
|
||||
};
|
||||
|
||||
function ReportModal({ account, post, onClose }) {
|
||||
const { masto } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [username, domain] = account.acct.split('@');
|
||||
|
||||
const [rules, currentDomain] = useMemo(() => {
|
||||
const { rules, domain } = getCurrentInstance();
|
||||
return [rules || [], domain];
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState(null);
|
||||
const [showRules, setShowRules] = useState(false);
|
||||
|
||||
const rulesRef = useRef(null);
|
||||
const [hasRules, setHasRules] = useState(false);
|
||||
|
||||
return (
|
||||
<div class="report-modal-container">
|
||||
<div class="top-controls">
|
||||
<h1>{post ? 'Report Post' : `Report @${username}`}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="plain4 small"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<Icon icon="x" size="xl" />
|
||||
</button>
|
||||
</div>
|
||||
<main>
|
||||
<div class="report-preview">
|
||||
{post ? (
|
||||
<Status status={post} size="s" previewMode />
|
||||
) : (
|
||||
<AccountBlock
|
||||
account={account}
|
||||
avatarSize="xxl"
|
||||
useAvatarStatic
|
||||
showStats
|
||||
showActivity
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!!selectedCategory &&
|
||||
!CATEGORIES_INFO[selectedCategory].excludeStamp && (
|
||||
<span
|
||||
class="rubber-stamp"
|
||||
key={selectedCategory}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{CATEGORIES_INFO[selectedCategory].stampLabel ||
|
||||
CATEGORIES_INFO[selectedCategory].label}
|
||||
<small>Pending review</small>
|
||||
</span>
|
||||
)}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const entries = Object.fromEntries(formData.entries());
|
||||
console.log('ENTRIES', entries);
|
||||
|
||||
let { category, comment, forward } = entries;
|
||||
if (!comment) comment = undefined;
|
||||
if (forward === 'on') forward = true;
|
||||
const ruleIds =
|
||||
category === 'violation'
|
||||
? Object.entries(entries)
|
||||
.filter(([key]) => key.startsWith('rule_ids'))
|
||||
.map(([key, value]) => value)
|
||||
: undefined;
|
||||
|
||||
const params = {
|
||||
category,
|
||||
comment,
|
||||
forward,
|
||||
ruleIds,
|
||||
};
|
||||
console.log('PARAMS', params);
|
||||
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.reports.create({
|
||||
accountId: account.id,
|
||||
statusIds: post?.id ? [post.id] : undefined,
|
||||
category,
|
||||
comment,
|
||||
ruleIds,
|
||||
forward,
|
||||
});
|
||||
setUIState('success');
|
||||
showToast(post ? 'Post reported' : 'Profile reported');
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setUIState('error');
|
||||
showToast(
|
||||
error?.message ||
|
||||
(post
|
||||
? 'Unable to report post'
|
||||
: 'Unable to report profile'),
|
||||
);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
{post
|
||||
? `What's the issue with this post?`
|
||||
: `What's the issue with this profile?`}
|
||||
</p>
|
||||
<section class="report-categories">
|
||||
{CATEGORIES.map((category) =>
|
||||
category === 'violation' && !rules?.length ? null : (
|
||||
<Fragment key={category}>
|
||||
<label class="report-category">
|
||||
<input
|
||||
type="radio"
|
||||
name="category"
|
||||
value={category}
|
||||
required
|
||||
disabled={uiState === 'loading'}
|
||||
onChange={(e) => {
|
||||
setSelectedCategory(e.target.value);
|
||||
setShowRules(e.target.value === 'violation');
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{CATEGORIES_INFO[category].label}
|
||||
<small class="ib insignificant">
|
||||
{CATEGORIES_INFO[category].description}
|
||||
</small>
|
||||
</span>
|
||||
</label>
|
||||
{category === 'violation' && !!rules?.length && (
|
||||
<div
|
||||
class="shazam-container no-animation"
|
||||
hidden={!showRules}
|
||||
>
|
||||
<div class="shazam-container-inner">
|
||||
<div class="report-rules" ref={rulesRef}>
|
||||
{rules.map((rule, i) => (
|
||||
<label class="report-rule" key={rule.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={`rule_ids[${i}]`}
|
||||
value={rule.id}
|
||||
required={showRules && !hasRules}
|
||||
disabled={uiState === 'loading'}
|
||||
onChange={(e) => {
|
||||
const { checked } = e.target;
|
||||
if (checked) {
|
||||
setHasRules(true);
|
||||
} else {
|
||||
const checkedInputs =
|
||||
rulesRef.current.querySelectorAll(
|
||||
'input:checked',
|
||||
);
|
||||
if (!checkedInputs.length) {
|
||||
setHasRules(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{rule.text}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
<section class="report-comment">
|
||||
<p>
|
||||
<label for="report-comment">Additional info</label>
|
||||
</p>
|
||||
<textarea
|
||||
maxlength="1000"
|
||||
rows="1"
|
||||
name="comment"
|
||||
id="report-comment"
|
||||
disabled={uiState === 'loading'}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
{domain !== currentDomain && (
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
switch
|
||||
name="forward"
|
||||
disabled={uiState === 'loading'}
|
||||
/>{' '}
|
||||
<span>
|
||||
Forward to <i>{domain}</i>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<footer>
|
||||
<button type="submit" disabled={uiState === 'loading'}>
|
||||
Send Report
|
||||
</button>{' '}
|
||||
<button
|
||||
type="submit"
|
||||
class="plain2"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await masto.v1.accounts.$select(account.id).mute(); // Infinite duration
|
||||
showToast(`Muted ${username}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(`Unable to mute ${username}`);
|
||||
}
|
||||
// onSubmit will still run
|
||||
}}
|
||||
>
|
||||
Send Report <small class="ib">+ Mute profile</small>
|
||||
</button>{' '}
|
||||
<button
|
||||
type="submit"
|
||||
class="plain2"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await masto.v1.accounts.$select(account.id).block();
|
||||
showToast(`Blocked ${username}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(`Unable to block ${username}`);
|
||||
}
|
||||
// onSubmit will still run
|
||||
}}
|
||||
>
|
||||
Send Report <small class="ib">+ Block profile</small>
|
||||
</button>
|
||||
<Loader hidden={uiState !== 'loading'} />
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportModal;
|
|
@ -153,6 +153,15 @@
|
|||
}
|
||||
#import-export-container section p {
|
||||
margin: 8px 0;
|
||||
|
||||
&.field-button {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#import-export-container section details > summary {
|
||||
cursor: pointer;
|
||||
|
@ -182,3 +191,14 @@
|
|||
font-size: 90%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#import-export-container {
|
||||
footer {
|
||||
font-size: 90%;
|
||||
color: var(--text-insignificant-color);
|
||||
|
||||
.icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { fetchFollowedTags } from '../utils/followed-tags';
|
|||
import pmem from '../utils/pmem';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
||||
import AsyncText from './AsyncText';
|
||||
import Icon from './icon';
|
||||
|
@ -391,7 +392,11 @@ function ShortcutsSettings({ onClose }) {
|
|||
</>
|
||||
) : (
|
||||
<div class="ui-state insignificant">
|
||||
<p>No shortcuts yet. Tap on the Add shortcut button.</p>
|
||||
<p>
|
||||
{snapStates.settings.shortcutsViewMode === 'multi-column'
|
||||
? 'No columns yet. Tap on the Add column button.'
|
||||
: 'No shortcuts yet. Tap on the Add shortcut button.'}
|
||||
</p>
|
||||
<p>
|
||||
Not sure what to add?
|
||||
<br />
|
||||
|
@ -418,7 +423,9 @@ function ShortcutsSettings({ onClose }) {
|
|||
)}
|
||||
<p class="insignificant">
|
||||
{shortcuts.length >= SHORTCUTS_LIMIT &&
|
||||
`Max ${SHORTCUTS_LIMIT} shortcuts`}
|
||||
(snapStates.settings.shortcutsViewMode === 'multi-column'
|
||||
? `Max ${SHORTCUTS_LIMIT} columns`
|
||||
: `Max ${SHORTCUTS_LIMIT} shortcuts`)}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
|
@ -450,7 +457,6 @@ function ShortcutsSettings({ onClose }) {
|
|||
</main>
|
||||
{showForm && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowForm(false);
|
||||
|
@ -474,7 +480,6 @@ function ShortcutsSettings({ onClose }) {
|
|||
)}
|
||||
{showImportExport && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowImportExport(false);
|
||||
|
@ -716,6 +721,7 @@ function ShortcutForm({
|
|||
}
|
||||
|
||||
function ImportExport({ shortcuts, onClose }) {
|
||||
const { masto } = api();
|
||||
const shortcutsStr = useMemo(() => {
|
||||
if (!shortcuts) return '';
|
||||
if (!shortcuts.filter(Boolean).length) return '';
|
||||
|
@ -754,6 +760,8 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
}, [importShortcutStr]);
|
||||
const hasCurrentSettings = states.shortcuts.length > 0;
|
||||
|
||||
const shortcutsImportFieldRef = useRef();
|
||||
|
||||
return (
|
||||
<div id="import-export-container" class="sheet">
|
||||
{!!onClose && (
|
||||
|
@ -772,8 +780,9 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
<Icon icon="arrow-down-circle" size="l" class="insignificant" />{' '}
|
||||
<span>Import</span>
|
||||
</h3>
|
||||
<p>
|
||||
<p class="field-button">
|
||||
<input
|
||||
ref={shortcutsImportFieldRef}
|
||||
type="text"
|
||||
name="import"
|
||||
placeholder="Paste shortcuts here"
|
||||
|
@ -782,6 +791,53 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
setImportShortcutStr(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{states.settings.shortcutSettingsCloudImportExport && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain2 small"
|
||||
disabled={importUIState === 'cloud-downloading'}
|
||||
onClick={async () => {
|
||||
setImportUIState('cloud-downloading');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
showToast(
|
||||
'Downloading saved shortcuts from instance server…',
|
||||
);
|
||||
try {
|
||||
const relationships =
|
||||
await masto.v1.accounts.relationships.fetch({
|
||||
id: [currentAccount],
|
||||
});
|
||||
const relationship = relationships[0];
|
||||
if (relationship) {
|
||||
const { note = '' } = relationship;
|
||||
if (
|
||||
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
|
||||
note,
|
||||
)
|
||||
) {
|
||||
const settings = note.match(
|
||||
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
|
||||
)[1];
|
||||
const { v, dt, data } = JSON.parse(settings);
|
||||
shortcutsImportFieldRef.current.value = data;
|
||||
shortcutsImportFieldRef.current.dispatchEvent(
|
||||
new Event('input'),
|
||||
);
|
||||
}
|
||||
}
|
||||
setImportUIState('default');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setImportUIState('error');
|
||||
showToast('Unable to download shortcuts');
|
||||
}
|
||||
}}
|
||||
title="Download shortcuts from instance server"
|
||||
>
|
||||
<Icon icon="cloud" />
|
||||
<Icon icon="arrow-down" />
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
{!!parsedImportShortcutStr &&
|
||||
Array.isArray(parsedImportShortcutStr) && (
|
||||
|
@ -991,8 +1047,64 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
<Icon icon="share" /> <span>Share</span>
|
||||
</button>
|
||||
)}{' '}
|
||||
{states.settings.shortcutSettingsCloudImportExport && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain2"
|
||||
disabled={importUIState === 'cloud-uploading'}
|
||||
onClick={async () => {
|
||||
setImportUIState('cloud-uploading');
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
try {
|
||||
const relationships =
|
||||
await masto.v1.accounts.relationships.fetch({
|
||||
id: [currentAccount],
|
||||
});
|
||||
const relationship = relationships[0];
|
||||
if (relationship) {
|
||||
const { note = '' } = relationship;
|
||||
// const newNote = `${note}\n\n\n$<phanpy-shortcuts-settings>{shortcutsStr}</phanpy-shortcuts-settings>`;
|
||||
let newNote = '';
|
||||
if (
|
||||
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/.test(
|
||||
note,
|
||||
)
|
||||
) {
|
||||
const settingsJSON = JSON.stringify({
|
||||
v: '1', // version
|
||||
dt: Date.now(), // datetime stamp
|
||||
data: shortcutsStr, // shortcuts settings string
|
||||
});
|
||||
newNote = note.replace(
|
||||
/<phanpy-shortcuts-settings>(.*)<\/phanpy-shortcuts-settings>/,
|
||||
`<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`,
|
||||
);
|
||||
} else {
|
||||
newNote = `${note}\n\n\n<phanpy-shortcuts-settings>${settingsJSON}</phanpy-shortcuts-settings>`;
|
||||
}
|
||||
showToast('Saving shortcuts to instance server…');
|
||||
await masto.v1.accounts
|
||||
.$select(currentAccount)
|
||||
.note.create({
|
||||
comment: newNote,
|
||||
});
|
||||
setImportUIState('default');
|
||||
showToast('Shortcuts saved');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setImportUIState('error');
|
||||
showToast('Unable to save shortcuts');
|
||||
}
|
||||
}}
|
||||
title="Sync to instance server"
|
||||
>
|
||||
<Icon icon="cloud" />
|
||||
<Icon icon="arrow-up" />
|
||||
</button>
|
||||
)}{' '}
|
||||
{shortcutsStr.length > 0 && (
|
||||
<small class="insignificant">
|
||||
<small class="insignificant ib">
|
||||
{shortcutsStr.length} characters
|
||||
</small>
|
||||
)}
|
||||
|
@ -1008,6 +1120,14 @@ function ImportExport({ shortcuts, onClose }) {
|
|||
</details>
|
||||
)}
|
||||
</section>
|
||||
{states.settings.shortcutSettingsCloudImportExport && (
|
||||
<footer>
|
||||
<p>
|
||||
<Icon icon="cloud" /> Import/export settings from/to instance
|
||||
server (Very experimental)
|
||||
</p>
|
||||
</footer>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -404,6 +404,24 @@
|
|||
font-size: 90%;
|
||||
line-height: var(--avatar-size);
|
||||
}
|
||||
|
||||
.status-filtered-badge.badge-meta {
|
||||
margin-top: 6px;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
color: var(--text-color);
|
||||
border-color: var(--text-color);
|
||||
background-color: var(--bg-blur-color);
|
||||
max-width: 100%;
|
||||
|
||||
> span + span {
|
||||
position: static;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status .container {
|
||||
|
@ -661,7 +679,9 @@
|
|||
animation: none !important; */
|
||||
}
|
||||
}
|
||||
.status .content-container.has-spoiler:not(.show-media) .spoiler-media-button {
|
||||
.status
|
||||
.content-container.has-spoiler:not(.show-media)
|
||||
:is(.spoiler-button, .spoiler-media-button) {
|
||||
~ :is(.media-container, .media-figure-multiple) figcaption {
|
||||
/* filter: blur(5px) invert(0.5);
|
||||
image-rendering: crisp-edges;
|
||||
|
@ -1591,6 +1611,11 @@ a.card:is(:hover, :focus):visited {
|
|||
.card.video {
|
||||
max-width: 320px;
|
||||
max-height: 320px;
|
||||
cursor: pointer;
|
||||
|
||||
lite\-youtube {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.card.video iframe {
|
||||
width: 100%;
|
||||
|
@ -1971,6 +1996,7 @@ a.card:is(:hover, :focus):visited {
|
|||
}
|
||||
|
||||
.status:focus &,
|
||||
.status:focus-within &,
|
||||
&.open {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
|
@ -2112,6 +2138,97 @@ a.card:is(:hover, :focus):visited {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* EMBED */
|
||||
|
||||
#embed-post {
|
||||
> main > section {
|
||||
p {
|
||||
margin-block: 0.5em;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-inline: 1em;
|
||||
}
|
||||
p + ul {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-code {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
min-height: 12em;
|
||||
max-height: 40vh;
|
||||
font-family: var(--monospace-font);
|
||||
font-size: 0.8em;
|
||||
border-color: var(--link-color);
|
||||
/* background-color: var(--bg-faded-color); */
|
||||
}
|
||||
|
||||
.links-list {
|
||||
a {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-preview {
|
||||
display: block;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
border: 2px dashed var(--link-light-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px -4px var(--drop-shadow-color),
|
||||
0 8px 32px -8px var(--drop-shadow-color);
|
||||
padding: 16px;
|
||||
|
||||
/* Interactive elements */
|
||||
button,
|
||||
a,
|
||||
video,
|
||||
audio,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
iframe,
|
||||
object,
|
||||
embed {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1em;
|
||||
border-inline-start: 4px solid var(--outline-color);
|
||||
padding-inline-start: 1em;
|
||||
|
||||
> p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-inline: 0;
|
||||
padding-inline: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin-inline: 0;
|
||||
|
||||
img,
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* DELETED */
|
||||
|
||||
.status-deleted {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from '@szhsin/react-menu';
|
||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { shallowEqual } from 'fast-equals';
|
||||
import prettify from 'html-prettify';
|
||||
import { memo } from 'preact/compat';
|
||||
import {
|
||||
useCallback,
|
||||
|
@ -451,6 +452,7 @@ function Status({
|
|||
]);
|
||||
|
||||
const [showEdited, setShowEdited] = useState(false);
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
const spoilerContentRef = useTruncated();
|
||||
const contentRef = useTruncated();
|
||||
|
@ -559,12 +561,11 @@ function Status({
|
|||
if (reblogged) {
|
||||
const newStatus = await masto.v1.statuses.$select(id).unreblog();
|
||||
saveStatus(newStatus, instance);
|
||||
return true;
|
||||
} else {
|
||||
const newStatus = await masto.v1.statuses.$select(id).reblog();
|
||||
saveStatus(newStatus, instance);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Revert optimistism
|
||||
|
@ -575,7 +576,8 @@ function Status({
|
|||
|
||||
const favouriteStatus = async () => {
|
||||
if (!sameInstance || !authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
alert(unauthInteractionErrorMessage);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Optimistic
|
||||
|
@ -591,16 +593,31 @@ function Status({
|
|||
const newStatus = await masto.v1.statuses.$select(id).favourite();
|
||||
saveStatus(newStatus, instance);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Revert optimistism
|
||||
states.statuses[sKey] = status;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const favouriteStatusNotify = async () => {
|
||||
try {
|
||||
const done = await favouriteStatus();
|
||||
if (!isSizeLarge && done) {
|
||||
showToast(
|
||||
favourited
|
||||
? `Unliked @${username || acct}'s post`
|
||||
: `Liked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const bookmarkStatus = async () => {
|
||||
if (!sameInstance || !authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
alert(unauthInteractionErrorMessage);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Optimistic
|
||||
|
@ -615,12 +632,26 @@ function Status({
|
|||
const newStatus = await masto.v1.statuses.$select(id).bookmark();
|
||||
saveStatus(newStatus, instance);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Revert optimistism
|
||||
states.statuses[sKey] = status;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const bookmarkStatusNotify = async () => {
|
||||
try {
|
||||
const done = await bookmarkStatus();
|
||||
if (!isSizeLarge && done) {
|
||||
showToast(
|
||||
bookmarked
|
||||
? `Unbookmarked @${username || acct}'s post`
|
||||
: `Bookmarked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const differentLanguage =
|
||||
!!language &&
|
||||
|
@ -680,6 +711,8 @@ function Status({
|
|||
}
|
||||
|
||||
const actionsRef = useRef();
|
||||
const isPublic = ['public', 'unlisted'].includes(visibility);
|
||||
const isPinnable = ['public', 'unlisted', 'private'].includes(visibility);
|
||||
const StatusMenuItems = (
|
||||
<>
|
||||
{isSizeLarge && (
|
||||
|
@ -752,18 +785,7 @@ function Status({
|
|||
</span>
|
||||
</MenuConfirm>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
try {
|
||||
favouriteStatus();
|
||||
if (!isSizeLarge) {
|
||||
showToast(
|
||||
favourited
|
||||
? `Unliked @${username || acct}'s post`
|
||||
: `Liked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
}}
|
||||
onClick={favouriteStatusNotify}
|
||||
className={`menu-favourite ${favourited ? 'checked' : ''}`}
|
||||
>
|
||||
<Icon icon="heart" />
|
||||
|
@ -776,18 +798,7 @@ function Status({
|
|||
</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
try {
|
||||
bookmarkStatus();
|
||||
if (!isSizeLarge) {
|
||||
showToast(
|
||||
bookmarked
|
||||
? `Unbookmarked @${username || acct}'s post`
|
||||
: `Bookmarked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
}}
|
||||
onClick={bookmarkStatusNotify}
|
||||
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
||||
>
|
||||
<Icon icon="bookmark" />
|
||||
|
@ -907,7 +918,8 @@ function Status({
|
|||
<Icon icon="link" />
|
||||
<span>Copy</span>
|
||||
</MenuItem>
|
||||
{navigator?.share &&
|
||||
{isPublic &&
|
||||
navigator?.share &&
|
||||
navigator?.canShare?.({
|
||||
url,
|
||||
}) && (
|
||||
|
@ -928,6 +940,16 @@ function Status({
|
|||
</MenuItem>
|
||||
)}
|
||||
</div>
|
||||
{isPublic && isSizeLarge && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowEmbed(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="code" />
|
||||
<span>Embed</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{(isSelf || mentionSelf) && <MenuDivider />}
|
||||
{(isSelf || mentionSelf) && (
|
||||
<MenuItem
|
||||
|
@ -961,7 +983,7 @@ function Status({
|
|||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isSelf && /(public|unlisted|private)/i.test(visibility) && (
|
||||
{isSelf && isPinnable && (
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
try {
|
||||
|
@ -1040,6 +1062,23 @@ function Status({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isSelf && isSizeLarge && (
|
||||
<>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
className="danger"
|
||||
onClick={() => {
|
||||
states.showReportModal = {
|
||||
account: status.account,
|
||||
post: status,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="flag" />
|
||||
<span>Report post…</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -1061,7 +1100,7 @@ function Status({
|
|||
const { clientX, clientY } = e.touches?.[0] || e;
|
||||
// link detection copied from onContextMenu because here it works
|
||||
const link = e.target.closest('a');
|
||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
||||
if (link && statusRef.current.contains(link)) return;
|
||||
e.preventDefault();
|
||||
setContextMenuProps({
|
||||
anchorPoint: {
|
||||
|
@ -1085,42 +1124,12 @@ function Status({
|
|||
const rRef = useHotkeys('r, shift+r', replyStatus, {
|
||||
enabled: hotkeysEnabled,
|
||||
});
|
||||
const fRef = useHotkeys(
|
||||
'f, l',
|
||||
() => {
|
||||
try {
|
||||
favouriteStatus();
|
||||
if (!isSizeLarge) {
|
||||
showToast(
|
||||
favourited
|
||||
? `Unliked @${username || acct}'s post`
|
||||
: `Liked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
{
|
||||
const fRef = useHotkeys('f, l', favouriteStatusNotify, {
|
||||
enabled: hotkeysEnabled,
|
||||
},
|
||||
);
|
||||
const dRef = useHotkeys(
|
||||
'd',
|
||||
() => {
|
||||
try {
|
||||
bookmarkStatus();
|
||||
if (!isSizeLarge) {
|
||||
showToast(
|
||||
bookmarked
|
||||
? `Unbookmarked @${username || acct}'s post`
|
||||
: `Bookmarked @${username || acct}'s post`,
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
{
|
||||
});
|
||||
const dRef = useHotkeys('d', bookmarkStatusNotify, {
|
||||
enabled: hotkeysEnabled,
|
||||
},
|
||||
);
|
||||
});
|
||||
const bRef = useHotkeys(
|
||||
'shift+b',
|
||||
() => {
|
||||
|
@ -1337,7 +1346,7 @@ function Status({
|
|||
if (e.metaKey) return;
|
||||
// console.log('context menu', e);
|
||||
const link = e.target.closest('a');
|
||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
||||
if (link && statusRef.current.contains(link)) return;
|
||||
|
||||
// If there's selected text, don't show custom context menu
|
||||
const selection = window.getSelection?.();
|
||||
|
@ -1420,16 +1429,7 @@ function Status({
|
|||
icon="heart"
|
||||
iconSize="m"
|
||||
count={favouritesCount}
|
||||
onClick={() => {
|
||||
try {
|
||||
favouriteStatus();
|
||||
showToast(
|
||||
favourited
|
||||
? `Unliked @${username || acct}'s post`
|
||||
: `Liked @${username || acct}'s post`,
|
||||
);
|
||||
} catch (e) {}
|
||||
}}
|
||||
onClick={favouriteStatusNotify}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
@ -1798,6 +1798,7 @@ function Status({
|
|||
media={media}
|
||||
autoAnimate={isSizeLarge}
|
||||
showCaption={mediaAttachments.length === 1}
|
||||
allowLongerCaption={!content}
|
||||
lang={language}
|
||||
altIndex={
|
||||
showMultipleMediaCaptions &&
|
||||
|
@ -1987,7 +1988,6 @@ function Status({
|
|||
</div>
|
||||
{!!showEdited && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowEdited(false);
|
||||
|
@ -2008,6 +2008,23 @@ function Status({
|
|||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{!!showEmbed && (
|
||||
<Modal
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowEmbed(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EmbedModal
|
||||
post={status}
|
||||
instance={instance}
|
||||
onClose={() => {
|
||||
setShowEmbed(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
|
@ -2195,7 +2212,11 @@ function Card({ card, selfReferential, instance }) {
|
|||
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
||||
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
||||
if (videoID) {
|
||||
return <lite-youtube videoid={videoID} nocookie></lite-youtube>;
|
||||
return (
|
||||
<a class="card video" onClick={handleClick}>
|
||||
<lite-youtube videoid={videoID} nocookie></lite-youtube>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
// return (
|
||||
|
@ -2308,6 +2329,360 @@ function EditedAtModal({
|
|||
);
|
||||
}
|
||||
|
||||
function generateHTMLCode(post, instance, level = 0) {
|
||||
const {
|
||||
account: {
|
||||
url: accountURL,
|
||||
displayName,
|
||||
acct,
|
||||
username,
|
||||
emojis: accountEmojis,
|
||||
bot,
|
||||
group,
|
||||
},
|
||||
id,
|
||||
poll,
|
||||
spoilerText,
|
||||
language,
|
||||
editedAt,
|
||||
createdAt,
|
||||
content,
|
||||
mediaAttachments,
|
||||
url,
|
||||
emojis,
|
||||
} = post;
|
||||
|
||||
const sKey = statusKey(id, instance);
|
||||
const quotes = states.statusQuotes[sKey] || [];
|
||||
const uniqueQuotes = quotes.filter(
|
||||
(q, i, arr) => arr.findIndex((q2) => q2.url === q.url) === i,
|
||||
);
|
||||
const quoteStatusesHTML =
|
||||
uniqueQuotes.length && level <= 2
|
||||
? uniqueQuotes
|
||||
.map((quote) => {
|
||||
const { id, instance } = quote;
|
||||
const sKey = statusKey(id, instance);
|
||||
const s = states.statuses[sKey];
|
||||
if (s) {
|
||||
return generateHTMLCode(s, instance, ++level);
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
: '';
|
||||
|
||||
const createdAtDate = new Date(createdAt);
|
||||
// const editedAtDate = editedAt && new Date(editedAt);
|
||||
|
||||
const contentHTML =
|
||||
emojifyText(content, emojis) +
|
||||
'\n' +
|
||||
quoteStatusesHTML +
|
||||
'\n' +
|
||||
(poll?.options?.length
|
||||
? `
|
||||
<p>📊:</p>
|
||||
<ul>
|
||||
${poll.options
|
||||
.map(
|
||||
(option) => `
|
||||
<li>
|
||||
${option.title}
|
||||
${option.votesCount >= 0 ? ` (${option.votesCount})` : ''}
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</ul>`
|
||||
: '') +
|
||||
(mediaAttachments.length > 0
|
||||
? '\n' +
|
||||
mediaAttachments
|
||||
.map((media) => {
|
||||
const {
|
||||
description,
|
||||
meta,
|
||||
previewRemoteUrl,
|
||||
previewUrl,
|
||||
remoteUrl,
|
||||
url,
|
||||
type,
|
||||
} = media;
|
||||
const { original = {}, small } = meta || {};
|
||||
const width = small?.width || original?.width;
|
||||
const height = small?.height || original?.height;
|
||||
|
||||
// Prefer remote over original
|
||||
const sourceMediaURL = remoteUrl || url;
|
||||
const previewMediaURL = previewRemoteUrl || previewUrl;
|
||||
const mediaURL = previewMediaURL || sourceMediaURL;
|
||||
|
||||
const sourceMediaURLObj = sourceMediaURL
|
||||
? new URL(sourceMediaURL)
|
||||
: null;
|
||||
const isVideoMaybe =
|
||||
type === 'unknown' &&
|
||||
sourceMediaURLObj &&
|
||||
/\.(mp4|m4r|m4v|mov|webm)$/i.test(sourceMediaURLObj.pathname);
|
||||
const isAudioMaybe =
|
||||
type === 'unknown' &&
|
||||
sourceMediaURLObj &&
|
||||
/\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(sourceMediaURLObj.pathname);
|
||||
const isImage =
|
||||
type === 'image' ||
|
||||
(type === 'unknown' &&
|
||||
previewMediaURL &&
|
||||
!isVideoMaybe &&
|
||||
!isAudioMaybe);
|
||||
const isVideo = type === 'gifv' || type === 'video' || isVideoMaybe;
|
||||
const isAudio = type === 'audio' || isAudioMaybe;
|
||||
|
||||
let mediaHTML = '';
|
||||
if (isImage) {
|
||||
mediaHTML = `<img src="${mediaURL}" width="${width}" height="${height}" alt="${description}" loading="lazy" />`;
|
||||
} else if (isVideo) {
|
||||
mediaHTML = `
|
||||
<video src="${sourceMediaURL}" width="${width}" height="${height}" controls preload="auto" poster="${previewMediaURL}" loading="lazy"></video>
|
||||
${description ? `<figcaption>${description}</figcaption>` : ''}
|
||||
`;
|
||||
} else if (isAudio) {
|
||||
mediaHTML = `
|
||||
<audio src="${sourceMediaURL}" controls preload="auto"></audio>
|
||||
${description ? `<figcaption>${description}</figcaption>` : ''}
|
||||
`;
|
||||
} else {
|
||||
mediaHTML = `
|
||||
<a href="${sourceMediaURL}">📄 ${
|
||||
description || sourceMediaURL
|
||||
}</a>
|
||||
`;
|
||||
}
|
||||
|
||||
return `<figure>${mediaHTML}</figure>`;
|
||||
})
|
||||
.join('\n')
|
||||
: '');
|
||||
|
||||
const htmlCode = `
|
||||
<blockquote lang="${language}" cite="${url}">
|
||||
${
|
||||
spoilerText
|
||||
? `
|
||||
<details>
|
||||
<summary>${spoilerText}</summary>
|
||||
${contentHTML}
|
||||
</details>
|
||||
`
|
||||
: contentHTML
|
||||
}
|
||||
<footer>
|
||||
— ${emojifyText(
|
||||
displayName,
|
||||
accountEmojis,
|
||||
)} (@${acct}) <a href="${url}"><time datetime="${createdAtDate.toISOString()}">${createdAtDate.toLocaleString()}</time></a>
|
||||
</footer>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
return prettify(htmlCode);
|
||||
}
|
||||
|
||||
function EmbedModal({ post, instance, onClose }) {
|
||||
const {
|
||||
account: {
|
||||
url: accountURL,
|
||||
displayName,
|
||||
username,
|
||||
emojis: accountEmojis,
|
||||
bot,
|
||||
group,
|
||||
},
|
||||
id,
|
||||
poll,
|
||||
spoilerText,
|
||||
language,
|
||||
editedAt,
|
||||
createdAt,
|
||||
content,
|
||||
mediaAttachments,
|
||||
url,
|
||||
emojis,
|
||||
} = post;
|
||||
|
||||
const htmlCode = generateHTMLCode(post, instance);
|
||||
return (
|
||||
<div id="embed-post" class="sheet">
|
||||
{!!onClose && (
|
||||
<button type="button" class="sheet-close" onClick={onClose}>
|
||||
<Icon icon="x" />
|
||||
</button>
|
||||
)}
|
||||
<header>
|
||||
<h2>Embed post</h2>
|
||||
</header>
|
||||
<main tabIndex="-1">
|
||||
<h3>HTML Code</h3>
|
||||
<textarea
|
||||
class="embed-code"
|
||||
readonly
|
||||
onClick={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
>
|
||||
{htmlCode}
|
||||
</textarea>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
try {
|
||||
navigator.clipboard.writeText(htmlCode);
|
||||
showToast('HTML code copied');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast('Unable to copy HTML code');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="clipboard" /> <span>Copy</span>
|
||||
</button>
|
||||
{!!mediaAttachments?.length && (
|
||||
<section>
|
||||
<p>Media attachments:</p>
|
||||
<ol class="links-list">
|
||||
{mediaAttachments.map((media) => {
|
||||
return (
|
||||
<li key={media.id}>
|
||||
<a
|
||||
href={media.remoteUrl || media.url}
|
||||
target="_blank"
|
||||
download
|
||||
>
|
||||
{media.remoteUrl || media.url}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
{!!accountEmojis?.length && (
|
||||
<section>
|
||||
<p>Account Emojis:</p>
|
||||
<ul class="links-list">
|
||||
{accountEmojis.map((emoji) => {
|
||||
return (
|
||||
<li key={emoji.shortcode}>
|
||||
<picture>
|
||||
<source
|
||||
srcset={emoji.staticUrl}
|
||||
media="(prefers-reduced-motion: reduce)"
|
||||
></source>
|
||||
<img
|
||||
class="shortcode-emoji emoji"
|
||||
src={emoji.url}
|
||||
alt={`:${emoji.shortcode}:`}
|
||||
width="16"
|
||||
height="16"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>{' '}
|
||||
<code>:{emoji.shortcode}:</code> (
|
||||
<a href={emoji.url} target="_blank" download>
|
||||
url
|
||||
</a>
|
||||
)
|
||||
{emoji.staticUrl ? (
|
||||
<>
|
||||
{' '}
|
||||
(
|
||||
<a href={emoji.staticUrl} target="_blank" download>
|
||||
static
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!emojis?.length && (
|
||||
<section>
|
||||
<p>Emojis:</p>
|
||||
<ul class="links-list">
|
||||
{emojis.map((emoji) => {
|
||||
return (
|
||||
<li key={emoji.shortcode}>
|
||||
<picture>
|
||||
<source
|
||||
srcset={emoji.staticUrl}
|
||||
media="(prefers-reduced-motion: reduce)"
|
||||
></source>
|
||||
<img
|
||||
class="shortcode-emoji emoji"
|
||||
src={emoji.url}
|
||||
alt={`:${emoji.shortcode}:`}
|
||||
width="16"
|
||||
height="16"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>{' '}
|
||||
<code>:{emoji.shortcode}:</code> (
|
||||
<a href={emoji.url} target="_blank" download>
|
||||
url
|
||||
</a>
|
||||
)
|
||||
{emoji.staticUrl ? (
|
||||
<>
|
||||
{' '}
|
||||
(
|
||||
<a href={emoji.staticUrl} target="_blank" download>
|
||||
static
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<small>
|
||||
<p>Notes:</p>
|
||||
<ul>
|
||||
<li>
|
||||
This is static, unstyled and scriptless. You may need to apply
|
||||
your own styles and edit as needed.
|
||||
</li>
|
||||
<li>
|
||||
Polls are not interactive, becomes a list with vote counts.
|
||||
</li>
|
||||
<li>
|
||||
Media attachments can be images, videos, audios or any file
|
||||
types.
|
||||
</li>
|
||||
<li>Post could be edited or deleted later.</li>
|
||||
</ul>
|
||||
</small>
|
||||
</section>
|
||||
<h3>Preview</h3>
|
||||
<output
|
||||
class="embed-preview"
|
||||
dangerouslySetInnerHTML={{ __html: htmlCode }}
|
||||
/>
|
||||
<p>
|
||||
<small>Note: This preview is lightly styled.</small>
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusButton({
|
||||
checked,
|
||||
count,
|
||||
|
@ -2420,13 +2795,21 @@ function StatusCompact({ sKey }) {
|
|||
visibility,
|
||||
content,
|
||||
language,
|
||||
filtered,
|
||||
} = status;
|
||||
if (sensitive || spoilerText) return null;
|
||||
if (!content) return null;
|
||||
|
||||
const srKey = statusKey(id, instance);
|
||||
|
||||
const statusPeekText = statusPeek(status);
|
||||
|
||||
const filterContext = useContext(FilterContext);
|
||||
const filterInfo = isFiltered(filtered, filterContext);
|
||||
|
||||
if (filterInfo?.action === 'hide') return null;
|
||||
|
||||
const filterTitleStr = filterInfo?.titlesStr || '';
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`status compact-reply ${
|
||||
|
@ -2442,7 +2825,14 @@ function StatusCompact({ sKey }) {
|
|||
lang={language}
|
||||
dir="auto"
|
||||
>
|
||||
{statusPeekText}
|
||||
{filterInfo ? (
|
||||
<b class="status-filtered-badge badge-meta" title={filterTitleStr}>
|
||||
<span>Filtered</span>
|
||||
<span>{filterTitleStr}</span>
|
||||
</b>
|
||||
) : (
|
||||
<span>{statusPeekText}</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
@ -2564,7 +2954,6 @@ function FilteredStatus({
|
|||
</article>
|
||||
{!!showPeek && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowPeek(false);
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
--outline-color: rgba(128, 128, 128, 0.2);
|
||||
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||
--divider-color: rgba(0, 0, 0, 0.1);
|
||||
--backdrop-color: rgba(0, 0, 0, 0.05);
|
||||
--backdrop-color: rgba(0, 0, 0, 0.1);
|
||||
--backdrop-darker-color: rgba(0, 0, 0, 0.25);
|
||||
--backdrop-solid-color: #eee;
|
||||
--img-bg-color: rgba(128, 128, 128, 0.2);
|
||||
|
@ -267,6 +267,14 @@ button[hidden] {
|
|||
:is(button, .button).plain5:not(:disabled, .disabled):is(:hover, :focus) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
:is(button, .button).plain6 {
|
||||
background-color: var(--bg-blur-color);
|
||||
color: var(--link-color);
|
||||
border: 1px solid var(--link-color);
|
||||
}
|
||||
:is(button, .button).plain6:not(:disabled, .disabled):is(:hover, :focus) {
|
||||
background-color: var(--link-bg-color);
|
||||
}
|
||||
:is(button, .button).light {
|
||||
background-color: var(--bg-faded-color);
|
||||
color: var(--text-color);
|
||||
|
|
|
@ -2,6 +2,9 @@ import './index.css';
|
|||
|
||||
import './cloak-mode.css';
|
||||
|
||||
// Polyfill needed for Firefox < 122
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1423593
|
||||
import '@formatjs/intl-segmenter/polyfill';
|
||||
import { render } from 'preact';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
|
|
|
@ -259,9 +259,14 @@ function AccountStatuses() {
|
|||
|
||||
const { displayName, acct, emojis } = account || {};
|
||||
|
||||
const accountInfoMemo = useMemo(() => {
|
||||
const filterBarRef = useRef();
|
||||
const TimelineStart = useMemo(() => {
|
||||
const filtered =
|
||||
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountInfo
|
||||
instance={instance}
|
||||
account={cachedAccount || id}
|
||||
|
@ -269,17 +274,6 @@ function AccountStatuses() {
|
|||
authenticated={authenticated}
|
||||
standalone
|
||||
/>
|
||||
);
|
||||
}, [id, instance, authenticated, fetchAccount]);
|
||||
|
||||
const filterBarRef = useRef();
|
||||
const TimelineStart = useMemo(() => {
|
||||
const filtered =
|
||||
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||
|
||||
return (
|
||||
<>
|
||||
{accountInfoMemo}
|
||||
<div
|
||||
class="filter-bar"
|
||||
ref={filterBarRef}
|
||||
|
@ -418,6 +412,7 @@ function AccountStatuses() {
|
|||
instance,
|
||||
authenticated,
|
||||
featuredTags,
|
||||
fetchAccount,
|
||||
searchEnabled,
|
||||
...allSearchParams,
|
||||
]);
|
||||
|
|
1082
src/pages/catchup.css
Normal file
1082
src/pages/catchup.css
Normal file
File diff suppressed because it is too large
Load diff
1704
src/pages/catchup.jsx
Normal file
1704
src/pages/catchup.jsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -143,7 +143,6 @@ function List(props) {
|
|||
/>
|
||||
{showListAddEditModal && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowListAddEditModal(false);
|
||||
|
@ -167,7 +166,6 @@ function List(props) {
|
|||
)}
|
||||
{showManageMembersModal && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowManageMembersModal(false);
|
||||
|
|
|
@ -108,7 +108,6 @@ function Lists() {
|
|||
</div>
|
||||
{showListAddEditModal && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowListAddEditModal(false);
|
||||
|
|
|
@ -57,13 +57,14 @@
|
|||
width: fit-content;
|
||||
margin: -0.25em auto 0;
|
||||
line-height: 1;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
background-color: var(--bg-blur-color);
|
||||
/* background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--bg-color),
|
||||
var(--bg-blur-color)
|
||||
); */
|
||||
backdrop-filter: blur(16px) saturate(3);
|
||||
padding: 2px 4px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -433,7 +433,7 @@ function Settings({ onClose }) {
|
|||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{!!IMG_ALT_API_URL && (
|
||||
{!!IMG_ALT_API_URL && authenticated && (
|
||||
<li>
|
||||
<label>
|
||||
<input
|
||||
|
@ -464,6 +464,39 @@ function Settings({ onClose }) {
|
|||
</div>
|
||||
</li>
|
||||
)}
|
||||
{authenticated && (
|
||||
<li>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
snapStates.settings.shortcutSettingsCloudImportExport
|
||||
}
|
||||
onChange={(e) => {
|
||||
states.settings.shortcutSettingsCloudImportExport =
|
||||
e.target.checked;
|
||||
}}
|
||||
/>{' '}
|
||||
"Cloud" import/export for shortcuts settings{' '}
|
||||
<Icon icon="cloud" class="more-insignificant" />
|
||||
</label>
|
||||
<div class="sub-section insignificant">
|
||||
<small>
|
||||
⚠️⚠️⚠️ Very experimental.
|
||||
<br />
|
||||
Stored in your own profile’s notes. Profile (private) notes
|
||||
are mainly used for other profiles, and hidden for own
|
||||
profile.
|
||||
</small>
|
||||
</div>
|
||||
<div class="sub-section insignificant">
|
||||
<small>
|
||||
Note: This feature uses currently-logged-in instance server
|
||||
API.
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<label>
|
||||
<input
|
||||
|
|
|
@ -906,7 +906,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
|||
!!heroStatus?.repliesCount &&
|
||||
!hasDescendants && (
|
||||
<div class="status-loading">
|
||||
<Loader />
|
||||
<Loader abrupt={heroStatus.repliesCount >= 3} />
|
||||
</div>
|
||||
)}
|
||||
{uiState === 'error' &&
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import './trending.css';
|
||||
import '../components/links-bar.css';
|
||||
|
||||
import { MenuItem } from '@szhsin/react-menu';
|
||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||
|
@ -67,7 +67,7 @@ function Trending({ columnMode, ...props }) {
|
|||
|
||||
// Get links
|
||||
try {
|
||||
const { value } = await fetchLinks(masto);
|
||||
const { value } = await fetchLinks(masto, instance);
|
||||
// 4 types available: link, photo, video, rich
|
||||
// Only want links for now
|
||||
const links = value?.filter?.((link) => link.type === 'link');
|
||||
|
|
|
@ -9,20 +9,20 @@ import {
|
|||
set,
|
||||
} from 'idb-keyval';
|
||||
|
||||
const draftsStore = createStore('drafts-db', 'drafts-store');
|
||||
|
||||
// Add additonal `draftsStore` parameter to all methods
|
||||
|
||||
const drafts = {
|
||||
set: (key, val) => set(key, val, draftsStore),
|
||||
get: (key) => get(key, draftsStore),
|
||||
getMany: (keys) => getMany(keys, draftsStore),
|
||||
del: (key) => del(key, draftsStore),
|
||||
delMany: (keys) => delMany(keys, draftsStore),
|
||||
clear: () => clear(draftsStore),
|
||||
keys: () => keys(draftsStore),
|
||||
function initDB(dbName, storeName) {
|
||||
const store = createStore(dbName, storeName);
|
||||
return {
|
||||
set: (key, val) => set(key, val, store),
|
||||
get: (key) => get(key, store),
|
||||
getMany: (keys) => getMany(keys, store),
|
||||
del: (key) => del(key, store),
|
||||
delMany: (keys) => delMany(keys, store),
|
||||
clear: () => clear(store),
|
||||
keys: () => keys(store),
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
drafts,
|
||||
drafts: initDB('drafts-db', 'drafts-store'),
|
||||
catchup: initDB('catchup-db', 'catchup-store'),
|
||||
};
|
||||
|
|
|
@ -52,6 +52,7 @@ const states = proxy({
|
|||
showGenericAccounts: false,
|
||||
showMediaAlt: false,
|
||||
showEmbedModal: false,
|
||||
showReportModal: false,
|
||||
// Shortcuts
|
||||
shortcuts: [],
|
||||
// Settings
|
||||
|
@ -64,6 +65,7 @@ const states = proxy({
|
|||
contentTranslationTargetLanguage: null,
|
||||
contentTranslationHideLanguages: [],
|
||||
contentTranslationAutoInline: false,
|
||||
shortcutSettingsCloudImportExport: false,
|
||||
mediaAltGenerator: false,
|
||||
cloakMode: false,
|
||||
},
|
||||
|
@ -93,6 +95,8 @@ export function initStates() {
|
|||
store.account.get('settings-contentTranslationHideLanguages') || [];
|
||||
states.settings.contentTranslationAutoInline =
|
||||
store.account.get('settings-contentTranslationAutoInline') ?? false;
|
||||
states.settings.shortcutSettingsCloudImportExport =
|
||||
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
|
||||
states.settings.mediaAltGenerator =
|
||||
store.account.get('settings-mediaAltGenerator') ?? false;
|
||||
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
||||
|
@ -120,6 +124,9 @@ subscribe(states, (changes) => {
|
|||
if (path.join('.') === 'settings.contentTranslationAutoInline') {
|
||||
store.account.set('settings-contentTranslationAutoInline', !!value);
|
||||
}
|
||||
if (path.join('.') === 'settings.shortcutSettingsCloudImportExport') {
|
||||
store.account.set('settings-shortcutSettingsCloudImportExport', !!value);
|
||||
}
|
||||
if (path.join('.') === 'settings.contentTranslationTargetLanguage') {
|
||||
console.log('SET', value);
|
||||
store.account.set('settings-contentTranslationTargetLanguage', value);
|
||||
|
|
|
@ -37,6 +37,7 @@ const rollbarCode = fs.readFileSync(
|
|||
export default defineConfig({
|
||||
base: './',
|
||||
envPrefix: allowedEnvPrefixes,
|
||||
appType: 'mpa',
|
||||
mode: NODE_ENV,
|
||||
define: {
|
||||
__BUILD_TIME__: JSON.stringify(now),
|
||||
|
@ -93,6 +94,7 @@ export default defineConfig({
|
|||
purpose: 'maskable',
|
||||
},
|
||||
],
|
||||
categories: ['social', 'news'],
|
||||
},
|
||||
strategies: 'injectManifest',
|
||||
injectRegister: 'inline',
|
||||
|
@ -115,6 +117,9 @@ export default defineConfig({
|
|||
compose: resolve(__dirname, 'compose/index.html'),
|
||||
},
|
||||
output: {
|
||||
manualChunks: {
|
||||
'intl-segmenter-polyfill': ['@formatjs/intl-segmenter/polyfill'],
|
||||
},
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
const { facadeModuleId } = chunkInfo;
|
||||
if (facadeModuleId && facadeModuleId.includes('icon')) {
|
||||
|
|
Loading…
Reference in a new issue