Merge pull request #173 from cheeaun/main

Update from main
This commit is contained in:
Chee Aun 2023-06-30 23:25:52 +08:00 committed by GitHub
commit 375da8d173
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 243 additions and 39 deletions

View file

@ -1358,6 +1358,9 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
.tag.collapsed { .tag.collapsed {
margin: 0; margin: 0;
} }
.tag.danger {
background-color: var(--red-color);
}
/* MENU POPUP */ /* MENU POPUP */

View file

@ -12,6 +12,7 @@ import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
import states, { hideAllModals } from '../utils/states'; import states, { hideAllModals } from '../utils/states';
import store from '../utils/store'; import store from '../utils/store';
import { updateAccount } from '../utils/store-utils';
import AccountBlock from './account-block'; import AccountBlock from './account-block';
import Avatar from './avatar'; import Avatar from './avatar';
@ -483,6 +484,12 @@ function RelatedActions({ info, instance, authenticated }) {
} }
}, [info, authenticated]); }, [info, authenticated]);
useEffect(() => {
if (info && isSelf) {
updateAccount(info);
}
}, [info, isSelf]);
const loading = relationshipUIState === 'loading'; const loading = relationshipUIState === 'loading';
const menuInstanceRef = useRef(null); const menuInstanceRef = useRef(null);
@ -524,18 +531,22 @@ function RelatedActions({ info, instance, authenticated }) {
</div> </div>
</div> </div>
<p class="actions"> <p class="actions">
{followedBy ? ( <span>
<span class="tag">Following you</span> {followedBy ? (
) : !!lastStatusAt ? ( <span class="tag">Following you</span>
<small class="insignificant"> ) : !!lastStatusAt ? (
Last post:{' '} <small class="insignificant">
{niceDateTime(lastStatusAt, { Last post:{' '}
hideTime: true, {niceDateTime(lastStatusAt, {
})} hideTime: true,
</small> })}
) : ( </small>
<span /> ) : (
)}{' '} <span />
)}
{muting && <span class="tag danger">Muted</span>}
{blocking && <span class="tag danger">Blocked</span>}
</span>{' '}
<span class="buttons"> <span class="buttons">
<Menu <Menu
instanceRef={menuInstanceRef} instanceRef={menuInstanceRef}

View file

@ -12,6 +12,8 @@ import Icon from './icon';
import Link from './link'; import Link from './link';
import { formatDuration } from './status'; import { formatDuration } from './status';
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // https://stackoverflow.com/a/23522755
/* /*
Media type Media type
=== ===
@ -117,6 +119,20 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
if (isImage) { if (isImage) {
// Note: type: unknown might not have width/height // Note: type: unknown might not have width/height
quickPinchZoomProps.containerProps.style.display = 'inherit'; quickPinchZoomProps.containerProps.style.display = 'inherit';
useLayoutEffect(() => {
if (!isSafari) return;
if (!showOriginal) return;
(async () => {
try {
await fetch(mediaURL, { mode: 'no-cors' });
mediaRef.current.src = mediaURL;
} catch (e) {
// Ignore
}
})();
}, [mediaURL]);
return ( return (
<Parent <Parent
ref={parentRef} ref={parentRef}
@ -170,6 +186,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
}} }}
onLoad={(e) => { onLoad={(e) => {
e.target.closest('.media-image').style.backgroundImage = ''; e.target.closest('.media-image').style.backgroundImage = '';
e.target.dataset.loaded = true;
}} }}
onError={(e) => { onError={(e) => {
const { src } = e.target; const { src } = e.target;

View file

@ -1,7 +1,14 @@
import './search.css'; import './search.css';
import { forwardRef } from 'preact/compat'; 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 { useParams, useSearchParams } from 'react-router-dom';
import AccountBlock from '../components/account-block'; import AccountBlock from '../components/account-block';
@ -13,6 +20,9 @@ import Status from '../components/status';
import { api } from '../utils/api'; import { api } from '../utils/api';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const SHORT_LIMIT = 5;
const LIMIT = 40;
function Search(props) { function Search(props) {
const params = useParams(); const params = useParams();
const { masto, instance, authenticated } = api({ const { masto, instance, authenticated } = api({
@ -40,35 +50,78 @@ function Search(props) {
`/search`, `/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 [statusResults, setStatusResults] = useState([]);
const [accountResults, setAccountResults] = useState([]); const [accountResults, setAccountResults] = useState([]);
const [hashtagResults, setHashtagResults] = 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 && !firstLoad) {
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(() => { useEffect(() => {
// searchFieldRef.current?.focus?.(); // searchFieldRef.current?.focus?.();
// searchFormRef.current?.focus?.(); // searchFormRef.current?.focus?.();
if (q) { if (q) {
// searchFieldRef.current.value = q; // searchFieldRef.current.value = q;
searchFormRef.current?.setValue?.(q); searchFormRef.current?.setValue?.(q);
loadResults(true);
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');
})();
} }
}, [q, type, instance]); }, [q, type, instance]);
return ( return (
<div id="search-page" class="deck-container"> <div id="search-page" class="deck-container" ref={scrollableRef}>
<div class="timeline-deck deck"> <div class="timeline-deck deck">
<header> <header>
<div class="header-grid"> <div class="header-grid">
@ -110,7 +163,7 @@ function Search(props) {
))} ))}
</div> </div>
)} )}
{!!q && uiState !== 'loading' ? ( {!!q ? (
<> <>
{(!type || type === 'accounts') && ( {(!type || type === 'accounts') && (
<> <>
@ -121,7 +174,7 @@ function Search(props) {
<> <>
<ul class="timeline flat accounts-list"> <ul class="timeline flat accounts-list">
{accountResults.map((account) => ( {accountResults.map((account) => (
<li> <li key={account.id}>
<AccountBlock <AccountBlock
account={account} account={account}
instance={instance} instance={instance}
@ -141,7 +194,14 @@ function Search(props) {
)} )}
</> </>
) : ( ) : (
<p class="ui-state">No accounts found.</p> !type &&
(uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
<p class="ui-state">No accounts found.</p>
))
)} )}
</> </>
)} )}
@ -154,7 +214,7 @@ function Search(props) {
<> <>
<ul class="link-list hashtag-list"> <ul class="link-list hashtag-list">
{hashtagResults.map((hashtag) => ( {hashtagResults.map((hashtag) => (
<li> <li key={hashtag.name}>
<Link <Link
to={ to={
instance instance
@ -180,7 +240,14 @@ function Search(props) {
)} )}
</> </>
) : ( ) : (
<p class="ui-state">No hashtags found.</p> !type &&
(uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
<p class="ui-state">No hashtags found.</p>
))
)} )}
</> </>
)} )}
@ -193,7 +260,7 @@ function Search(props) {
<> <>
<ul class="timeline"> <ul class="timeline">
{statusResults.map((status) => ( {statusResults.map((status) => (
<li> <li key={status.id}>
<Link <Link
class="status-link" class="status-link"
to={ to={
@ -219,10 +286,50 @@ function Search(props) {
)} )}
</> </>
) : ( ) : (
<p class="ui-state">No posts found.</p> !type &&
(uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : (
<p class="ui-state">No posts found.</p>
))
)} )}
</> </>
)} )}
{!!type &&
(uiState === 'default' ? (
showMore ? (
<InView
onChange={(inView) => {
if (inView) {
loadResults();
}
}}
>
<button
type="button"
class="plain block"
onClick={() => loadResults()}
style={{ marginBlockEnd: '6em' }}
>
Show more&hellip;
</button>
</InView>
) : (
<p class="ui-state insignificant">The end.</p>
)
) : (
!!(
hashtagResults.length ||
accountResults.length ||
statusResults.length
) && (
<p class="ui-state">
<Loader abrupt />
</p>
)
))}
</> </>
) : uiState === 'loading' ? ( ) : uiState === 'loading' ? (
<p class="ui-state"> <p class="ui-state">

View file

@ -119,7 +119,10 @@ function StatusPage(params) {
instance={instance} instance={instance}
index={mediaIndex - 1} index={mediaIndex - 1}
onClose={() => { onClose={() => {
if (snapStates.prevLocation) { if (
!window.matchMedia('(min-width: calc(40em + 350px))').matches &&
snapStates.prevLocation
) {
history.back(); history.back();
} else { } else {
if (showMediaOnly) { if (showMediaOnly) {
@ -638,6 +641,14 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
} ${initialPageState.current === 'status' ? 'slide-in' : ''} ${ } ${initialPageState.current === 'status' ? 'slide-in' : ''} ${
viewMode ? `deck-view-${viewMode}` : '' 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;
}
}}
> >
<header <header
class={`${heroInView ? 'inview' : ''} ${ class={`${heroInView ? 'inview' : ''} ${

View file

@ -1,9 +1,10 @@
import { Menu, MenuItem } from '@szhsin/react-menu'; 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 { useNavigate, useParams } from 'react-router-dom';
import { useSnapshot } from 'valtio'; import { useSnapshot } from 'valtio';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Link from '../components/link';
import Menu2 from '../components/menu2'; import Menu2 from '../components/menu2';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
@ -25,12 +26,23 @@ function Trending(props) {
const navigate = useNavigate(); const navigate = useNavigate();
const latestItem = useRef(); const latestItem = useRef();
const [hashtags, setHashtags] = useState([]);
const trendIterator = useRef(); const trendIterator = useRef();
async function fetchTrend(firstLoad) { async function fetchTrend(firstLoad) {
if (firstLoad || !trendIterator.current) { if (firstLoad || !trendIterator.current) {
trendIterator.current = masto.v1.trends.listStatuses({ trendIterator.current = masto.v1.trends.listStatuses({
limit: LIMIT, 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(); const results = await trendIterator.current.next();
let { value } = results; let { value } = results;
@ -71,6 +83,28 @@ function Trending(props) {
} }
} }
const TimelineStart = useMemo(() => {
if (!hashtags.length) return null;
return (
<div class="filter-bar">
<Icon icon="chart" class="insignificant" size="l" />
{hashtags.map((tag, i) => {
const { name, history } = tag;
const total = history.reduce((acc, cur) => acc + +cur.uses, 0);
return (
<Link to={`/${instance}/t/${name}`}>
<span>
<span class="more-insignificant">#</span>
{name}
</span>
<span class="filter-count">{total.toLocaleString()}</span>
</Link>
);
})}
</div>
);
}, [hashtags]);
return ( return (
<Timeline <Timeline
key={instance} key={instance}
@ -92,6 +126,7 @@ function Trending(props) {
headerStart={<></>} headerStart={<></>}
boostsCarousel={snapStates.settings.boostsCarousel} boostsCarousel={snapStates.settings.boostsCarousel}
allowFilters allowFilters
timelineStart={TimelineStart}
headerEnd={ headerEnd={
<Menu2 <Menu2
portal portal

View file

@ -3,6 +3,7 @@ export default function isMastodonLinkMaybe(url) {
return ( return (
/^\/.*\/\d+$/i.test(pathname) || /^\/.*\/\d+$/i.test(pathname) ||
/^\/@[^/]+\/statuses\/\w+$/i.test(pathname) || // GoToSocial /^\/@[^/]+\/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
); );
} }

View file

@ -34,6 +34,25 @@ export function saveAccount(account) {
store.session.set('currentAccount', account.info.id); 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; let currentInstance = null;
export function getCurrentInstance() { export function getCurrentInstance() {
if (currentInstance) return currentInstance; if (currentInstance) return currentInstance;