commit
11407d0f3c
6
.github/workflows/prodtag.yml
vendored
6
.github/workflows/prodtag.yml
vendored
|
@ -20,11 +20,13 @@ jobs:
|
|||
with:
|
||||
node-version: 18
|
||||
- run: npm ci && npm run build
|
||||
- run: cd dist && zip -r ../phanpy-dist.zip . && cd ..
|
||||
- run: cd dist && zip -r ../phanpy-dist.zip . && tar -czf ../phanpy-dist.tar.gz . && cd ..
|
||||
- id: tag_name
|
||||
run: echo ::set-output name=tag_name::$(date +%Y.%m.%d).$(git rev-parse --short HEAD)
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.tag_name.outputs.tag_name }}
|
||||
generate_release_notes: true
|
||||
files: phanpy-dist.zip
|
||||
files: |
|
||||
phanpy-dist.zip
|
||||
phanpy-dist.tar.gz
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -27,3 +27,4 @@ dist-ssr
|
|||
.env.dev
|
||||
src/data/instances-full.json
|
||||
phanpy-dist.zip
|
||||
phanpy-dist.tar.gz
|
|
@ -172,6 +172,7 @@ And here I am. Building a Mastodon web client.
|
|||
- [Litterbox](https://litterbox.koyu.space/)
|
||||
- [Statuzer](https://statuzer.com/)
|
||||
- [Tusked](https://tusked.app/)
|
||||
- [Mastodon Glitch Edition (standalone frontend)](https://iceshrimp.dev/iceshrimp/masto-fe-standalone)
|
||||
- [More...](https://github.com/hueyy/awesome-mastodon/#clients)
|
||||
|
||||
## 💁♂️ Notice to all other social media client developers
|
||||
|
|
58
package-lock.json
generated
58
package-lock.json
generated
|
@ -8,8 +8,8 @@
|
|||
"name": "phanpy",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.4.2",
|
||||
"@formkit/auto-animate": "~0.8.0",
|
||||
"@formatjs/intl-localematcher": "~0.5.0",
|
||||
"@formkit/auto-animate": "~0.8.1",
|
||||
"@github/text-expander-element": "~2.5.0",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
|
@ -36,7 +36,7 @@
|
|||
"swiped-events": "~1.1.7",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~9.0.4",
|
||||
"use-debounce": "~10.0.0",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.9.0"
|
||||
|
@ -51,7 +51,7 @@
|
|||
"vite": "~4.5.0",
|
||||
"vite-plugin-generate-file": "~0.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.16.6",
|
||||
"vite-plugin-pwa": "~0.16.7",
|
||||
"vite-plugin-remove-console": "~2.1.1",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
@ -3082,17 +3082,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
|
||||
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.0.tgz",
|
||||
"integrity": "sha512-K1Xpg/8oyfCMxisJQa/fILoeoeyndcM0wcN8QiNG/uM5OAe1BcO1+2yd0gIboDI2tRJEsUi/sSBEYPbgkIdq4A==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formkit/auto-animate": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz",
|
||||
"integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw=="
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz",
|
||||
"integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig=="
|
||||
},
|
||||
"node_modules/@github/combobox-nav": {
|
||||
"version": "2.1.5",
|
||||
|
@ -7232,11 +7232,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
|
||||
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
|
||||
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
|
@ -7446,9 +7446,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
|
||||
"integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz",
|
||||
"integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
|
@ -9602,17 +9602,17 @@
|
|||
"optional": true
|
||||
},
|
||||
"@formatjs/intl-localematcher": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz",
|
||||
"integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==",
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.0.tgz",
|
||||
"integrity": "sha512-K1Xpg/8oyfCMxisJQa/fILoeoeyndcM0wcN8QiNG/uM5OAe1BcO1+2yd0gIboDI2tRJEsUi/sSBEYPbgkIdq4A==",
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formkit/auto-animate": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.0.tgz",
|
||||
"integrity": "sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw=="
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.1.tgz",
|
||||
"integrity": "sha512-0/Z2cuNXWVVIG/l0SpcHAWFhGdvLJ8DRvEfRWvmojtmRWfEy+LWNwgDazbZqY0qQYtkHcoEK3jBLkhiZaB/4Ig=="
|
||||
},
|
||||
"@github/combobox-nav": {
|
||||
"version": "2.1.5",
|
||||
|
@ -12457,9 +12457,9 @@
|
|||
}
|
||||
},
|
||||
"use-debounce": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz",
|
||||
"integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==",
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.0.tgz",
|
||||
"integrity": "sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-long-press": {
|
||||
|
@ -12580,9 +12580,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"vite-plugin-pwa": {
|
||||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
|
||||
"integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz",
|
||||
"integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
"sourcemap": "npx source-map-explorer dist/assets/*.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.4.2",
|
||||
"@formkit/auto-animate": "~0.8.0",
|
||||
"@formatjs/intl-localematcher": "~0.5.0",
|
||||
"@formkit/auto-animate": "~0.8.1",
|
||||
"@github/text-expander-element": "~2.5.0",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
|
@ -38,7 +38,7 @@
|
|||
"swiped-events": "~1.1.7",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~9.0.4",
|
||||
"use-debounce": "~10.0.0",
|
||||
"use-long-press": "~3.2.0",
|
||||
"use-resize-observer": "~9.1.0",
|
||||
"valtio": "1.9.0"
|
||||
|
@ -53,7 +53,7 @@
|
|||
"vite": "~4.5.0",
|
||||
"vite-plugin-generate-file": "~0.0.4",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.16.6",
|
||||
"vite-plugin-pwa": "~0.16.7",
|
||||
"vite-plugin-remove-console": "~2.1.1",
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
|
|
|
@ -1135,6 +1135,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
touch-action: pan-x;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
.carousel::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
|
|
@ -13,6 +13,7 @@ function AccountBlock({
|
|||
skeleton,
|
||||
account,
|
||||
avatarSize = 'xl',
|
||||
useAvatarStatic = false,
|
||||
instance,
|
||||
external,
|
||||
internal,
|
||||
|
@ -81,7 +82,11 @@ function AccountBlock({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Avatar url={avatar} size={avatarSize} squircle={bot} />
|
||||
<Avatar
|
||||
url={useAvatarStatic ? avatarStatic : avatar || avatarStatic}
|
||||
size={avatarSize}
|
||||
squircle={bot}
|
||||
/>
|
||||
<span class="account-block-content">
|
||||
{!hideDisplayName && (
|
||||
<>
|
||||
|
|
|
@ -137,14 +137,6 @@
|
|||
border-color: transparent;
|
||||
|
||||
&.compose-field {
|
||||
@media (width < 30em) {
|
||||
margin-inline: calc(-1 * var(--form-padding-inline));
|
||||
width: 100vw !important;
|
||||
max-width: 100vw;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
max-height: 65vh;
|
||||
}
|
||||
|
@ -212,11 +204,12 @@
|
|||
left: -100vw !important;
|
||||
}
|
||||
#compose-container .toolbar-button select {
|
||||
background-color: inherit;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0 0 0 8px;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
line-height: 1em;
|
||||
}
|
||||
#compose-container .toolbar-button:not(.show-field) select {
|
||||
right: 0;
|
||||
|
@ -619,3 +612,91 @@
|
|||
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.compose-field-container {
|
||||
display: grid !important;
|
||||
|
||||
@media (width < 30em) {
|
||||
margin-inline: calc(-1 * var(--form-padding-inline));
|
||||
width: 100vw !important;
|
||||
max-width: 100vw;
|
||||
|
||||
.compose-field {
|
||||
border-radius: 0;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.debug {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
> * {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
}
|
||||
|
||||
.compose-highlight {
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
padding: 8px;
|
||||
color: transparent;
|
||||
background-color: transparent;
|
||||
border: 2px solid transparent;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
unicode-bidi: plaintext;
|
||||
-webkit-rtl-ordering: logical;
|
||||
rtl-ordering: logical;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
min-height: 5em;
|
||||
max-height: 50vh;
|
||||
|
||||
/* Follow textarea styles */
|
||||
@media (min-width: 40em) {
|
||||
max-height: 65vh;
|
||||
}
|
||||
|
||||
mark {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.compose-highlight-url,
|
||||
.compose-highlight-hashtag {
|
||||
background-color: transparent;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--link-faded-color);
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.compose-highlight-mention,
|
||||
.compose-highlight-emoji-shortcode,
|
||||
.compose-highlight-exceeded {
|
||||
mix-blend-mode: multiply;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 1px;
|
||||
}
|
||||
.compose-highlight-mention {
|
||||
background-color: var(--orange-light-bg-color);
|
||||
box-shadow-color: var(--orange-light-bg-color);
|
||||
}
|
||||
.compose-highlight-emoji-shortcode {
|
||||
background-color: var(--bg-faded-color);
|
||||
box-shadow-color: var(--bg-faded-color);
|
||||
}
|
||||
.compose-highlight-exceeded {
|
||||
background-color: var(--red-bg-color);
|
||||
box-shadow-color: var(--red-bg-color);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.compose-highlight-mention,
|
||||
.compose-highlight-emoji-shortcode,
|
||||
.compose-highlight-exceeded {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import stringLength from 'string-length';
|
||||
import { uid } from 'uid/single';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import supportedLanguages from '../data/status-supported-languages';
|
||||
|
@ -104,6 +104,54 @@ function countableText(inputText) {
|
|||
.replace(usernameRegex, '$1@$3');
|
||||
}
|
||||
|
||||
// https://github.com/mastodon/mastodon/blob/c03bd2a238741a012aa4b98dc4902d6cf948ab63/app/models/account.rb#L69
|
||||
const USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i;
|
||||
const MENTION_RE = new RegExp(
|
||||
`(?<![=\\/\\w])@((${USERNAME_RE.source})(?:@[\\w.-]+[\\w]+)?)`,
|
||||
'ig',
|
||||
);
|
||||
|
||||
// AI-generated, all other regexes are too complicated
|
||||
const HASHTAG_RE = new RegExp(
|
||||
`(?<![=\\/\\w])#([a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)(?![\\/\\w])`,
|
||||
'ig',
|
||||
);
|
||||
|
||||
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
|
||||
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
|
||||
const SCAN_RE = new RegExp(
|
||||
`(?<=[^A-Za-z0-9_:\\n]|^):(${SHORTCODE_RE_FRAGMENT}):(?=[^A-Za-z0-9_:]|$)`,
|
||||
'g',
|
||||
);
|
||||
|
||||
function highlightText(text, { maxCharacters = Infinity }) {
|
||||
// Accept text string, return formatted HTML string
|
||||
let html = text;
|
||||
// Exceeded characters limit
|
||||
const { composerCharacterCount } = states;
|
||||
let leftoverHTML = '';
|
||||
if (composerCharacterCount > maxCharacters) {
|
||||
const leftoverCount = composerCharacterCount - maxCharacters;
|
||||
// Highlight exceeded characters
|
||||
leftoverHTML =
|
||||
'<mark class="compose-highlight-exceeded">' +
|
||||
html.slice(-leftoverCount) +
|
||||
'</mark>';
|
||||
html = html.slice(0, -leftoverCount);
|
||||
}
|
||||
|
||||
html = html
|
||||
.replace(urlRegexObj, '$2<mark class="compose-highlight-url">$3</mark>') // URLs
|
||||
.replace(MENTION_RE, '<mark class="compose-highlight-mention">$&</mark>') // Mentions
|
||||
.replace(HASHTAG_RE, '<mark class="compose-highlight-hashtag">#$1</mark>') // Hashtags
|
||||
.replace(
|
||||
SCAN_RE,
|
||||
'<mark class="compose-highlight-emoji-shortcode">$&</mark>',
|
||||
); // Emoji shortcodes
|
||||
|
||||
return html + leftoverHTML;
|
||||
}
|
||||
|
||||
function Compose({
|
||||
onClose,
|
||||
replyToStatus,
|
||||
|
@ -565,6 +613,7 @@ function Compose({
|
|||
account={currentAccountInfo}
|
||||
accountInstance={currentAccount.instanceURL}
|
||||
hideDisplayName
|
||||
useAvatarStatic
|
||||
/>
|
||||
)}
|
||||
{!standalone ? (
|
||||
|
@ -1221,7 +1270,8 @@ function autoResizeTextarea(textarea) {
|
|||
// NOTE: This check is needed because the offsetHeight return 50000 (really large number) on first render
|
||||
// No idea why it does that, will re-investigate in far future
|
||||
const offset = offsetHeight - clientHeight;
|
||||
textarea.style.height = value ? scrollHeight + offset + 'px' : null;
|
||||
const height = value ? scrollHeight + offset + 'px' : null;
|
||||
textarea.style.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1387,6 +1437,11 @@ const Textarea = forwardRef((props, ref) => {
|
|||
handleCommited = (e) => {
|
||||
const { input } = e.detail;
|
||||
setText(input.value);
|
||||
// fire input event
|
||||
if (ref.current) {
|
||||
const event = new Event('input', { bubbles: true });
|
||||
ref.current.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
textExpanderRef.current.addEventListener(
|
||||
|
@ -1413,8 +1468,33 @@ const Textarea = forwardRef((props, ref) => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Resize observer for textarea
|
||||
const textarea = ref.current;
|
||||
if (!textarea) return;
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
// Get height of textarea, set height to textExpander
|
||||
const { height } = textarea.getBoundingClientRect();
|
||||
textExpanderRef.current.style.height = height + 'px';
|
||||
});
|
||||
resizeObserver.observe(textarea);
|
||||
}, []);
|
||||
|
||||
const composeHighlightRef = useRef();
|
||||
const throttleHighlightText = useThrottledCallback((text) => {
|
||||
composeHighlightRef.current.innerHTML =
|
||||
highlightText(text, {
|
||||
maxCharacters,
|
||||
}) + '\n';
|
||||
// Newline to prevent multiple line breaks at the end from being collapsed, no idea why
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<text-expander ref={textExpanderRef} keys="@ # :">
|
||||
<text-expander
|
||||
ref={textExpanderRef}
|
||||
keys="@ # :"
|
||||
class="compose-field-container"
|
||||
>
|
||||
<textarea
|
||||
class="compose-field"
|
||||
autoCapitalize="sentences"
|
||||
|
@ -1466,15 +1546,26 @@ const Textarea = forwardRef((props, ref) => {
|
|||
}}
|
||||
onInput={(e) => {
|
||||
const { target } = e;
|
||||
setText(target.value);
|
||||
const text = target.value;
|
||||
setText(text);
|
||||
autoResizeTextarea(target);
|
||||
props.onInput?.(e);
|
||||
throttleHighlightText(text);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '4em',
|
||||
// '--text-weight': (1 + charCount / 140).toFixed(1) || 1,
|
||||
}}
|
||||
onScroll={(e) => {
|
||||
const { scrollTop } = e.target;
|
||||
composeHighlightRef.current.scrollTop = scrollTop;
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={composeHighlightRef}
|
||||
class="compose-highlight"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</text-expander>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { Menu } from '@szhsin/react-menu';
|
||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
|
||||
|
@ -102,6 +108,41 @@ function MediaModal({
|
|||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const mediaAccentColors = useMemo(() => {
|
||||
return mediaAttachments?.map((media) => {
|
||||
const { blurhash } = media;
|
||||
if (blurhash) {
|
||||
const averageColor = getBlurHashAverageColor(blurhash);
|
||||
const labAverageColor = rgb2oklab(averageColor);
|
||||
return oklab2rgb([0.6, labAverageColor[1], labAverageColor[2]]);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}, [mediaAttachments]);
|
||||
const mediaAccentGradient = useMemo(() => {
|
||||
const gap = 5;
|
||||
const range = 100 / mediaAccentColors.length;
|
||||
return (
|
||||
mediaAccentColors
|
||||
?.map((color, i) => {
|
||||
const start = i * range + gap;
|
||||
const end = (i + 1) * range - gap;
|
||||
if (color) {
|
||||
return `
|
||||
rgba(${color?.join(',')}, 0.4) ${start}%,
|
||||
rgba(${color?.join(',')}, 0.4) ${end}%
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
transparent ${start}%,
|
||||
transparent ${end}%
|
||||
`;
|
||||
})
|
||||
?.join(', ') || 'transparent'
|
||||
);
|
||||
}, [mediaAccentColors]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
|
||||
|
@ -120,26 +161,32 @@ function MediaModal({
|
|||
onClose();
|
||||
}
|
||||
}}
|
||||
style={
|
||||
mediaAttachments.length > 1
|
||||
? {
|
||||
backgroundAttachment: 'local',
|
||||
backgroundImage: `linear-gradient(
|
||||
to right, ${mediaAccentGradient})`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{mediaAttachments?.map((media, i) => {
|
||||
const { blurhash } = media;
|
||||
let accentColor;
|
||||
if (blurhash) {
|
||||
const averageColor = getBlurHashAverageColor(blurhash);
|
||||
const labAverageColor = rgb2oklab(averageColor);
|
||||
accentColor = oklab2rgb([
|
||||
0.6,
|
||||
labAverageColor[1],
|
||||
labAverageColor[2],
|
||||
]);
|
||||
}
|
||||
const accentColor =
|
||||
mediaAttachments.length === 1 ? mediaAccentColors[i] : null;
|
||||
return (
|
||||
<div
|
||||
class="carousel-item"
|
||||
style={{
|
||||
style={
|
||||
accentColor
|
||||
? {
|
||||
'--accent-color': `rgb(${accentColor?.join(',')})`,
|
||||
'--accent-alpha-color': `rgba(${accentColor?.join(',')}, 0.4)`,
|
||||
}}
|
||||
'--accent-alpha-color': `rgba(${accentColor?.join(
|
||||
',',
|
||||
)}, 0.4)`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
tabindex="0"
|
||||
key={media.id}
|
||||
ref={i === currentIndex ? carouselFocusItem : null}
|
||||
|
|
|
@ -65,12 +65,28 @@ function Timeline({
|
|||
try {
|
||||
let { done, value } = await fetchItems(firstLoad);
|
||||
if (Array.isArray(value)) {
|
||||
// Avoid grouping for pinned posts
|
||||
const [pinnedPosts, otherPosts] = value.reduce(
|
||||
(acc, item) => {
|
||||
if (item._pinned) {
|
||||
acc[0].push(item);
|
||||
} else {
|
||||
acc[1].push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[[], []],
|
||||
);
|
||||
value = otherPosts;
|
||||
if (allowGrouping) {
|
||||
if (boostsCarousel) {
|
||||
value = groupBoosts(value);
|
||||
}
|
||||
value = groupContext(value);
|
||||
}
|
||||
if (pinnedPosts.length) {
|
||||
value = pinnedPosts.concat(value);
|
||||
}
|
||||
console.log(value);
|
||||
if (firstLoad) {
|
||||
setItems(value);
|
||||
|
@ -282,7 +298,9 @@ function Timeline({
|
|||
// checkForUpdates interval
|
||||
useInterval(
|
||||
loadOrCheckUpdates,
|
||||
visible && !showNew ? checkForUpdatesInterval : null,
|
||||
visible && !showNew
|
||||
? checkForUpdatesInterval * (nearReachStart ? 1 : 2)
|
||||
: null,
|
||||
);
|
||||
|
||||
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||
|
|
|
@ -18,7 +18,13 @@
|
|||
--purple-color: blueviolet;
|
||||
--green-color: darkgreen;
|
||||
--orange-color: darkorange;
|
||||
--orange-light-bg-color: color-mix(
|
||||
in srgb,
|
||||
var(--orange-color) 20%,
|
||||
transparent
|
||||
);
|
||||
--red-color: orangered;
|
||||
--red-bg-color: color-mix(in lch, var(--red-color) 40%, transparent);
|
||||
--bg-color: #fff;
|
||||
--bg-faded-color: #f0f2f5;
|
||||
--bg-blur-color: #fff9;
|
||||
|
|
|
@ -33,14 +33,6 @@
|
|||
max-height: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
Loading…
Reference in a new issue