Merge pull request #143 from cheeaun/main

Update from main
This commit is contained in:
Chee Aun 2023-05-22 23:40:44 +08:00 committed by GitHub
commit 0a5d7267d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 613 additions and 498 deletions

728
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,11 +24,11 @@
"masto": "~5.11.3",
"mem": "~9.0.2",
"p-retry": "~5.1.2",
"p-throttle": "~5.0.0",
"preact": "~10.13.2",
"p-throttle": "~5.1.0",
"preact": "~10.15.0",
"react-hotkeys-hook": "~4.4.0",
"react-intersection-observer": "~9.4.3",
"react-quick-pinch-zoom": "~4.6.0",
"react-quick-pinch-zoom": "~4.9.0",
"react-router-dom": "6.6.2",
"string-length": "~5.0.1",
"swiped-events": "~1.1.7",
@ -44,12 +44,12 @@
"@trivago/prettier-plugin-sort-imports": "~4.1.1",
"postcss": "~8.4.23",
"postcss-dark-theme-class": "~0.7.3",
"postcss-preset-env": "~8.3.2",
"postcss-preset-env": "~8.4.1",
"twitter-text": "~3.1.0",
"vite": "~4.3.5",
"vite": "~4.3.8",
"vite-plugin-generate-file": "~0.0.4",
"vite-plugin-html-config": "~1.0.11",
"vite-plugin-pwa": "~0.14.7",
"vite-plugin-pwa": "~0.15.0",
"vite-plugin-remove-console": "~2.1.1",
"workbox-cacheable-response": "~6.5.4",
"workbox-expiration": "~6.5.4",

View file

