diff --git a/package-lock.json b/package-lock.json index 376051c03..37c34121e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@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-twitter": "~0.5.0", @@ -3886,19 +3885,6 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, - "node_modules/@uidotdev/usehooks": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz", - "integrity": "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, "node_modules/@vue/compiler-core": { "version": "3.2.45", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", diff --git a/package.json b/package.json index 21feda6f2..8c33cfd0f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "vite build", "preview": "vite preview", "fetch-instances": "env $(cat .env.local | grep -v \"#\" | xargs) node scripts/fetch-instances-list.js", - "sourcemap": "npx source-map-explorer dist/assets/*.js" + "sourcemap": "npx source-map-explorer dist/assets/*.js", + "bundle-visualizer": "npx vite-bundle-visualizer" }, "dependencies": { "@formatjs/intl-localematcher": "~0.5.4", @@ -17,7 +18,6 @@ "@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-twitter": "~0.5.0", diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index 1704e446f..5eff593f6 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -1159,8 +1159,8 @@ function RelatedActions({ setRelationshipUIState('default'); showToast( rel.showingReblogs - ? `Boosts from @${username} disabled.` - : `Boosts from @${username} enabled.`, + ? `Boosts from @${username} enabled.` + : `Boosts from @${username} disabled.`, ); } catch (e) { alert(e); diff --git a/src/components/menu2.jsx b/src/components/menu2.jsx index 6ca683207..3d83a72de 100644 --- a/src/components/menu2.jsx +++ b/src/components/menu2.jsx @@ -1,8 +1,8 @@ import { Menu } from '@szhsin/react-menu'; -import { useWindowSize } from '@uidotdev/usehooks'; import { useRef } from 'preact/hooks'; import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding'; +import useWindowSize from '../utils/useWindowSize'; // It's like Menu but with sensible defaults, bug fixes and improvements. function Menu2(props) { diff --git a/src/components/notification.jsx b/src/components/notification.jsx index 1308948b7..cb2180c7f 100644 --- a/src/components/notification.jsx +++ b/src/components/notification.jsx @@ -285,7 +285,7 @@ function Notification({ {type !== 'mention' && ( <>

- {!/poll|update/i.test(type) && ( + {!/poll|update|severed_relationships/i.test(type) && ( <> {_accounts?.length > 1 ? ( <> diff --git a/src/components/status.jsx b/src/components/status.jsx index daf4ba64f..327f3ae94 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -2456,6 +2456,22 @@ function MediaFirstContainer(props) { ); } +function getDomain(url) { + return punycode.toUnicode( + URL.parse(url) + .hostname.replace(/^www\./, '') + .replace(/\/$/, ''), + ); +} + +// "Post": Quote post + card link preview combo +// Assume all links from these domains are "posts" +// Mastodon links are "posts" too but they are converted to real quote posts and there's too many domains to check +// This is just "Progressive Enhancement" +function isCardPost(domain) { + return ['x.com', 'twitter.com', 'threads.net', 'bsky.app'].includes(domain); +} + function Card({ card, selfReferential, instance }) { const snapStates = useSnapshot(states); const { @@ -2534,11 +2550,7 @@ function Card({ card, selfReferential, instance }) { ); if (hasText && (image || (type === 'photo' && blurhash))) { - const domain = punycode.toUnicode( - URL.parse(url) - .hostname.replace(/^www\./, '') - .replace(/\/$/, ''), - ); + const domain = getDomain(url); let blurhashImage; const rgbAverageColor = image && blurhash ? getBlurHashAverageColor(blurhash) : null; @@ -2559,11 +2571,7 @@ function Card({ card, selfReferential, instance }) { blurhashImage = canvas.toDataURL(); } - // "Post": Quote post + card link preview combo - // Assume all links from these domains are "posts" - // Mastodon links are "posts" too but they are converted to real quote posts and there's too many domains to check - // This is just "Progressive Enhancement" - const isPost = ['x.com', 'twitter.com', 'threads.net'].includes(domain); + const isPost = isCardPost(domain); return ( diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index 7cd31d25f..d2ec714e2 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -140,6 +140,26 @@ function Hashtags({ media: mediaView, columnMode, ...props }) { const reachLimit = hashtags.length >= TOTAL_TAGS_LIMIT; + const [featuredUIState, setFeaturedUIState] = useState('default'); + const [featuredTags, setFeaturedTags] = useState([]); + const [isFeaturedTag, setIsFeaturedTag] = useState(false); + useEffect(() => { + if (!authenticated) return; + (async () => { + try { + const featuredTags = await masto.v1.featuredTags.list(); + setFeaturedTags(featuredTags); + setIsFeaturedTag( + featuredTags.some( + (tag) => tag.name.toLowerCase() === hashtag.toLowerCase(), + ), + ); + } catch (e) { + console.error(e); + } + })(); + }, []); + return ( )} + { + setFeaturedUIState('loading'); + if (isFeaturedTag) { + const featuredTagID = featuredTags.find( + (tag) => tag.name.toLowerCase() === hashtag.toLowerCase(), + ).id; + if (featuredTagID) { + masto.v1.featuredTags + .$select(featuredTagID) + .remove() + .then(() => { + setIsFeaturedTag(false); + showToast('Unfeatured on profile'); + setFeaturedTags( + featuredTags.filter( + (tag) => tag.id !== featuredTagID, + ), + ); + }) + .catch((e) => { + console.error(e); + }) + .finally(() => { + setFeaturedUIState('default'); + }); + } else { + showToast('Unable to unfeature on profile'); + } + } else { + masto.v1.featuredTags + .create({ + name: hashtag, + }) + .then((value) => { + setIsFeaturedTag(true); + showToast('Featured on profile'); + setFeaturedTags(featuredTags.concat(value)); + }) + .catch((e) => { + console.error(e); + }) + .finally(() => { + setFeaturedUIState('default'); + }); + } + }} + > + {isFeaturedTag ? ( + <> + + Featured on profile + + ) : ( + <> + + Feature on profile + + )} + )} @@ -366,7 +449,7 @@ function Hashtags({ media: mediaView, columnMode, ...props }) { } }} > - Add to Shorcuts + Add to Shortcuts { diff --git a/src/pages/trending.jsx b/src/pages/trending.jsx index 1b85de774..670e30d45 100644 --- a/src/pages/trending.jsx +++ b/src/pages/trending.jsx @@ -72,6 +72,8 @@ function Trending({ columnMode, ...props }) { // const navigate = useNavigate(); const latestItem = useRef(); + const sameCurrentInstance = instance === currentInstance; + const [hashtags, setHashtags] = useState([]); const [links, setLinks] = useState([]); const trendIterator = useRef(); @@ -137,7 +139,8 @@ function Trending({ columnMode, ...props }) { const [currentLink, setCurrentLink] = useState(null); const hasCurrentLink = !!currentLink; const currentLinkRef = useRef(); - const supportsTrendingLinkPosts = supports('@mastodon/trending-hashtags'); + const supportsTrendingLinkPosts = + sameCurrentInstance && supports('@mastodon/trending-hashtags'); useEffect(() => { if (currentLink && currentLinkRef.current) { diff --git a/src/utils/timeline-utils.jsx b/src/utils/timeline-utils.jsx index 20225f12c..af9027669 100644 --- a/src/utils/timeline-utils.jsx +++ b/src/utils/timeline-utils.jsx @@ -227,7 +227,6 @@ export function groupContext(items, instance) { const replyToStatus = await fetchStatus(inReplyToId, masto); saveStatus(replyToStatus, instance, { skipThreading: true, - skipUnfurling: true, }); states.statusReply[sKey] = { id: replyToStatus.id, @@ -253,7 +252,6 @@ export function groupContext(items, instance) { for (const replyToStatus of replyToStatuses) { saveStatus(replyToStatus, instance, { skipThreading: true, - skipUnfurling: true, }); const sKey = inReplyToIds.find( ({ inReplyToId }) => inReplyToId === replyToStatus.id, diff --git a/src/utils/useWindowSize.js b/src/utils/useWindowSize.js new file mode 100644 index 000000000..0907cee77 --- /dev/null +++ b/src/utils/useWindowSize.js @@ -0,0 +1,28 @@ +import { useLayoutEffect, useState } from 'preact/hooks'; + +export default function useWindowSize() { + const [size, setSize] = useState({ + width: null, + height: null, + }); + + useLayoutEffect(() => { + const handleResize = () => { + setSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + handleResize(); + window.addEventListener('resize', handleResize, { + passive: true, + }); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return size; +}