Compare commits
10 commits
7fa08d4b6d
...
e8a4bf58b1
Author | SHA1 | Date | |
---|---|---|---|
Natsu Kagami | e8a4bf58b1 | ||
Natsu Kagami | f50445e508 | ||
Natsu Kagami | 94141db483 | ||
Natsu Kagami | 1e1bab23e9 | ||
Natsu Kagami | e91c124116 | ||
Natsu Kagami | 2d81a7a590 | ||
Natsu Kagami | 991dd0d93d | ||
Natsu Kagami | 0caed40558 | ||
Natsu Kagami | adfa10b696 | ||
Natsu Kagami | 069dc4fde3 |
|
@ -244,8 +244,6 @@ And here I am. Building a Mastodon web client.
|
|||
|
||||
## Alternative web clients
|
||||
|
||||
- Phanpy forks ↓
|
||||
- [Agora](https://agorasocial.app/)
|
||||
- [Pinafore](https://pinafore.social/) ([retired](https://nolanlawson.com/2023/01/09/retiring-pinafore/)) - forks ↓
|
||||
- [Semaphore](https://semaphore.social/)
|
||||
- [Enafore](https://enafore.social/)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
src = lib.cleanSource ./.;
|
||||
|
||||
npmFlags = [ "--legacy-peer-deps" ];
|
||||
npmDepsHash = "sha256-pL/bqTNMtekD5GzTCNPOy60OD2MC36zlg1A6MP0Fn2M=";
|
||||
npmDepsHash = "sha256-9tcZ3jQg+mHeI2K/dNFs0C1k7CfHwxbx+/+I8pdO/wQ=";
|
||||
# npmDepsHash = lib.fakeHash;
|
||||
|
||||
# DTTH-specific env variables
|
||||
|
|
3062
package-lock.json
generated
3062
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
@ -12,34 +12,33 @@
|
|||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "~0.5.4",
|
||||
"@formatjs/intl-segmenter": "~11.5.5",
|
||||
"@formkit/auto-animate": "~0.8.2",
|
||||
"@formkit/auto-animate": "~0.8.1",
|
||||
"@github/text-expander-element": "~2.6.1",
|
||||
"@iconify-icons/mingcute": "~1.2.9",
|
||||
"@justinribeiro/lite-youtube": "~1.5.0",
|
||||
"@szhsin/react-menu": "~4.1.0",
|
||||
"@uidotdev/usehooks": "~2.4.1",
|
||||
"compare-versions": "~6.1.0",
|
||||
"dayjs": "~1.11.11",
|
||||
"dayjs": "~1.11.10",
|
||||
"dayjs-twitter": "~0.5.0",
|
||||
"fast-blurhash": "~1.1.2",
|
||||
"fast-equals": "~5.0.1",
|
||||
"fuse.js": "~7.0.0",
|
||||
"html-prettify": "^1.0.7",
|
||||
"idb-keyval": "~6.2.1",
|
||||
"just-debounce-it": "~3.2.0",
|
||||
"lz-string": "~1.5.0",
|
||||
"masto": "~6.7.7",
|
||||
"masto": "~6.7.0",
|
||||
"moize": "~6.1.6",
|
||||
"p-retry": "~6.2.0",
|
||||
"p-throttle": "~6.1.0",
|
||||
"preact": "~10.21.0",
|
||||
"preact": "~10.20.1",
|
||||
"punycode": "~2.3.1",
|
||||
"react-hotkeys-hook": "~4.5.0",
|
||||
"react-intersection-observer": "~9.10.2",
|
||||
"react-intersection-observer": "~9.8.1",
|
||||
"react-quick-pinch-zoom": "~5.1.0",
|
||||
"react-router-dom": "6.6.2",
|
||||
"string-length": "6.0.0",
|
||||
"swiped-events": "~1.2.0",
|
||||
"swiped-events": "~1.1.9",
|
||||
"toastify-js": "~1.12.0",
|
||||
"uid": "~2.0.2",
|
||||
"use-debounce": "~10.0.0",
|
||||
|
@ -51,18 +50,18 @@
|
|||
"@preact/preset-vite": "~2.8.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "~4.3.0",
|
||||
"postcss": "~8.4.38",
|
||||
"postcss-dark-theme-class": "~1.3.0",
|
||||
"postcss-preset-env": "~9.5.11",
|
||||
"postcss-dark-theme-class": "~1.2.1",
|
||||
"postcss-preset-env": "~9.5.4",
|
||||
"twitter-text": "~3.1.0",
|
||||
"vite": "~5.2.11",
|
||||
"vite": "~5.2.8",
|
||||
"vite-plugin-generate-file": "~0.1.1",
|
||||
"vite-plugin-html-config": "~1.0.11",
|
||||
"vite-plugin-pwa": "~0.20.0",
|
||||
"vite-plugin-pwa": "~0.19.7",
|
||||
"vite-plugin-remove-console": "~2.2.0",
|
||||
"workbox-cacheable-response": "~7.1.0",
|
||||
"workbox-expiration": "~7.1.0",
|
||||
"workbox-routing": "~7.1.0",
|
||||
"workbox-strategies": "~7.1.0"
|
||||
"workbox-cacheable-response": "~7.0.0",
|
||||
"workbox-expiration": "~7.0.0",
|
||||
"workbox-routing": "~7.0.0",
|
||||
"workbox-strategies": "~7.0.0"
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
|
|
|
@ -62,7 +62,7 @@ const iconsRoute = new Route(
|
|||
cacheName: 'icons',
|
||||
plugins: [
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 300,
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 3 * 24 * 60 * 60, // 3 days
|
||||
purgeOnQuotaError: true,
|
||||
}),
|
||||
|
|
|
@ -1966,10 +1966,6 @@ body > .szh-menu-container {
|
|||
.szh-menu
|
||||
.szh-menu__item.danger:not(.szh-menu__item--disabled).szh-menu__item--hover {
|
||||
background-color: var(--red-text-color);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: var(--red-color);
|
||||
}
|
||||
}
|
||||
.szh-menu
|
||||
.szh-menu__item:not(.szh-menu__item--disabled):not(
|
||||
|
|
|
@ -597,123 +597,41 @@
|
|||
#custom-emojis-sheet {
|
||||
max-height: 50vh;
|
||||
max-height: 50dvh;
|
||||
|
||||
header {
|
||||
.loader-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 8px 0 0;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
mask-image: none;
|
||||
min-height: 40vh;
|
||||
padding-bottom: 88px;
|
||||
}
|
||||
|
||||
.custom-emojis-matches {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-emojis-list {
|
||||
.section-header {
|
||||
font-size: 80%;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-insignificant-color);
|
||||
padding: 8px 0 4px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg-color);
|
||||
z-index: 1;
|
||||
}
|
||||
section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
background-image: radial-gradient(
|
||||
closest-side,
|
||||
var(--img-bg-color),
|
||||
transparent
|
||||
);
|
||||
text-shadow: 0 1px 0 var(--bg-color);
|
||||
position: relative;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
font-variant-numeric: slashed-zero;
|
||||
font-feature-settings: 'ss01';
|
||||
|
||||
&[data-title]:after {
|
||||
max-width: 50vw;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
content: attr(data-title);
|
||||
left: 50%;
|
||||
top: 0;
|
||||
background-color: var(--bg-color);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--text-color);
|
||||
transform: translate(-50%, -110%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease-out 0.1s;
|
||||
font-family: var(--monospace-font);
|
||||
line-height: 1;
|
||||
}
|
||||
&.edge-left[data-title]:after {
|
||||
left: 0;
|
||||
transform: translate(0, -110%);
|
||||
}
|
||||
&.edge-right[data-title]:after {
|
||||
left: 100%;
|
||||
transform: translate(-100%, -110%);
|
||||
}
|
||||
|
||||
&:is(:hover, :focus) {
|
||||
z-index: 1;
|
||||
filter: none;
|
||||
background-color: var(--bg-faded-color);
|
||||
|
||||
&[data-title]:after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
&:is(:hover, :focus) img {
|
||||
transform: scale(2);
|
||||
}
|
||||
&.edge-left img {
|
||||
transform-origin: left center;
|
||||
}
|
||||
&.edge-right img {
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#custom-emojis-sheet main {
|
||||
mask-image: none;
|
||||
}
|
||||
#custom-emojis-sheet .custom-emojis-list .section-header {
|
||||
font-size: 80%;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-insignificant-color);
|
||||
padding: 8px 0 4px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
#custom-emojis-sheet .custom-emojis-list section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#custom-emojis-sheet .custom-emojis-list button {
|
||||
border-radius: 8px;
|
||||
background-image: radial-gradient(
|
||||
closest-side,
|
||||
var(--img-bg-color),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) {
|
||||
filter: none;
|
||||
background-color: var(--bg-faded-color);
|
||||
}
|
||||
#custom-emojis-sheet .custom-emojis-list button img {
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.compose-field-container {
|
||||
|
|
|
@ -3,16 +3,8 @@ import './compose.css';
|
|||
import '@github/text-expander-element';
|
||||
import { MenuItem } from '@szhsin/react-menu';
|
||||
import { deepEqual } from 'fast-equals';
|
||||
import Fuse from 'fuse.js';
|
||||
import { memo } from 'preact/compat';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import stringLength from 'string-length';
|
||||
import { uid } from 'uid/single';
|
||||
|
@ -29,7 +21,6 @@ import db from '../utils/db';
|
|||
import emojifyText from '../utils/emojify-text';
|
||||
import localeMatch from '../utils/locale-match';
|
||||
import openCompose from '../utils/open-compose';
|
||||
import pmem from '../utils/pmem';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
|
@ -190,8 +181,6 @@ function highlightText(text, { maxCharacters = Infinity }) {
|
|||
|
||||
const rtf = new Intl.RelativeTimeFormat();
|
||||
|
||||
const CUSTOM_EMOJIS_COUNT = 100;
|
||||
|
||||
function Compose({
|
||||
onClose,
|
||||
replyToStatus,
|
||||
|
@ -1434,40 +1423,25 @@ function autoResizeTextarea(textarea) {
|
|||
}
|
||||
}
|
||||
|
||||
async function _getCustomEmojis(instance, masto) {
|
||||
const emojis = await masto.v1.customEmojis.list();
|
||||
const visibleEmojis = emojis.filter((e) => e.visibleInPicker);
|
||||
const searcher = new Fuse(visibleEmojis, {
|
||||
keys: ['shortcode'],
|
||||
findAllMatches: true,
|
||||
});
|
||||
return [visibleEmojis, searcher];
|
||||
}
|
||||
const getCustomEmojis = pmem(_getCustomEmojis, {
|
||||
// Limit by time to reduce memory usage
|
||||
// Cached by instance
|
||||
matchesArg: (cacheKeyArg, keyArg) => cacheKeyArg.instance === keyArg.instance,
|
||||
maxAge: 30 * 60 * 1000, // 30 minutes
|
||||
});
|
||||
|
||||
const Textarea = forwardRef((props, ref) => {
|
||||
const { masto, instance } = api();
|
||||
const { masto } = api();
|
||||
const [text, setText] = useState(ref.current?.value || '');
|
||||
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
|
||||
// const snapStates = useSnapshot(states);
|
||||
// const charCount = snapStates.composerCharacterCount;
|
||||
|
||||
// const customEmojis = useRef();
|
||||
const searcherRef = useRef();
|
||||
const customEmojis = useRef();
|
||||
useEffect(() => {
|
||||
getCustomEmojis(instance, masto)
|
||||
.then((r) => {
|
||||
const [emojis, searcher] = r;
|
||||
searcherRef.current = searcher;
|
||||
})
|
||||
.catch((e) => {
|
||||
(async () => {
|
||||
try {
|
||||
const emojis = await masto.v1.customEmojis.list();
|
||||
console.log({ emojis });
|
||||
customEmojis.current = emojis;
|
||||
} catch (e) {
|
||||
// silent fail
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const textExpanderRef = useRef();
|
||||
|
@ -1493,26 +1467,23 @@ const Textarea = forwardRef((props, ref) => {
|
|||
// const emojis = customEmojis.current.filter((emoji) =>
|
||||
// emoji.shortcode.startsWith(text),
|
||||
// );
|
||||
// const emojis = filterShortcodes(customEmojis.current, text);
|
||||
const results = searcherRef.current?.search(text, {
|
||||
limit: 5,
|
||||
});
|
||||
const emojis = filterShortcodes(customEmojis.current, text);
|
||||
let html = '';
|
||||
results.forEach(({ item: emoji }) => {
|
||||
emojis.forEach((emoji) => {
|
||||
const { shortcode, url } = emoji;
|
||||
html += `
|
||||
<li role="option" data-value="${encodeHTML(shortcode)}">
|
||||
<img src="${encodeHTML(
|
||||
url,
|
||||
)}" width="16" height="16" alt="" loading="lazy" />
|
||||
${encodeHTML(shortcode)}
|
||||
:${encodeHTML(shortcode)}:
|
||||
</li>`;
|
||||
});
|
||||
// console.log({ emojis, html });
|
||||
menu.innerHTML = html;
|
||||
provide(
|
||||
Promise.resolve({
|
||||
matched: results.length > 0,
|
||||
matched: emojis.length > 0,
|
||||
fragment: menu,
|
||||
}),
|
||||
);
|
||||
|
@ -2214,19 +2185,38 @@ function CustomEmojisModal({
|
|||
}) {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const customEmojisList = useRef([]);
|
||||
const [customEmojis, setCustomEmojis] = useState([]);
|
||||
const [customEmojis, setCustomEmojis] = useState({});
|
||||
const recentlyUsedCustomEmojis = useMemo(
|
||||
() => store.account.get('recentlyUsedCustomEmojis') || [],
|
||||
);
|
||||
const searcherRef = useRef();
|
||||
useEffect(() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const [emojis, searcher] = await getCustomEmojis(instance, masto);
|
||||
console.log('emojis', emojis);
|
||||
searcherRef.current = searcher;
|
||||
setCustomEmojis(emojis);
|
||||
const emojis = await masto.v1.customEmojis.list();
|
||||
// Group emojis by category
|
||||
const emojisCat = {
|
||||
'--recent--': recentlyUsedCustomEmojis.filter((emoji) =>
|
||||
emojis.find((e) => e.shortcode === emoji.shortcode),
|
||||
),
|
||||
};
|
||||
const othersCat = [];
|
||||
emojis.forEach((emoji) => {
|
||||
if (!emoji.visibleInPicker) return;
|
||||
customEmojisList.current?.push?.(emoji);
|
||||
if (!emoji.category) {
|
||||
othersCat.push(emoji);
|
||||
return;
|
||||
}
|
||||
if (!emojisCat[emoji.category]) {
|
||||
emojisCat[emoji.category] = [];
|
||||
}
|
||||
emojisCat[emoji.category].push(emoji);
|
||||
});
|
||||
if (othersCat.length) {
|
||||
emojisCat['--others--'] = othersCat;
|
||||
}
|
||||
setCustomEmojis(emojisCat);
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
setUIState('error');
|
||||
|
@ -2235,83 +2225,6 @@ function CustomEmojisModal({
|
|||
})();
|
||||
}, []);
|
||||
|
||||
const customEmojisCatList = useMemo(() => {
|
||||
// Group emojis by category
|
||||
const emojisCat = {
|
||||
'--recent--': recentlyUsedCustomEmojis.filter((emoji) =>
|
||||
customEmojis.find((e) => e.shortcode === emoji.shortcode),
|
||||
),
|
||||
};
|
||||
const othersCat = [];
|
||||
customEmojis.forEach((emoji) => {
|
||||
customEmojisList.current?.push?.(emoji);
|
||||
if (!emoji.category) {
|
||||
othersCat.push(emoji);
|
||||
return;
|
||||
}
|
||||
if (!emojisCat[emoji.category]) {
|
||||
emojisCat[emoji.category] = [];
|
||||
}
|
||||
emojisCat[emoji.category].push(emoji);
|
||||
});
|
||||
if (othersCat.length) {
|
||||
emojisCat['--others--'] = othersCat;
|
||||
}
|
||||
return emojisCat;
|
||||
}, [customEmojis]);
|
||||
|
||||
const scrollableRef = useRef();
|
||||
const [matches, setMatches] = useState(null);
|
||||
const onFind = useCallback(
|
||||
(e) => {
|
||||
const { value } = e.target;
|
||||
if (value) {
|
||||
const results = searcherRef.current?.search(value, {
|
||||
limit: CUSTOM_EMOJIS_COUNT,
|
||||
});
|
||||
setMatches(results.map((r) => r.item));
|
||||
scrollableRef.current?.scrollTo?.(0, 0);
|
||||
} else {
|
||||
setMatches(null);
|
||||
}
|
||||
},
|
||||
[customEmojis],
|
||||
);
|
||||
|
||||
const onSelectEmoji = useCallback(
|
||||
(emoji) => {
|
||||
onSelect?.(emoji);
|
||||
onClose?.();
|
||||
|
||||
queueMicrotask(() => {
|
||||
let recentlyUsedCustomEmojis =
|
||||
store.account.get('recentlyUsedCustomEmojis') || [];
|
||||
const recentlyUsedEmojiIndex = recentlyUsedCustomEmojis.findIndex(
|
||||
(e) => e.shortcode === emoji.shortcode,
|
||||
);
|
||||
if (recentlyUsedEmojiIndex !== -1) {
|
||||
// Move emoji to index 0
|
||||
recentlyUsedCustomEmojis.splice(recentlyUsedEmojiIndex, 1);
|
||||
recentlyUsedCustomEmojis.unshift(emoji);
|
||||
} else {
|
||||
recentlyUsedCustomEmojis.unshift(emoji);
|
||||
// Remove unavailable ones
|
||||
recentlyUsedCustomEmojis = recentlyUsedCustomEmojis.filter((e) =>
|
||||
customEmojisList.current?.find?.(
|
||||
(emoji) => emoji.shortcode === e.shortcode,
|
||||
),
|
||||
);
|
||||
// Limit to 10
|
||||
recentlyUsedCustomEmojis = recentlyUsedCustomEmojis.slice(0, 10);
|
||||
}
|
||||
|
||||
// Store back
|
||||
store.account.set('recentlyUsedCustomEmojis', recentlyUsedCustomEmojis);
|
||||
});
|
||||
},
|
||||
[onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="custom-emojis-sheet" class="sheet">
|
||||
{!!onClose && (
|
||||
|
@ -2320,167 +2233,107 @@ function CustomEmojisModal({
|
|||
</button>
|
||||
)}
|
||||
<header>
|
||||
<div>
|
||||
<b>Custom emojis</b>{' '}
|
||||
{uiState === 'loading' ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<small class="insignificant"> • {instance}</small>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const emoji = matches[0];
|
||||
if (emoji) {
|
||||
onSelectEmoji(`:${emoji.shortcode}:`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search emoji"
|
||||
onInput={onFind}
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellCheck="false"
|
||||
dir="auto"
|
||||
/>
|
||||
</form>
|
||||
</header>
|
||||
<main ref={scrollableRef}>
|
||||
{matches !== null ? (
|
||||
<ul class="custom-emojis-matches custom-emojis-list">
|
||||
{matches.map((emoji) => (
|
||||
<li key={emoji.shortcode} class="custom-emojis-match">
|
||||
<CustomEmojiButton
|
||||
emoji={emoji}
|
||||
onClick={() => {
|
||||
onSelectEmoji(`:${emoji.shortcode}:`);
|
||||
}}
|
||||
showCode
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<b>Custom emojis</b>{' '}
|
||||
{uiState === 'loading' ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<div class="custom-emojis-list">
|
||||
{uiState === 'error' && (
|
||||
<div class="ui-state">
|
||||
<p>Error loading custom emojis</p>
|
||||
</div>
|
||||
)}
|
||||
{uiState === 'default' &&
|
||||
Object.entries(customEmojisCatList).map(
|
||||
([category, emojis]) =>
|
||||
!!emojis?.length && (
|
||||
<>
|
||||
<div class="section-header">
|
||||
{{
|
||||
'--recent--': 'Recently used',
|
||||
'--others--': 'Others',
|
||||
}[category] || category}
|
||||
</div>
|
||||
<CustomEmojisList
|
||||
emojis={emojis}
|
||||
onSelect={onSelectEmoji}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<small class="insignificant"> • {instance}</small>
|
||||
)}
|
||||
</header>
|
||||
<main>
|
||||
<div class="custom-emojis-list">
|
||||
{uiState === 'error' && (
|
||||
<div class="ui-state">
|
||||
<p>Error loading custom emojis</p>
|
||||
</div>
|
||||
)}
|
||||
{uiState === 'default' &&
|
||||
Object.entries(customEmojis).map(
|
||||
([category, emojis]) =>
|
||||
!!emojis?.length && (
|
||||
<>
|
||||
<div class="section-header">
|
||||
{{
|
||||
'--recent--': 'Recently used',
|
||||
'--others--': 'Others',
|
||||
}[category] || category}
|
||||
</div>
|
||||
<section>
|
||||
{emojis.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
class="plain4"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
requestAnimationFrame(() => {
|
||||
onSelect(`:${emoji.shortcode}:`);
|
||||
});
|
||||
let recentlyUsedCustomEmojis =
|
||||
store.account.get('recentlyUsedCustomEmojis') ||
|
||||
[];
|
||||
const recentlyUsedEmojiIndex =
|
||||
recentlyUsedCustomEmojis.findIndex(
|
||||
(e) => e.shortcode === emoji.shortcode,
|
||||
);
|
||||
if (recentlyUsedEmojiIndex !== -1) {
|
||||
// Move emoji to index 0
|
||||
recentlyUsedCustomEmojis.splice(
|
||||
recentlyUsedEmojiIndex,
|
||||
1,
|
||||
);
|
||||
recentlyUsedCustomEmojis.unshift(emoji);
|
||||
} else {
|
||||
recentlyUsedCustomEmojis.unshift(emoji);
|
||||
// Remove unavailable ones
|
||||
recentlyUsedCustomEmojis =
|
||||
recentlyUsedCustomEmojis.filter((e) =>
|
||||
customEmojisList.current?.find?.(
|
||||
(emoji) => emoji.shortcode === e.shortcode,
|
||||
),
|
||||
);
|
||||
// Limit to 10
|
||||
recentlyUsedCustomEmojis =
|
||||
recentlyUsedCustomEmojis.slice(0, 10);
|
||||
}
|
||||
|
||||
// Store back
|
||||
store.account.set(
|
||||
'recentlyUsedCustomEmojis',
|
||||
recentlyUsedCustomEmojis,
|
||||
);
|
||||
}}
|
||||
title={`:${emoji.shortcode}:`}
|
||||
>
|
||||
<picture>
|
||||
{!!emoji.staticUrl && (
|
||||
<source
|
||||
srcset={emoji.staticUrl}
|
||||
media="(prefers-reduced-motion: reduce)"
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
class="shortcode-emoji"
|
||||
src={emoji.url || emoji.staticUrl}
|
||||
alt={emoji.shortcode}
|
||||
width="16"
|
||||
height="16"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomEmojisList = memo(({ emojis, onSelect }) => {
|
||||
const [max, setMax] = useState(CUSTOM_EMOJIS_COUNT);
|
||||
const showMore = emojis.length > max;
|
||||
return (
|
||||
<section>
|
||||
{emojis.slice(0, max).map((emoji) => (
|
||||
<CustomEmojiButton
|
||||
key={emoji.shortcode}
|
||||
emoji={emoji}
|
||||
onClick={() => {
|
||||
onSelect(`:${emoji.shortcode}:`);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{showMore && (
|
||||
<button
|
||||
type="button"
|
||||
class="plain small"
|
||||
onClick={() => setMax(max + CUSTOM_EMOJIS_COUNT)}
|
||||
>
|
||||
{(emojis.length - max).toLocaleString()} more…
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
const CustomEmojiButton = memo(({ emoji, onClick, showCode }) => {
|
||||
const addEdges = (e) => {
|
||||
// Add edge-left or edge-right class based on self position relative to scrollable parent
|
||||
// If near left edge, add edge-left, if near right edge, add edge-right
|
||||
const buffer = 88;
|
||||
const parent = e.currentTarget.closest('main');
|
||||
if (parent) {
|
||||
const rect = parent.getBoundingClientRect();
|
||||
const selfRect = e.currentTarget.getBoundingClientRect();
|
||||
const targetClassList = e.currentTarget.classList;
|
||||
if (selfRect.left < rect.left + buffer) {
|
||||
targetClassList.add('edge-left');
|
||||
targetClassList.remove('edge-right');
|
||||
} else if (selfRect.right > rect.right - buffer) {
|
||||
targetClassList.add('edge-right');
|
||||
targetClassList.remove('edge-left');
|
||||
} else {
|
||||
targetClassList.remove('edge-left', 'edge-right');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="plain4"
|
||||
onClick={onClick}
|
||||
data-title={showCode ? undefined : emoji.shortcode}
|
||||
onPointerEnter={addEdges}
|
||||
onFocus={addEdges}
|
||||
>
|
||||
<picture>
|
||||
{!!emoji.staticUrl && (
|
||||
<source
|
||||
srcSet={emoji.staticUrl}
|
||||
media="(prefers-reduced-motion: reduce)"
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
className="shortcode-emoji"
|
||||
src={emoji.url || emoji.staticUrl}
|
||||
alt={emoji.shortcode}
|
||||
width="24"
|
||||
height="24"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>
|
||||
{showCode && (
|
||||
<>
|
||||
{' '}
|
||||
<code>{emoji.shortcode}</code>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
const GIFS_PER_PAGE = 20;
|
||||
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
|
|
@ -17,21 +17,6 @@
|
|||
);
|
||||
filter: saturate(0.5);
|
||||
}
|
||||
|
||||
&:is(a) {
|
||||
pointer-events: auto;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--outline-hover-color);
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accounts-list {
|
||||
|
|
|
@ -11,7 +11,6 @@ import useLocationChange from '../utils/useLocationChange';
|
|||
|
||||
import AccountBlock from './account-block';
|
||||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import Loader from './loader';
|
||||
import Status from './status';
|
||||
|
||||
|
@ -144,12 +143,9 @@ export default function GenericAccounts({
|
|||
</header>
|
||||
<main>
|
||||
{post && (
|
||||
<Link
|
||||
to={`/${instance || currentInstance}/s/${post.id}`}
|
||||
class="post-preview"
|
||||
>
|
||||
<div class="post-preview">
|
||||
<Status status={post} size="s" readOnly />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{accounts.length > 0 ? (
|
||||
<>
|
||||
|
|
|
@ -7,13 +7,10 @@ import { useInView } from 'react-intersection-observer';
|
|||
// The sticky header, usually at the top
|
||||
const TOP = 48;
|
||||
|
||||
const shazamIDs = {};
|
||||
|
||||
export default function LazyShazam({ id, children }) {
|
||||
export default function LazyShazam({ children }) {
|
||||
const containerRef = useRef();
|
||||
const hasID = !!shazamIDs[id];
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [visibleStart, setVisibleStart] = useState(hasID || false);
|
||||
const [visibleStart, setVisibleStart] = useState(false);
|
||||
|
||||
const { ref } = useInView({
|
||||
root: null,
|
||||
|
@ -23,7 +20,6 @@ export default function LazyShazam({ id, children }) {
|
|||
onChange: (inView) => {
|
||||
if (inView) {
|
||||
setVisible(true);
|
||||
if (id) shazamIDs[id] = true;
|
||||
}
|
||||
},
|
||||
triggerOnce: true,
|
||||
|
@ -39,7 +35,6 @@ export default function LazyShazam({ id, children }) {
|
|||
} else {
|
||||
setVisibleStart(true);
|
||||
}
|
||||
if (id) shazamIDs[id] = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ const NOTIFICATION_ICONS = {
|
|||
'admin.signup': 'account-edit',
|
||||
'admin.report': 'account-warning',
|
||||
severed_relationships: 'heart-break',
|
||||
moderation_warning: 'alert',
|
||||
emoji_reaction: 'emoji2',
|
||||
'pleroma:emoji_reaction': 'emoji2',
|
||||
};
|
||||
|
@ -46,8 +45,6 @@ poll = A poll you have voted in or created has ended
|
|||
update = A status you interacted with has been edited
|
||||
admin.sign_up = Someone signed up (optionally sent to admins)
|
||||
admin.report = A new report has been filed
|
||||
severed_relationships = Severed relationships
|
||||
moderation_warning = Moderation warning
|
||||
*/
|
||||
|
||||
function emojiText(emoji, emoji_url) {
|
||||
|
@ -94,7 +91,6 @@ const contentText = {
|
|||
Lost connections with <i>{name}</i>.
|
||||
</>
|
||||
),
|
||||
moderation_warning: <b>Moderation warning</b>,
|
||||
emoji_reaction: emojiText,
|
||||
'pleroma:emoji_reaction': emojiText,
|
||||
};
|
||||
|
@ -121,17 +117,6 @@ const SEVERED_RELATIONSHIPS_TEXT = {
|
|||
),
|
||||
};
|
||||
|
||||
const MODERATION_WARNING_TEXT = {
|
||||
none: 'Your account has received a moderation warning.',
|
||||
disable: 'Your account has been disabled.',
|
||||
mark_statuses_as_sensitive:
|
||||
'Some of your posts have been marked as sensitive.',
|
||||
delete_statuses: 'Some of your posts have been deleted.',
|
||||
sensitive: 'Your posts will be marked as sensitive from now on.',
|
||||
silence: 'Your account has been limited.',
|
||||
suspend: 'Your account has been suspended.',
|
||||
};
|
||||
|
||||
const AVATARS_LIMIT = 50;
|
||||
|
||||
function Notification({
|
||||
|
@ -140,16 +125,8 @@ function Notification({
|
|||
isStatic,
|
||||
disableContextMenu,
|
||||
}) {
|
||||
const {
|
||||
id,
|
||||
status,
|
||||
account,
|
||||
report,
|
||||
event,
|
||||
moderation_warning,
|
||||
_accounts,
|
||||
_statuses,
|
||||
} = notification;
|
||||
const { id, status, account, report, event, _accounts, _statuses } =
|
||||
notification;
|
||||
let { type } = notification;
|
||||
|
||||
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
||||
|
@ -337,20 +314,6 @@ function Notification({
|
|||
.
|
||||
</div>
|
||||
)}
|
||||
{type === 'moderation_warning' && !!moderation_warning && (
|
||||
<div>
|
||||
{MODERATION_WARNING_TEXT[moderation_warning.action]}
|
||||
<br />
|
||||
<a
|
||||
href={`/disputes/strikes/${moderation_warning.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more <Icon icon="external" size="s" />
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{_accounts?.length > 1 && (
|
||||
|
|
|
@ -569,15 +569,8 @@
|
|||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
|
||||
&.horizontal {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.status-filtered-badge:not(.horizontal).badge-meta {
|
||||
.status-filtered-badge.badge-meta {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
@ -591,10 +584,10 @@
|
|||
border-color: var(--text-color);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
.status-filtered-badge:not(.horizontal).badge-meta > span:first-child {
|
||||
.status-filtered-badge.badge-meta > span:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-filtered-badge:not(.horizontal).badge-meta > span + span {
|
||||
.status-filtered-badge.badge-meta > span + span {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
font-weight: normal;
|
||||
|
@ -608,10 +601,6 @@
|
|||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.status-filtered-badge.horizontal.badge-meta > span + span {
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.status.large > .container > .content-container {
|
||||
margin-left: calc(-50px - 16px);
|
||||
|
@ -836,12 +825,6 @@
|
|||
.timeline-deck .status .content.truncated ~ .card {
|
||||
display: none;
|
||||
}
|
||||
.status .content .inner-content {
|
||||
> img[height] {
|
||||
height: auto;
|
||||
aspect-ratio: var(--original-aspect-ratio);
|
||||
}
|
||||
}
|
||||
.status .content .inner-content a:not(.mention, .has-url-text) {
|
||||
color: var(--link-text-color);
|
||||
}
|
||||
|
@ -2397,8 +2380,8 @@ a.card:is(:hover, :focus):visited {
|
|||
max-width: 100%;
|
||||
height: 1.2em;
|
||||
vertical-align: text-bottom;
|
||||
object-fit: contain;
|
||||
/* object-position: left; */
|
||||
object-fit: cover;
|
||||
object-position: left;
|
||||
}
|
||||
|
||||
/* EDIT HISTORY */
|
||||
|
|
|
@ -3337,7 +3337,7 @@ const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
|
|||
|
||||
return uniqueQuotes.map((q) => {
|
||||
return (
|
||||
<LazyShazam id={q.instance + q.id}>
|
||||
<LazyShazam>
|
||||
<Link
|
||||
key={q.instance + q.id}
|
||||
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
||||
|
|
|
@ -209,8 +209,8 @@ function Timeline({
|
|||
|
||||
const oRef = useHotkeys(['enter', 'o'], () => {
|
||||
// open active status
|
||||
const activeItem = document.activeElement;
|
||||
if (activeItem?.matches(itemsSelector)) {
|
||||
const activeItem = document.activeElement.closest(itemsSelector);
|
||||
if (activeItem) {
|
||||
activeItem.click();
|
||||
}
|
||||
});
|
||||
|
@ -646,11 +646,7 @@ const TimelineItem = memo(
|
|||
>
|
||||
<Link class="status-link timeline-item" to={url}>
|
||||
{showCompact ? (
|
||||
<TimelineStatusCompact
|
||||
status={item}
|
||||
instance={instance}
|
||||
filterContext={filterContext}
|
||||
/>
|
||||
<TimelineStatusCompact status={item} instance={instance} />
|
||||
) : useItemID ? (
|
||||
<Status
|
||||
statusID={statusID}
|
||||
|
@ -824,12 +820,11 @@ function StatusCarousel({ title, class: className, children }) {
|
|||
);
|
||||
}
|
||||
|
||||
function TimelineStatusCompact({ status, instance, filterContext }) {
|
||||
function TimelineStatusCompact({ status, instance }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { id, visibility, language } = status;
|
||||
const statusPeekText = statusPeek(status);
|
||||
const sKey = statusKey(id, instance);
|
||||
const filterInfo = isFiltered(status.filtered, filterContext);
|
||||
return (
|
||||
<article
|
||||
class={`status compact-thread ${
|
||||
|
@ -855,24 +850,13 @@ function TimelineStatusCompact({ status, instance, filterContext }) {
|
|||
lang={language}
|
||||
dir="auto"
|
||||
>
|
||||
{!!filterInfo ? (
|
||||
<b
|
||||
class="status-filtered-badge badge-meta horizontal"
|
||||
title={filterInfo?.titlesStr || ''}
|
||||
>
|
||||
<span>Filtered</span>: <span>{filterInfo?.titlesStr || ''}</span>
|
||||
</b>
|
||||
) : (
|
||||
{statusPeekText}
|
||||
{status.sensitive && status.spoilerText && (
|
||||
<>
|
||||
{statusPeekText}
|
||||
{status.sensitive && status.spoilerText && (
|
||||
<>
|
||||
{' '}
|
||||
<span class="spoiler-badge">
|
||||
<Icon icon="eye-close" size="s" />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{' '}
|
||||
<span class="spoiler-badge">
|
||||
<Icon icon="eye-close" size="s" />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,6 @@ import { lazy } from 'preact/compat';
|
|||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import IntlSegmenterSuspense from './components/intl-segmenter-suspense';
|
||||
import { initStates } from './utils/states';
|
||||
// import Compose from './components/compose';
|
||||
import useTitle from './utils/useTitle';
|
||||
|
||||
|
@ -32,10 +31,6 @@ function App() {
|
|||
: 'Compose',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initStates();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (uiState === 'closed') {
|
||||
try {
|
||||
|
|
|
@ -286,13 +286,7 @@ function FiltersAddEdit({ filter, onClose }) {
|
|||
// Preserve existing expiry if not specified
|
||||
// Seconds from now to expiresAtDate
|
||||
// Other clients don't do this
|
||||
if (hasExpiry) {
|
||||
expiresIn = Math.floor(
|
||||
(expiresAtDate - new Date()) / 1000,
|
||||
);
|
||||
} else {
|
||||
expiresIn = null;
|
||||
}
|
||||
expiresIn = Math.floor((expiresAtDate - new Date()) / 1000);
|
||||
} else if (expiresIn === '0' || expiresIn === 0) {
|
||||
// 0 = Never
|
||||
expiresIn = null;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import './login.css';
|
||||
|
||||
import Fuse from 'fuse.js';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
|
@ -29,14 +28,12 @@ function Login() {
|
|||
);
|
||||
|
||||
const [instancesList, setInstancesList] = useState([]);
|
||||
const searcher = useRef();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(instancesListURL);
|
||||
const data = await res.json();
|
||||
setInstancesList(data);
|
||||
searcher.current = new Fuse(data);
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
console.error(e);
|
||||
|
@ -94,11 +91,21 @@ function Login() {
|
|||
!/[\s\/\\@]/.test(cleanInstanceText);
|
||||
|
||||
const instancesSuggestions = cleanInstanceText
|
||||
? searcher.current
|
||||
?.search(cleanInstanceText, {
|
||||
limit: 10,
|
||||
? instancesList
|
||||
.filter((instance) => instance.includes(instanceText))
|
||||
.sort((a, b) => {
|
||||
// Move text that starts with instanceText to the start
|
||||
const aStartsWith = a
|
||||
.toLowerCase()
|
||||
.startsWith(instanceText.toLowerCase());
|
||||
const bStartsWith = b
|
||||
.toLowerCase()
|
||||
.startsWith(instanceText.toLowerCase());
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
return 0;
|
||||
})
|
||||
?.map((match) => match.item)
|
||||
.slice(0, 10)
|
||||
: [];
|
||||
|
||||
const selectedInstanceText = instanceTextLooksLikeDomain
|
||||
|
|
|
@ -102,17 +102,6 @@ function Notifications({ columnMode }) {
|
|||
// },
|
||||
// });
|
||||
|
||||
// TEST: Slot in a fake notification to test 'moderation_warning'
|
||||
// notifications.unshift({
|
||||
// id: '123123',
|
||||
// type: 'moderation_warning',
|
||||
// createdAt: new Date().toISOString(),
|
||||
// moderation_warning: {
|
||||
// id: '1231234',
|
||||
// action: 'mark_statuses_as_sensitive',
|
||||
// },
|
||||
// });
|
||||
|
||||
// console.log({ notifications });
|
||||
|
||||
const groupedNotifications = groupNotifications(notifications);
|
||||
|
|
|
@ -242,17 +242,6 @@ function _enhanceContent(content, opts = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// ADD ASPECT RATIO TO ALL IMAGES
|
||||
if (enhancedContent.includes('<img')) {
|
||||
dom.querySelectorAll('img').forEach((img) => {
|
||||
const width = img.getAttribute('width') || img.naturalWidth;
|
||||
const height = img.getAttribute('height') || img.naturalHeight;
|
||||
if (width && height) {
|
||||
img.style.setProperty('--original-aspect-ratio', `${width}/${height}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (postEnhanceDOM) {
|
||||
queueMicrotask(() => postEnhanceDOM(dom));
|
||||
// postEnhanceDOM(dom); // mutate dom
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const { locale } = Intl.NumberFormat().resolvedOptions();
|
||||
const shortenNumber = Intl.NumberFormat(locale, {
|
||||
notation: 'compact',
|
||||
roundingMode: 'floor',
|
||||
}).format;
|
||||
export default shortenNumber;
|
||||
|
|
Loading…
Reference in a new issue