kopia lustrzana https://github.com/cheeaun/phanpy
Porównaj commity
39 Commity
30eca61e85
...
fd298051df
Autor | SHA1 | Data |
---|---|---|
Alyx | fd298051df | |
Lim Chee Aun | 7100937e79 | |
Lim Chee Aun | c18efef7b6 | |
Lim Chee Aun | ff336628f8 | |
Lim Chee Aun | 28882d98d9 | |
Lim Chee Aun | f6ad22e58f | |
Lim Chee Aun | aa664e15f6 | |
Chee Aun | f2f203c9d8 | |
snail-coupe | ae0e4a0792 | |
Lim Chee Aun | 4def6eef5a | |
Lim Chee Aun | 1004a5f176 | |
Lim Chee Aun | 2b6beee875 | |
Lim Chee Aun | e35e02593a | |
Lim Chee Aun | 5e56ba9fb9 | |
Lim Chee Aun | a7cc0785f9 | |
Lim Chee Aun | bb5d34c94c | |
Lim Chee Aun | 671d2c9bb1 | |
Lim Chee Aun | 49fa48bd28 | |
Lim Chee Aun | 32fb406629 | |
Lim Chee Aun | 6950698935 | |
Lim Chee Aun | fd9d8059bc | |
Lim Chee Aun | 3b975e899b | |
Lim Chee Aun | b1950046d4 | |
Lim Chee Aun | d2af509eaf | |
Lim Chee Aun | 311160983f | |
Lim Chee Aun | 9d7d5df7f2 | |
Lim Chee Aun | 927430853a | |
Lim Chee Aun | 1692637e22 | |
Lim Chee Aun | 2bc24cc495 | |
Lim Chee Aun | 66e58c74ef | |
Lim Chee Aun | e3591514a1 | |
Lim Chee Aun | 4abb1aeaed | |
Lim Chee Aun | 7cac17a043 | |
Lim Chee Aun | 7049166b40 | |
Lim Chee Aun | 0a695410d9 | |
Lim Chee Aun | d671178c02 | |
Lim Chee Aun | 67a05450cf | |
Lim Chee Aun | 438b520970 | |
Alyx | fa196a2c94 |
|
@ -0,0 +1,23 @@
|
||||||
|
FROM busybox:1 AS build
|
||||||
|
ARG PHANPY_RELEASE_VERSION
|
||||||
|
|
||||||
|
WORKDIR /root/phanpy_release
|
||||||
|
|
||||||
|
RUN wget "https://github.com/cheeaun/phanpy/releases/download/${PHANPY_RELEASE_VERSION}/phanpy-dist.tar.gz" && \
|
||||||
|
tar -xvf "phanpy-dist.tar.gz" -C /root/phanpy_release && \
|
||||||
|
rm "phanpy-dist.tar.gz"
|
||||||
|
|
||||||
|
# ---
|
||||||
|
FROM busybox:1
|
||||||
|
|
||||||
|
# Create a non-root user to own the files and run our server
|
||||||
|
RUN adduser -D static
|
||||||
|
USER static
|
||||||
|
WORKDIR /home/static
|
||||||
|
|
||||||
|
# Copy the static website
|
||||||
|
# Use the .dockerignore file to control what ends up inside the image!
|
||||||
|
COPY --chown=static:static --from=build /root/phanpy_release /home/static
|
||||||
|
|
||||||
|
# Run BusyBox httpd
|
||||||
|
CMD ["httpd", "-f", "-v", "-p", "8080"]
|
|
@ -138,7 +138,7 @@ Download or `git clone` this repository. Use `production` branch for *stable* re
|
||||||
Customization can be done by passing environment variables to the build command. Examples:
|
Customization can be done by passing environment variables to the build command. Examples:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PHANPY_APP_TITLE="Phanpy Dev" \
|
PHANPY_CLIENT_NAME="Phanpy Dev" \
|
||||||
PHANPY_WEBSITE="https://dev.phanpy.social" \
|
PHANPY_WEBSITE="https://dev.phanpy.social" \
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
@ -179,6 +179,10 @@ Available variables:
|
||||||
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
|
||||||
- List of fallback instances hard-coded in `/.env`
|
- List of fallback instances hard-coded in `/.env`
|
||||||
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
|
||||||
|
- `PHANPY_GIPHY_API_KEY` (optional, no defaults):
|
||||||
|
- API key for [GIPHY](https://developers.giphy.com/). See [API docs](https://developers.giphy.com/docs/api/).
|
||||||
|
- If provided, a setting will appear for users to enable the GIF picker in the composer. Disabled by default.
|
||||||
|
- This is not self-hosted.
|
||||||
|
|
||||||
### Static site hosting
|
### Static site hosting
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"p-retry": "~6.2.0",
|
"p-retry": "~6.2.0",
|
||||||
"p-throttle": "~6.1.0",
|
"p-throttle": "~6.1.0",
|
||||||
"preact": "~10.20.1",
|
"preact": "~10.20.1",
|
||||||
|
"punycode": "~2.3.1",
|
||||||
"react-hotkeys-hook": "~4.5.0",
|
"react-hotkeys-hook": "~4.5.0",
|
||||||
"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",
|
||||||
|
@ -7154,11 +7155,9 @@
|
||||||
"integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="
|
"integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="
|
||||||
},
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"p-retry": "~6.2.0",
|
"p-retry": "~6.2.0",
|
||||||
"p-throttle": "~6.1.0",
|
"p-throttle": "~6.1.0",
|
||||||
"preact": "~10.20.1",
|
"preact": "~10.20.1",
|
||||||
|
"punycode": "~2.3.1",
|
||||||
"react-hotkeys-hook": "~4.5.0",
|
"react-hotkeys-hook": "~4.5.0",
|
||||||
"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",
|
||||||
|
|
|
@ -295,7 +295,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
video,
|
video,
|
||||||
img,
|
img,
|
||||||
audio {
|
audio {
|
||||||
min-height: 88px; /* for extreme dimensions */
|
min-height: var(--min-dimension); /* for extreme dimensions */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 641 223">
|
||||||
|
<path fill="#aaa" d="M86 214c-9-1-17-4-24-8l-6-3-5-5-5-4-4-6-4-6-3-8-2-8v-27l2-9 3-9 4-6 4-6 5-5 5-5 7-3 6-4 7-2 7-2 12-1h12l7 1 8 2 7 4 7 3 5 5 5 4-10 10-10 9-4-3-10-5-5-1H88l-5 2-6 3-3 4-4 4-2 5-2 6v6l-1 7 1 7 2 7 3 5 2 4 4 3 4 3 5 2 6 2h9l10-1 5-2 6-3v-16H91v-27h59v54l-1 3-2 3-5 4-4 4-5 3-5 2-8 2-8 2-10 1H92l-6-1zm266-62V91h34v46h44V91h34v121h-34v-46h-44v46h-34v-61zm-182-1V90h34v121h-34v-60zm59-1V90h35l36 1 5 2c3 0 8 2 10 4l5 2 4 5 5 4 3 7 3 7 1 13v13l-4 6-3 7-4 4-5 5-5 2-5 3-6 2-5 1-18 1h-18v32h-34v-61zm67-2 3-2 2-4 2-5v-5l-2-4-2-4-3-2-3-3h-30v31h30l3-2zm226 39v-24l-8-12-18-28a1751 1751 0 0 0-20-31v-2h39l7 12 12 21 6 9 13-21 13-21h38v2l-41 61-7 10v48h-34v-24zM109 66l-4-1-5-5-5-4-1-5-3-9v-5l1-5c2-7 3-10 8-15l4-4 7-2 7-2h7l6 1 5 2 5 2 3 4 4 3 2 6 2 5v13l-2 5-2 6-4 4-3 3-5 2-4 2-9 1h-9l-5-2zm22-11 4-2 3-4 2-5V34l-2-4-2-4-3-2-4-3-5-1h-6l-4 2-5 2-2 4-3 5-1 3v4l1 5 2 5 2 2 5 3 4 2h10l4-2zM37 39V11h33l3 1 3 2 4 3 3 3 1 5 1 4v5l-1 4-3 4-3 5-4 1-3 2-11 1H49v16H37V39zm31 0 3-2 1-2 1-2v-4l-1-3-3-2-2-2H49v18h15l4-1zm107 25a512 512 0 0 0-19-53h14l4 14 6 19 1 4 1-1 7-19 5-17h9l6 19 7 18v-1l2-6 5-17 4-13h14v1l-4 12-16 41v2h-5l-5-1-6-15-6-15-1 1-3 7-6 15-2 8h-11l-1-3zm74-25V11h42v11h-29v2l-1 5v4h29v11h-28v11h2l15 1h13v11h-43V39zm55 0V11h33l5 3 5 2 2 4 2 5v10l-2 3-1 4-5 3-5 3 5 5 8 10 3 4h-14l-7-9-8-10h-9v19h-12V39zm33-3 2-3v-6l-3-3-2-3h-18v16h1v1h17l2-2zm26 3V11h42v11h-29l-1 6v5h29v11h-28v5l-1 5 1 1v1h30v11h-43V39zm54 0V11h17l18 1 4 2 5 3 2 4 3 4 2 6 1 6v5c-1 6-3 12-6 15l-3 4-5 3-5 2-17 1h-16V39zm33 14 5-5 2-3v-6l-1-6-1-3-1-3-4-3-3-2h-5l-6-1-3 1h-3v34h9l8-1 3-2zm50-14V11h34l5 2 4 2 2 3 2 3v9l-2 2-3 4-1 1 3 3 3 4 1 3 1 4-1 4-1 4-3 3-3 3-5 1-5 1h-31V39zm34 15 2-1v-6l-2-2-2-2h-20v13h20l2-2zm-3-22 4-2v-6l-2-1-2-2h-19v12h16l4-1zm42 24V45l-6-9-11-17-5-8h15l4 8 7 11 2 3 7-11 7-11h14l-11 16-11 17v23h-12V56z"/>
|
||||||
|
</svg>
|
Po Szerokość: | Wysokość: | Rozmiar: 1.9 KiB |
|
@ -15,7 +15,8 @@ body.cloak,
|
||||||
.account-block,
|
.account-block,
|
||||||
.catchup-filters .filter-author *,
|
.catchup-filters .filter-author *,
|
||||||
.post-peek-html *,
|
.post-peek-html *,
|
||||||
.post-peek-content > * {
|
.post-peek-content > *,
|
||||||
|
.request-notifications-account * {
|
||||||
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; */
|
||||||
|
@ -51,7 +52,8 @@ body.cloak,
|
||||||
.cloak {
|
.cloak {
|
||||||
.media-container figcaption,
|
.media-container figcaption,
|
||||||
.media-container figcaption > *,
|
.media-container figcaption > *,
|
||||||
.catchup-filters .filter-author * {
|
.catchup-filters .filter-author *,
|
||||||
|
.request-notifications-account * {
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,5 +106,5 @@ export const ICONS = {
|
||||||
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
|
copy: () => import('@iconify-icons/mingcute/copy-2-line'),
|
||||||
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
|
quote: () => import('@iconify-icons/mingcute/quote-left-line'),
|
||||||
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
|
settings: () => import('@iconify-icons/mingcute/settings-6-line'),
|
||||||
unlink: () => import('@iconify-icons/mingcute/unlink-line'),
|
'heart-break': () => import('@iconify-icons/mingcute/heart-crack-line'),
|
||||||
};
|
};
|
||||||
|
|
|
@ -831,3 +831,58 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handle-info {
|
||||||
|
.handle-handle {
|
||||||
|
display: inline-block;
|
||||||
|
margin-block: 5px;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 0 0 5px var(--bg-blur-color);
|
||||||
|
|
||||||
|
&.handle-username {
|
||||||
|
color: var(--orange-fg-color);
|
||||||
|
background-color: var(--orange-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.handle-server {
|
||||||
|
color: var(--purple-fg-color);
|
||||||
|
background-color: var(--purple-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-at {
|
||||||
|
display: inline-block;
|
||||||
|
margin-inline: -3px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-legend {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle-legend-icon {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
|
||||||
|
&.username {
|
||||||
|
background-color: var(--orange-fg-color);
|
||||||
|
border-color: var(--orange-bg-color);
|
||||||
|
}
|
||||||
|
&.server {
|
||||||
|
background-color: var(--purple-fg-color);
|
||||||
|
border-color: var(--purple-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
|
import punycode from 'punycode';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
|
@ -228,7 +229,7 @@ function AccountInfo({
|
||||||
|
|
||||||
const accountInstance = useMemo(() => {
|
const accountInstance = useMemo(() => {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
const domain = new URL(url).hostname;
|
const domain = punycode.toUnicode(new URL(url).hostname);
|
||||||
return domain;
|
return domain;
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
|
@ -541,13 +542,55 @@ function AccountInfo({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<header>
|
<header>
|
||||||
<AccountBlock
|
{standalone ? (
|
||||||
account={info}
|
<Menu2
|
||||||
instance={instance}
|
shift={
|
||||||
avatarSize="xxxl"
|
window.matchMedia('(min-width: calc(40em))').matches
|
||||||
external={standalone}
|
? 114
|
||||||
internal={!standalone}
|
: 64
|
||||||
/>
|
}
|
||||||
|
menuButton={
|
||||||
|
<div>
|
||||||
|
<AccountBlock
|
||||||
|
account={info}
|
||||||
|
instance={instance}
|
||||||
|
avatarSize="xxxl"
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="szh-menu__header">
|
||||||
|
<AccountHandleInfo acct={acct} instance={instance} />
|
||||||
|
</div>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
const handle = `@${acct}`;
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(handle);
|
||||||
|
showToast('Handle copied');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast('Unable to copy handle');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="link" />
|
||||||
|
<span>Copy handle</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem href={url} target="_blank">
|
||||||
|
<Icon icon="external" />
|
||||||
|
<span>Go to original profile page</span>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu2>
|
||||||
|
) : (
|
||||||
|
<AccountBlock
|
||||||
|
account={info}
|
||||||
|
instance={instance}
|
||||||
|
avatarSize="xxxl"
|
||||||
|
internal
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
<div class="faux-header-bg" aria-hidden="true" />
|
<div class="faux-header-bg" aria-hidden="true" />
|
||||||
<main>
|
<main>
|
||||||
|
@ -767,38 +810,40 @@ function AccountInfo({
|
||||||
</div>
|
</div>
|
||||||
</LinkOrDiv>
|
</LinkOrDiv>
|
||||||
)}
|
)}
|
||||||
<div class="account-metadata-box">
|
{!moved && (
|
||||||
<div
|
<div class="account-metadata-box">
|
||||||
class="shazam-container no-animation"
|
<div
|
||||||
hidden={!!postingStats}
|
class="shazam-container no-animation"
|
||||||
>
|
hidden={!!postingStats}
|
||||||
<div class="shazam-container-inner">
|
>
|
||||||
<button
|
<div class="shazam-container-inner">
|
||||||
type="button"
|
<button
|
||||||
class="posting-stats-button"
|
type="button"
|
||||||
disabled={postingStatsUIState === 'loading'}
|
class="posting-stats-button"
|
||||||
onClick={() => {
|
disabled={postingStatsUIState === 'loading'}
|
||||||
renderPostingStats();
|
onClick={() => {
|
||||||
}}
|
renderPostingStats();
|
||||||
>
|
|
||||||
<div
|
|
||||||
class={`posting-stats-bar posting-stats-icon ${
|
|
||||||
postingStatsUIState === 'loading' ? 'loading' : ''
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
'--originals-percentage': '33%',
|
|
||||||
'--replies-percentage': '66%',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
View post stats{' '}
|
<div
|
||||||
{/* <Loader
|
class={`posting-stats-bar posting-stats-icon ${
|
||||||
|
postingStatsUIState === 'loading' ? 'loading' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
'--originals-percentage': '33%',
|
||||||
|
'--replies-percentage': '66%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
View post stats{' '}
|
||||||
|
{/* <Loader
|
||||||
abrupt
|
abrupt
|
||||||
hidden={postingStatsUIState !== 'loading'}
|
hidden={postingStatsUIState !== 'loading'}
|
||||||
/> */}
|
/> */}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<RelatedActions
|
<RelatedActions
|
||||||
|
@ -897,7 +942,7 @@ function RelatedActions({
|
||||||
|
|
||||||
accountID.current = currentID;
|
accountID.current = currentID;
|
||||||
|
|
||||||
if (moved) return;
|
// if (moved) return;
|
||||||
|
|
||||||
setRelationshipUIState('loading');
|
setRelationshipUIState('loading');
|
||||||
|
|
||||||
|
@ -1395,7 +1440,7 @@ function RelatedActions({
|
||||||
{!relationship && relationshipUIState === 'loading' && (
|
{!relationship && relationshipUIState === 'loading' && (
|
||||||
<Loader abrupt />
|
<Loader abrupt />
|
||||||
)}
|
)}
|
||||||
{!!relationship && (
|
{!!relationship && !moved && (
|
||||||
<MenuConfirm
|
<MenuConfirm
|
||||||
confirm={following || requested}
|
confirm={following || requested}
|
||||||
confirmLabel={
|
confirmLabel={
|
||||||
|
@ -1554,7 +1599,7 @@ function niceAccountURL(url) {
|
||||||
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
|
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span class="more-insignificant">{host}/</span>
|
<span class="more-insignificant">{punycode.toUnicode(host)}/</span>
|
||||||
<wbr />
|
<wbr />
|
||||||
<span>{path}</span>
|
<span>{path}</span>
|
||||||
</>
|
</>
|
||||||
|
@ -2000,4 +2045,27 @@ function FieldsAttributesRow({ name, value, disabled, index: i }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AccountHandleInfo({ acct, instance }) {
|
||||||
|
// acct = username or username@server
|
||||||
|
let [username, server] = acct.split('@');
|
||||||
|
if (!server) server = instance;
|
||||||
|
return (
|
||||||
|
<div class="handle-info">
|
||||||
|
<span class="handle-handle">
|
||||||
|
<b class="handle-username">{username}</b>
|
||||||
|
<span class="handle-at">@</span>
|
||||||
|
<b class="handle-server">{server}</b>
|
||||||
|
</span>
|
||||||
|
<div class="handle-legend">
|
||||||
|
<span class="ib">
|
||||||
|
<span class="handle-legend-icon username" /> username
|
||||||
|
</span>{' '}
|
||||||
|
<span class="ib">
|
||||||
|
<span class="handle-legend-icon server" /> server domain name
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default AccountInfo;
|
export default AccountInfo;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
|
import openOSK from '../utils/open-osk';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -14,6 +15,7 @@ export default function ComposeButton() {
|
||||||
states.showCompose = true;
|
states.showCompose = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
openOSK();
|
||||||
states.showCompose = true;
|
states.showCompose = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -727,3 +727,165 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes gif-shake {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gif-picker-button {
|
||||||
|
span {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11.5px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
span {
|
||||||
|
animation: gif-shake 0.3s 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#gif-picker-sheet {
|
||||||
|
height: 50vh;
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 2px,
|
||||||
|
black 16px,
|
||||||
|
black calc(100% - 16px),
|
||||||
|
transparent calc(100% - 2px)
|
||||||
|
);
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-state {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
min-height: 100px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
list-style: none;
|
||||||
|
padding: 8px 2px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) {
|
||||||
|
background-color: var(--link-bg-color);
|
||||||
|
box-shadow: 0 0 0 2px var(--link-light-color);
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: var(--figure-width);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--text-insignificant-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
background-color: var(--img-bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
vertical-align: top;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
@media (min-height: 480px) {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { uid } from 'uid/single';
|
||||||
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import poweredByGiphyURL from '../assets/powered-by-giphy.svg';
|
||||||
|
|
||||||
import Menu2 from '../components/menu2';
|
import Menu2 from '../components/menu2';
|
||||||
import supportedLanguages from '../data/status-supported-languages';
|
import supportedLanguages from '../data/status-supported-languages';
|
||||||
import urlRegex from '../data/url-regex';
|
import urlRegex from '../data/url-regex';
|
||||||
|
@ -41,7 +43,10 @@ import Loader from './loader';
|
||||||
import Modal from './modal';
|
import Modal from './modal';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
const { PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL } = import.meta.env;
|
const {
|
||||||
|
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
||||||
|
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
|
||||||
|
} = import.meta.env;
|
||||||
|
|
||||||
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
|
||||||
const [code, common, native] = l;
|
const [code, common, native] = l;
|
||||||
|
@ -299,7 +304,7 @@ function Compose({
|
||||||
setVisibility(visibility);
|
setVisibility(visibility);
|
||||||
setLanguage(language || presf.postingDefaultLanguage || DEFAULT_LANG);
|
setLanguage(language || presf.postingDefaultLanguage || DEFAULT_LANG);
|
||||||
setSensitive(sensitive);
|
setSensitive(sensitive);
|
||||||
setPoll(composablePoll);
|
if (composablePoll) setPoll(composablePoll);
|
||||||
setMediaAttachments(mediaAttachments);
|
setMediaAttachments(mediaAttachments);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -610,6 +615,7 @@ function Compose({
|
||||||
}, [mediaAttachments]);
|
}, [mediaAttachments]);
|
||||||
|
|
||||||
const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
|
const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
|
||||||
|
const [showGIFPicker, setShowGIFPicker] = useState(false);
|
||||||
|
|
||||||
const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
|
const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
|
||||||
const topLanguages = [];
|
const topLanguages = [];
|
||||||
|
@ -1235,6 +1241,18 @@ function Compose({
|
||||||
>
|
>
|
||||||
<Icon icon="emoji2" />
|
<Icon icon="emoji2" />
|
||||||
</button>
|
</button>
|
||||||
|
{!!states.settings.composerGIFPicker && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toolbar-button gif-picker-button"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
setShowGIFPicker(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>GIF</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
{uiState === 'loading' ? (
|
{uiState === 'loading' ? (
|
||||||
|
@ -1319,6 +1337,64 @@ function Compose({
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{showGIFPicker && (
|
||||||
|
<Modal
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowGIFPicker(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GIFPickerModal
|
||||||
|
onClose={() => setShowGIFPicker(false)}
|
||||||
|
onSelect={({ url, type, alt_text }) => {
|
||||||
|
console.log('GIF URL', url);
|
||||||
|
if (mediaAttachments.length >= maxMediaAttachments) {
|
||||||
|
alert(
|
||||||
|
`You can only attach up to ${maxMediaAttachments} files.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Download the GIF and insert it as media attachment
|
||||||
|
(async () => {
|
||||||
|
let theToast;
|
||||||
|
try {
|
||||||
|
theToast = showToast({
|
||||||
|
text: 'Downloading GIF…',
|
||||||
|
duration: -1,
|
||||||
|
});
|
||||||
|
const blob = await fetch(url, {
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
}).then((res) => res.blob());
|
||||||
|
const file = new File(
|
||||||
|
[blob],
|
||||||
|
type === 'video/mp4' ? 'video.mp4' : 'image.gif',
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const newMediaAttachments = [
|
||||||
|
...mediaAttachments,
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
type,
|
||||||
|
size: file.size,
|
||||||
|
id: null,
|
||||||
|
description: alt_text || '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setMediaAttachments(newMediaAttachments);
|
||||||
|
theToast?.hideToast?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
theToast?.hideToast?.();
|
||||||
|
showToast('Failed to download GIF');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1711,6 +1787,9 @@ function MediaAttachment({
|
||||||
onDescriptionChange,
|
onDescriptionChange,
|
||||||
250,
|
250,
|
||||||
);
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedOnDescriptionChange(description);
|
||||||
|
}, [description, debouncedOnDescriptionChange]);
|
||||||
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
|
@ -1759,7 +1838,7 @@ function MediaAttachment({
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
setDescription(value);
|
setDescription(value);
|
||||||
debouncedOnDescriptionChange(value);
|
// debouncedOnDescriptionChange(value);
|
||||||
}}
|
}}
|
||||||
></textarea>
|
></textarea>
|
||||||
)}
|
)}
|
||||||
|
@ -2243,4 +2322,225 @@ function CustomEmojisModal({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GIFS_PER_PAGE = 20;
|
||||||
|
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [results, setResults] = useState([]);
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const qRef = useRef(null);
|
||||||
|
const currentOffset = useRef(0);
|
||||||
|
const scrollableRef = useRef(null);
|
||||||
|
|
||||||
|
function fetchGIFs({ offset }) {
|
||||||
|
console.log('fetchGIFs', { offset });
|
||||||
|
if (!qRef.current?.value) return;
|
||||||
|
setUIState('loading');
|
||||||
|
scrollableRef.current?.scrollTo?.({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const query = {
|
||||||
|
api_key: GIPHY_API_KEY,
|
||||||
|
q: qRef.current.value,
|
||||||
|
rating: 'g',
|
||||||
|
limit: GIFS_PER_PAGE,
|
||||||
|
bundle: 'messaging_non_clips',
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
const response = await fetch(
|
||||||
|
'https://api.giphy.com/v1/gifs/search?' + new URLSearchParams(query),
|
||||||
|
{
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
},
|
||||||
|
).then((r) => r.json());
|
||||||
|
currentOffset.current = response.pagination?.offset || 0;
|
||||||
|
setResults(response);
|
||||||
|
setUIState('results');
|
||||||
|
} catch (e) {
|
||||||
|
setUIState('error');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
qRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="gif-picker-sheet" class="sheet">
|
||||||
|
{!!onClose && (
|
||||||
|
<button type="button" class="sheet-close" onClick={onClose}>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<header>
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchGIFs({ offset: 0 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={qRef}
|
||||||
|
type="search"
|
||||||
|
name="q"
|
||||||
|
placeholder="Search GIFs"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="image"
|
||||||
|
class="powered-button"
|
||||||
|
src={poweredByGiphyURL}
|
||||||
|
width="86"
|
||||||
|
height="30"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
<main ref={scrollableRef} class={uiState === 'loading' ? 'loading' : ''}>
|
||||||
|
{uiState === 'default' && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<p class="insignificant">Type to search GIFs</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uiState === 'loading' && !results?.data?.length && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<Loader abrupt />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{results?.data?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<ul>
|
||||||
|
{results.data.map((gif) => {
|
||||||
|
const { id, images, title, alt_text } = gif;
|
||||||
|
const {
|
||||||
|
fixed_height_small,
|
||||||
|
fixed_height_downsampled,
|
||||||
|
fixed_height,
|
||||||
|
original,
|
||||||
|
} = images;
|
||||||
|
const theImage = fixed_height_small?.url
|
||||||
|
? fixed_height_small
|
||||||
|
: fixed_height_downsampled?.url
|
||||||
|
? fixed_height_downsampled
|
||||||
|
: fixed_height;
|
||||||
|
let { url, webp, width, height } = theImage;
|
||||||
|
if (+height > 100) {
|
||||||
|
width = (width / height) * 100;
|
||||||
|
height = 100;
|
||||||
|
}
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const strippedURL = urlObj.origin + urlObj.pathname;
|
||||||
|
let strippedWebP;
|
||||||
|
if (webp) {
|
||||||
|
const webpObj = new URL(webp);
|
||||||
|
strippedWebP = webpObj.origin + webpObj.pathname;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={id}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const { mp4, url } = original;
|
||||||
|
const theURL = mp4 || url;
|
||||||
|
const urlObj = new URL(theURL);
|
||||||
|
const strippedURL = urlObj.origin + urlObj.pathname;
|
||||||
|
onClose();
|
||||||
|
onSelect({
|
||||||
|
url: strippedURL,
|
||||||
|
type: mp4 ? 'video/mp4' : 'image/gif',
|
||||||
|
alt_text: alt_text || title,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<figure
|
||||||
|
style={{
|
||||||
|
'--figure-width': width + 'px',
|
||||||
|
// width: width + 'px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<picture>
|
||||||
|
{strippedWebP && (
|
||||||
|
<source srcset={strippedWebP} type="image/webp" />
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={strippedURL}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
alt={alt_text}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
onLoad={(e) => {
|
||||||
|
e.target.style.backgroundColor = 'transparent';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
<figcaption>{alt_text || title}</figcaption>
|
||||||
|
</figure>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<p class="pagination">
|
||||||
|
{results.pagination?.offset > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="light small"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
fetchGIFs({
|
||||||
|
offset: results.pagination?.offset - GIFS_PER_PAGE,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="chevron-left" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span />
|
||||||
|
{results.pagination?.offset + results.pagination?.count <
|
||||||
|
results.pagination?.total_count && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="light small"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
fetchGIFs({
|
||||||
|
offset: results.pagination?.offset + GIFS_PER_PAGE,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Next</span> <Icon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
uiState === 'results' && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<p>No results</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{uiState === 'error' && (
|
||||||
|
<div class="ui-state">
|
||||||
|
<p>Error loading GIFs</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Compose;
|
export default Compose;
|
||||||
|
|
|
@ -2,11 +2,13 @@ import { shouldPolyfill } from '@formatjs/intl-segmenter/should-polyfill';
|
||||||
import { Suspense } from 'preact/compat';
|
import { Suspense } from 'preact/compat';
|
||||||
import { useEffect, useState } from 'preact/hooks';
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import Loader from './loader';
|
||||||
|
|
||||||
const supportsIntlSegmenter = !shouldPolyfill();
|
const supportsIntlSegmenter = !shouldPolyfill();
|
||||||
|
|
||||||
export default function IntlSegmenterSuspense({ children }) {
|
export default function IntlSegmenterSuspense({ children }) {
|
||||||
if (supportsIntlSegmenter) {
|
if (supportsIntlSegmenter) {
|
||||||
return <Suspense>{children}</Suspense>;
|
return <Suspense fallback={<Loader />}>{children}</Suspense>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [polyfillLoaded, setPolyfillLoaded] = useState(false);
|
const [polyfillLoaded, setPolyfillLoaded] = useState(false);
|
||||||
|
@ -17,5 +19,9 @@ export default function IntlSegmenterSuspense({ children }) {
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return polyfillLoaded && <Suspense>{children}</Suspense>;
|
return polyfillLoaded ? (
|
||||||
|
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||||
|
) : (
|
||||||
|
<Loader />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
Rendered but hidden. Only show when visible
|
||||||
|
*/
|
||||||
|
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
|
||||||
|
export default function LazyShazam({ children }) {
|
||||||
|
const containerRef = useRef();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [visibleStart, setVisibleStart] = useState(false);
|
||||||
|
|
||||||
|
const { ref } = useInView({
|
||||||
|
root: null,
|
||||||
|
trackVisibility: true,
|
||||||
|
delay: 1000,
|
||||||
|
onChange: (inView) => {
|
||||||
|
if (inView) {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
triggerOnce: true,
|
||||||
|
skip: visibleStart || visible,
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
if (rect.bottom > 0) {
|
||||||
|
setVisibleStart(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (visibleStart) return children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
class="shazam-container no-animation"
|
||||||
|
hidden={!visible}
|
||||||
|
>
|
||||||
|
<div ref={ref} class="shazam-container-inner">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,12 +9,12 @@ import {
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
||||||
|
|
||||||
|
import formatDuration from '../utils/format-duration';
|
||||||
import mem from '../utils/mem';
|
import mem from '../utils/mem';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
import { formatDuration } from './status';
|
|
||||||
|
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
|
||||||
|
|
||||||
|
|
|
@ -88,3 +88,7 @@
|
||||||
.sparkle-icon {
|
.sparkle-icon {
|
||||||
animation: sparkle-icon 0.3s ease-in-out infinite alternate;
|
animation: sparkle-icon 0.3s ease-in-out infinite alternate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-submenu {
|
||||||
|
max-width: 14em;
|
||||||
|
}
|
||||||
|
|
|
@ -21,15 +21,18 @@ import Avatar from './avatar';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import MenuLink from './menu-link';
|
import MenuLink from './menu-link';
|
||||||
|
|
||||||
|
const supportsTouch = 'ontouchstart' in window;
|
||||||
|
|
||||||
function NavMenu(props) {
|
function NavMenu(props) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const { masto, instance, authenticated } = api();
|
const { masto, instance, authenticated } = api();
|
||||||
|
|
||||||
const [currentAccount, moreThanOneAccount] = useMemo(() => {
|
const [currentAccount, moreThanOneAccount] = useMemo(() => {
|
||||||
const accounts = store.local.getJSON('accounts') || [];
|
const accounts = store.local.getJSON('accounts') || [];
|
||||||
const acc = accounts.find(
|
const acc =
|
||||||
(account) => account.info.id === store.session.get('currentAccount'),
|
accounts.find(
|
||||||
);
|
(account) => account.info.id === store.session.get('currentAccount'),
|
||||||
|
) || accounts[0];
|
||||||
return [acc, accounts.length > 1];
|
return [acc, accounts.length > 1];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -147,7 +150,7 @@ function NavMenu(props) {
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
viewScroll="close"
|
// viewScroll="close"
|
||||||
position="anchor"
|
position="anchor"
|
||||||
align="center"
|
align="center"
|
||||||
boundingBoxPadding={boundingBoxPadding}
|
boundingBoxPadding={boundingBoxPadding}
|
||||||
|
@ -209,6 +212,8 @@ function NavMenu(props) {
|
||||||
)}
|
)}
|
||||||
{lists?.length > 0 ? (
|
{lists?.length > 0 ? (
|
||||||
<SubMenu
|
<SubMenu
|
||||||
|
openTrigger={supportsTouch ? 'clickOnly' : undefined}
|
||||||
|
menuClassName="nav-submenu"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
gap={-8}
|
gap={-8}
|
||||||
label={
|
label={
|
||||||
|
@ -243,6 +248,8 @@ function NavMenu(props) {
|
||||||
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<SubMenu
|
<SubMenu
|
||||||
|
openTrigger={supportsTouch ? 'clickOnly' : undefined}
|
||||||
|
menuClassName="nav-submenu"
|
||||||
overflow="auto"
|
overflow="auto"
|
||||||
gap={-8}
|
gap={-8}
|
||||||
label={
|
label={
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
|
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states, { statusKey } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
import useTruncated from '../utils/useTruncated';
|
import useTruncated from '../utils/useTruncated';
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ const NOTIFICATION_ICONS = {
|
||||||
update: 'pencil',
|
update: 'pencil',
|
||||||
'admin.signup': 'account-edit',
|
'admin.signup': 'account-edit',
|
||||||
'admin.report': 'account-warning',
|
'admin.report': 'account-warning',
|
||||||
severed_relationships: 'unlink',
|
severed_relationships: 'heart-break',
|
||||||
emoji_reaction: 'emoji2',
|
emoji_reaction: 'emoji2',
|
||||||
'pleroma:emoji_reaction': 'emoji2',
|
'pleroma:emoji_reaction': 'emoji2',
|
||||||
};
|
};
|
||||||
|
@ -85,16 +85,35 @@ const contentText = {
|
||||||
'favourite+reblog_reply': 'boosted & liked your reply.',
|
'favourite+reblog_reply': 'boosted & liked your reply.',
|
||||||
'admin.sign_up': 'signed up.',
|
'admin.sign_up': 'signed up.',
|
||||||
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
|
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
|
||||||
severed_relationships: (name) => `Relationships with ${name} severed.`,
|
severed_relationships: (name) => (
|
||||||
|
<>
|
||||||
|
Lost connections with <i>{name}</i>.
|
||||||
|
</>
|
||||||
|
),
|
||||||
emoji_reaction: emojiText,
|
emoji_reaction: emojiText,
|
||||||
'pleroma:emoji_reaction': emojiText,
|
'pleroma:emoji_reaction': emojiText,
|
||||||
};
|
};
|
||||||
|
|
||||||
// account_suspension, domain_block, user_domain_block
|
// account_suspension, domain_block, user_domain_block
|
||||||
const SEVERED_RELATIONSHIPS_TEXT = {
|
const SEVERED_RELATIONSHIPS_TEXT = {
|
||||||
account_suspension: 'Account has been suspended.',
|
account_suspension: ({ from, targetName }) => (
|
||||||
domain_block: 'Domain has been blocked.',
|
<>
|
||||||
user_domain_block: 'You blocked this domain.',
|
An admin from <i>{from}</i> has suspended <i>{targetName}</i>, which means
|
||||||
|
you can no longer receive updates from them or interact with them.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
domain_block: ({ from, targetName, followersCount, followingCount }) => (
|
||||||
|
<>
|
||||||
|
An admin from <i>{from}</i> has blocked <i>{targetName}</i>. Affected
|
||||||
|
followers: {followersCount}, followings: {followingCount}.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
user_domain_block: ({ targetName, followersCount, followingCount }) => (
|
||||||
|
<>
|
||||||
|
You have blocked <i>{targetName}</i>. Removed followers: {followersCount},
|
||||||
|
followings: {followingCount}.
|
||||||
|
</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const AVATARS_LIMIT = 50;
|
const AVATARS_LIMIT = 50;
|
||||||
|
@ -209,6 +228,7 @@ function Notification({
|
||||||
accounts: _accounts,
|
accounts: _accounts,
|
||||||
showReactions: type === 'favourite+reblog',
|
showReactions: type === 'favourite+reblog',
|
||||||
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
|
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
|
||||||
|
postID: statusKey(actualStatusID, instance),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -277,42 +297,21 @@ function Notification({
|
||||||
<FollowRequestButtons accountID={account.id} />
|
<FollowRequestButtons accountID={account.id} />
|
||||||
)}
|
)}
|
||||||
{type === 'severed_relationships' && (
|
{type === 'severed_relationships' && (
|
||||||
<>
|
<div>
|
||||||
<p>
|
{SEVERED_RELATIONSHIPS_TEXT[event.type]({
|
||||||
<span class="insignificant">
|
from: instance,
|
||||||
{event?.purge ? (
|
...event,
|
||||||
'Purged by administrators.'
|
})}
|
||||||
) : (
|
<br />
|
||||||
<>
|
<a
|
||||||
{event.relationshipsCount} relationship
|
href={`https://${instance}/severed_relationships`}
|
||||||
{event.relationshipsCount === 1 ? '' : 's'}
|
target="_blank"
|
||||||
{!!event.createdAt && (
|
rel="noopener noreferrer"
|
||||||
<>
|
>
|
||||||
{' '}
|
Learn more <Icon icon="external" size="s" />
|
||||||
•{' '}
|
</a>
|
||||||
<RelativeTime
|
.
|
||||||
datetime={event.createdAt}
|
</div>
|
||||||
format="micro"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<b>{SEVERED_RELATIONSHIPS_TEXT[event.type]}</b>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
href={`https://${instance}/severed_relationships`}
|
|
||||||
class="button plain6"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span>View</span> <Icon icon="external" />
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import dayjs from 'dayjs';
|
||||||
import dayjsTwitter from 'dayjs-twitter';
|
import dayjsTwitter from 'dayjs-twitter';
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { useMemo } from 'preact/hooks';
|
import { useEffect, useMemo, useReducer } from 'preact/hooks';
|
||||||
|
|
||||||
dayjs.extend(dayjsTwitter);
|
dayjs.extend(dayjsTwitter);
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
|
@ -18,22 +18,49 @@ const dtf = new Intl.DateTimeFormat();
|
||||||
|
|
||||||
export default function RelativeTime({ datetime, format }) {
|
export default function RelativeTime({ datetime, format }) {
|
||||||
if (!datetime) return null;
|
if (!datetime) return null;
|
||||||
|
const [renderCount, rerender] = useReducer((x) => x + 1, 0);
|
||||||
const date = useMemo(() => dayjs(datetime), [datetime]);
|
const date = useMemo(() => dayjs(datetime), [datetime]);
|
||||||
const dateStr = useMemo(() => {
|
const [dateStr, dt, title] = useMemo(() => {
|
||||||
|
let str;
|
||||||
if (format === 'micro') {
|
if (format === 'micro') {
|
||||||
// If date <= 1 day ago or day is within this year
|
// If date <= 1 day ago or day is within this year
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
const dayDiff = now.diff(date, 'day');
|
const dayDiff = now.diff(date, 'day');
|
||||||
if (dayDiff <= 1 || now.year() === date.year()) {
|
if (dayDiff <= 1 || now.year() === date.year()) {
|
||||||
return date.twitter();
|
str = date.twitter();
|
||||||
} else {
|
} else {
|
||||||
return dtf.format(date.toDate());
|
str = dtf.format(date.toDate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return date.fromNow();
|
if (!str) str = date.fromNow();
|
||||||
}, [date, format]);
|
return [str, date.toISOString(), date.format('LLLL')];
|
||||||
const dt = useMemo(() => date.toISOString(), [date]);
|
}, [date, format, renderCount]);
|
||||||
const title = useMemo(() => date.format('LLLL'), [date]);
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout;
|
||||||
|
let raf;
|
||||||
|
function rafRerender() {
|
||||||
|
raf = requestAnimationFrame(() => {
|
||||||
|
rerender();
|
||||||
|
scheduleRerender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function scheduleRerender() {
|
||||||
|
// If less than 1 minute, rerender every 10s
|
||||||
|
// If less than 1 hour rerender every 1m
|
||||||
|
// Else, don't need to rerender
|
||||||
|
if (date.diff(dayjs(), 'minute', true) < 1) {
|
||||||
|
timeout = setTimeout(rafRerender, 10_000);
|
||||||
|
} else if (date.diff(dayjs(), 'hour', true) < 1) {
|
||||||
|
timeout = setTimeout(rafRerender, 60_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleRerender();
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<time datetime={dt} title={title}>
|
<time datetime={dt} title={title}>
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
text-shadow: 0 1px var(--bg-color);
|
/* text-shadow: 0 1px var(--bg-color); */
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
@ -623,26 +623,26 @@
|
||||||
.spoiler-media-button
|
.spoiler-media-button
|
||||||
),
|
),
|
||||||
~ .card .meta-container {
|
~ .card .meta-container {
|
||||||
/* filter: blur(5px) invert(0.5);
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
image-rendering: pixelated; */
|
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
text-decoration-thickness: 1.5em;
|
text-decoration-thickness: 1.5em;
|
||||||
text-decoration-line: line-through;
|
text-decoration-line: line-through;
|
||||||
/* text-rendering: optimizeSpeed; */
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
/* contain: layout; */
|
|
||||||
/* transform: scale(0.97);
|
|
||||||
transition: transform 0.1s ease-in-out; */
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
text-decoration-color: inherit;
|
text-decoration-color: inherit;
|
||||||
text-decoration-thickness: 1.5em;
|
text-decoration-thickness: 1.5em;
|
||||||
text-decoration-line: line-through;
|
text-decoration-line: line-through;
|
||||||
/* text-rendering: optimizeSpeed; */
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ *:not(
|
||||||
|
.media-container,
|
||||||
|
.card,
|
||||||
|
.media-figure-multiple,
|
||||||
|
.spoiler-media-button
|
||||||
|
),
|
||||||
|
~ .card .meta-container {
|
||||||
img {
|
img {
|
||||||
filter: invert(0.5);
|
filter: invert(0.5);
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
@ -908,7 +908,7 @@
|
||||||
grid-auto-rows: 1fr;
|
grid-auto-rows: 1fr;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
/* height: 160px; */
|
/* height: 160px; */
|
||||||
min-height: 88px;
|
min-height: var(--min-dimension);
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: max(160px, 33vh);
|
max-height: max(160px, 33vh);
|
||||||
}
|
}
|
||||||
|
@ -1037,9 +1037,9 @@
|
||||||
.status .media-container.media-eq1 .media {
|
.status .media-container.media-eq1 .media {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
min-width: 88px;
|
min-width: var(--min-dimension);
|
||||||
/* width: auto; */
|
/* width: auto; */
|
||||||
min-height: 88px;
|
min-height: var(--min-dimension);
|
||||||
/* --maxAspectHeight: max(160px, 33vh);
|
/* --maxAspectHeight: max(160px, 33vh);
|
||||||
--aspectWidth: calc(--width / --height * var(--maxAspectHeight)); */
|
--aspectWidth: calc(--width / --height * var(--maxAspectHeight)); */
|
||||||
width: min(var(--aspectWidth), var(--width), 100%);
|
width: min(var(--aspectWidth), var(--width), 100%);
|
||||||
|
@ -1300,7 +1300,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
:is(.status, .media-post) .media-audio {
|
:is(.status, .media-post) .media-audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 88px;
|
min-height: var(--min-dimension);
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle at center center,
|
circle at center center,
|
||||||
transparent,
|
transparent,
|
||||||
|
@ -1696,6 +1696,7 @@ a.card:is(:hover, :focus):visited {
|
||||||
.poll-label input:is([type='radio'], [type='checkbox']) {
|
.poll-label input:is([type='radio'], [type='checkbox']) {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
|
min-height: 1em;
|
||||||
}
|
}
|
||||||
.poll-option-votes {
|
.poll-option-votes {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -1719,6 +1720,7 @@ a.card:is(:hover, :focus):visited {
|
||||||
}
|
}
|
||||||
.poll-option-title {
|
.poll-option-title {
|
||||||
text-shadow: 0 1px var(--bg-color);
|
text-shadow: 0 1px var(--bg-color);
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.poll-option-title .icon {
|
.poll-option-title .icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
|
@ -20,12 +20,14 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
|
import punycode from 'punycode';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useLongPress } from 'use-long-press';
|
import { useLongPress } from 'use-long-press';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import CustomEmoji from '../components/custom-emoji';
|
import CustomEmoji from '../components/custom-emoji';
|
||||||
import EmojiText from '../components/emoji-text';
|
import EmojiText from '../components/emoji-text';
|
||||||
|
import LazyShazam from '../components/lazy-shazam';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import Menu2 from '../components/menu2';
|
import Menu2 from '../components/menu2';
|
||||||
import MenuConfirm from '../components/menu-confirm';
|
import MenuConfirm from '../components/menu-confirm';
|
||||||
|
@ -1592,11 +1594,14 @@ function Status({
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
visibility !== 'public' &&
|
||||||
icon={visibilityIconsMap[visibility]}
|
visibility !== 'direct' && (
|
||||||
alt={visibilityText[visibility]}
|
<Icon
|
||||||
size="s"
|
icon={visibilityIconsMap[visibility]}
|
||||||
/>
|
alt={visibilityText[visibility]}
|
||||||
|
size="s"
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||||
{!previewMode && !readOnly && (
|
{!previewMode && !readOnly && (
|
||||||
|
@ -1647,11 +1652,15 @@ function Status({
|
||||||
// {StatusMenuItems}
|
// {StatusMenuItems}
|
||||||
// </Menu>
|
// </Menu>
|
||||||
<span class="time">
|
<span class="time">
|
||||||
<Icon
|
{visibility !== 'public' && visibility !== 'direct' && (
|
||||||
icon={visibilityIconsMap[visibility]}
|
<>
|
||||||
alt={visibilityText[visibility]}
|
<Icon
|
||||||
size="s"
|
icon={visibilityIconsMap[visibility]}
|
||||||
/>{' '}
|
alt={visibilityText[visibility]}
|
||||||
|
size="s"
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
@ -2223,9 +2232,9 @@ function Card({ card, selfReferential, instance }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasText && (image || (type === 'photo' && blurhash))) {
|
if (hasText && (image || (type === 'photo' && blurhash))) {
|
||||||
const domain = new URL(url).hostname
|
const domain = punycode.toUnicode(
|
||||||
.replace(/^www\./, '')
|
new URL(url).hostname.replace(/^www\./, '').replace(/\/$/, ''),
|
||||||
.replace(/\/$/, '');
|
);
|
||||||
let blurhashImage;
|
let blurhashImage;
|
||||||
const rgbAverageColor =
|
const rgbAverageColor =
|
||||||
image && blurhash ? getBlurHashAverageColor(blurhash) : null;
|
image && blurhash ? getBlurHashAverageColor(blurhash) : null;
|
||||||
|
@ -2341,7 +2350,9 @@ function Card({ card, selfReferential, instance }) {
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
if (hasText && !image) {
|
if (hasText && !image) {
|
||||||
const domain = new URL(url).hostname.replace(/^www\./, '');
|
const domain = punycode.toUnicode(
|
||||||
|
new URL(url).hostname.replace(/^www\./, ''),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={cardStatusURL || url}
|
href={cardStatusURL || url}
|
||||||
|
@ -2864,21 +2875,6 @@ function StatusButton({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDuration(time) {
|
|
||||||
if (!time) return;
|
|
||||||
let hours = Math.floor(time / 3600);
|
|
||||||
let minutes = Math.floor((time % 3600) / 60);
|
|
||||||
let seconds = Math.round(time % 60);
|
|
||||||
|
|
||||||
if (hours === 0) {
|
|
||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
||||||
} else {
|
|
||||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nicePostURL(url) {
|
function nicePostURL(url) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
|
@ -2888,7 +2884,7 @@ function nicePostURL(url) {
|
||||||
const [_, username, restPath] = path.match(/\/(@[^\/]+)\/(.*)/) || [];
|
const [_, username, restPath] = path.match(/\/(@[^\/]+)\/(.*)/) || [];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{host}
|
{punycode.toUnicode(host)}
|
||||||
{username ? (
|
{username ? (
|
||||||
<>
|
<>
|
||||||
/{username}
|
/{username}
|
||||||
|
@ -3128,20 +3124,22 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
|
||||||
|
|
||||||
return uniqueQuotes.map((q) => {
|
return uniqueQuotes.map((q) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<LazyShazam>
|
||||||
key={q.instance + q.id}
|
<Link
|
||||||
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
key={q.instance + q.id}
|
||||||
class="status-card-link"
|
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
||||||
data-read-more="Read more →"
|
class="status-card-link"
|
||||||
>
|
data-read-more="Read more →"
|
||||||
<Status
|
>
|
||||||
statusID={q.id}
|
<Status
|
||||||
instance={q.instance}
|
statusID={q.id}
|
||||||
size="s"
|
instance={q.instance}
|
||||||
quoted={level + 1}
|
size="s"
|
||||||
enableCommentHint
|
quoted={level + 1}
|
||||||
/>
|
enableCommentHint
|
||||||
</Link>
|
/>
|
||||||
|
</Link>
|
||||||
|
</LazyShazam>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,7 +51,7 @@ function Timeline({
|
||||||
}) {
|
}) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('start');
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
const [showNew, setShowNew] = useState(false);
|
const [showNew, setShowNew] = useState(false);
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
|
@ -496,7 +496,8 @@ function Timeline({
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
|
uiState !== 'error' &&
|
||||||
|
uiState !== 'start' && <p class="ui-state">{emptyText}</p>
|
||||||
)}
|
)}
|
||||||
{uiState === 'error' && (
|
{uiState === 'error' && (
|
||||||
<p class="ui-state">
|
<p class="ui-state">
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
|
|
||||||
--blue-color: royalblue;
|
--blue-color: royalblue;
|
||||||
--purple-color: blueviolet;
|
--purple-color: blueviolet;
|
||||||
|
--purple-fg-color: color-mix(
|
||||||
|
in srgb-linear,
|
||||||
|
var(--purple-color) 60%,
|
||||||
|
var(--text-color) 40%
|
||||||
|
);
|
||||||
|
--purple-bg-color: color-mix(in srgb, var(--purple-color) 10%, transparent);
|
||||||
--green-color: darkgreen;
|
--green-color: darkgreen;
|
||||||
--orange-color: darkorange;
|
--orange-color: darkorange;
|
||||||
--orange-light-bg-color: color-mix(
|
--orange-light-bg-color: color-mix(
|
||||||
|
@ -23,6 +29,12 @@
|
||||||
var(--orange-color) 20%,
|
var(--orange-color) 20%,
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
|
--orange-fg-color: color-mix(
|
||||||
|
in srgb-linear,
|
||||||
|
var(--orange-color) 60%,
|
||||||
|
var(--text-color) 40%
|
||||||
|
);
|
||||||
|
--orange-bg-color: color-mix(in srgb, var(--orange-color) 10%, transparent);
|
||||||
--red-color: orangered;
|
--red-color: orangered;
|
||||||
--red-text-color: color-mix(
|
--red-text-color: color-mix(
|
||||||
in srgb-linear,
|
in srgb-linear,
|
||||||
|
@ -96,6 +108,8 @@
|
||||||
|
|
||||||
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
|
||||||
--spring-timing-funtion: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
--spring-timing-funtion: cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
|
||||||
|
--min-dimension: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-resolution: 2dppx) {
|
@media (min-resolution: 2dppx) {
|
||||||
|
@ -333,6 +347,7 @@ button[hidden] {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'],
|
input[type='text'],
|
||||||
|
input[type='search'],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
@ -342,6 +357,7 @@ select {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
input[type='text']:focus,
|
input[type='text']:focus,
|
||||||
|
input[type='search']:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
border-color: var(--outline-color);
|
border-color: var(--outline-color);
|
||||||
|
@ -357,7 +373,7 @@ textarea:disabled {
|
||||||
background-color: var(--bg-faded-color);
|
background-color: var(--bg-faded-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(input[type='text'], textarea, select).block {
|
:is(input[type='text'], input[type='search'], textarea, select).block {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
|
import punycode from 'punycode';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -516,7 +517,13 @@ function AccountStatuses() {
|
||||||
>
|
>
|
||||||
<Icon icon="transfer" />{' '}
|
<Icon icon="transfer" />{' '}
|
||||||
<small class="menu-double-lines">
|
<small class="menu-double-lines">
|
||||||
Switch to account's instance (<b>{accountInstance}</b>)
|
Switch to account's instance{' '}
|
||||||
|
{accountInstance ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
(<b>{punycode.toUnicode(accountInstance)}</b>)
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</small>
|
</small>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!sameCurrentInstance && (
|
{!sameCurrentInstance && (
|
||||||
|
|
|
@ -813,6 +813,10 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-decoration-color: transparent;
|
text-decoration-color: transparent;
|
||||||
color: var(--link-text-color);
|
color: var(--link-text-color);
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
|
import punycode from 'punycode';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
|
@ -191,6 +192,7 @@ function Catchup() {
|
||||||
|
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
const catchupRangeRef = useRef();
|
const catchupRangeRef = useRef();
|
||||||
|
const catchupLastRef = useRef();
|
||||||
const NS = useMemo(() => getCurrentAccountNS(), []);
|
const NS = useMemo(() => getCurrentAccountNS(), []);
|
||||||
const handleCatchupClick = useCallback(async ({ duration } = {}) => {
|
const handleCatchupClick = useCallback(async ({ duration } = {}) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
@ -925,7 +927,15 @@ function Catchup() {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (range < RANGES[RANGES.length - 1].value) {
|
if (range < RANGES[RANGES.length - 1].value) {
|
||||||
const duration = range * 60 * 60 * 1000;
|
let duration;
|
||||||
|
if (
|
||||||
|
range === RANGES[RANGES.length - 1].value &&
|
||||||
|
catchupLastRef.current?.checked
|
||||||
|
) {
|
||||||
|
duration = Date.now() - lastCatchupEndAt;
|
||||||
|
} else {
|
||||||
|
duration = range * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
handleCatchupClick({ duration });
|
handleCatchupClick({ duration });
|
||||||
} else {
|
} else {
|
||||||
handleCatchupClick();
|
handleCatchupClick();
|
||||||
|
@ -935,11 +945,25 @@ function Catchup() {
|
||||||
Catch up
|
Catch up
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{lastCatchupRange && range > lastCatchupRange && (
|
{lastCatchupRange && range > lastCatchupRange ? (
|
||||||
<p class="catchup-info">
|
<p class="catchup-info">
|
||||||
<Icon icon="info" /> Overlaps with your last catch-up
|
<Icon icon="info" /> Overlaps with your last catch-up
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : range === RANGES[RANGES.length - 1].value &&
|
||||||
|
lastCatchupEndAt ? (
|
||||||
|
<p class="catchup-info">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
switch
|
||||||
|
checked
|
||||||
|
ref={catchupLastRef}
|
||||||
|
/>{' '}
|
||||||
|
Until the last catch-up (
|
||||||
|
{dtf.format(new Date(lastCatchupEndAt))})
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<p class="insignificant">
|
<p class="insignificant">
|
||||||
<small>
|
<small>
|
||||||
Note: your instance might only show a maximum of 800 posts in
|
Note: your instance might only show a maximum of 800 posts in
|
||||||
|
@ -1076,9 +1100,11 @@ function Catchup() {
|
||||||
height,
|
height,
|
||||||
publishedAt,
|
publishedAt,
|
||||||
} = card;
|
} = card;
|
||||||
const domain = new URL(url).hostname
|
const domain = punycode.toUnicode(
|
||||||
.replace(/^www\./, '')
|
new URL(url).hostname
|
||||||
.replace(/\/$/, '');
|
.replace(/^www\./, '')
|
||||||
|
.replace(/\/$/, ''),
|
||||||
|
);
|
||||||
let accentColor;
|
let accentColor;
|
||||||
if (blurhash) {
|
if (blurhash) {
|
||||||
const averageColor = getBlurHashAverageColor(blurhash);
|
const averageColor = getBlurHashAverageColor(blurhash);
|
||||||
|
@ -1263,7 +1289,7 @@ function Catchup() {
|
||||||
authors[author].avatarStatic || authors[author].avatar
|
authors[author].avatarStatic || authors[author].avatar
|
||||||
}
|
}
|
||||||
size="xxl"
|
size="xxl"
|
||||||
alt={`${authors[author].displayName} (@${authors[author].username})`}
|
alt={`${authors[author].displayName} (@${authors[author].acct})`}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<span class="count">{authorCounts[author]}</span>
|
<span class="count">{authorCounts[author]}</span>
|
||||||
<span class="username">{authors[author].username}</span>
|
<span class="username">{authors[author].username}</span>
|
||||||
|
|
|
@ -180,6 +180,8 @@ function Filters() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _id = 1;
|
||||||
|
const incID = () => _id++;
|
||||||
function FiltersAddEdit({ filter, onClose }) {
|
function FiltersAddEdit({ filter, onClose }) {
|
||||||
const { masto } = api();
|
const { masto } = api();
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
@ -193,7 +195,12 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
|
|
||||||
// Hacky way of handling removed keywords for both existing and new ones
|
// Hacky way of handling removed keywords for both existing and new ones
|
||||||
const [removedKeywordIDs, setRemovedKeywordIDs] = useState([]);
|
const [removedKeywordIDs, setRemovedKeywordIDs] = useState([]);
|
||||||
const [removedNewKeywordIndices, setRemovedNewKeywordIndices] = useState([]);
|
const [removedKeyword_IDs, setRemovedKeyword_IDs] = useState([]);
|
||||||
|
|
||||||
|
const filteredEditKeywords = editKeywords.filter(
|
||||||
|
(k) =>
|
||||||
|
!removedKeywordIDs.includes(k.id) && !removedKeyword_IDs.includes(k._id),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="sheet" id="filters-add-edit-modal">
|
<div class="sheet" id="filters-add-edit-modal">
|
||||||
|
@ -335,16 +342,12 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-form-keywords" ref={keywordsRef}>
|
<div class="filter-form-keywords" ref={keywordsRef}>
|
||||||
{editKeywords.length ? (
|
{filteredEditKeywords.length ? (
|
||||||
<ul class="filter-keywords">
|
<ul class="filter-keywords">
|
||||||
{editKeywords.map((k, index) => {
|
{filteredEditKeywords.map((k) => {
|
||||||
const { id, keyword, wholeWord } = k;
|
const { id, keyword, wholeWord, _id } = k;
|
||||||
const removed =
|
|
||||||
removedKeywordIDs.includes(id) ||
|
|
||||||
removedNewKeywordIndices.includes(index);
|
|
||||||
if (removed) return null;
|
|
||||||
return (
|
return (
|
||||||
<li key={`${index}-${id}`}>
|
<li key={`${id}-${_id}`}>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="keyword_attributes[][id]"
|
name="keyword_attributes[][id]"
|
||||||
|
@ -376,12 +379,9 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
if (id) {
|
if (id) {
|
||||||
removedKeywordIDs.push(id);
|
removedKeywordIDs.push(id);
|
||||||
setRemovedKeywordIDs([...removedKeywordIDs]);
|
setRemovedKeywordIDs([...removedKeywordIDs]);
|
||||||
} else {
|
} else if (_id) {
|
||||||
// If no id, remove by index
|
removedKeyword_IDs.push(_id);
|
||||||
removedNewKeywordIndices.push(index);
|
setRemovedKeyword_IDs([...removedKeyword_IDs]);
|
||||||
setRemovedNewKeywordIndices([
|
|
||||||
...removedNewKeywordIndices,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -405,6 +405,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
setEditKeywords([
|
setEditKeywords([
|
||||||
...editKeywords,
|
...editKeywords,
|
||||||
{
|
{
|
||||||
|
_id: incID(),
|
||||||
keyword: '',
|
keyword: '',
|
||||||
wholeWord: true,
|
wholeWord: true,
|
||||||
},
|
},
|
||||||
|
@ -421,10 +422,10 @@ function FiltersAddEdit({ filter, onClose }) {
|
||||||
>
|
>
|
||||||
Add keyword
|
Add keyword
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
{editKeywords?.length > 1 && (
|
{filteredEditKeywords?.length > 1 && (
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{editKeywords.length} keyword
|
{filteredEditKeywords.length} keyword
|
||||||
{editKeywords.length === 1 ? '' : 's'}
|
{filteredEditKeywords.length === 1 ? '' : 's'}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -532,66 +532,72 @@ function Notifications({ columnMode }) {
|
||||||
)}
|
)}
|
||||||
{supportsFilteredNotifications &&
|
{supportsFilteredNotifications &&
|
||||||
notificationsPolicy?.summary?.pendingRequestsCount > 0 && (
|
notificationsPolicy?.summary?.pendingRequestsCount > 0 && (
|
||||||
<div class="filtered-notifications">
|
<div class="shazam-container">
|
||||||
<details
|
<div class="shazam-container-inner">
|
||||||
onToggle={async (e) => {
|
<div class="filtered-notifications">
|
||||||
const { open } = e.target;
|
<details
|
||||||
if (open) {
|
onToggle={async (e) => {
|
||||||
const requests = await fetchNotificationsRequest();
|
const { open } = e.target;
|
||||||
setNotificationsRequests(requests);
|
if (open) {
|
||||||
console.log({ open, requests });
|
const requests = await fetchNotificationsRequest();
|
||||||
}
|
setNotificationsRequests(requests);
|
||||||
}}
|
console.log({ open, requests });
|
||||||
>
|
}
|
||||||
<summary>
|
}}
|
||||||
Filtered notifications from{' '}
|
>
|
||||||
{notificationsPolicy.summary.pendingRequestsCount} people
|
<summary>
|
||||||
</summary>
|
Filtered notifications from{' '}
|
||||||
{!notificationsRequests ? (
|
{notificationsPolicy.summary.pendingRequestsCount} people
|
||||||
<p class="ui-state">
|
</summary>
|
||||||
<Loader abrupt />
|
{!notificationsRequests ? (
|
||||||
</p>
|
<p class="ui-state">
|
||||||
) : (
|
<Loader abrupt />
|
||||||
notificationsRequests?.length > 0 && (
|
</p>
|
||||||
<ul>
|
) : (
|
||||||
{notificationsRequests.map((request) => (
|
notificationsRequests?.length > 0 && (
|
||||||
<li key={request.id}>
|
<ul>
|
||||||
<div class="request-notifcations">
|
{notificationsRequests.map((request) => (
|
||||||
{!request.lastStatus?.id && (
|
<li key={request.id}>
|
||||||
<AccountBlock
|
<div class="request-notifcations">
|
||||||
useAvatarStatic
|
{!request.lastStatus?.id && (
|
||||||
showStats
|
<AccountBlock
|
||||||
account={request.account}
|
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>
|
)}
|
||||||
|
{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>
|
</div>
|
||||||
)}
|
<NotificationRequestButtons
|
||||||
<NotificationRequestModalButton request={request} />
|
request={request}
|
||||||
</div>
|
onChange={() => {
|
||||||
<NotificationRequestButtons
|
loadNotifications(true);
|
||||||
request={request}
|
}}
|
||||||
onChange={() => {
|
/>
|
||||||
loadNotifications(true);
|
</li>
|
||||||
}}
|
))}
|
||||||
/>
|
</ul>
|
||||||
</li>
|
)
|
||||||
))}
|
)}
|
||||||
</ul>
|
</details>
|
||||||
)
|
</div>
|
||||||
)}
|
</div>
|
||||||
</details>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div id="mentions-option">
|
<div id="mentions-option">
|
||||||
|
|
|
@ -28,6 +28,7 @@ const {
|
||||||
PHANPY_WEBSITE: WEBSITE,
|
PHANPY_WEBSITE: WEBSITE,
|
||||||
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
|
||||||
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
|
||||||
|
PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
|
||||||
} = import.meta.env;
|
} = import.meta.env;
|
||||||
|
|
||||||
function Settings({ onClose }) {
|
function Settings({ onClose }) {
|
||||||
|
@ -433,6 +434,37 @@ function Settings({ onClose }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
{!!GIPHY_API_KEY && authenticated && (
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={snapStates.settings.composerGIFPicker}
|
||||||
|
onChange={(e) => {
|
||||||
|
states.settings.composerGIFPicker = e.target.checked;
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
GIF Picker for composer
|
||||||
|
</label>
|
||||||
|
<div class="sub-section insignificant">
|
||||||
|
<small>
|
||||||
|
Note: This feature uses external GIF search service, powered
|
||||||
|
by{' '}
|
||||||
|
<a
|
||||||
|
href="https://developers.giphy.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
GIPHY
|
||||||
|
</a>
|
||||||
|
. G-rated (suitable for viewing by all ages), tracking
|
||||||
|
parameters are stripped, referrer information is omitted
|
||||||
|
from requests, but search queries and IP address information
|
||||||
|
will still reach their servers.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{!!IMG_ALT_API_URL && authenticated && (
|
{!!IMG_ALT_API_URL && authenticated && (
|
||||||
<li>
|
<li>
|
||||||
<label>
|
<label>
|
||||||
|
|
|
@ -12,10 +12,10 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'preact/hooks';
|
} from 'preact/hooks';
|
||||||
|
import punycode from 'punycode';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { matchPath, useSearchParams } from 'react-router-dom';
|
import { matchPath, useSearchParams } from 'react-router-dom';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Avatar from '../components/avatar';
|
import Avatar from '../components/avatar';
|
||||||
|
@ -122,7 +122,7 @@ function StatusPage(params) {
|
||||||
}, [showMedia]);
|
}, [showMedia]);
|
||||||
|
|
||||||
const mediaAttachments = mediaStatusID
|
const mediaAttachments = mediaStatusID
|
||||||
? mediaStatus?.mediaAttachments
|
? snapStates.statuses[statusKey(mediaStatusID, instance)]?.mediaAttachments
|
||||||
: heroStatus?.mediaAttachments;
|
: heroStatus?.mediaAttachments;
|
||||||
|
|
||||||
const handleMediaClose = useCallback(() => {
|
const handleMediaClose = useCallback(() => {
|
||||||
|
@ -1208,7 +1208,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
|
||||||
{postInstance ? (
|
{postInstance ? (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
(<b>{postInstance}</b>)
|
(<b>{punycode.toUnicode(postInstance)}</b>)
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
|
|
|
@ -3,6 +3,7 @@ import '../components/links-bar.css';
|
||||||
import { MenuItem } from '@szhsin/react-menu';
|
import { MenuItem } from '@szhsin/react-menu';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||||
|
import punycode from 'punycode';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -161,9 +162,9 @@ function Trending({ columnMode, ...props }) {
|
||||||
url,
|
url,
|
||||||
width,
|
width,
|
||||||
} = link;
|
} = link;
|
||||||
const domain = new URL(url).hostname
|
const domain = punycode.toUnicode(
|
||||||
.replace(/^www\./, '')
|
new URL(url).hostname.replace(/^www\./, '').replace(/\/$/, ''),
|
||||||
.replace(/\/$/, '');
|
);
|
||||||
let accentColor;
|
let accentColor;
|
||||||
if (blurhash) {
|
if (blurhash) {
|
||||||
const averageColor = getBlurHashAverageColor(blurhash);
|
const averageColor = getBlurHashAverageColor(blurhash);
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
export default function formatDuration(time) {
|
||||||
|
if (!time) return;
|
||||||
|
let hours = Math.floor(time / 3600);
|
||||||
|
let minutes = Math.floor((time % 3600) / 60);
|
||||||
|
let seconds = Math.round(time % 60);
|
||||||
|
|
||||||
|
if (hours === 0) {
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
} else {
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
|
||||||
|
|
||||||
|
export default function openOSK() {
|
||||||
|
if (isSafari) {
|
||||||
|
const fauxEl = document.createElement('input');
|
||||||
|
fauxEl.style.position = 'absolute';
|
||||||
|
fauxEl.style.top = '0';
|
||||||
|
fauxEl.style.left = '0';
|
||||||
|
fauxEl.style.opacity = '0';
|
||||||
|
document.body.appendChild(fauxEl);
|
||||||
|
fauxEl.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(fauxEl);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,6 +67,7 @@ const states = proxy({
|
||||||
contentTranslationAutoInline: false,
|
contentTranslationAutoInline: false,
|
||||||
shortcutSettingsCloudImportExport: false,
|
shortcutSettingsCloudImportExport: false,
|
||||||
mediaAltGenerator: false,
|
mediaAltGenerator: false,
|
||||||
|
composerGIFPicker: false,
|
||||||
cloakMode: false,
|
cloakMode: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -99,6 +100,8 @@ export function initStates() {
|
||||||
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
|
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
|
||||||
states.settings.mediaAltGenerator =
|
states.settings.mediaAltGenerator =
|
||||||
store.account.get('settings-mediaAltGenerator') ?? false;
|
store.account.get('settings-mediaAltGenerator') ?? false;
|
||||||
|
states.settings.composerGIFPicker =
|
||||||
|
store.account.get('settings-composerGIFPicker') ?? false;
|
||||||
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,6 +143,9 @@ subscribe(states, (changes) => {
|
||||||
if (path.join('.') === 'settings.mediaAltGenerator') {
|
if (path.join('.') === 'settings.mediaAltGenerator') {
|
||||||
store.account.set('settings-mediaAltGenerator', !!value);
|
store.account.set('settings-mediaAltGenerator', !!value);
|
||||||
}
|
}
|
||||||
|
if (path.join('.') === 'settings.composerGIFPicker') {
|
||||||
|
store.account.set('settings-composerGIFPicker', !!value);
|
||||||
|
}
|
||||||
if (path?.[0] === 'shortcuts') {
|
if (path?.[0] === 'shortcuts') {
|
||||||
store.account.set('shortcuts', states.shortcuts);
|
store.account.set('shortcuts', states.shortcuts);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import store from './store';
|
||||||
|
|
||||||
export function getAccount(id) {
|
export function getAccount(id) {
|
||||||
const accounts = store.local.getJSON('accounts') || [];
|
const accounts = store.local.getJSON('accounts') || [];
|
||||||
|
if (!id) return accounts[0];
|
||||||
return accounts.find((a) => a.info.id === id) || accounts[0];
|
return accounts.find((a) => a.info.id === id) || accounts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,15 +83,23 @@ function _unfurlMastodonLink(instance, url) {
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
if (results.statuses.length > 0) {
|
const { statuses } = results;
|
||||||
const status = results.statuses[0];
|
if (statuses.length > 0) {
|
||||||
return {
|
// Filter out statuses that has content that contains the URL, in-case-sensitive
|
||||||
status,
|
const theStatuses = statuses.filter(
|
||||||
instance,
|
(status) =>
|
||||||
};
|
!status.content?.toLowerCase().includes(theURL.toLowerCase()),
|
||||||
} else {
|
);
|
||||||
throw new Error('No results');
|
|
||||||
|
if (theStatuses.length === 1) {
|
||||||
|
return {
|
||||||
|
status: theStatuses[0],
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// If there are multiple statuses, give up, something is wrong
|
||||||
}
|
}
|
||||||
|
throw new Error('No results');
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleFulfill(result) {
|
function handleFulfill(result) {
|
||||||
|
|
Ładowanie…
Reference in New Issue