@ -59,8 +59,9 @@ registerRoute(iconsRoute);
// - /api/v1/custom_emojis
// - /api/v1/preferences
// - /api/v1/lists/:id
// - /api/v1/announcements
const apiExtendedRoute = new RegExpRoute(
/^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis|preferences|lists\/\d+)$/,
/^https?:\/\/[^\/]+\/api\/v\d+\/(instance|custom_emojis|preferences|lists\/\d+|announcements)$/,
new StaleWhileRevalidate({
cacheName: 'api-extended',
plugins: [

View file

@ -262,10 +262,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
inset 0 3px var(--comment-line-color);
}
.timeline.contextual .replies[data-comments-level='4'] {
overflow: auto;
overflow-x: auto;
}
.timeline.contextual .replies[data-comments-level='4']:has(.replies) {
overflow: auto;
overflow-x: auto;
mask-image: linear-gradient(to left, transparent, black 32px);
}
.timeline.contextual

View file

@ -91,7 +91,9 @@ function App() {
useEffect(() => {
const instanceURL = store.local.get('instanceURL');
const code = (window.location.search.match(/code=([^&]+)/) || [])[1];
const code = decodeURIComponent(
(window.location.search.match(/code=([^&]+)/) || [, ''])[1],
);
if (code) {
console.log({ code });

3
src/assets/phanpy-bg.svg Normal file
View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 90 85">
<path fill="#fff" fill-opacity="0.4" fill-rule="evenodd" d="M88 64c6.4-29.6-2.4-55.2-31.7-62.7C31.9-4.9 8.1 11.6.8 42c-3.3 13.8 3.3 30.3 24.6 35.6l19.3 4.8c14.5 4 24.3 2.3 30.7-1.1a25 25 0 0 0 12.7-17.4Zm-8-1.8c5.4-24.7-1.1-46.7-25.7-53C34 4 14.8 18.6 8.8 44c-2.5 10.2 2.9 22 18.6 25.8l19.4 4.8c23.4 6.4 31.4-3.4 33.3-12.3ZM35.8 28.4c-3-1.4-14.5 6.4-15.3 17.8-.4 4.8 9 6.5 10.1-.3.7-4.8 2.6-9 4-12s2.3-5 1.2-5.5Zm19.8 15c-.5-5.9-1-10.6 1.7-11 4-.7 10.4 15.3 8.2 25.4-.6 3-9.6 1.6-9.4-4 .2-3.5-.2-7.1-.5-10.4Z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

View file

@ -493,7 +493,7 @@ function RelatedActions({ info, instance, authenticated }) {
>
<div class="shazam-container-inner">
<p>
Also followed by{' '}
Followed by{' '}
<span class="ib">
{familiarFollowers.map((follower) => (
<a

View file

@ -388,6 +388,7 @@
background-color: var(--bg-faded-color);
border-radius: 8px;
margin: 8px 0 0;
display: block;
}
#compose-container .poll-choices {

View file

@ -1,6 +1,5 @@
import './compose.css';
import { match } from '@formatjs/intl-localematcher';
import '@github/text-expander-element';
import equal from 'fast-deep-equal';
import { forwardRef } from 'preact/compat';
@ -16,6 +15,7 @@ import urlRegex from '../data/url-regex';
import { api } from '../utils/api';
import db from '../utils/db';
import emojifyText from '../utils/emojify-text';
import localeMatch from '../utils/locale-match';
import openCompose from '../utils/open-compose';
import states, { saveStatus } from '../utils/states';
import store from '../utils/store';
@ -85,7 +85,7 @@ const observer = new IntersectionObserver((entries) => {
});
observer.observe(menu);
const DEFAULT_LANG = match(
const DEFAULT_LANG = localeMatch(
[new Intl.DateTimeFormat().resolvedOptions().locale, ...navigator.languages],
supportedLanguages.map((l) => l[0]),
'en',

View file

@ -0,0 +1,43 @@
@media (min-width: 23em) {
.nav-menu {
display: flex;
width: auto;
padding: 0;
}
.nav-menu section {
padding: 8px 0;
width: 50%;
}
@keyframes phanpying {
0% {
background-position: 0 0, 0 0, 3em 85%;
}
100% {
background-position: 0 0, 0 0, 2em 90%;
}
}
.nav-menu section:last-child {
background-color: var(--bg-faded-color);
background-image: linear-gradient(
to right,
var(--divider-color) 1px,
transparent 1px
),
linear-gradient(to bottom left, var(--bg-blur-color), transparent),
url(../assets/phanpy-bg.svg);
background-repeat: no-repeat;
/* background-size: auto, auto, 200%; */
background-position: 0 0, 0 0, 2em 90%;
background-blend-mode: normal, normal, overlay;
box-shadow: inset 0 0 1px var(--bg-color);
position: sticky;
top: 0;
animation: phanpying 0.2s ease-in-out both;
}
.nav-menu section:last-child > .szh-menu__divider:first-child {
display: none;
}
.nav-menu .szh-menu__item span {
white-space: normal;
}
}

View file

@ -1,11 +1,7 @@
import {
ControlledMenu,
MenuDivider,
MenuItem,
useClick,
useMenuState,
} from '@szhsin/react-menu';
import { useRef } from 'preact/hooks';
import './nav-menu.css';
import { ControlledMenu, MenuDivider, MenuItem } from '@szhsin/react-menu';
import { useRef, useState } from 'preact/hooks';
import { useLongPress } from 'use-long-press';
import { useSnapshot } from 'valtio';
@ -47,8 +43,7 @@ function NavMenu(props) {
);
const buttonRef = useRef();
const [menuState, toggleMenu] = useMenuState();
const anchorProps = useClick(menuState.state, toggleMenu);
const [menuState, setMenuState] = useState(undefined);
return (
<>
@ -59,7 +54,9 @@ function NavMenu(props) {
moreThanOneAccount ? 'with-avatar' : ''
} ${open ? 'active' : ''}`}
style={{ position: 'relative' }}
{...anchorProps}
onClick={() => {
setMenuState((state) => (!state ? 'open' : undefined));
}}
onContextMenu={(e) => {
e.preventDefault();
states.showAccounts = true;
@ -78,17 +75,18 @@ function NavMenu(props) {
<Icon icon="menu" size={moreThanOneAccount ? 's' : 'l'} />
</button>
<ControlledMenu
{...menuState}
menuClassName="nav-menu"
state={menuState}
anchorRef={buttonRef}
onClose={() => {
toggleMenu(false);
setMenuState(undefined);
}}
containerProps={{
style: {
zIndex: 10,
},
onClick: () => {
toggleMenu(false);
setMenuState(undefined);
},
}}
portal={{
@ -102,109 +100,113 @@ function NavMenu(props) {
boundingBoxPadding="8 8 8 8"
unmountOnClose
>
{!!snapStates.appVersion?.commitHash &&
__COMMIT_HASH__ !== snapStates.appVersion.commitHash && (
<section>
{!!snapStates.appVersion?.commitHash &&
__COMMIT_HASH__ !== snapStates.appVersion.commitHash && (
<>
<MenuItem
onClick={() => {
const yes = confirm('Reload page now to update?');
if (yes) {
(async () => {
try {
location.reload();
} catch (e) {}
})();
}
}}
>
<Icon icon="sparkles" size="l" />{' '}
<span>New update available</span>
</MenuItem>
<MenuDivider />
</>
)}
<MenuLink to="/">
<Icon icon="home" size="l" /> <span>Home</span>
</MenuLink>
{authenticated && (
<>
<MenuItem
onClick={() => {
const yes = confirm('Reload page now to update?');
if (yes) {
(async () => {
try {
location.reload();
} catch (e) {}
})();
}
}}
>
<Icon icon="sparkles" size="l" />{' '}
<span>New update available</span>
</MenuItem>
{showFollowing && (
<MenuLink to="/following">
<Icon icon="following" size="l" /> <span>Following</span>
</MenuLink>
)}
<MenuLink to="/mentions">
<Icon icon="at" size="l" /> <span>Mentions</span>
</MenuLink>
<MenuLink to="/notifications">
<Icon icon="notification" size="l" /> <span>Notifications</span>
{snapStates.notificationsShowNew && (
<sup title="New" style={{ opacity: 0.5 }}>
{' '}
&bull;
</sup>
)}
</MenuLink>
<MenuDivider />
<MenuLink to="/l">
<Icon icon="list" size="l" /> <span>Lists</span>
</MenuLink>
<MenuLink to="/ft">
<Icon icon="hashtag" size="l" /> <span>Followed Hashtags</span>
</MenuLink>
<MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<MenuLink to="/f">
<Icon icon="heart" size="l" /> <span>Favourites</span>
</MenuLink>
</>
)}
<MenuLink to="/">
<Icon icon="home" size="l" /> <span>Home</span>
</MenuLink>
{authenticated && (
<>
{showFollowing && (
<MenuLink to="/following">
<Icon icon="following" size="l" /> <span>Following</span>
</MenuLink>
)}
<MenuLink to="/mentions">
<Icon icon="at" size="l" /> <span>Mentions</span>
</MenuLink>
<MenuLink to="/notifications">
<Icon icon="notification" size="l" /> <span>Notifications</span>
{snapStates.notificationsShowNew && (
<sup title="New" style={{ opacity: 0.5 }}>
{' '}
&bull;
</sup>
<MenuDivider />
<MenuLink to={`/search`}>
<Icon icon="search" size="l" /> <span>Search</span>
</MenuLink>
<MenuLink to={`/${instance}/p/l`}>
<Icon icon="group" size="l" /> <span>Local</span>
</MenuLink>
<MenuLink to={`/${instance}/p`}>
<Icon icon="earth" size="l" /> <span>Federated</span>
</MenuLink>
<MenuLink to={`/${instance}/trending`}>
<Icon icon="chart" size="l" /> <span>Trending</span>
</MenuLink>
</section>
<section>
{authenticated && (
<>
<MenuDivider />
{currentAccount?.info?.id && (
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
<Icon icon="user" size="l" /> <span>Profile</span>
</MenuLink>
)}
</MenuLink>
<MenuDivider />
<MenuLink to="/l">
<Icon icon="list" size="l" /> <span>Lists</span>
</MenuLink>
<MenuLink to="/ft">
<Icon icon="hashtag" size="l" /> <span>Followed Hashtags</span>
</MenuLink>
<MenuLink to="/b">
<Icon icon="bookmark" size="l" /> <span>Bookmarks</span>
</MenuLink>
<MenuLink to="/f">
<Icon icon="heart" size="l" /> <span>Favourites</span>
</MenuLink>
</>
)}
<MenuDivider />
<MenuLink to={`/search`}>
<Icon icon="search" size="l" /> <span>Search</span>
</MenuLink>
<MenuLink to={`/${instance}/p/l`}>
<Icon icon="group" size="l" /> <span>Local</span>
</MenuLink>
<MenuLink to={`/${instance}/p`}>
<Icon icon="earth" size="l" /> <span>Federated</span>
</MenuLink>
<MenuLink to={`/${instance}/trending`}>
<Icon icon="chart" size="l" /> <span>Trending</span>
</MenuLink>
{authenticated && (
<>
<MenuDivider />
{currentAccount?.info?.id && (
<MenuLink to={`/${instance}/a/${currentAccount.info.id}`}>
<Icon icon="user" size="l" /> <span>Profile</span>
</MenuLink>
)}
<MenuItem
onClick={() => {
states.showAccounts = true;
}}
>
<Icon icon="group" size="l" /> <span>Accounts&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {
states.showShortcutsSettings = true;
}}
>
<Icon icon="shortcut" size="l" />{' '}
<span>Shortcuts Settings&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {
states.showSettings = true;
}}
>
<Icon icon="gear" size="l" /> <span>Settings&hellip;</span>
</MenuItem>
</>
)}
<MenuItem
onClick={() => {
states.showAccounts = true;
}}
>
<Icon icon="group" size="l" /> <span>Accounts&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {
states.showShortcutsSettings = true;
}}
>
<Icon icon="shortcut" size="l" />{' '}
<span>Shortcuts Settings&hellip;</span>
</MenuItem>
<MenuItem
onClick={() => {
states.showSettings = true;
}}
>
<Icon icon="gear" size="l" /> <span>Settings&hellip;</span>
</MenuItem>
</>
)}
</section>
</ControlledMenu>
</>
);

View file

@ -518,7 +518,7 @@
.status .content > div > :is(ul, ol) {
margin-block: min(0.75em, 12px);
margin-inline: 0;
padding-inline-start: 1em;
padding-inline-start: 1.5em;
}
.status .content .invisible {
display: none;

View file

@ -1,6 +1,5 @@
import './status.css';
import { match } from '@formatjs/intl-localematcher';
import '@justinribeiro/lite-youtube';
import {
ControlledMenu,
@ -33,6 +32,7 @@ import getHTMLText from '../utils/getHTMLText';
import handleContentLinks from '../utils/handle-content-links';
import htmlContentLength from '../utils/html-content-length';
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
import localeMatch from '../utils/locale-match';
import niceDateTime from '../utils/nice-date-time';
import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast';
@ -105,10 +105,11 @@ function Status({
const { instance: currentInstance } = api();
const sameInstance = instance === currentInstance;
const sKey = statusKey(statusID, instance);
let sKey = statusKey(statusID, instance);
const snapStates = useSnapshot(states);
if (!status) {
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
sKey = statusKey(status?.id, instance);
}
if (!status) {
return null;
@ -408,9 +409,9 @@ function Status({
const differentLanguage =
language &&
language !== targetLanguage &&
!match([language], [targetLanguage]) &&
!localeMatch([language], [targetLanguage]) &&
!contentTranslationHideLanguages.find(
(l) => language === l || match([language], [l]),
(l) => language === l || localeMatch([language], [l]),
);
const menuInstanceRef = useRef();
@ -977,6 +978,7 @@ function Status({
(result) => {
if (!result) return;
a.removeAttribute('target');
if (!sKey) return;
if (!Array.isArray(states.statusQuotes[sKey])) {
states.statusQuotes[sKey] = [];
}
@ -1102,7 +1104,7 @@ function Status({
<>
<Icon
icon={visibilityIconsMap[visibility]}
alt={visibility}
alt={visibilityText[visibility]}
/>{' '}
<a href={url} target="_blank">
<time
@ -1954,6 +1956,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
}
const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
if (!id || !instance) return;
const snapStates = useSnapshot(states);
const sKey = statusKey(id, instance);
const quotes = snapStates.statusQuotes[sKey];

View file

@ -103,14 +103,11 @@ function Notifications() {
setUIState('loading');
(async () => {
try {
const fetchNotificationsPromise = fetchNotifications(firstLoad);
const fetchFollowRequestsPromise = fetchFollowRequests();
const fetchAnnouncementsPromise = fetchAnnouncements();
const { done } = await fetchNotifications(firstLoad);
setShowMore(!done);
if (firstLoad) {
const requests = await fetchFollowRequestsPromise;
setFollowRequests(requests);
const announcements = await fetchAnnouncementsPromise;
announcements.sort((a, b) => {
// Sort by updatedAt first, then createdAt
@ -119,8 +116,13 @@ function Notifications() {
return bDate - aDate;
});
setAnnouncements(announcements);
const requests = await fetchFollowRequestsPromise;
setFollowRequests(requests);
}
const { done } = await fetchNotificationsPromise;
setShowMore(!done);
setUIState('default');
} catch (e) {
setUIState('error');

View file

@ -248,10 +248,17 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
totalDescendants.current = descendants?.length || 0;
const missingStatuses = new Set();
ancestors.forEach((status) => {
saveStatus(status, instance, {
skipThreading: true,
});
if (
status.inReplyToId &&
!ancestors.find((s) => s.id === status.inReplyToId)
) {
missingStatuses.add(status.inReplyToId);
}
});
const ancestorsIsThread = ancestors.every(
(s) => s.account.id === heroStatus.account.id,
@ -261,6 +268,15 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
saveStatus(status, instance, {
skipThreading: true,
});
if (
status.inReplyToId &&
!descendants.find((s) => s.id === status.inReplyToId) &&
status.inReplyToId !== heroStatus.id
) {
missingStatuses.add(status.inReplyToId);
}
if (status.inReplyToAccountId === status.account.id) {
// If replying to self, it's part of the thread, level 1
nestedDescendants.push(status);
@ -290,6 +306,9 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
});
console.log({ ancestors, descendants, nestedDescendants });
if (missingStatuses.size) {
console.error('Missing statuses', [...missingStatuses]);
}
function expandReplies(_replies) {
return _replies?.map((_r) => ({
@ -591,14 +610,17 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
const initialPageState = useRef(showMedia ? 'media+status' : 'status');
const handleMediaClick = useCallback((e, i, media, status) => {
e.preventDefault();
e.stopPropagation();
setSearchParams({
media: i + 1,
mediaStatusID: status.id,
});
}, []);
const handleMediaClick = useCallback(
(e, i, media, status) => {
e.preventDefault();
e.stopPropagation();
setSearchParams({
media: i + 1,
mediaStatusID: status.id,
});
},
[id],
);
return (
<div

View file

@ -1,7 +1,6 @@
import { match } from '@formatjs/intl-localematcher';
import translationTargetLanguages from '../data/lingva-target-languages';
import localeMatch from './locale-match';
import states from './states';
function getTranslateTargetLanguage(fromSettings = false) {
@ -11,7 +10,7 @@ function getTranslateTargetLanguage(fromSettings = false) {
return contentTranslationTargetLanguage;
}
}
return match(
return localeMatch(
[
new Intl.DateTimeFormat().resolvedOptions().locale,
...navigator.languages,

View file

@ -0,0 +1,12 @@
import { match } from '@formatjs/intl-localematcher';
function localeMatch(...args) {
// Wrap in try/catch because localeMatcher throws on invalid locales
try {
return match(...args);
} catch (e) {
return false;
}
}
export default localeMatch;

View file

@ -110,6 +110,7 @@ export function hideAllModals() {
}
export function statusKey(id, instance) {
if (!id) return;
return instance ? `${instance}/${id}` : id;
}