Compare commits
119 commits
8cc8e03d66
...
9d09e32f24
Author | SHA1 | Date | |
---|---|---|---|
Alexander Yakovlev | 9d09e32f24 | ||
5695b3ca1e | |||
15c113ecb1 | |||
4a75d6f172 | |||
8f43099840 | |||
a2743f9940 | |||
4c2210c68b | |||
da909e4084 | |||
552ad249e5 | |||
9a5704ee95 | |||
c7f68c8971 | |||
e8219e458d | |||
6157ee105c | |||
4718ef36b0 | |||
2723ef4593 | |||
d1965a84b5 | |||
c7762cc56f | |||
cf05568e0c | |||
69c47489e3 | |||
861ad83423 | |||
cd3ed64e48 | |||
2e28c147b9 | |||
fef033b282 | |||
3dbbba0be2 | |||
0b8cbbef51 | |||
f72ec0aba5 | |||
d63e6c87c4 | |||
f5ea96a093 | |||
0e1be5dbdc | |||
4843970e1b | |||
a0367f4860 | |||
687a08b2a4 | |||
ac07479edd | |||
306a96eec3 | |||
061d769901 | |||
cf1c10b338 | |||
7f6ef4ff96 | |||
ce190cbc50 | |||
e7e4f15234 | |||
c005745ad0 | |||
0b81b5bfd2 | |||
b48d32e503 | |||
ed309b289f | |||
ecc5fc5bbe | |||
7eb77f5d1b | |||
3f4832965d | |||
b7ed27ef70 | |||
c9a48cf482 | |||
c0ad216227 | |||
8a9f1a3c25 | |||
375c4b5d00 | |||
f522d8e932 | |||
bd46af6166 | |||
29e9e15d3f | |||
42dac0720f | |||
d348c458b3 | |||
427207ae5a | |||
531147cbc3 | |||
e0c2570875 | |||
2b2f6c28a9 | |||
4a9cae9cb6 | |||
c578b41105 | |||
cfdbecc608 | |||
7c81548320 | |||
8cab77415e | |||
8b36cef510 | |||
4e67edac5e | |||
0bf5ef52ac | |||
7a7d51f56e | |||
48e1a0753a | |||
195c2e2960 | |||
60c0d1cca0 | |||
6292557bc9 | |||
b79ce92aef | |||
6bb6b9c350 | |||
0b4c720153 | |||
02d1339b29 | |||
Alexander Yakovlev | 2197dac514 | ||
93c871353a | |||
641d22a7cc | |||
0fd378811f | |||
afb1f6d520 | |||
fcb0074f49 | |||
8108151fb6 | |||
d8b0adfe97 | |||
cef4e6373e | |||
4d138f5773 | |||
0db10bf7d0 | |||
7ab6da5e9b | |||
beed3ca18c | |||
abd5031602 | |||
346dba9ed7 | |||
0ceb6ffd06 | |||
488aece050 | |||
ecde88d6a1 | |||
94dcd1606a | |||
b479fa1f35 | |||
ab0472de02 | |||
1bf8616957 | |||
631333ba9e | |||
69d77c368e | |||
bb3621e424 | |||
e1447053b3 | |||
aaf64bbc34 | |||
52b60fa38b | |||
3acfc00ec0 | |||
f8b5e9563c | |||
6f3f83a620 | |||
315ce98511 | |||
3cfc35898b | |||
ffc216cfed | |||
35e34c0bc6 | |||
b023a43fee | |||
44f6d9cda0 | |||
c466e0c279 | |||
fa99debabd | |||
58778aba45 | |||
b913c8817d | |||
ffb7ce1c63 |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -10,6 +10,7 @@ assignees: ''
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
- Which site: [e.g. dev.phanpy.social OR phanpy.social]
|
- Which site: [e.g. dev.phanpy.social OR phanpy.social]
|
||||||
|
- Which site version: [On Phanpy, go to Settings -> About]
|
||||||
- Which instance: [e.g. mastodon.social]
|
- Which instance: [e.g. mastodon.social]
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
|
|
|
@ -200,6 +200,7 @@ These are self-hosted by other wonderful folks.
|
||||||
- [phanpy.bauxite.tech](https://phanpy.bauxite.tech) by [@b4ux1t3@hachyderm.io](https://hachyderm.io/@b4ux1t3)
|
- [phanpy.bauxite.tech](https://phanpy.bauxite.tech) by [@b4ux1t3@hachyderm.io](https://hachyderm.io/@b4ux1t3)
|
||||||
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
|
- [phanpy.hear-me.social](https://phanpy.hear-me.social) by [@admin@hear-me.social](https://hear-me.social/@admin)
|
||||||
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
|
- [phanpy.fulda.social](https://phanpy.fulda.social) by [@Ganneff@fulda.social](https://fulda.social/@Ganneff)
|
||||||
|
- [phanpy.crmbl.uk](https://phanpy.crmbl.uk) by [@snail@crmbl.uk](https://mstdn.crmbl.uk/@snail)
|
||||||
|
|
||||||
> Note: Add yours by creating a pull request.
|
> Note: Add yours by creating a pull request.
|
||||||
|
|
||||||
|
|
571
package-lock.json
generated
571
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "~0.5.4",
|
"@formatjs/intl-localematcher": "~0.5.4",
|
||||||
|
"@formatjs/intl-segmenter": "~11.5.5",
|
||||||
"@formkit/auto-animate": "~0.8.1",
|
"@formkit/auto-animate": "~0.8.1",
|
||||||
"@github/text-expander-element": "~2.6.1",
|
"@github/text-expander-element": "~2.6.1",
|
||||||
"@iconify-icons/mingcute": "~1.2.9",
|
"@iconify-icons/mingcute": "~1.2.9",
|
||||||
|
@ -22,10 +23,11 @@
|
||||||
"dayjs-twitter": "~0.5.0",
|
"dayjs-twitter": "~0.5.0",
|
||||||
"fast-blurhash": "~1.1.2",
|
"fast-blurhash": "~1.1.2",
|
||||||
"fast-equals": "~5.0.1",
|
"fast-equals": "~5.0.1",
|
||||||
|
"html-prettify": "^1.0.7",
|
||||||
"idb-keyval": "~6.2.1",
|
"idb-keyval": "~6.2.1",
|
||||||
"just-debounce-it": "~3.2.0",
|
"just-debounce-it": "~3.2.0",
|
||||||
"lz-string": "~1.5.0",
|
"lz-string": "~1.5.0",
|
||||||
"masto": "~6.6.0",
|
"masto": "~6.6.4",
|
||||||
"moize": "~6.1.6",
|
"moize": "~6.1.6",
|
||||||
"p-retry": "~6.2.0",
|
"p-retry": "~6.2.0",
|
||||||
"p-throttle": "~6.1.0",
|
"p-throttle": "~6.1.0",
|
||||||
|
@ -34,27 +36,26 @@
|
||||||
"react-intersection-observer": "~9.8.1",
|
"react-intersection-observer": "~9.8.1",
|
||||||
"react-quick-pinch-zoom": "~5.1.0",
|
"react-quick-pinch-zoom": "~5.1.0",
|
||||||
"react-router-dom": "6.6.2",
|
"react-router-dom": "6.6.2",
|
||||||
"runes2": "~1.1.4",
|
"string-length": "6.0.0",
|
||||||
"string-length": "5.0.1",
|
|
||||||
"swiped-events": "~1.1.9",
|
"swiped-events": "~1.1.9",
|
||||||
"toastify-js": "~1.12.0",
|
"toastify-js": "~1.12.0",
|
||||||
"uid": "~2.0.2",
|
"uid": "~2.0.2",
|
||||||
"use-debounce": "~10.0.0",
|
"use-debounce": "~10.0.0",
|
||||||
"use-long-press": "~3.2.0",
|
"use-long-press": "~3.2.0",
|
||||||
"use-resize-observer": "~9.1.0",
|
"use-resize-observer": "~9.1.0",
|
||||||
"valtio": "1.9.0"
|
"valtio": "1.13.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "~2.8.1",
|
"@preact/preset-vite": "~2.8.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||||
"postcss": "~8.4.35",
|
"postcss": "~8.4.35",
|
||||||
"postcss-dark-theme-class": "~1.2.1",
|
"postcss-dark-theme-class": "~1.2.1",
|
||||||
"postcss-preset-env": "~9.4.0",
|
"postcss-preset-env": "~9.5.1",
|
||||||
"twitter-text": "~3.1.0",
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~5.1.4",
|
"vite": "~5.1.6",
|
||||||
"vite-plugin-generate-file": "~0.1.1",
|
"vite-plugin-generate-file": "~0.1.1",
|
||||||
"vite-plugin-html-config": "~1.0.11",
|
"vite-plugin-html-config": "~1.0.11",
|
||||||
"vite-plugin-pwa": "~0.19.0",
|
"vite-plugin-pwa": "~0.19.4",
|
||||||
"vite-plugin-remove-console": "~2.2.0",
|
"vite-plugin-remove-console": "~2.2.0",
|
||||||
"workbox-cacheable-response": "~7.0.0",
|
"workbox-cacheable-response": "~7.0.0",
|
||||||
"workbox-expiration": "~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 isRemote = !sameOrigin;
|
||||||
const isImage = request.destination === 'image';
|
const isImage = request.destination === 'image';
|
||||||
const isAvatar = request.url.includes('/avatars/');
|
const isAvatar = request.url.includes('/avatars/');
|
||||||
|
const isCustomEmoji = request.url.includes('/custom/_emojis');
|
||||||
const isEmoji = request.url.includes('/emoji/');
|
const isEmoji = request.url.includes('/emoji/');
|
||||||
return isRemote && isImage && (isAvatar || isEmoji);
|
return isRemote && isImage && (isAvatar || isCustomEmoji || isEmoji);
|
||||||
},
|
},
|
||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: 'remote-images',
|
cacheName: 'remote-images',
|
||||||
|
|
13
src/app.css
13
src/app.css
|
@ -34,6 +34,8 @@ a.mention span {
|
||||||
text-decoration-color: inherit;
|
text-decoration-color: inherit;
|
||||||
text-decoration-thickness: 2px;
|
text-decoration-thickness: 2px;
|
||||||
text-underline-offset: 2px;
|
text-underline-offset: 2px;
|
||||||
|
font-variant-numeric: slashed-zero;
|
||||||
|
font-feature-settings: 'ss01';
|
||||||
}
|
}
|
||||||
/* a.mention:has(span).hashtag {
|
/* a.mention:has(span).hashtag {
|
||||||
color: var(--link-light-color);
|
color: var(--link-light-color);
|
||||||
|
@ -1757,7 +1759,7 @@ body > .szh-menu-container {
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
/* overflow: hidden; */
|
/* overflow: hidden; */
|
||||||
}
|
}
|
||||||
.szh-menu[aria-label='Submenu'] {
|
.szh-menu[aria-label='Submenu'].menu-blur {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
|
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
|
||||||
|
@ -1789,6 +1791,7 @@ body > .szh-menu-container {
|
||||||
animation-duration: 0.3s;
|
animation-duration: 0.3s;
|
||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
min-width: min(12em, 90vw);
|
||||||
}
|
}
|
||||||
.szh-menu .footer {
|
.szh-menu .footer {
|
||||||
margin: 8px 0 -8px;
|
margin: 8px 0 -8px;
|
||||||
|
@ -1925,6 +1928,10 @@ body > .szh-menu-container {
|
||||||
).danger {
|
).danger {
|
||||||
color: var(--red-color);
|
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
|
||||||
.szh-menu__item:not(.szh-menu__item--disabled):not(
|
.szh-menu__item:not(.szh-menu__item--disabled):not(
|
||||||
.szh-menu__item--hover
|
.szh-menu__item--hover
|
||||||
|
@ -2669,6 +2676,10 @@ ul.link-list li a .icon {
|
||||||
box-shadow: 0px 1px var(--bg-blur-color);
|
box-shadow: 0px 1px var(--bg-blur-color);
|
||||||
transition: transform 0.4s var(--timing-function);
|
transition: transform 0.4s var(--timing-function);
|
||||||
--back-transition: transform 0.4s ease-out;
|
--back-transition: transform 0.4s ease-out;
|
||||||
|
|
||||||
|
&:is(:empty, :has(> a:empty)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline:not(.flat) > li > a {
|
.timeline:not(.flat) > li > a {
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
|
|
21
src/app.jsx
21
src/app.jsx
|
@ -1,6 +1,7 @@
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
|
import { lazy, Suspense } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
@ -17,14 +18,14 @@ import ComposeButton from './components/compose-button';
|
||||||
import { ICONS } from './components/ICONS';
|
import { ICONS } from './components/ICONS';
|
||||||
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
|
||||||
import Loader from './components/loader';
|
import Loader from './components/loader';
|
||||||
import Modals from './components/modals';
|
// import Modals from './components/modals';
|
||||||
import NotificationService from './components/notification-service';
|
import NotificationService from './components/notification-service';
|
||||||
import SearchCommand from './components/search-command';
|
import SearchCommand from './components/search-command';
|
||||||
import Shortcuts from './components/shortcuts';
|
import Shortcuts from './components/shortcuts';
|
||||||
import NotFound from './pages/404';
|
import NotFound from './pages/404';
|
||||||
import AccountStatuses from './pages/account-statuses';
|
import AccountStatuses from './pages/account-statuses';
|
||||||
import Bookmarks from './pages/bookmarks';
|
import Bookmarks from './pages/bookmarks';
|
||||||
import Catchup from './pages/catchup';
|
// import Catchup from './pages/catchup';
|
||||||
import Favourites from './pages/favourites';
|
import Favourites from './pages/favourites';
|
||||||
import FollowedHashtags from './pages/followed-hashtags';
|
import FollowedHashtags from './pages/followed-hashtags';
|
||||||
import Following from './pages/following';
|
import Following from './pages/following';
|
||||||
|
@ -55,6 +56,9 @@ import store from './utils/store';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
import { getCurrentAccount } from './utils/store-utils';
|
||||||
import './utils/toast-alert';
|
import './utils/toast-alert';
|
||||||
|
|
||||||
|
const Catchup = lazy(() => import('./pages/catchup'));
|
||||||
|
const Modals = lazy(() => import('./components/modals'));
|
||||||
|
|
||||||
window.__STATES__ = states;
|
window.__STATES__ = states;
|
||||||
window.__STATES_STATS__ = () => {
|
window.__STATES_STATS__ = () => {
|
||||||
const keys = [
|
const keys = [
|
||||||
|
@ -382,7 +386,9 @@ function App() {
|
||||||
)}
|
)}
|
||||||
{isLoggedIn && <ComposeButton />}
|
{isLoggedIn && <ComposeButton />}
|
||||||
{isLoggedIn && <Shortcuts />}
|
{isLoggedIn && <Shortcuts />}
|
||||||
<Modals />
|
<Suspense>
|
||||||
|
<Modals />
|
||||||
|
</Suspense>
|
||||||
{isLoggedIn && <NotificationService />}
|
{isLoggedIn && <NotificationService />}
|
||||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||||
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
|
{uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
|
||||||
|
@ -458,7 +464,14 @@ function SecondaryRoutes({ isLoggedIn }) {
|
||||||
<Route path=":id" element={<List />} />
|
<Route path=":id" element={<List />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/ft" element={<FollowedHashtags />} />
|
<Route path="/ft" element={<FollowedHashtags />} />
|
||||||
<Route path="/catchup" element={<Catchup />} />
|
<Route
|
||||||
|
path="/catchup"
|
||||||
|
element={
|
||||||
|
<Suspense>
|
||||||
|
<Catchup />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
|
<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,12 +8,14 @@ body.cloak,
|
||||||
.name-text *,
|
.name-text *,
|
||||||
.status .content-container,
|
.status .content-container,
|
||||||
.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)),
|
||||||
.account-container :is(header, main > *:not(.actions)) *,
|
.account-container :is(header, main > *:not(.actions)) *,
|
||||||
.header-double-lines,
|
.header-double-lines,
|
||||||
.account-block,
|
.account-block,
|
||||||
.post-peek-html * {
|
.catchup-filters .filter-author *,
|
||||||
|
.post-peek-html *,
|
||||||
|
.post-peek-content > * {
|
||||||
text-decoration-thickness: 1.1em;
|
text-decoration-thickness: 1.1em;
|
||||||
text-decoration-line: line-through;
|
text-decoration-line: line-through;
|
||||||
/* text-rendering: optimizeSpeed; */
|
/* text-rendering: optimizeSpeed; */
|
||||||
|
@ -21,7 +23,8 @@ body.cloak,
|
||||||
}
|
}
|
||||||
.name-text *,
|
.name-text *,
|
||||||
.status .content-container *,
|
.status .content-container *,
|
||||||
.account-container :is(header, main > *:not(.actions)) * {
|
.account-container :is(header, main > *:not(.actions)) *,
|
||||||
|
.post-peek-content > * {
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +42,16 @@ body.cloak,
|
||||||
/* SPECIAL CASES */
|
/* SPECIAL CASES */
|
||||||
|
|
||||||
@supports (display: -webkit-box) {
|
@supports (display: -webkit-box) {
|
||||||
body.cloak .card :is(.title, .meta) {
|
:is(body.cloak, .cloak) .card :is(.title, .meta) {
|
||||||
background-color: var(--text-color) !important;
|
background-color: currentColor !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.cloak,
|
||||||
|
.cloak {
|
||||||
|
.media-container figcaption,
|
||||||
|
.media-container figcaption > *,
|
||||||
|
.catchup-filters .filter-author * {
|
||||||
|
color: var(--text-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,11 @@ export const ICONS = {
|
||||||
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
||||||
speak: () => import('@iconify-icons/mingcute/radar-line'),
|
speak: () => import('@iconify-icons/mingcute/radar-line'),
|
||||||
building: () => import('@iconify-icons/mingcute/building-5-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'),
|
document: () => import('@iconify-icons/mingcute/document-line'),
|
||||||
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-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'),
|
||||||
|
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
|
||||||
|
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@ function AccountBlock({
|
||||||
<span>
|
<span>
|
||||||
<b>████████</b>
|
<b>████████</b>
|
||||||
<br />
|
<br />
|
||||||
<span class="account-block-acct">@██████</span>
|
<span class="account-block-acct">██████</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -62,6 +62,7 @@ function AccountBlock({
|
||||||
group,
|
group,
|
||||||
followersCount,
|
followersCount,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
locked,
|
||||||
} = account;
|
} = account;
|
||||||
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
|
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
|
||||||
if (accountInstance) {
|
if (accountInstance) {
|
||||||
|
@ -86,7 +87,7 @@ function AccountBlock({
|
||||||
class="account-block"
|
class="account-block"
|
||||||
href={url}
|
href={url}
|
||||||
target={external ? '_blank' : null}
|
target={external ? '_blank' : null}
|
||||||
title={`@${acct}`}
|
title={acct2 ? acct : `@${acct}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (external) return;
|
if (external) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -120,9 +121,16 @@ function AccountBlock({
|
||||||
</>
|
</>
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<span class="account-block-acct">
|
<span class="account-block-acct">
|
||||||
@{acct1}
|
{acct2 ? '' : '@'}
|
||||||
|
{acct1}
|
||||||
<wbr />
|
<wbr />
|
||||||
{acct2}
|
{acct2}
|
||||||
|
{locked && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<Icon icon="lock" size="s" alt="Locked" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{showActivity && (
|
{showActivity && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -343,7 +343,7 @@ function AccountInfo({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
'--header-color-1': headerCornerColors[0],
|
'--header-color-1': headerCornerColors[0],
|
||||||
'--header-color-2': headerCornerColors[1],
|
'--header-color-2': headerCornerColors[1],
|
||||||
|
@ -453,12 +453,15 @@ function AccountInfo({
|
||||||
e.target.classList.add('loaded');
|
e.target.classList.add('loaded');
|
||||||
try {
|
try {
|
||||||
// Get color from four corners of image
|
// Get color from four corners of image
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = window.OffscreenCanvas
|
||||||
|
? new OffscreenCanvas(1, 1)
|
||||||
|
: document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d', {
|
const ctx = canvas.getContext('2d', {
|
||||||
willReadFrequently: true,
|
willReadFrequently: true,
|
||||||
});
|
});
|
||||||
canvas.width = e.target.width;
|
canvas.width = e.target.width;
|
||||||
canvas.height = e.target.height;
|
canvas.height = e.target.height;
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
ctx.drawImage(e.target, 0, 0);
|
ctx.drawImage(e.target, 0, 0);
|
||||||
// const colors = [
|
// const colors = [
|
||||||
// ctx.getImageData(0, 0, 1, 1).data,
|
// ctx.getImageData(0, 0, 1, 1).data,
|
||||||
|
@ -1053,6 +1056,27 @@ function RelatedActions({
|
||||||
<MenuDivider />
|
<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">
|
<MenuItem href={url} target="_blank">
|
||||||
<Icon icon="external" />
|
<Icon icon="external" />
|
||||||
<small class="menu-double-lines">{niceAccountURL(url)}</small>
|
<small class="menu-double-lines">{niceAccountURL(url)}</small>
|
||||||
|
@ -1124,6 +1148,7 @@ function RelatedActions({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
<SubMenu
|
<SubMenu
|
||||||
|
menuClassName="menu-blur"
|
||||||
openTrigger="clickOnly"
|
openTrigger="clickOnly"
|
||||||
direction="bottom"
|
direction="bottom"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
|
@ -1352,7 +1377,6 @@ function RelatedActions({
|
||||||
</div>
|
</div>
|
||||||
{!!showTranslatedBio && (
|
{!!showTranslatedBio && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowTranslatedBio(false);
|
setShowTranslatedBio(false);
|
||||||
}}
|
}}
|
||||||
|
@ -1366,7 +1390,6 @@ function RelatedActions({
|
||||||
)}
|
)}
|
||||||
{!!showAddRemoveLists && (
|
{!!showAddRemoveLists && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowAddRemoveLists(false);
|
setShowAddRemoveLists(false);
|
||||||
}}
|
}}
|
||||||
|
@ -1379,7 +1402,6 @@ function RelatedActions({
|
||||||
)}
|
)}
|
||||||
{!!showPrivateNoteModal && (
|
{!!showPrivateNoteModal && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowPrivateNoteModal(false);
|
setShowPrivateNoteModal(false);
|
||||||
}}
|
}}
|
||||||
|
@ -1571,7 +1593,6 @@ function AddRemoveListsSheet({ accountID, onClose }) {
|
||||||
</main>
|
</main>
|
||||||
{showListAddEditModal && (
|
{showListAddEditModal && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowListAddEditModal(false);
|
setShowListAddEditModal(false);
|
||||||
|
|
|
@ -21,6 +21,7 @@ const canvas = window.OffscreenCanvas
|
||||||
const ctx = canvas.getContext('2d', {
|
const ctx = canvas.getContext('2d', {
|
||||||
willReadFrequently: true,
|
willReadFrequently: true,
|
||||||
});
|
});
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
function Avatar({ url, size, alt = '', squircle, ...props }) {
|
function Avatar({ url, size, alt = '', squircle, ...props }) {
|
||||||
size = SIZES[size] || size || SIZES.m;
|
size = SIZES[size] || size || SIZES.m;
|
||||||
|
|
|
@ -95,6 +95,10 @@
|
||||||
0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
0 1px 10px var(--bg-color), 0 1px 10px var(--bg-color),
|
||||||
0 1px 10px var(--bg-color);
|
0 1px 10px var(--bg-color);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#_compose-container .status-preview-legend.reply-to {
|
#_compose-container .status-preview-legend.reply-to {
|
||||||
color: var(--reply-to-color);
|
color: var(--reply-to-color);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { deepEqual } from 'fast-equals';
|
||||||
import { forwardRef } from 'preact/compat';
|
import { forwardRef } from 'preact/compat';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { substring } from 'runes2';
|
|
||||||
import stringLength from 'string-length';
|
import stringLength from 'string-length';
|
||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
||||||
|
@ -131,6 +130,7 @@ const SCAN_RE = new RegExp(
|
||||||
'g',
|
'g',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const segmenter = new Intl.Segmenter();
|
||||||
function highlightText(text, { maxCharacters = Infinity }) {
|
function highlightText(text, { maxCharacters = Infinity }) {
|
||||||
// Accept text string, return formatted HTML string
|
// Accept text string, return formatted HTML string
|
||||||
// Escape all HTML special characters
|
// Escape all HTML special characters
|
||||||
|
@ -143,19 +143,25 @@ function highlightText(text, { maxCharacters = Infinity }) {
|
||||||
|
|
||||||
// Exceeded characters limit
|
// Exceeded characters limit
|
||||||
const { composerCharacterCount } = states;
|
const { composerCharacterCount } = states;
|
||||||
let leftoverHTML = '';
|
|
||||||
if (composerCharacterCount > maxCharacters) {
|
if (composerCharacterCount > maxCharacters) {
|
||||||
// NOTE: runes2 substring considers surrogate pairs
|
|
||||||
// const leftoverCount = composerCharacterCount - maxCharacters;
|
|
||||||
// Highlight exceeded characters
|
// Highlight exceeded characters
|
||||||
leftoverHTML =
|
let withinLimitHTML = '',
|
||||||
'<mark class="compose-highlight-exceeded">' +
|
exceedLimitHTML = '';
|
||||||
// html.slice(-leftoverCount) +
|
const htmlSegments = segmenter.segment(html);
|
||||||
substring(html, maxCharacters) +
|
for (const { segment, index } of htmlSegments) {
|
||||||
'</mark>';
|
if (index < maxCharacters) {
|
||||||
// html = html.slice(0, -leftoverCount);
|
withinLimitHTML += segment;
|
||||||
html = substring(html, 0, maxCharacters);
|
} else {
|
||||||
return html + leftoverHTML;
|
exceedLimitHTML += segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exceedLimitHTML) {
|
||||||
|
exceedLimitHTML =
|
||||||
|
'<mark class="compose-highlight-exceeded">' +
|
||||||
|
exceedLimitHTML +
|
||||||
|
'</mark>';
|
||||||
|
}
|
||||||
|
return withinLimitHTML + exceedLimitHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
@ -168,6 +174,8 @@ function highlightText(text, { maxCharacters = Infinity }) {
|
||||||
); // Emoji shortcodes
|
); // Emoji shortcodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rtf = new Intl.RelativeTimeFormat();
|
||||||
|
|
||||||
function Compose({
|
function Compose({
|
||||||
onClose,
|
onClose,
|
||||||
replyToStatus,
|
replyToStatus,
|
||||||
|
@ -229,6 +237,12 @@ function Compose({
|
||||||
};
|
};
|
||||||
const focusTextarea = () => {
|
const focusTextarea = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (!textareaRef.current) return;
|
||||||
|
// status starts with newline, focus on first position
|
||||||
|
if (draftStatus?.status?.startsWith?.('\n')) {
|
||||||
|
textareaRef.current.selectionStart = 0;
|
||||||
|
textareaRef.current.selectionEnd = 0;
|
||||||
|
}
|
||||||
console.debug('FOCUS textarea');
|
console.debug('FOCUS textarea');
|
||||||
textareaRef.current?.focus();
|
textareaRef.current?.focus();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
@ -625,6 +639,16 @@ function Compose({
|
||||||
return [topLanguages, restLanguages];
|
return [topLanguages, restLanguages];
|
||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
|
const replyToStatusMonthsAgo = useMemo(
|
||||||
|
() =>
|
||||||
|
!!replyToStatus?.createdAt &&
|
||||||
|
Math.floor(
|
||||||
|
(Date.now() - new Date(replyToStatus.createdAt)) /
|
||||||
|
(1000 * 60 * 60 * 24 * 30),
|
||||||
|
),
|
||||||
|
[replyToStatus],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="compose-container-outer">
|
<div id="compose-container-outer">
|
||||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||||
|
@ -774,6 +798,16 @@ function Compose({
|
||||||
Replying to @
|
Replying to @
|
||||||
{replyToStatus.account.acct || replyToStatus.account.username}
|
{replyToStatus.account.acct || replyToStatus.account.username}
|
||||||
’s post
|
’s post
|
||||||
|
{replyToStatusMonthsAgo >= 3 && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
(
|
||||||
|
<strong>
|
||||||
|
{rtf.format(-replyToStatusMonthsAgo, 'month')}
|
||||||
|
</strong>
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1254,7 +1288,6 @@ function Compose({
|
||||||
</div>
|
</div>
|
||||||
{showEmoji2Picker && (
|
{showEmoji2Picker && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowEmoji2Picker(false);
|
setShowEmoji2Picker(false);
|
||||||
|
@ -1768,7 +1801,6 @@ function MediaAttachment({
|
||||||
</div>
|
</div>
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default memo(function KeyboardShortcutsHelp() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!!snapStates.showKeyboardShortcutsHelp && (
|
!!snapStates.showKeyboardShortcutsHelp && (
|
||||||
<Modal class="light" onClose={onClose}>
|
<Modal onClose={onClose}>
|
||||||
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
|
<div id="keyboard-shortcuts-help-container" class="sheet" tabindex="-1">
|
||||||
<button type="button" class="sheet-close" onClick={onClose}>
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
<Icon icon="x" />
|
<Icon icon="x" />
|
||||||
|
|
|
@ -54,6 +54,7 @@ const AltBadge = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MEDIA_CAPTION_LIMIT = 140;
|
const MEDIA_CAPTION_LIMIT = 140;
|
||||||
|
const MEDIA_CAPTION_LIMIT_LONGER = 280;
|
||||||
export const isMediaCaptionLong = mem((caption) =>
|
export const isMediaCaptionLong = mem((caption) =>
|
||||||
caption?.length
|
caption?.length
|
||||||
? caption.length > MEDIA_CAPTION_LIMIT ||
|
? caption.length > MEDIA_CAPTION_LIMIT ||
|
||||||
|
@ -69,6 +70,7 @@ function Media({
|
||||||
showOriginal,
|
showOriginal,
|
||||||
autoAnimate,
|
autoAnimate,
|
||||||
showCaption,
|
showCaption,
|
||||||
|
allowLongerCaption,
|
||||||
altIndex,
|
altIndex,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
}) {
|
}) {
|
||||||
|
@ -198,8 +200,15 @@ function Media({
|
||||||
};
|
};
|
||||||
|
|
||||||
const longDesc = isMediaCaptionLong(description);
|
const longDesc = isMediaCaptionLong(description);
|
||||||
const showInlineDesc =
|
let showInlineDesc =
|
||||||
!!showCaption && !showOriginal && !!description && !longDesc;
|
!!showCaption && !showOriginal && !!description && !longDesc;
|
||||||
|
if (
|
||||||
|
allowLongerCaption &&
|
||||||
|
!showInlineDesc &&
|
||||||
|
description?.length <= MEDIA_CAPTION_LIMIT_LONGER
|
||||||
|
) {
|
||||||
|
showInlineDesc = true;
|
||||||
|
}
|
||||||
const Figure = !showInlineDesc
|
const Figure = !showInlineDesc
|
||||||
? Fragment
|
? Fragment
|
||||||
: (props) => {
|
: (props) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Menu, MenuItem, SubMenu } from '@szhsin/react-menu';
|
import { MenuItem, SubMenu } from '@szhsin/react-menu';
|
||||||
import { cloneElement } from 'preact';
|
import { cloneElement } from 'preact';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useRef } from 'preact/hooks';
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ function MenuConfirm({
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
menuItemClassName,
|
menuItemClassName,
|
||||||
menuFooter,
|
menuFooter,
|
||||||
|
menuExtras,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
const { children, onClick, ...restProps } = props;
|
const { children, onClick, ...restProps } = props;
|
||||||
|
@ -53,6 +54,7 @@ function MenuConfirm({
|
||||||
<MenuItem className={menuItemClassName} onClick={onClick}>
|
<MenuItem className={menuItemClassName} onClick={onClick}>
|
||||||
{confirmLabel}
|
{confirmLabel}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{menuExtras}
|
||||||
{menuFooter}
|
{menuFooter}
|
||||||
</Parent>
|
</Parent>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { FocusableItem } from '@szhsin/react-menu';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
|
|
||||||
function MenuLink(props) {
|
function MenuLink(props) {
|
||||||
|
const { className, disabled, ...restProps } = props;
|
||||||
return (
|
return (
|
||||||
<FocusableItem>
|
<FocusableItem className={className} disabled={disabled}>
|
||||||
{({ ref, closeMenu }) => (
|
{({ ref, closeMenu }) => (
|
||||||
<Link
|
<Link
|
||||||
{...props}
|
{...restProps}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={({ detail }) =>
|
onClick={({ detail }) =>
|
||||||
closeMenu(detail === 0 ? 'Enter' : undefined)
|
closeMenu(detail === 0 ? 'Enter' : undefined)
|
||||||
|
|
|
@ -9,17 +9,18 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--backdrop-color);
|
background-color: var(--backdrop-color);
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
animation: appear 0.5s var(--timing-function) both;
|
animation: appear 0.5s var(--timing-function) both;
|
||||||
}
|
|
||||||
#modal-container > div .sheet {
|
|
||||||
transition: transform 0.3s var(--timing-function);
|
|
||||||
transform-origin: center bottom;
|
|
||||||
}
|
|
||||||
#modal-container > div:has(~ div) .sheet {
|
|
||||||
transform: scale(0.975);
|
|
||||||
}
|
|
||||||
|
|
||||||
#modal-container > .light {
|
&.solid {
|
||||||
backdrop-filter: saturate(0.75);
|
background-color: var(--backdrop-solid-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet {
|
||||||
|
transition: transform 0.3s var(--timing-function);
|
||||||
|
transform-origin: center bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(~ div) .sheet {
|
||||||
|
transform: scale(0.975);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ function Modal({ children, onClose, onClick, class: className }) {
|
||||||
const focusElement =
|
const focusElement =
|
||||||
modalRef.current?.querySelector('[tabindex="-1"]');
|
modalRef.current?.querySelector('[tabindex="-1"]');
|
||||||
const isFocusable =
|
const isFocusable =
|
||||||
|
!!focusElement &&
|
||||||
getComputedStyle(focusElement)?.pointerEvents !== 'none';
|
getComputedStyle(focusElement)?.pointerEvents !== 'none';
|
||||||
if (focusElement && isFocusable) {
|
if (focusElement && isFocusable) {
|
||||||
focusElement.focus();
|
focusElement.focus();
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function Modals() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!snapStates.showCompose && (
|
{!!snapStates.showCompose && (
|
||||||
<Modal>
|
<Modal class="solid">
|
||||||
<Compose
|
<Compose
|
||||||
replyToStatus={
|
replyToStatus={
|
||||||
typeof snapStates.showCompose !== 'boolean'
|
typeof snapStates.showCompose !== 'boolean'
|
||||||
|
@ -109,7 +109,6 @@ export default function Modals() {
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showAccount && (
|
{!!snapStates.showAccount && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
states.showAccount = false;
|
states.showAccount = false;
|
||||||
}}
|
}}
|
||||||
|
@ -160,7 +159,6 @@ export default function Modals() {
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showShortcutsSettings && (
|
{!!snapStates.showShortcutsSettings && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
states.showShortcutsSettings = false;
|
states.showShortcutsSettings = false;
|
||||||
}}
|
}}
|
||||||
|
@ -172,7 +170,6 @@ export default function Modals() {
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showGenericAccounts && (
|
{!!snapStates.showGenericAccounts && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
states.showGenericAccounts = false;
|
states.showGenericAccounts = false;
|
||||||
}}
|
}}
|
||||||
|
@ -188,7 +185,6 @@ export default function Modals() {
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showMediaAlt && (
|
{!!snapStates.showMediaAlt && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClose={(e) => {
|
onClose={(e) => {
|
||||||
states.showMediaAlt = false;
|
states.showMediaAlt = false;
|
||||||
}}
|
}}
|
||||||
|
@ -204,6 +200,7 @@ export default function Modals() {
|
||||||
)}
|
)}
|
||||||
{!!snapStates.showEmbedModal && (
|
{!!snapStates.showEmbedModal && (
|
||||||
<Modal
|
<Modal
|
||||||
|
class="solid"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
states.showEmbedModal = false;
|
states.showEmbedModal = false;
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -8,6 +8,11 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
unicode-bidi: isolate;
|
unicode-bidi: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-variant-numeric: slashed-zero;
|
||||||
|
font-feature-settings: 'ss01';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.name-text.show-acct {
|
.name-text.show-acct {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -50,7 +50,11 @@ function NameText({
|
||||||
class={`name-text ${showAcct ? 'show-acct' : ''} ${short ? 'short' : ''}`}
|
class={`name-text ${showAcct ? 'show-acct' : ''} ${short ? 'short' : ''}`}
|
||||||
href={url}
|
href={url}
|
||||||
target={external ? '_blank' : null}
|
target={external ? '_blank' : null}
|
||||||
title={`${displayName ? `${displayName} ` : ''}@${acct}`}
|
title={
|
||||||
|
displayName
|
||||||
|
? `${displayName} (${acct2 ? '' : '@'}${acct})`
|
||||||
|
: `${acct2 ? '' : '@'}${acct}`
|
||||||
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (external) return;
|
if (external) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -88,8 +92,9 @@ function NameText({
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<i>
|
<i>
|
||||||
@{acct1}
|
{acct2 ? '' : '@'}
|
||||||
<span class="ib">{acct2}</span>
|
{acct1}
|
||||||
|
{!!acct2 && <span class="ib">{acct2}</span>}
|
||||||
</i>
|
</i>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import './nav-menu.css';
|
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 { memo } from 'preact/compat';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useLongPress } from 'use-long-press';
|
import { useLongPress } from 'use-long-press';
|
||||||
|
@ -130,7 +135,7 @@ function NavMenu(props) {
|
||||||
if (Date.now() - buttonClickTS.current < 300) {
|
if (Date.now() - buttonClickTS.current < 300) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMenuState(undefined);
|
// setMenuState(undefined);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
portal={{
|
portal={{
|
||||||
|
@ -169,7 +174,7 @@ function NavMenu(props) {
|
||||||
<MenuLink to="/">
|
<MenuLink to="/">
|
||||||
<Icon icon="home" size="l" /> <span>Home</span>
|
<Icon icon="home" size="l" /> <span>Home</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
{authenticated && (
|
{authenticated ? (
|
||||||
<>
|
<>
|
||||||
{showFollowing && (
|
{showFollowing && (
|
||||||
<MenuLink to="/following">
|
<MenuLink to="/following">
|
||||||
|
@ -177,7 +182,7 @@ function NavMenu(props) {
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)}
|
)}
|
||||||
<MenuLink to="/catchup">
|
<MenuLink to="/catchup">
|
||||||
<Icon icon="history" />
|
<Icon icon="history2" size="l" />
|
||||||
<span>Catch-up</span>
|
<span>Catch-up</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuLink to="/mentions">
|
<MenuLink to="/mentions">
|
||||||
|
@ -192,44 +197,64 @@ function NavMenu(props) {
|
||||||
</sup>
|
</sup>
|
||||||
)}
|
)}
|
||||||
</MenuLink>
|
</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 />
|
<MenuDivider />
|
||||||
{currentAccount?.info?.id && (
|
{currentAccount?.info?.id && (
|
||||||
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
|
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
|
||||||
<Icon icon="user" size="l" /> <span>Profile</span>
|
<Icon icon="user" size="l" /> <span>Profile</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
)}
|
)}
|
||||||
|
<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" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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 = {
|
||||||
|
id: 'mute',
|
||||||
|
heading: 'Muted users',
|
||||||
|
fetchAccounts: fetchMutes,
|
||||||
|
excludeRelationshipAttrs: ['muting'],
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="mute" size="l" /> Muted users…
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
id: 'block',
|
||||||
|
heading: 'Blocked users',
|
||||||
|
fetchAccounts: fetchBlocks,
|
||||||
|
excludeRelationshipAttrs: ['blocking'],
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="block" size="l" />
|
||||||
|
Blocked users…
|
||||||
|
</MenuItem>{' '}
|
||||||
|
</SubMenu>
|
||||||
|
<MenuDivider />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showAccounts = true;
|
states.showAccounts = true;
|
||||||
|
@ -237,31 +262,32 @@ function NavMenu(props) {
|
||||||
>
|
>
|
||||||
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
<Icon icon="group" size="l" /> <span>Accounts…</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
</>
|
||||||
onClick={() => {
|
) : (
|
||||||
states.showGenericAccounts = {
|
<>
|
||||||
id: 'mute',
|
<MenuDivider />
|
||||||
heading: 'Muted users',
|
<MenuLink to="/login">
|
||||||
fetchAccounts: fetchMutes,
|
<Icon icon="user" size="l" /> <span>Log in</span>
|
||||||
excludeRelationshipAttrs: ['muting'],
|
</MenuLink>
|
||||||
};
|
</>
|
||||||
}}
|
)}
|
||||||
>
|
</section>
|
||||||
<Icon icon="mute" size="l" /> Muted users…
|
<section>
|
||||||
</MenuItem>
|
<MenuDivider />
|
||||||
<MenuItem
|
<MenuLink to={`/search`}>
|
||||||
onClick={() => {
|
<Icon icon="search" size="l" /> <span>Search</span>
|
||||||
states.showGenericAccounts = {
|
</MenuLink>
|
||||||
id: 'block',
|
<MenuLink to={`/${instance}/trending`}>
|
||||||
heading: 'Blocked users',
|
<Icon icon="chart" size="l" /> <span>Trending</span>
|
||||||
fetchAccounts: fetchBlocks,
|
</MenuLink>
|
||||||
excludeRelationshipAttrs: ['blocking'],
|
<MenuLink to={`/${instance}/p/l`}>
|
||||||
};
|
<Icon icon="building" size="l" /> <span>Local</span>
|
||||||
}}
|
</MenuLink>
|
||||||
>
|
<MenuLink to={`/${instance}/p`}>
|
||||||
<Icon icon="block" size="l" />
|
<Icon icon="earth" size="l" /> <span>Federated</span>
|
||||||
Blocked users…
|
</MenuLink>
|
||||||
</MenuItem>
|
{authenticated ? (
|
||||||
|
<>
|
||||||
<MenuDivider className="divider-grow" />
|
<MenuDivider className="divider-grow" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -290,9 +316,6 @@ function NavMenu(props) {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuLink to="/login">
|
|
||||||
<Icon icon="user" size="l" /> <span>Log in</span>
|
|
||||||
</MenuLink>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
states.showSettings = true;
|
states.showSettings = true;
|
||||||
|
|
|
@ -144,7 +144,6 @@ export default memo(function NotificationService() {
|
||||||
const { id, account, notification, sameInstance } = showNotificationSheet;
|
const { id, account, notification, sameInstance } = showNotificationSheet;
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
padding: calc(var(--sai-top, 0) + 16px) calc(var(--sai-right, 0) + 16px)
|
||||||
|
16px calc(var(--sai-left, 0) + 16px);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -41,6 +43,8 @@
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
|
padding: 0 calc(var(--sai-right, 0) + 16px)
|
||||||
|
calc(var(--sai-bottom, 0) + 16px) calc(var(--sai-left, 0) + 16px);
|
||||||
/* display: flex;
|
/* display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px; */
|
gap: 16px; */
|
||||||
|
|
|
@ -232,8 +232,8 @@ function ReportModal({ account, post, onClose }) {
|
||||||
disabled={uiState === 'loading'}
|
disabled={uiState === 'loading'}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
{!!domain && domain !== currentDomain && (
|
||||||
{domain !== currentDomain && (
|
<section>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -247,8 +247,8 @@ function ReportModal({ account, post, onClose }) {
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
)}
|
</section>
|
||||||
</section>
|
)}
|
||||||
<footer>
|
<footer>
|
||||||
<button type="submit" disabled={uiState === 'loading'}>
|
<button type="submit" disabled={uiState === 'loading'}>
|
||||||
Send Report
|
Send Report
|
||||||
|
|
|
@ -73,7 +73,7 @@ const SearchForm = forwardRef((props, ref) => {
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
spellcheck="false"
|
spellCheck="false"
|
||||||
onSearch={(e) => {
|
onSearch={(e) => {
|
||||||
if (!e.target.value) {
|
if (!e.target.value) {
|
||||||
setSearchParams({});
|
setSearchParams({});
|
||||||
|
|
|
@ -392,7 +392,11 @@ function ShortcutsSettings({ onClose }) {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div class="ui-state insignificant">
|
<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>
|
<p>
|
||||||
Not sure what to add?
|
Not sure what to add?
|
||||||
<br />
|
<br />
|
||||||
|
@ -419,7 +423,9 @@ function ShortcutsSettings({ onClose }) {
|
||||||
)}
|
)}
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
{shortcuts.length >= SHORTCUTS_LIMIT &&
|
{shortcuts.length >= SHORTCUTS_LIMIT &&
|
||||||
`Max ${SHORTCUTS_LIMIT} shortcuts`}
|
(snapStates.settings.shortcutsViewMode === 'multi-column'
|
||||||
|
? `Max ${SHORTCUTS_LIMIT} columns`
|
||||||
|
: `Max ${SHORTCUTS_LIMIT} shortcuts`)}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
|
@ -451,7 +457,6 @@ function ShortcutsSettings({ onClose }) {
|
||||||
</main>
|
</main>
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
|
@ -475,7 +480,6 @@ function ShortcutsSettings({ onClose }) {
|
||||||
)}
|
)}
|
||||||
{showImportExport && (
|
{showImportExport && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowImportExport(false);
|
setShowImportExport(false);
|
||||||
|
@ -667,7 +671,7 @@ function ShortcutForm({
|
||||||
}
|
}
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
spellcheck={false}
|
spellCheck={false}
|
||||||
pattern={pattern}
|
pattern={pattern}
|
||||||
/>
|
/>
|
||||||
{currentType === 'hashtag' &&
|
{currentType === 'hashtag' &&
|
||||||
|
|
|
@ -404,6 +404,24 @@
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
line-height: var(--avatar-size);
|
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 {
|
.status .container {
|
||||||
|
@ -661,7 +679,9 @@
|
||||||
animation: none !important; */
|
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 {
|
~ :is(.media-container, .media-figure-multiple) figcaption {
|
||||||
/* filter: blur(5px) invert(0.5);
|
/* filter: blur(5px) invert(0.5);
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
|
@ -1591,6 +1611,11 @@ a.card:is(:hover, :focus):visited {
|
||||||
.card.video {
|
.card.video {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
lite\-youtube {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.card.video iframe {
|
.card.video iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1971,6 +1996,7 @@ a.card:is(:hover, :focus):visited {
|
||||||
}
|
}
|
||||||
|
|
||||||
.status:focus &,
|
.status:focus &,
|
||||||
|
.status:focus-within &,
|
||||||
&.open {
|
&.open {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
@ -2112,6 +2138,97 @@ a.card:is(:hover, :focus):visited {
|
||||||
pointer-events: none;
|
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 {
|
||||||
|
li > 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 */
|
/* DELETED */
|
||||||
|
|
||||||
.status-deleted {
|
.status-deleted {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { shallowEqual } from 'fast-equals';
|
import { shallowEqual } from 'fast-equals';
|
||||||
|
import prettify from 'html-prettify';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -84,6 +85,8 @@ const isIOS =
|
||||||
window.ontouchstart !== undefined &&
|
window.ontouchstart !== undefined &&
|
||||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
|
||||||
|
const rtf = new Intl.RelativeTimeFormat();
|
||||||
|
|
||||||
const REACTIONS_LIMIT = 80;
|
const REACTIONS_LIMIT = 80;
|
||||||
|
|
||||||
function getPollText(poll) {
|
function getPollText(poll) {
|
||||||
|
@ -451,6 +454,7 @@ function Status({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
|
const [showEmbed, setShowEmbed] = useState(false);
|
||||||
|
|
||||||
const spoilerContentRef = useTruncated();
|
const spoilerContentRef = useTruncated();
|
||||||
const contentRef = useTruncated();
|
const contentRef = useTruncated();
|
||||||
|
@ -506,6 +510,13 @@ function Status({
|
||||||
(attachment) => !attachment.description?.trim?.(),
|
(attachment) => !attachment.description?.trim?.(),
|
||||||
);
|
);
|
||||||
}, [mediaAttachments]);
|
}, [mediaAttachments]);
|
||||||
|
|
||||||
|
const statusMonthsAgo = useMemo(() => {
|
||||||
|
return Math.floor(
|
||||||
|
(new Date() - createdAtDate) / (1000 * 60 * 60 * 24 * 30),
|
||||||
|
);
|
||||||
|
}, [createdAtDate]);
|
||||||
|
|
||||||
const boostStatus = async () => {
|
const boostStatus = async () => {
|
||||||
if (!sameInstance || !authenticated) {
|
if (!sameInstance || !authenticated) {
|
||||||
alert(unauthInteractionErrorMessage);
|
alert(unauthInteractionErrorMessage);
|
||||||
|
@ -709,6 +720,8 @@ function Status({
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionsRef = useRef();
|
const actionsRef = useRef();
|
||||||
|
const isPublic = ['public', 'unlisted'].includes(visibility);
|
||||||
|
const isPinnable = ['public', 'unlisted', 'private'].includes(visibility);
|
||||||
const StatusMenuItems = (
|
const StatusMenuItems = (
|
||||||
<>
|
<>
|
||||||
{isSizeLarge && (
|
{isSizeLarge && (
|
||||||
|
@ -744,17 +757,41 @@ function Status({
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
<>
|
<>
|
||||||
<Icon icon="rocket" />
|
<Icon icon="rocket" />
|
||||||
<span>{reblogged ? 'Unboost?' : 'Boost to everyone?'}</span>
|
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
className={`menu-reblog ${reblogged ? 'checked' : ''}`}
|
className={`menu-reblog ${reblogged ? 'checked' : ''}`}
|
||||||
|
menuExtras={
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showCompose = {
|
||||||
|
draftStatus: {
|
||||||
|
status: `\n${url}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="quote" />
|
||||||
|
<span>Quote</span>
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
menuFooter={
|
menuFooter={
|
||||||
mediaNoDesc &&
|
mediaNoDesc && !reblogged ? (
|
||||||
!reblogged && (
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<Icon icon="alert" />
|
<Icon icon="alert" />
|
||||||
Some media have no descriptions.
|
Some media have no descriptions.
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
statusMonthsAgo >= 3 && (
|
||||||
|
<div class="footer">
|
||||||
|
<Icon icon="info" />
|
||||||
|
<span>
|
||||||
|
Old post (
|
||||||
|
<strong>{rtf.format(-statusMonthsAgo, 'month')}</strong>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={!canBoost}
|
disabled={!canBoost}
|
||||||
|
@ -854,13 +891,12 @@ function Status({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{!isSizeLarge ||
|
{((!isSizeLarge && sameInstance) ||
|
||||||
((enableTranslate || !language || differentLanguage) && (
|
enableTranslate ||
|
||||||
<MenuDivider />
|
!language ||
|
||||||
))}
|
differentLanguage) && <MenuDivider />}
|
||||||
{!isSizeLarge && (
|
{!isSizeLarge && (
|
||||||
<>
|
<>
|
||||||
<MenuDivider />
|
|
||||||
<MenuLink
|
<MenuLink
|
||||||
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -914,7 +950,8 @@ function Status({
|
||||||
<Icon icon="link" />
|
<Icon icon="link" />
|
||||||
<span>Copy</span>
|
<span>Copy</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{navigator?.share &&
|
{isPublic &&
|
||||||
|
navigator?.share &&
|
||||||
navigator?.canShare?.({
|
navigator?.canShare?.({
|
||||||
url,
|
url,
|
||||||
}) && (
|
}) && (
|
||||||
|
@ -935,6 +972,16 @@ function Status({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isPublic && isSizeLarge && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowEmbed(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="code" />
|
||||||
|
<span>Embed post</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{(isSelf || mentionSelf) && <MenuDivider />}
|
{(isSelf || mentionSelf) && <MenuDivider />}
|
||||||
{(isSelf || mentionSelf) && (
|
{(isSelf || mentionSelf) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -968,7 +1015,7 @@ function Status({
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{isSelf && /(public|unlisted|private)/i.test(visibility) && (
|
{isSelf && isPinnable && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -1085,7 +1132,12 @@ function Status({
|
||||||
const { clientX, clientY } = e.touches?.[0] || e;
|
const { clientX, clientY } = e.touches?.[0] || e;
|
||||||
// link detection copied from onContextMenu because here it works
|
// link detection copied from onContextMenu because here it works
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
if (
|
||||||
|
link &&
|
||||||
|
statusRef.current.contains(link) &&
|
||||||
|
!link.getAttribute('href').startsWith('#')
|
||||||
|
)
|
||||||
|
return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenuProps({
|
setContextMenuProps({
|
||||||
anchorPoint: {
|
anchorPoint: {
|
||||||
|
@ -1331,7 +1383,12 @@ function Status({
|
||||||
if (e.metaKey) return;
|
if (e.metaKey) return;
|
||||||
// console.log('context menu', e);
|
// console.log('context menu', e);
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
if (
|
||||||
|
link &&
|
||||||
|
statusRef.current.contains(link) &&
|
||||||
|
!link.getAttribute('href').startsWith('#')
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
// If there's selected text, don't show custom context menu
|
// If there's selected text, don't show custom context menu
|
||||||
const selection = window.getSelection?.();
|
const selection = window.getSelection?.();
|
||||||
|
@ -1783,6 +1840,9 @@ function Status({
|
||||||
media={media}
|
media={media}
|
||||||
autoAnimate={isSizeLarge}
|
autoAnimate={isSizeLarge}
|
||||||
showCaption={mediaAttachments.length === 1}
|
showCaption={mediaAttachments.length === 1}
|
||||||
|
allowLongerCaption={
|
||||||
|
!content && mediaAttachments.length === 1
|
||||||
|
}
|
||||||
lang={language}
|
lang={language}
|
||||||
altIndex={
|
altIndex={
|
||||||
showMultipleMediaCaptions &&
|
showMultipleMediaCaptions &&
|
||||||
|
@ -1894,11 +1954,23 @@ function Status({
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
<>
|
<>
|
||||||
<Icon icon="rocket" />
|
<Icon icon="rocket" />
|
||||||
<span>
|
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
|
||||||
{reblogged ? 'Unboost?' : 'Boost to everyone?'}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
menuExtras={
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showCompose = {
|
||||||
|
draftStatus: {
|
||||||
|
status: `\n${url}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="quote" />
|
||||||
|
<span>Quote</span>
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
menuFooter={
|
menuFooter={
|
||||||
mediaNoDesc &&
|
mediaNoDesc &&
|
||||||
!reblogged && (
|
!reblogged && (
|
||||||
|
@ -1972,7 +2044,6 @@ function Status({
|
||||||
</div>
|
</div>
|
||||||
{!!showEdited && (
|
{!!showEdited && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowEdited(false);
|
setShowEdited(false);
|
||||||
|
@ -1993,6 +2064,23 @@ function Status({
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{!!showEmbed && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowEmbed(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmbedModal
|
||||||
|
post={status}
|
||||||
|
instance={instance}
|
||||||
|
onClose={() => {
|
||||||
|
setShowEmbed(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -2099,10 +2187,13 @@ function Card({ card, selfReferential, instance }) {
|
||||||
const w = 44;
|
const w = 44;
|
||||||
const h = 44;
|
const h = 44;
|
||||||
const blurhashPixels = decodeBlurHash(blurhash, w, h);
|
const blurhashPixels = decodeBlurHash(blurhash, w, h);
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = window.OffscreenCanvas
|
||||||
|
? new OffscreenCanvas(1, 1)
|
||||||
|
: document.createElement('canvas');
|
||||||
canvas.width = w;
|
canvas.width = w;
|
||||||
canvas.height = h;
|
canvas.height = h;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
const imageData = ctx.createImageData(w, h);
|
const imageData = ctx.createImageData(w, h);
|
||||||
imageData.data.set(blurhashPixels);
|
imageData.data.set(blurhashPixels);
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
@ -2140,10 +2231,10 @@ function Card({ card, selfReferential, instance }) {
|
||||||
<p class="meta domain" dir="auto">
|
<p class="meta domain" dir="auto">
|
||||||
{domain}
|
{domain}
|
||||||
</p>
|
</p>
|
||||||
<p class="title" dir="auto">
|
<p class="title" dir="auto" title={title}>
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<p class="meta" dir="auto">
|
<p class="meta" dir="auto" title={description}>
|
||||||
{description ||
|
{description ||
|
||||||
(!!publishedAt && (
|
(!!publishedAt && (
|
||||||
<RelativeTime datetime={publishedAt} format="micro" />
|
<RelativeTime datetime={publishedAt} format="micro" />
|
||||||
|
@ -2180,7 +2271,11 @@ function Card({ card, selfReferential, instance }) {
|
||||||
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
||||||
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
||||||
if (videoID) {
|
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 (
|
// return (
|
||||||
|
@ -2208,8 +2303,12 @@ function Card({ card, selfReferential, instance }) {
|
||||||
<p class="meta domain">
|
<p class="meta domain">
|
||||||
<Icon icon="link" size="s" /> <span>{domain}</span>
|
<Icon icon="link" size="s" /> <span>{domain}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="title">{title}</p>
|
<p class="title" title={title}>
|
||||||
<p class="meta">{description || providerName || authorName}</p>
|
{title}
|
||||||
|
</p>
|
||||||
|
<p class="meta" title={description || providerName || authorName}>
|
||||||
|
{description || providerName || authorName}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -2293,6 +2392,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>
|
||||||
|
{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>
|
||||||
|
{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({
|
function StatusButton({
|
||||||
checked,
|
checked,
|
||||||
count,
|
count,
|
||||||
|
@ -2405,13 +2858,21 @@ function StatusCompact({ sKey }) {
|
||||||
visibility,
|
visibility,
|
||||||
content,
|
content,
|
||||||
language,
|
language,
|
||||||
|
filtered,
|
||||||
} = status;
|
} = status;
|
||||||
if (sensitive || spoilerText) return null;
|
if (sensitive || spoilerText) return null;
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
const srKey = statusKey(id, instance);
|
const srKey = statusKey(id, instance);
|
||||||
|
|
||||||
const statusPeekText = statusPeek(status);
|
const statusPeekText = statusPeek(status);
|
||||||
|
|
||||||
|
const filterContext = useContext(FilterContext);
|
||||||
|
const filterInfo = isFiltered(filtered, filterContext);
|
||||||
|
|
||||||
|
if (filterInfo?.action === 'hide') return null;
|
||||||
|
|
||||||
|
const filterTitleStr = filterInfo?.titlesStr || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
class={`status compact-reply ${
|
class={`status compact-reply ${
|
||||||
|
@ -2427,7 +2888,14 @@ function StatusCompact({ sKey }) {
|
||||||
lang={language}
|
lang={language}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
{statusPeekText}
|
{filterInfo ? (
|
||||||
|
<b class="status-filtered-badge badge-meta" title={filterTitleStr}>
|
||||||
|
<span>Filtered</span>
|
||||||
|
<span>{filterTitleStr}</span>
|
||||||
|
</b>
|
||||||
|
) : (
|
||||||
|
<span>{statusPeekText}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
@ -2549,7 +3017,6 @@ function FilteredStatus({
|
||||||
</article>
|
</article>
|
||||||
{!!showPeek && (
|
{!!showPeek && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowPeek(false);
|
setShowPeek(false);
|
||||||
|
|
|
@ -535,15 +535,15 @@ const TimelineItem = memo(
|
||||||
const url = instance
|
const url = instance
|
||||||
? `/${instance}/s/${actualStatusID}`
|
? `/${instance}/s/${actualStatusID}`
|
||||||
: `/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) {
|
if (items) {
|
||||||
const fItems = filteredItems(items, filterContext);
|
const fItems = filteredItems(items, filterContext);
|
||||||
|
let title = '';
|
||||||
|
if (type === 'boosts') {
|
||||||
|
title = `${fItems.length} Boosts`;
|
||||||
|
} else if (type === 'pinned') {
|
||||||
|
title = 'Pinned posts';
|
||||||
|
}
|
||||||
|
const isCarousel = type === 'boosts' || type === 'pinned';
|
||||||
if (isCarousel) {
|
if (isCarousel) {
|
||||||
// Here, we don't hide filtered posts, but we sort them last
|
// Here, we don't hide filtered posts, but we sort them last
|
||||||
fItems.sort((a, b) => {
|
fItems.sort((a, b) => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"@mastodon/edit-media-attributes": ">=4.1",
|
"@mastodon/edit-media-attributes": ">=4.1",
|
||||||
"@mastodon/list-exclusive": ">=4.2"
|
"@mastodon/list-exclusive": ">=4.2",
|
||||||
|
"@mastodon/filtered-notifications": "~4.3 || >=4.3"
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
--outline-color: rgba(128, 128, 128, 0.2);
|
--outline-color: rgba(128, 128, 128, 0.2);
|
||||||
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||||
--divider-color: rgba(0, 0, 0, 0.1);
|
--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-darker-color: rgba(0, 0, 0, 0.25);
|
||||||
--backdrop-solid-color: #eee;
|
--backdrop-solid-color: #eee;
|
||||||
--img-bg-color: rgba(128, 128, 128, 0.2);
|
--img-bg-color: rgba(128, 128, 128, 0.2);
|
||||||
|
@ -227,7 +227,7 @@ button[hidden] {
|
||||||
}
|
}
|
||||||
:is(button, .button):not(:disabled, .disabled):is(:hover, :focus) {
|
:is(button, .button):not(:disabled, .disabled):is(:hover, :focus) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
filter: brightness(1.2);
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
:is(button, .button):not(:disabled, .disabled):active {
|
:is(button, .button):not(:disabled, .disabled):active {
|
||||||
filter: brightness(0.8);
|
filter: brightness(0.8);
|
||||||
|
@ -267,6 +267,14 @@ button[hidden] {
|
||||||
:is(button, .button).plain5:not(:disabled, .disabled):is(:hover, :focus) {
|
:is(button, .button).plain5:not(:disabled, .disabled):is(:hover, :focus) {
|
||||||
text-decoration: underline;
|
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 {
|
:is(button, .button).light {
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
|
@ -2,6 +2,9 @@ import './index.css';
|
||||||
|
|
||||||
import './cloak-mode.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 { render } from 'preact';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
|
@ -206,8 +206,12 @@ function AccountStatuses() {
|
||||||
const [featuredTags, setFeaturedTags] = useState([]);
|
const [featuredTags, setFeaturedTags] = useState([]);
|
||||||
useTitle(
|
useTitle(
|
||||||
account?.acct
|
account?.acct
|
||||||
? `${account?.displayName ? account.displayName + ' ' : ''}@${
|
? `${
|
||||||
account.acct
|
account?.displayName
|
||||||
|
? `${account.displayName} (${/@/.test(account.acct) ? '' : '@'}${
|
||||||
|
account.acct
|
||||||
|
})`
|
||||||
|
: `${/@/.test(account.acct) ? '' : '@'}${account.acct}`
|
||||||
}${
|
}${
|
||||||
!excludeReplies
|
!excludeReplies
|
||||||
? ' (+ Replies)'
|
? ' (+ Replies)'
|
||||||
|
@ -259,27 +263,21 @@ function AccountStatuses() {
|
||||||
|
|
||||||
const { displayName, acct, emojis } = account || {};
|
const { displayName, acct, emojis } = account || {};
|
||||||
|
|
||||||
const accountInfoMemo = useMemo(() => {
|
|
||||||
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
|
||||||
return (
|
|
||||||
<AccountInfo
|
|
||||||
instance={instance}
|
|
||||||
account={cachedAccount || id}
|
|
||||||
fetchAccount={fetchAccount}
|
|
||||||
authenticated={authenticated}
|
|
||||||
standalone
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [id, instance, authenticated, fetchAccount]);
|
|
||||||
|
|
||||||
const filterBarRef = useRef();
|
const filterBarRef = useRef();
|
||||||
const TimelineStart = useMemo(() => {
|
const TimelineStart = useMemo(() => {
|
||||||
const filtered =
|
const filtered =
|
||||||
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
!excludeReplies || excludeBoosts || tagged || media || !!month;
|
||||||
|
const cachedAccount = snapStates.accounts[`${id}@${instance}`];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{accountInfoMemo}
|
<AccountInfo
|
||||||
|
instance={instance}
|
||||||
|
account={cachedAccount || id}
|
||||||
|
fetchAccount={fetchAccount}
|
||||||
|
authenticated={authenticated}
|
||||||
|
standalone
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="filter-bar"
|
class="filter-bar"
|
||||||
ref={filterBarRef}
|
ref={filterBarRef}
|
||||||
|
@ -418,6 +416,7 @@ function AccountStatuses() {
|
||||||
instance,
|
instance,
|
||||||
authenticated,
|
authenticated,
|
||||||
featuredTags,
|
featuredTags,
|
||||||
|
fetchAccount,
|
||||||
searchEnabled,
|
searchEnabled,
|
||||||
...allSearchParams,
|
...allSearchParams,
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -32,6 +32,57 @@
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
|
|
||||||
|
details {
|
||||||
|
border-radius: 16px;
|
||||||
|
text-wrap: balance;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
padding: 1em;
|
||||||
|
margin: -1em 0;
|
||||||
|
transition: all 0.3s var(--timing-function);
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
transform: translateY(-10vh);
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
background-image: radial-gradient(
|
||||||
|
farthest-corner at 25% 0,
|
||||||
|
transparent 80%,
|
||||||
|
var(--bg-faded-color) 95%,
|
||||||
|
var(--bg-color)
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
farthest-corner at 100% 100%,
|
||||||
|
transparent 80%,
|
||||||
|
var(--bg-faded-blur-color)
|
||||||
|
);
|
||||||
|
outline: 1px solid var(--bg-color);
|
||||||
|
box-shadow: 0 16px 32px -16px var(--drop-shadow-color);
|
||||||
|
|
||||||
|
~ * {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 480px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.catchup-info {
|
.catchup-info {
|
||||||
animation: appear 0.3s ease-out;
|
animation: appear 0.3s ease-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -68,6 +119,13 @@
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,14 +175,14 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--bg-color);
|
border: 1px solid var(--bg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1px;
|
gap: var(--hairline-width);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
|
||||||
&:has(.post-dot:nth-child(320)) {
|
/* &:has(.post-dot:nth-child(320)) {
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.post-dot {
|
.post-dot {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -147,6 +205,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catchup-posts-viz-time-bar {
|
||||||
|
margin: 0 16px;
|
||||||
|
padding: 1px;
|
||||||
|
display: flex;
|
||||||
|
row-gap: var(--hairline-width);
|
||||||
|
pointer-events: none;
|
||||||
|
justify-content: stretch;
|
||||||
|
background-image: linear-gradient(to bottom, transparent, var(--bg-color));
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
column-gap: var(--hairline-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-bin {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--hairline-width);
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.post-dot {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
opacity: 0.2;
|
||||||
|
background-color: var(--link-color);
|
||||||
|
transition: 0.25s ease-in-out;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
contain: none;
|
||||||
|
|
||||||
|
&.post-dot-highlight {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.catchup-filters {
|
.catchup-filters {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -179,6 +273,10 @@
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
/* appearance: none;
|
/* appearance: none;
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
|
@ -262,6 +360,7 @@
|
||||||
|
|
||||||
img {
|
img {
|
||||||
transition: filter 0.15s ease;
|
transition: filter 0.15s ease;
|
||||||
|
will-change: filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,11 +414,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(.filter-author :checked)
|
&:has(.filter-author :checked) .filter-author:not(:has(:checked)) {
|
||||||
.filter-author:not(:has(:checked)):not(:is(:hover, :focus)) {
|
|
||||||
.avatar img {
|
.avatar img {
|
||||||
filter: grayscale(1) contrast(2) opacity(0.5);
|
filter: grayscale(1) contrast(2) opacity(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
.avatar img {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-field-group {
|
.radio-field-group {
|
||||||
|
@ -360,6 +464,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.catchup-list {
|
.catchup-list {
|
||||||
|
min-height: 85vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@ -385,18 +490,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
margin: 0 0 1px;
|
margin: 0 0 var(--hairline-width);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
/* border-bottom: var(--hairline-width) solid var(--outline-color); */
|
/* border-bottom: var(--hairline-width) solid var(--outline-color); */
|
||||||
|
|
||||||
&.separator {
|
&.separator {
|
||||||
height: 16px;
|
height: 32px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
@media (min-width: 40em) {
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 40em) {
|
@media (min-width: 40em) {
|
||||||
|
@ -424,10 +525,13 @@
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
box-shadow: 0 8px 16px -8px var(--drop-shadow-color),
|
box-shadow: 0 8px 16px -8px var(--drop-shadow-color),
|
||||||
inset 0 1px var(--bg-color);
|
inset 0 1px var(--bg-color);
|
||||||
outline: 1px solid var(--outline-color);
|
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover:not(:focus-visible) {
|
||||||
|
outline: 1px solid var(--outline-color);
|
||||||
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
filter: brightness(0.95);
|
filter: brightness(0.95);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -450,6 +554,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-line {
|
.post-line {
|
||||||
|
font-size: 0.95em;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
animation: appear-smooth 0.3s ease-in-out both;
|
animation: appear-smooth 0.3s ease-in-out both;
|
||||||
--pad: 16px;
|
--pad: 16px;
|
||||||
|
@ -468,9 +573,9 @@
|
||||||
'content content';
|
'content content';
|
||||||
/* align-items: center; */
|
/* align-items: center; */
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
160deg,
|
140deg,
|
||||||
var(--post-bg-color),
|
var(--post-bg-color),
|
||||||
transparent min(80px, 50%)
|
transparent min(160px, 50%)
|
||||||
);
|
);
|
||||||
/* background-image: linear-gradient(
|
/* background-image: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
|
@ -524,10 +629,24 @@
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-height: 24px;
|
||||||
|
|
||||||
.icon {
|
> .avatar {
|
||||||
|
outline: 1px solid var(--bg-blur-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .avatar ~ .avatar {
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .icon {
|
||||||
color: var(--reblog-color);
|
color: var(--reblog-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .name-text {
|
||||||
|
opacity: 0.75;
|
||||||
|
filter: grayscale(0.75);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-author {
|
.post-author {
|
||||||
|
@ -559,10 +678,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> li:first-child .post-line {
|
||||||
|
animation-duration: 0.1s;
|
||||||
|
}
|
||||||
|
> li:nth-child(2) .post-line {
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
}
|
||||||
> li:nth-child(10) ~ li .post-line {
|
> li:nth-child(10) ~ li .post-line {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:is(.catchup-group-account, .catchup-selected-author):is(
|
||||||
|
.catchup-filter-original,
|
||||||
|
.catchup-filter-reply
|
||||||
|
)
|
||||||
|
> li {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
&:first-child ~ li {
|
||||||
|
.post-author:not(:has(.post-reblog-avatar)) {
|
||||||
|
opacity: 0.25;
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.separator + li .post-author {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-peek {
|
.post-peek {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -573,7 +720,7 @@
|
||||||
/* align-items: center; */
|
/* align-items: center; */
|
||||||
/* margin-left: 24px; */
|
/* margin-left: 24px; */
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
flex-wrap: wrap;
|
/* flex-wrap: wrap; */
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
/* CLOAK - uncomment when taking screenshots */
|
/* CLOAK - uncomment when taking screenshots */
|
||||||
|
@ -597,7 +744,6 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
/* font-size: 0.9em; */
|
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
@ -647,7 +793,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
br:after {
|
br:after,
|
||||||
|
:not(br, span, a) + :is(p, div, blockquote, ul, ol, pre):before {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
content: ' ↵ ';
|
content: ' ↵ ';
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
|
@ -670,6 +817,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-peek-spoiler {
|
.post-peek-spoiler {
|
||||||
|
display: inline-block;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
padding-inline: 0.5em;
|
padding-inline: 0.5em;
|
||||||
|
@ -737,6 +885,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.post-peek-media:not(:last-child) {
|
||||||
|
margin-right: -24px;
|
||||||
|
box-shadow: 0 0 0 2px var(--bg-blur-color);
|
||||||
|
}
|
||||||
|
/* Max 10, I'm not going to code more than this */
|
||||||
|
.post-peek-media:nth-child(1) {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(2) {
|
||||||
|
z-index: 9;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(3) {
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(4) {
|
||||||
|
z-index: 7;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(5) {
|
||||||
|
z-index: 6;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(6) {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(7) {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(8) {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(9) {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.post-peek-media:nth-child(10) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.post-peek-media:hover {
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-peek-faux-media {
|
.post-peek-faux-media {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
@ -807,20 +996,86 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-stats {
|
.post-stats {
|
||||||
opacity: 0;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 2px;
|
gap: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transform: translateX(4px);
|
|
||||||
/* transition: all 0.25s ease-out; */
|
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.post-line:hover .post-stats {
|
@media (hover: hover) {
|
||||||
opacity: 1;
|
.post-stats {
|
||||||
transform: translateX(0);
|
opacity: 0;
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
.post-line:hover .post-stats {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.catchup-sort-repliesCount {
|
||||||
|
.post-stats {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
.post-stat-replies {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(.post-stat-replies) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.post-line:hover .post-stats > * {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.catchup-sort-favouritesCount {
|
||||||
|
.post-stats {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
.post-stat-likes {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(.post-stat-likes) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.post-line:hover .post-stats > * {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.catchup-sort-reblogsCount {
|
||||||
|
.post-stats {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
.post-stat-boosts {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(.post-stat-boosts) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.post-line:hover .post-stats > * {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+ footer {
|
+ footer {
|
||||||
|
@ -830,3 +1085,29 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#catchup-help-sheet {
|
||||||
|
dl {
|
||||||
|
dt {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
margin-block-end: 1em;
|
||||||
|
margin-inline: 1em;
|
||||||
|
|
||||||
|
+ dd {
|
||||||
|
margin-block-start: -0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0.3em;
|
||||||
|
margin: 1px 0;
|
||||||
|
line-height: 1;
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -285,7 +285,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
required
|
required
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
spellcheck={false}
|
spellCheck={false}
|
||||||
// no spaces, no hashtags
|
// no spaces, no hashtags
|
||||||
pattern="[^#][^\s#]+[^#]"
|
pattern="[^#][^\s#]+[^#]"
|
||||||
disabled={reachLimit}
|
disabled={reachLimit}
|
||||||
|
|
|
@ -143,7 +143,6 @@ function List(props) {
|
||||||
/>
|
/>
|
||||||
{showListAddEditModal && (
|
{showListAddEditModal && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowListAddEditModal(false);
|
setShowListAddEditModal(false);
|
||||||
|
@ -167,7 +166,6 @@ function List(props) {
|
||||||
)}
|
)}
|
||||||
{showManageMembersModal && (
|
{showManageMembersModal && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowManageMembersModal(false);
|
setShowManageMembersModal(false);
|
||||||
|
|
|
@ -108,7 +108,6 @@ function Lists() {
|
||||||
</div>
|
</div>
|
||||||
{showListAddEditModal && (
|
{showListAddEditModal && (
|
||||||
<Modal
|
<Modal
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowListAddEditModal(false);
|
setShowListAddEditModal(false);
|
||||||
|
|
|
@ -160,7 +160,7 @@ function Login() {
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck={false}
|
spellCheck={false}
|
||||||
placeholder="instance domain"
|
placeholder="instance domain"
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setInstanceText(e.target.value);
|
setInstanceText(e.target.value);
|
||||||
|
|
|
@ -57,13 +57,14 @@
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: -0.25em auto 0;
|
margin: -0.25em auto 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
/* background-image: linear-gradient(
|
/* background-image: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
var(--bg-color),
|
var(--bg-color),
|
||||||
var(--bg-blur-color)
|
var(--bg-blur-color)
|
||||||
); */
|
); */
|
||||||
backdrop-filter: blur(16px) saturate(3);
|
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -142,6 +143,7 @@
|
||||||
border-color: var(--reply-to-color);
|
border-color: var(--reply-to-color);
|
||||||
box-shadow: 0 0 0 3px var(--reply-to-faded-color);
|
box-shadow: 0 0 0 3px var(--reply-to-faded-color);
|
||||||
}
|
}
|
||||||
|
.notification:focus-visible .status-link,
|
||||||
.notification .status-link:is(:hover, :focus) {
|
.notification .status-link:is(:hover, :focus) {
|
||||||
background-color: var(--bg-blur-color);
|
background-color: var(--bg-blur-color);
|
||||||
filter: saturate(1);
|
filter: saturate(1);
|
||||||
|
@ -419,3 +421,145 @@
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background-color: var(--link-faded-color);
|
background-color: var(--link-faded-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FILTERED NOTIFICATIONS */
|
||||||
|
|
||||||
|
.filtered-notifications {
|
||||||
|
padding-block-end: 16px;
|
||||||
|
|
||||||
|
summary {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
user-select: none;
|
||||||
|
margin: 16px 0 0;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
|
||||||
|
&::marker,
|
||||||
|
&::-webkit-details-marker {
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
details[open] summary {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary + ul {
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-height: 50vh;
|
||||||
|
max-height: 50dvh;
|
||||||
|
overflow: auto;
|
||||||
|
border-top: 1px solid var(--outline-color);
|
||||||
|
border-bottom: 1px solid var(--outline-color);
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
row-gap: 8px;
|
||||||
|
column-gap: 16px;
|
||||||
|
border-bottom: 1px solid var(--outline-color);
|
||||||
|
}
|
||||||
|
li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-notifcations {
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.last-post {
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
> .status-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
--max-height: 160px;
|
||||||
|
max-height: var(--max-height);
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
|
||||||
|
&:is(:hover, :focus-visible) {
|
||||||
|
border-color: var(--outline-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
black calc(var(--max-height) / 2),
|
||||||
|
transparent calc(var(--max-height) - 8px)
|
||||||
|
);
|
||||||
|
font-size: calc(var(--text-size) * 0.9);
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
pointer-events: none;
|
||||||
|
filter: saturate(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-notifications-account {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-request-buttons {
|
||||||
|
grid-area: buttons;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
max-width: 30vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-request-states {
|
||||||
|
min-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-inline: 8px;
|
||||||
|
|
||||||
|
&.notification-accepted {
|
||||||
|
color: var(--green-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notification-dismissed {
|
||||||
|
color: var(--red-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#notifications-settings {
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import './notifications.css';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -13,8 +14,10 @@ import FollowRequestButtons from '../components/follow-request-buttons';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
|
import Modal from '../components/modal';
|
||||||
import NavMenu from '../components/nav-menu';
|
import NavMenu from '../components/nav-menu';
|
||||||
import Notification from '../components/notification';
|
import Notification from '../components/notification';
|
||||||
|
import Status from '../components/status';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import groupNotifications from '../utils/group-notifications';
|
import groupNotifications from '../utils/group-notifications';
|
||||||
|
@ -22,8 +25,10 @@ import handleContentLinks from '../utils/handle-content-links';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import { getRegistration } from '../utils/push-notifications';
|
import { getRegistration } from '../utils/push-notifications';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
import showToast from '../utils/show-toast';
|
||||||
import states, { saveStatus } from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
import { getCurrentInstance } from '../utils/store-utils';
|
import { getCurrentInstance } from '../utils/store-utils';
|
||||||
|
import supports from '../utils/supports';
|
||||||
import usePageVisibility from '../utils/usePageVisibility';
|
import usePageVisibility from '../utils/usePageVisibility';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
@ -31,6 +36,12 @@ import useTitle from '../utils/useTitle';
|
||||||
const LIMIT = 30; // 30 is the maximum limit :(
|
const LIMIT = 30; // 30 is the maximum limit :(
|
||||||
const emptySearchParams = new URLSearchParams();
|
const emptySearchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
const scrollIntoViewOptions = {
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center',
|
||||||
|
behavior: 'smooth',
|
||||||
|
};
|
||||||
|
|
||||||
function Notifications({ columnMode }) {
|
function Notifications({ columnMode }) {
|
||||||
useTitle('Notifications', '/notifications');
|
useTitle('Notifications', '/notifications');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
|
@ -129,6 +140,28 @@ function Notifications({ columnMode }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const supportsFilteredNotifications = supports(
|
||||||
|
'@mastodon/filtered-notifications',
|
||||||
|
);
|
||||||
|
const [showNotificationsSettings, setShowNotificationsSettings] =
|
||||||
|
useState(false);
|
||||||
|
const [notificationsPolicy, setNotificationsPolicy] = useState({});
|
||||||
|
function fetchNotificationsPolicy() {
|
||||||
|
return masto.v1.notifications.policy.fetch().catch(() => {});
|
||||||
|
}
|
||||||
|
function loadNotificationsPolicy() {
|
||||||
|
fetchNotificationsPolicy()
|
||||||
|
.then((policy) => {
|
||||||
|
console.log('✨ Notifications policy', policy);
|
||||||
|
setNotificationsPolicy(policy);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
const [notificationsRequests, setNotificationsRequests] = useState(null);
|
||||||
|
function fetchNotificationsRequest() {
|
||||||
|
return masto.v1.notifications.requests.list();
|
||||||
|
}
|
||||||
|
|
||||||
const loadNotifications = (firstLoad) => {
|
const loadNotifications = (firstLoad) => {
|
||||||
setShowNew(false);
|
setShowNew(false);
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
|
@ -154,6 +187,10 @@ function Notifications({ columnMode }) {
|
||||||
setFollowRequests(requests);
|
setFollowRequests(requests);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
|
if (supportsFilteredNotifications) {
|
||||||
|
loadNotificationsPolicy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { done } = await fetchNotificationsPromise;
|
const { done } = await fetchNotificationsPromise;
|
||||||
|
@ -221,6 +258,9 @@ function Notifications({ columnMode }) {
|
||||||
lastHiddenTime.current = Date.now();
|
lastHiddenTime.current = Date.now();
|
||||||
}
|
}
|
||||||
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
|
||||||
|
if (uiState === 'loading') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (v) {
|
if (v) {
|
||||||
loadUpdates();
|
loadUpdates();
|
||||||
}
|
}
|
||||||
|
@ -270,11 +310,84 @@ function Notifications({ columnMode }) {
|
||||||
// }
|
// }
|
||||||
// }, [uiState]);
|
// }, [uiState]);
|
||||||
|
|
||||||
|
const itemsSelector = '.notification';
|
||||||
|
const jRef = useHotkeys('j', () => {
|
||||||
|
const activeItem = document.activeElement.closest(itemsSelector);
|
||||||
|
const activeItemRect = activeItem?.getBoundingClientRect();
|
||||||
|
const allItems = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(itemsSelector),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
activeItem &&
|
||||||
|
activeItemRect.top < scrollableRef.current.clientHeight &&
|
||||||
|
activeItemRect.bottom > 0
|
||||||
|
) {
|
||||||
|
const activeItemIndex = allItems.indexOf(activeItem);
|
||||||
|
let nextItem = allItems[activeItemIndex + 1];
|
||||||
|
if (nextItem) {
|
||||||
|
nextItem.focus();
|
||||||
|
nextItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const topmostItem = allItems.find((item) => {
|
||||||
|
const itemRect = item.getBoundingClientRect();
|
||||||
|
return itemRect.top >= 44 && itemRect.left >= 0;
|
||||||
|
});
|
||||||
|
if (topmostItem) {
|
||||||
|
topmostItem.focus();
|
||||||
|
topmostItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const kRef = useHotkeys('k', () => {
|
||||||
|
// focus on previous status after active item
|
||||||
|
const activeItem = document.activeElement.closest(itemsSelector);
|
||||||
|
const activeItemRect = activeItem?.getBoundingClientRect();
|
||||||
|
const allItems = Array.from(
|
||||||
|
scrollableRef.current.querySelectorAll(itemsSelector),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
activeItem &&
|
||||||
|
activeItemRect.top < scrollableRef.current.clientHeight &&
|
||||||
|
activeItemRect.bottom > 0
|
||||||
|
) {
|
||||||
|
const activeItemIndex = allItems.indexOf(activeItem);
|
||||||
|
let prevItem = allItems[activeItemIndex - 1];
|
||||||
|
if (prevItem) {
|
||||||
|
prevItem.focus();
|
||||||
|
prevItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const topmostItem = allItems.find((item) => {
|
||||||
|
const itemRect = item.getBoundingClientRect();
|
||||||
|
return itemRect.top >= 44 && itemRect.left >= 0;
|
||||||
|
});
|
||||||
|
if (topmostItem) {
|
||||||
|
topmostItem.focus();
|
||||||
|
topmostItem.scrollIntoView(scrollIntoViewOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const oRef = useHotkeys(['enter', 'o'], () => {
|
||||||
|
const activeItem = document.activeElement.closest(itemsSelector);
|
||||||
|
const statusLink = activeItem?.querySelector('.status-link');
|
||||||
|
if (statusLink) {
|
||||||
|
statusLink.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="notifications-page"
|
id="notifications-page"
|
||||||
class="deck-container"
|
class="deck-container"
|
||||||
ref={scrollableRef}
|
ref={(node) => {
|
||||||
|
scrollableRef.current = node;
|
||||||
|
jRef.current = node;
|
||||||
|
kRef.current = node;
|
||||||
|
oRef.current = node;
|
||||||
|
}}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
>
|
>
|
||||||
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
||||||
|
@ -301,7 +414,17 @@ function Notifications({ columnMode }) {
|
||||||
</div>
|
</div>
|
||||||
<h1>Notifications</h1>
|
<h1>Notifications</h1>
|
||||||
<div class="header-side">
|
<div class="header-side">
|
||||||
{/* <Loader hidden={uiState !== 'loading'} /> */}
|
{supportsFilteredNotifications && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button plain"
|
||||||
|
onClick={() => {
|
||||||
|
setShowNotificationsSettings(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="settings" size="l" alt="Notifications settings" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showNew && uiState !== 'loading' && (
|
{showNew && uiState !== 'loading' && (
|
||||||
|
@ -406,6 +529,70 @@ function Notifications({ columnMode }) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{supportsFilteredNotifications &&
|
||||||
|
notificationsPolicy?.summary?.pendingRequestsCount > 0 && (
|
||||||
|
<div class="filtered-notifications">
|
||||||
|
<details
|
||||||
|
onToggle={async (e) => {
|
||||||
|
const { open } = e.target;
|
||||||
|
if (open) {
|
||||||
|
const requests = await fetchNotificationsRequest();
|
||||||
|
setNotificationsRequests(requests);
|
||||||
|
console.log({ open, requests });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<summary>
|
||||||
|
Filtered notifications from{' '}
|
||||||
|
{notificationsPolicy.summary.pendingRequestsCount} people
|
||||||
|
</summary>
|
||||||
|
{!notificationsRequests ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
notificationsRequests?.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{notificationsRequests.map((request) => (
|
||||||
|
<li key={request.id}>
|
||||||
|
<div class="request-notifcations">
|
||||||
|
{!request.lastStatus?.id && (
|
||||||
|
<AccountBlock
|
||||||
|
useAvatarStatic
|
||||||
|
showStats
|
||||||
|
account={request.account}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{request.lastStatus?.id && (
|
||||||
|
<div class="last-post">
|
||||||
|
<Link
|
||||||
|
class="status-link"
|
||||||
|
to={`/${instance}/s/${request.lastStatus.id}`}
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
status={request.lastStatus}
|
||||||
|
size="s"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<NotificationRequestModalButton request={request} />
|
||||||
|
</div>
|
||||||
|
<NotificationRequestButtons
|
||||||
|
request={request}
|
||||||
|
onChange={() => {
|
||||||
|
loadNotifications(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div id="mentions-option">
|
<div id="mentions-option">
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
@ -514,6 +701,109 @@ function Notifications({ columnMode }) {
|
||||||
</InView>
|
</InView>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{supportsFilteredNotifications && showNotificationsSettings && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowNotificationsSettings(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="sheet" id="notifications-settings" tabIndex="-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sheet-close"
|
||||||
|
onClick={() => setShowNotificationsSettings(false)}
|
||||||
|
>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
<header>
|
||||||
|
<h2>Notifications settings</h2>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const {
|
||||||
|
filterNotFollowing,
|
||||||
|
filterNotFollowers,
|
||||||
|
filterNewAccounts,
|
||||||
|
filterPrivateMentions,
|
||||||
|
} = e.target;
|
||||||
|
const allFilters = {
|
||||||
|
filterNotFollowing: filterNotFollowing.checked,
|
||||||
|
filterNotFollowers: filterNotFollowers.checked,
|
||||||
|
filterNewAccounts: filterNewAccounts.checked,
|
||||||
|
filterPrivateMentions: filterPrivateMentions.checked,
|
||||||
|
};
|
||||||
|
setNotificationsPolicy({
|
||||||
|
...notificationsPolicy,
|
||||||
|
...allFilters,
|
||||||
|
});
|
||||||
|
setShowNotificationsSettings(false);
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.notifications.policy.update(allFilters);
|
||||||
|
showToast('Notifications settings updated');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>Filter out notifications from people:</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
switch
|
||||||
|
defaultChecked={notificationsPolicy.filterNotFollowing}
|
||||||
|
name="filterNotFollowing"
|
||||||
|
/>{' '}
|
||||||
|
You don't follow
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
switch
|
||||||
|
defaultChecked={notificationsPolicy.filterNotFollowers}
|
||||||
|
name="filterNotFollowers"
|
||||||
|
/>{' '}
|
||||||
|
Who don't follow you
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
switch
|
||||||
|
defaultChecked={notificationsPolicy.filterNewAccounts}
|
||||||
|
name="filterNewAccounts"
|
||||||
|
/>{' '}
|
||||||
|
With a new account
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
switch
|
||||||
|
defaultChecked={notificationsPolicy.filterPrivateMentions}
|
||||||
|
name="filterPrivateMentions"
|
||||||
|
/>{' '}
|
||||||
|
Who unsolicitedly private mention you
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -596,4 +886,186 @@ function AnnouncementBlock({ announcement }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchNotficationsByAccount(accountID) {
|
||||||
|
const { masto } = api();
|
||||||
|
return masto.v1.notifications.list({
|
||||||
|
accountID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function NotificationRequestModalButton({ request }) {
|
||||||
|
const { instance } = api();
|
||||||
|
const [uiState, setUIState] = useState('loading');
|
||||||
|
const { account, lastStatus } = request;
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
setShowModal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!request?.account?.id) return;
|
||||||
|
if (!showModal) return;
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
const notifs = await fetchNotficationsByAccount(request.account.id);
|
||||||
|
setNotifications(notifs || []);
|
||||||
|
setUIState('default');
|
||||||
|
})();
|
||||||
|
}, [showModal, request?.account?.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain4 request-notifications-account"
|
||||||
|
onClick={() => {
|
||||||
|
setShowModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="notification" class="more-insignificant" />{' '}
|
||||||
|
<small>View notifications from @{account.username}</small>{' '}
|
||||||
|
<Icon icon="chevron-down" />
|
||||||
|
</button>
|
||||||
|
{showModal && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="sheet" tabIndex="-1">
|
||||||
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
<header>
|
||||||
|
<b>Notifications from @{account.username}</b>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{uiState === 'loading' ? (
|
||||||
|
<p class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
notifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
class="notification-peek"
|
||||||
|
onClick={(e) => {
|
||||||
|
const { target } = e;
|
||||||
|
// If button or links
|
||||||
|
if (
|
||||||
|
e.target.tagName === 'BUTTON' ||
|
||||||
|
e.target.tagName === 'A'
|
||||||
|
) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Notification
|
||||||
|
instance={instance}
|
||||||
|
notification={notification}
|
||||||
|
isStatic
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationRequestButtons({ request, onChange }) {
|
||||||
|
const { masto } = api();
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [requestState, setRequestState] = useState(null); // accept, dismiss
|
||||||
|
const hasRequestState = requestState !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p class="notification-request-buttons">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={uiState === 'loading' || hasRequestState}
|
||||||
|
onClick={() => {
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.notifications.requests
|
||||||
|
.$select(request.id)
|
||||||
|
.accept();
|
||||||
|
setRequestState('accept');
|
||||||
|
setUIState('default');
|
||||||
|
onChange({
|
||||||
|
request,
|
||||||
|
state: 'accept',
|
||||||
|
});
|
||||||
|
showToast(
|
||||||
|
`Notifications from @${request.account.username} will not be filtered from now on.`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setUIState('error');
|
||||||
|
console.error(error);
|
||||||
|
showToast(`Unable to accept notification request`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Allow
|
||||||
|
</button>{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={uiState === 'loading' || hasRequestState}
|
||||||
|
class="light danger"
|
||||||
|
onClick={() => {
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await masto.v1.notifications.requests
|
||||||
|
.$select(request.id)
|
||||||
|
.dismiss();
|
||||||
|
setRequestState('dismiss');
|
||||||
|
setUIState('default');
|
||||||
|
onChange({
|
||||||
|
request,
|
||||||
|
state: 'dismiss',
|
||||||
|
});
|
||||||
|
showToast(
|
||||||
|
`Notifications from @${request.account.username} will not show up in Filtered notifications from now on.`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setUIState('error');
|
||||||
|
console.error(error);
|
||||||
|
showToast(`Unable to dismiss notification request`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
<span class="notification-request-states">
|
||||||
|
{uiState === 'loading' ? (
|
||||||
|
<Loader abrupt />
|
||||||
|
) : requestState === 'accept' ? (
|
||||||
|
<Icon
|
||||||
|
icon="check-circle"
|
||||||
|
alt="Accepted"
|
||||||
|
class="notification-accepted"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
requestState === 'dismiss' && (
|
||||||
|
<Icon
|
||||||
|
icon="x-circle"
|
||||||
|
alt="Dismissed"
|
||||||
|
class="notification-dismissed"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(Notifications);
|
export default memo(Notifications);
|
||||||
|
|
|
@ -433,7 +433,7 @@ function Settings({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{!!IMG_ALT_API_URL && (
|
{!!IMG_ALT_API_URL && authenticated && (
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -906,7 +906,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
!!heroStatus?.repliesCount &&
|
!!heroStatus?.repliesCount &&
|
||||||
!hasDescendants && (
|
!hasDescendants && (
|
||||||
<div class="status-loading">
|
<div class="status-loading">
|
||||||
<Loader />
|
<Loader abrupt={heroStatus.repliesCount >= 3} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uiState === 'error' &&
|
{uiState === 'error' &&
|
||||||
|
|
|
@ -67,7 +67,7 @@ function Trending({ columnMode, ...props }) {
|
||||||
|
|
||||||
// Get links
|
// Get links
|
||||||
try {
|
try {
|
||||||
const { value } = await fetchLinks(masto);
|
const { value } = await fetchLinks(masto, instance);
|
||||||
// 4 types available: link, photo, video, rich
|
// 4 types available: link, photo, video, rich
|
||||||
// Only want links for now
|
// Only want links for now
|
||||||
const links = value?.filter?.((link) => link.type === 'link');
|
const links = value?.filter?.((link) => link.type === 'link');
|
||||||
|
|
|
@ -16,7 +16,9 @@ function handleContentLinks(opts) {
|
||||||
const textBeforeLinkIsAt = prevText?.endsWith('@');
|
const textBeforeLinkIsAt = prevText?.endsWith('@');
|
||||||
const textStartsWithAt = target.innerText.startsWith('@');
|
const textStartsWithAt = target.innerText.startsWith('@');
|
||||||
if (
|
if (
|
||||||
(target.classList.contains('u-url') && textStartsWithAt) ||
|
((target.classList.contains('u-url') ||
|
||||||
|
target.classList.contains('mention')) &&
|
||||||
|
textStartsWithAt) ||
|
||||||
(textBeforeLinkIsAt && !textStartsWithAt)
|
(textBeforeLinkIsAt && !textStartsWithAt)
|
||||||
) {
|
) {
|
||||||
const targetText = (
|
const targetText = (
|
||||||
|
@ -24,12 +26,14 @@ function handleContentLinks(opts) {
|
||||||
).innerText.trim();
|
).innerText.trim();
|
||||||
const username = targetText.replace(/^@/, '');
|
const username = targetText.replace(/^@/, '');
|
||||||
const url = target.getAttribute('href');
|
const url = target.getAttribute('href');
|
||||||
const mention = mentions.find(
|
// Only fallback to acct/username check if url doesn't match
|
||||||
(mention) =>
|
const mention =
|
||||||
mention.username === username ||
|
mentions.find((mention) => mention.url === url) ||
|
||||||
mention.acct === username ||
|
mentions.find(
|
||||||
mention.url === url,
|
(mention) =>
|
||||||
);
|
mention.acct === username || mention.username === username,
|
||||||
|
);
|
||||||
|
console.warn('MENTION', mention, url);
|
||||||
if (mention) {
|
if (mention) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
@ -9,8 +9,10 @@ function statusPeek(status) {
|
||||||
text += getHTMLText(content);
|
text += getHTMLText(content);
|
||||||
}
|
}
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
if (poll) {
|
if (poll?.options?.length) {
|
||||||
text += ' 📊';
|
text += `\n\n📊:\n${poll.options
|
||||||
|
.map((o) => `${poll.multiple ? '▪️' : '•'} ${o.title}`)
|
||||||
|
.join('\n')}`;
|
||||||
}
|
}
|
||||||
if (mediaAttachments?.length) {
|
if (mediaAttachments?.length) {
|
||||||
text +=
|
text +=
|
||||||
|
|
|
@ -37,6 +37,7 @@ const rollbarCode = fs.readFileSync(
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: './',
|
base: './',
|
||||||
envPrefix: allowedEnvPrefixes,
|
envPrefix: allowedEnvPrefixes,
|
||||||
|
appType: 'mpa',
|
||||||
mode: NODE_ENV,
|
mode: NODE_ENV,
|
||||||
define: {
|
define: {
|
||||||
__BUILD_TIME__: JSON.stringify(now),
|
__BUILD_TIME__: JSON.stringify(now),
|
||||||
|
@ -93,6 +94,7 @@ export default defineConfig({
|
||||||
purpose: 'maskable',
|
purpose: 'maskable',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
categories: ['social', 'news'],
|
||||||
},
|
},
|
||||||
strategies: 'injectManifest',
|
strategies: 'injectManifest',
|
||||||
injectRegister: 'inline',
|
injectRegister: 'inline',
|
||||||
|
@ -115,6 +117,9 @@ export default defineConfig({
|
||||||
compose: resolve(__dirname, 'compose/index.html'),
|
compose: resolve(__dirname, 'compose/index.html'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'intl-segmenter-polyfill': ['@formatjs/intl-segmenter/polyfill'],
|
||||||
|
},
|
||||||
chunkFileNames: (chunkInfo) => {
|
chunkFileNames: (chunkInfo) => {
|
||||||
const { facadeModuleId } = chunkInfo;
|
const { facadeModuleId } = chunkInfo;
|
||||||
if (facadeModuleId && facadeModuleId.includes('icon')) {
|
if (facadeModuleId && facadeModuleId.includes('icon')) {
|
||||||
|
|
Loading…
Reference in a new issue