From fe713edee92cb3d63f54015be7f2510a0c777dbb Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 27 Jun 2023 12:19:55 +0800 Subject: [PATCH 01/12] Unfurl Pleroma links --- src/utils/isMastodonLinkMaybe.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/isMastodonLinkMaybe.jsx b/src/utils/isMastodonLinkMaybe.jsx index 809d256b..bc3a31ef 100644 --- a/src/utils/isMastodonLinkMaybe.jsx +++ b/src/utils/isMastodonLinkMaybe.jsx @@ -3,6 +3,7 @@ export default function isMastodonLinkMaybe(url) { return ( /^\/.*\/\d+$/i.test(pathname) || /^\/@[^/]+\/statuses\/\w+$/i.test(pathname) || // GoToSocial - /^\/notes\/[a-z0-9]+$/i.test(pathname) // Misskey, Calckey + /^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey + /^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) // Pleroma ); } From 730fba7ad9acd293306bd4af0c3bb1013ca7796a Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Tue, 27 Jun 2023 19:39:33 +0800 Subject: [PATCH 02/12] Show trending hashtags Very minimal UI for now --- src/pages/trending.jsx | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/pages/trending.jsx b/src/pages/trending.jsx index 5e1d5c2e..92c03b21 100644 --- a/src/pages/trending.jsx +++ b/src/pages/trending.jsx @@ -1,9 +1,10 @@ import { Menu, MenuItem } from '@szhsin/react-menu'; -import { useRef } from 'preact/hooks'; +import { useMemo, useRef, useState } from 'preact/hooks'; import { useNavigate, useParams } from 'react-router-dom'; import { useSnapshot } from 'valtio'; import Icon from '../components/icon'; +import Link from '../components/link'; import Menu2 from '../components/menu2'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; @@ -25,12 +26,23 @@ function Trending(props) { const navigate = useNavigate(); const latestItem = useRef(); + const [hashtags, setHashtags] = useState([]); const trendIterator = useRef(); async function fetchTrend(firstLoad) { if (firstLoad || !trendIterator.current) { trendIterator.current = masto.v1.trends.listStatuses({ limit: LIMIT, }); + + // Get hashtags + try { + const iterator = masto.v1.trends.listTags(); + const { value: tags } = await iterator.next(); + console.log(tags); + setHashtags(tags); + } catch (e) { + console.error(e); + } } const results = await trendIterator.current.next(); let { value } = results; @@ -71,6 +83,28 @@ function Trending(props) { } } + const TimelineStart = useMemo(() => { + if (!hashtags.length) return null; + return ( +
+ + {hashtags.map((tag, i) => { + const { name, history } = tag; + const total = history.reduce((acc, cur) => acc + +cur.uses, 0); + return ( + + + # + {name} + + {total.toLocaleString()} + + ); + })} +
+ ); + }, [hashtags]); + return ( } boostsCarousel={snapStates.settings.boostsCarousel} allowFilters + timelineStart={TimelineStart} headerEnd={ Date: Tue, 27 Jun 2023 22:02:10 +0800 Subject: [PATCH 03/12] Show muted/blocked tags on account info --- src/components/account-info.jsx | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index 91fd45af..da2c399b 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -524,18 +524,22 @@ function RelatedActions({ info, instance, authenticated }) {

- {followedBy ? ( - Following you - ) : !!lastStatusAt ? ( - - Last post:{' '} - {niceDateTime(lastStatusAt, { - hideTime: true, - })} - - ) : ( - - )}{' '} + + {followedBy ? ( + Following you + ) : !!lastStatusAt ? ( + + Last post:{' '} + {niceDateTime(lastStatusAt, { + hideTime: true, + })} + + ) : ( + + )} + {muting && Muted} + {blocking && Blocked} + {' '}

Date: Wed, 28 Jun 2023 17:38:01 +0800 Subject: [PATCH 04/12] Update account info if there's name or avatar change --- src/components/account-info.jsx | 7 +++++++ src/utils/store-utils.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index da2c399b..af30aaca 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -12,6 +12,7 @@ import shortenNumber from '../utils/shorten-number'; import showToast from '../utils/show-toast'; import states, { hideAllModals } from '../utils/states'; import store from '../utils/store'; +import { updateAccount } from '../utils/store-utils'; import AccountBlock from './account-block'; import Avatar from './avatar'; @@ -483,6 +484,12 @@ function RelatedActions({ info, instance, authenticated }) { } }, [info, authenticated]); + useEffect(() => { + if (info && isSelf) { + updateAccount(info); + } + }, [info, isSelf]); + const loading = relationshipUIState === 'loading'; const menuInstanceRef = useRef(null); diff --git a/src/utils/store-utils.js b/src/utils/store-utils.js index 059727e1..f624f9cd 100644 --- a/src/utils/store-utils.js +++ b/src/utils/store-utils.js @@ -34,6 +34,25 @@ export function saveAccount(account) { store.session.set('currentAccount', account.info.id); } +export function updateAccount(accountInfo) { + // Only update if displayName or avatar or avatar_static is different + const accounts = store.local.getJSON('accounts') || []; + const acc = accounts.find((a) => a.info.id === accountInfo.id); + if (acc) { + if ( + acc.info.displayName !== accountInfo.displayName || + acc.info.avatar !== accountInfo.avatar || + acc.info.avatar_static !== accountInfo.avatar_static + ) { + acc.info = { + ...acc.info, + ...accountInfo, + }; + store.local.setJSON('accounts', accounts); + } + } +} + let currentInstance = null; export function getCurrentInstance() { if (currentInstance) return currentInstance; From 1a835c32abe04e70f1c3bb347ded33005b893de9 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 28 Jun 2023 23:35:22 +0800 Subject: [PATCH 05/12] Attempt to fix Safari's cut-off images bug --- src/components/media.jsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/media.jsx b/src/components/media.jsx index b49bc519..3e4d324e 100644 --- a/src/components/media.jsx +++ b/src/components/media.jsx @@ -12,6 +12,8 @@ import Icon from './icon'; import Link from './link'; import { formatDuration } from './status'; +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755 + /* Media type === @@ -117,6 +119,19 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { if (isImage) { // Note: type: unknown might not have width/height quickPinchZoomProps.containerProps.style.display = 'inherit'; + + useLayoutEffect(() => { + if (!isSafari) return; + (async () => { + try { + await fetch(mediaURL, { cache: 'reload', mode: 'no-cors' }); + mediaRef.current.src = mediaURL; + } catch (e) { + // Ignore + } + })(); + }, [mediaURL]); + return ( Date: Wed, 28 Jun 2023 23:36:37 +0800 Subject: [PATCH 06/12] Pagination for search results This code is really hacky, may need to revisit one day --- src/pages/search.jsx | 134 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 116 insertions(+), 18 deletions(-) diff --git a/src/pages/search.jsx b/src/pages/search.jsx index 5b451e3b..24b52faa 100644 --- a/src/pages/search.jsx +++ b/src/pages/search.jsx @@ -1,7 +1,14 @@ import './search.css'; import { forwardRef } from 'preact/compat'; -import { useEffect, useImperativeHandle, useRef, useState } from 'preact/hooks'; +import { + useEffect, + useImperativeHandle, + useLayoutEffect, + useRef, + useState, +} from 'preact/hooks'; +import { InView } from 'react-intersection-observer'; import { useParams, useSearchParams } from 'react-router-dom'; import AccountBlock from '../components/account-block'; @@ -13,6 +20,9 @@ import Status from '../components/status'; import { api } from '../utils/api'; import useTitle from '../utils/useTitle'; +const SHORT_LIMIT = 5; +const LIMIT = 40; + function Search(props) { const params = useParams(); const { masto, instance, authenticated } = api({ @@ -40,35 +50,78 @@ function Search(props) { `/search`, ); + const [showMore, setShowMore] = useState(false); + const offsetRef = useRef(0); + useEffect(() => { + offsetRef.current = 0; + }, [type]); + + const scrollableRef = useRef(); + useLayoutEffect(() => { + scrollableRef.current?.scrollTo?.(0, 0); + }, [q, type]); + const [statusResults, setStatusResults] = useState([]); const [accountResults, setAccountResults] = useState([]); const [hashtagResults, setHashtagResults] = useState([]); + + function loadResults(firstLoad) { + setUiState('loading'); + if (firstLoad && !type) { + setStatusResults(statusResults.slice(0, SHORT_LIMIT)); + setAccountResults(accountResults.slice(0, SHORT_LIMIT)); + setHashtagResults(hashtagResults.slice(0, SHORT_LIMIT)); + } + + (async () => { + const params = { + q, + resolve: authenticated, + limit: SHORT_LIMIT, + }; + if (type) { + params.limit = LIMIT; + params.type = type; + params.offset = offsetRef.current; + } + try { + const results = await masto.v2.search(params); + console.log(results); + if (type) { + if (type === 'statuses') { + setStatusResults((prev) => [...prev, ...results.statuses]); + } else if (type === 'accounts') { + setAccountResults((prev) => [...prev, ...results.accounts]); + } else if (type === 'hashtags') { + setHashtagResults((prev) => [...prev, ...results.hashtags]); + } + offsetRef.current = offsetRef.current + LIMIT; + setShowMore(!!results[type]?.length); + } else { + setStatusResults(results.statuses); + setAccountResults(results.accounts); + setHashtagResults(results.hashtags); + } + setUiState('default'); + } catch (err) { + console.error(err); + setUiState('error'); + } + })(); + } + useEffect(() => { // searchFieldRef.current?.focus?.(); // searchFormRef.current?.focus?.(); if (q) { // searchFieldRef.current.value = q; searchFormRef.current?.setValue?.(q); - - setUiState('loading'); - (async () => { - const results = await masto.v2.search({ - q, - limit: type ? 40 : 5, - resolve: authenticated, - type, - }); - console.log(results); - setStatusResults(results.statuses); - setAccountResults(results.accounts); - setHashtagResults(results.hashtags); - setUiState('default'); - })(); + loadResults(true); } }, [q, type, instance]); return ( -
+
@@ -110,7 +163,7 @@ function Search(props) { ))}
)} - {!!q && uiState !== 'loading' ? ( + {!!q ? ( <> {(!type || type === 'accounts') && ( <> @@ -140,6 +193,10 @@ function Search(props) {
)} + ) : uiState === 'loading' ? ( +

+ +

) : (

No accounts found.

)} @@ -179,6 +236,10 @@ function Search(props) {
)} + ) : uiState === 'loading' ? ( +

+ +

) : (

No hashtags found.

)} @@ -218,11 +279,48 @@ function Search(props) {
)} + ) : uiState === 'loading' ? ( +

+ +

) : (

No posts found.

)} )} + {!!type && + (uiState === 'default' ? ( + showMore ? ( + { + if (inView) { + loadResults(); + } + }} + > + + + ) : ( +

The end.

+ ) + ) : ( + !!( + hashtagResults.length || + accountResults.length || + statusResults.length + ) && ( +

+ +

+ ) + ))} ) : uiState === 'loading' ? (

From 61630d25e2eaf9dcab6a051a75eca35177f88ccf Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Wed, 28 Jun 2023 23:37:05 +0800 Subject: [PATCH 07/12] Forgot this danger tag style --- src/app.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app.css b/src/app.css index 0e2fd50a..2da3904d 100644 --- a/src/app.css +++ b/src/app.css @@ -1358,6 +1358,9 @@ body:has(.media-modal-container + .status-deck) .media-post-link { .tag.collapsed { margin: 0; } +.tag.danger { + background-color: var(--red-color); +} /* MENU POPUP */ From 950114b9f70120f5f1aa8bbd09b1f2c8d142b9d3 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Thu, 29 Jun 2023 00:27:15 +0800 Subject: [PATCH 08/12] Try without cache: reload It's probably not needed; image is possibly cached, just not rendered properly --- src/components/media.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/media.jsx b/src/components/media.jsx index 3e4d324e..1d552bf8 100644 --- a/src/components/media.jsx +++ b/src/components/media.jsx @@ -124,7 +124,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { if (!isSafari) return; (async () => { try { - await fetch(mediaURL, { cache: 'reload', mode: 'no-cors' }); + await fetch(mediaURL, { mode: 'no-cors' }); mediaRef.current.src = mediaURL; } catch (e) { // Ignore From 8efc7a226ed4f4d70979eced220ec14c4f69730b Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Thu, 29 Jun 2023 09:52:41 +0800 Subject: [PATCH 09/12] Fix regression: close media modal, not the status page Clicking close goes *back* from media=1 to media-only=1 --- src/pages/status.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/status.jsx b/src/pages/status.jsx index 6fe888a8..0a93036e 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -119,7 +119,10 @@ function StatusPage(params) { instance={instance} index={mediaIndex - 1} onClose={() => { - if (snapStates.prevLocation) { + if ( + !window.matchMedia('(min-width: calc(40em + 350px))').matches && + snapStates.prevLocation + ) { history.back(); } else { if (showMediaOnly) { From c609ba0194dcb71c63b9440b5575a0fc3b21199f Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Thu, 29 Jun 2023 10:08:31 +0800 Subject: [PATCH 10/12] Fix bounce effect bug when switching view modes --- src/pages/status.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pages/status.jsx b/src/pages/status.jsx index 0a93036e..4762d5ec 100644 --- a/src/pages/status.jsx +++ b/src/pages/status.jsx @@ -641,6 +641,14 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) { } ${initialPageState.current === 'status' ? 'slide-in' : ''} ${ viewMode ? `deck-view-${viewMode}` : '' }`} + onAnimationEnd={(e) => { + // Fix the bounce effect when switching viewMode + // `slide-in` animation kicks in when switching viewMode + if (initialPageState.current === 'status') { + // e.target.classList.remove('slide-in'); + initialPageState.current = null; + } + }} >

Date: Thu, 29 Jun 2023 18:55:17 +0800 Subject: [PATCH 11/12] Only run this when showing original --- src/components/media.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/media.jsx b/src/components/media.jsx index 1d552bf8..3462ecfe 100644 --- a/src/components/media.jsx +++ b/src/components/media.jsx @@ -122,6 +122,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { useLayoutEffect(() => { if (!isSafari) return; + if (!showOriginal) return; (async () => { try { await fetch(mediaURL, { mode: 'no-cors' }); @@ -185,6 +186,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { }} onLoad={(e) => { e.target.closest('.media-image').style.backgroundImage = ''; + e.target.dataset.loaded = true; }} onError={(e) => { const { src } = e.target; From d035d18aa08deceea61541031ca998e8eb0d200d Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 30 Jun 2023 09:48:52 +0800 Subject: [PATCH 12/12] Fix duplicated search results Also fix other stuff --- src/pages/search.jsx | 47 ++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/pages/search.jsx b/src/pages/search.jsx index 24b52faa..bb507f1a 100644 --- a/src/pages/search.jsx +++ b/src/pages/search.jsx @@ -87,7 +87,7 @@ function Search(props) { try { const results = await masto.v2.search(params); console.log(results); - if (type) { + if (type && !firstLoad) { if (type === 'statuses') { setStatusResults((prev) => [...prev, ...results.statuses]); } else if (type === 'accounts') { @@ -174,7 +174,7 @@ function Search(props) { <>
    {accountResults.map((account) => ( -
  • +
  • )} - ) : uiState === 'loading' ? ( -

    - -

    ) : ( -

    No accounts found.

    + !type && + (uiState === 'loading' ? ( +

    + +

    + ) : ( +

    No accounts found.

    + )) )} )} @@ -211,7 +214,7 @@ function Search(props) { <>