Add all the relationships

pull/373/head
Lim Chee Aun 2023-12-20 13:55:56 +08:00
rodzic c16532d4c2
commit 8ce720f305
14 zmienionych plików z 350 dodań i 242 usunięć

Wyświetl plik

@ -1578,6 +1578,13 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
.tag.danger {
background-color: var(--red-color);
}
.tag.minimal {
margin: 0;
color: var(--text-insignificant-color);
background-color: var(--bg-faded-color);
text-shadow: 0 1px var(--bg-color);
line-height: 1;
}
/* MENU POPUP */

Wyświetl plik

@ -4,6 +4,10 @@
gap: 8px;
color: var(--text-color);
text-decoration: none;
.account-block-acct {
display: inline-block;
}
}
.account-block:hover b {
text-decoration: underline;
@ -13,44 +17,54 @@
color: var(--bg-faded-color);
}
.account-block .short-desc {
max-height: 1.2em; /* just in case clamping ain't working */
}
.account-block .short-desc,
.account-block .short-desc > * {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.account-block .short-desc > * + * {
display: none;
}
.account-block .short-desc * {
margin: 0;
padding: 0;
color: inherit;
pointer-events: none;
}
.account-block .verified-field {
color: var(--green-color);
display: inline-flex;
align-items: center;
align-items: baseline;
gap: 2px;
}
.account-block .verified-field .icon {
}
.account-block .verified-field .invisible {
display: none;
* {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
}
a {
pointer-events: none;
color: color-mix(
in lch,
var(--green-color) 20%,
var(--text-insignificant-color) 80%
) !important;
}
.icon {
color: var(--green-color);
transform: translateY(1px);
}
.invisible {
display: none;
}
.ellipsis:after {
content: '…';
}
}
.account-block .account-block-stats {
line-height: 1.25;
margin-top: 2px;
font-size: 0.9em;
color: var(--text-insignificant-color);
}
.account-block .account-block-stats a {
color: inherit;
text-decoration: none;
display: flex;
flex-wrap: wrap;
align-items: center;
column-gap: 4px;
a {
color: inherit;
text-decoration: none;
}
}

Wyświetl plik

@ -3,6 +3,7 @@ import './account-block.css';
// import { useNavigate } from 'react-router-dom';
import enhanceContent from '../utils/enhance-content';
import niceDateTime from '../utils/nice-date-time';
import shortenNumber from '../utils/shorten-number';
import states from '../utils/states';
import Avatar from './avatar';
@ -22,6 +23,8 @@ function AccountBlock({
showStats = false,
accountInstance,
hideDisplayName = false,
relationship = {},
excludeRelationshipAttrs = [],
}) {
if (skeleton) {
return (
@ -53,6 +56,7 @@ function AccountBlock({
fields,
note,
group,
followersCount,
} = account;
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
if (accountInstance) {
@ -61,6 +65,17 @@ function AccountBlock({
const verifiedField = fields?.find((f) => !!f.verifiedAt && !!f.value);
const excludedRelationship = {};
for (const r in relationship) {
if (!excludeRelationshipAttrs.includes(r)) {
excludedRelationship[r] = relationship[r];
}
}
const hasRelationship =
excludedRelationship.following ||
excludedRelationship.followedBy ||
excludedRelationship.requested;
return (
<a
class="account-block"
@ -97,9 +112,8 @@ function AccountBlock({
) : (
<b>{username}</b>
)}
<br />
</>
)}
)}{' '}
<span class="account-block-acct">
@{acct1}
<wbr />
@ -124,28 +138,44 @@ function AccountBlock({
)}
{showStats && (
<div class="account-block-stats">
<div
class="short-desc"
dangerouslySetInnerHTML={{
__html: enhanceContent(note, { emojis }),
}}
/>
{bot && (
<>
<span class="tag">
<span class="tag collapsed">
<Icon icon="bot" /> Automated
</span>
</>
)}
{!!group && (
<>
<span class="tag">
<span class="tag collapsed">
<Icon icon="group" /> Group
</span>
</>
)}
{hasRelationship && (
<div key={relationship.id} class="shazam-container-horizontal">
<div class="shazam-container-inner">
{excludedRelationship.following &&
excludedRelationship.followedBy ? (
<span class="tag minimal">Mutual</span>
) : excludedRelationship.requested ? (
<span class="tag minimal">Requested</span>
) : excludedRelationship.following ? (
<span class="tag minimal">Following</span>
) : excludedRelationship.followedBy ? (
<span class="tag minimal">Follows you</span>
) : null}
</div>
</div>
)}
{!!followersCount && (
<span class="ib">
{shortenNumber(followersCount)}{' '}
{followersCount === 1 ? 'follower' : 'followers'}
</span>
)}
{!!verifiedField && (
<span class="verified-field ib">
<span class="verified-field">
<Icon icon="check-circle" size="s" />{' '}
<span
dangerouslySetInnerHTML={{

Wyświetl plik

@ -177,6 +177,7 @@
}
.account-container .account-block .account-block-acct {
display: block;
opacity: 0.7;
}

Wyświetl plik

@ -604,6 +604,8 @@ function AccountInfo({
states.showGenericAccounts = {
heading: 'Followers',
fetchAccounts: fetchFollowers,
instance,
excludeRelationshipAttrs: ['followedBy'],
};
}, 0);
}}
@ -637,6 +639,8 @@ function AccountInfo({
states.showGenericAccounts = {
heading: 'Following',
fetchAccounts: fetchFollowing,
instance,
excludeRelationshipAttrs: ['following'],
};
}, 0);
}}

Wyświetl plik

@ -1,5 +1,6 @@
#generic-accounts-container {
.accounts-list {
--list-gap: 16px;
list-style: none;
margin: 0;
padding: 8px 0;
@ -7,29 +8,46 @@
flex-wrap: wrap;
flex-direction: row;
column-gap: 1.5em;
row-gap: 16px;
row-gap: var(--list-gap);
li {
display: flex;
flex-grow: 1;
flex-basis: 16em;
align-items: center;
/* align-items: center; */
margin: 0;
padding: 0;
gap: 8px;
position: relative;
&:before {
content: '';
display: block;
border-top: var(--hairline-width) solid var(--divider-color);
position: absolute;
bottom: calc(-1 * var(--list-gap) / 2);
left: 40px;
right: 0;
}
&:has(.reactions-block):before {
/* avatar + reactions + gap */
left: calc(40px + 16px + 8px);
}
}
.account-block-acct {
font-size: 80%;
font-size: 0.9em;
color: var(--text-insignificant-color);
display: block;
/* display: block; */
}
}
.reactions-block {
display: flex;
flex-direction: column;
align-self: center;
/* align-self: center; */
.favourite-icon {
color: var(--favourite-color);
@ -38,5 +56,21 @@
.reblog-icon {
color: var(--reblog-color);
}
> .icon:only-child {
margin-top: 8px; /* half of icon dimension */
}
}
.account-relationships {
flex-grow: 1;
.tag {
animation: appear 0.3s ease-out;
}
}
.account-block {
align-items: flex-start;
}
}

Wyświetl plik

@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import { InView } from 'react-intersection-observer';
import { useSnapshot } from 'valtio';
import { api } from '../utils/api';
import { fetchRelationships } from '../utils/relationships';
import states from '../utils/states';
import useLocationChange from '../utils/useLocationChange';
@ -11,8 +13,15 @@ import AccountBlock from './account-block';
import Icon from './icon';
import Loader from './loader';
export default function GenericAccounts({ onClose = () => {} }) {
export default function GenericAccounts({
instance,
excludeRelationshipAttrs = [],
onClose = () => {},
}) {
const { masto, instance: currentInstance } = api();
const isCurrentInstance = instance ? instance === currentInstance : true;
const snapStates = useSnapshot(states);
``;
const [uiState, setUIState] = useState('default');
const [accounts, setAccounts] = useState([]);
const [showMore, setShowMore] = useState(false);
@ -31,6 +40,20 @@ export default function GenericAccounts({ onClose = () => {} }) {
showReactions,
} = snapStates.showGenericAccounts;
const [relationshipsMap, setRelationshipsMap] = useState({});
const loadRelationships = async (accounts) => {
if (!accounts?.length) return;
if (!isCurrentInstance) return;
const relationships = await fetchRelationships(accounts, relationshipsMap);
if (relationships) {
setRelationshipsMap({
...relationshipsMap,
...relationships,
});
}
};
const loadAccounts = (firstLoad) => {
if (!fetchAccounts) return;
if (firstLoad) setAccounts([]);
@ -40,11 +63,41 @@ export default function GenericAccounts({ onClose = () => {} }) {
const { done, value } = await fetchAccounts(firstLoad);
if (Array.isArray(value)) {
if (firstLoad) {
setAccounts(value);
const accounts = [];
for (let i = 0; i < value.length; i++) {
const account = value[i];
const theAccount = accounts.find(
(a, j) => a.id === account.id && i !== j,
);
if (!theAccount) {
accounts.push({
_types: [],
...account,
});
} else {
theAccount._types.push(...account._types);
}
}
setAccounts(accounts);
} else {
setAccounts((prev) => [...prev, ...value]);
// setAccounts((prev) => [...prev, ...value]);
// Merge accounts by id and _types
setAccounts((prev) => {
const newAccounts = prev;
for (const account of value) {
const theAccount = newAccounts.find((a) => a.id === account.id);
if (!theAccount) {
newAccounts.push(account);
} else {
theAccount._types.push(...account._types);
}
}
return newAccounts;
});
}
setShowMore(!done);
loadRelationships(value);
} else {
setShowMore(false);
}
@ -60,6 +113,7 @@ export default function GenericAccounts({ onClose = () => {} }) {
useEffect(() => {
if (staticAccounts?.length > 0) {
setAccounts(staticAccounts);
loadRelationships(staticAccounts);
} else {
loadAccounts(true);
firstLoad.current = false;
@ -87,26 +141,37 @@ export default function GenericAccounts({ onClose = () => {} }) {
{accounts.length > 0 ? (
<>
<ul class="accounts-list">
{accounts.map((account) => (
<li key={account.id + (account._types || '')}>
{showReactions && account._types?.length > 0 && (
<div class="reactions-block">
{account._types.map((type) => (
<Icon
icon={
{
reblog: 'rocket',
favourite: 'heart',
}[type]
}
class={`${type}-icon`}
/>
))}
{accounts.map((account) => {
const relationship = relationshipsMap[account.id];
const key = `${account.id}-${account._types?.length || ''}`;
return (
<li key={key}>
{showReactions && account._types?.length > 0 && (
<div class="reactions-block">
{account._types.map((type) => (
<Icon
icon={
{
reblog: 'rocket',
favourite: 'heart',
}[type]
}
class={`${type}-icon`}
/>
))}
</div>
)}
<div class="account-relationships">
<AccountBlock
account={account}
showStats
relationship={relationship}
excludeRelationshipAttrs={excludeRelationshipAttrs}
/>
</div>
)}
<AccountBlock account={account} />
</li>
))}
</li>
);
})}
</ul>
{uiState === 'default' ? (
showMore ? (

Wyświetl plik

@ -176,6 +176,10 @@ export default function Modals() {
}}
>
<GenericAccounts
instance={snapStates.showGenericAccounts.instance}
excludeRelationshipAttrs={
snapStates.showGenericAccounts.excludeRelationshipAttrs
}
onClose={() => (states.showGenericAccounts = false)}
/>
</Modal>

Wyświetl plik

@ -233,6 +233,7 @@ function NavMenu(props) {
id: 'mute',
heading: 'Muted users',
fetchAccounts: fetchMutes,
excludeRelationshipAttrs: ['muting'],
};
}}
>
@ -244,6 +245,7 @@ function NavMenu(props) {
id: 'block',
heading: 'Blocked users',
fetchAccounts: fetchBlocks,
excludeRelationshipAttrs: ['blocking'],
};
}}
>

Wyświetl plik

@ -158,6 +158,7 @@ function Notification({
heading: genericAccountsHeading,
accounts: _accounts,
showReactions: type === 'favourite+reblog',
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
};
};

Wyświetl plik

@ -88,6 +88,8 @@ const isIOS =
window.ontouchstart !== undefined &&
/iPad|iPhone|iPod/.test(navigator.userAgent);
const REACTIONS_LIMIT = 80;
function Status({
statusID,
status,
@ -380,7 +382,6 @@ function Status({
]);
const [showEdited, setShowEdited] = useState(false);
const [showReactions, setShowReactions] = useState(false);
const spoilerContentRef = useTruncated();
const contentRef = useTruncated();
@ -560,6 +561,55 @@ function Status({
(l) => language === l || localeMatch([language], [l]),
);
const reblogIterator = useRef();
const favouriteIterator = useRef();
async function fetchBoostedLikedByAccounts(firstLoad) {
if (firstLoad) {
reblogIterator.current = masto.v1.statuses
.$select(statusID)
.rebloggedBy.list({
limit: REACTIONS_LIMIT,
});
favouriteIterator.current = masto.v1.statuses
.$select(statusID)
.favouritedBy.list({
limit: REACTIONS_LIMIT,
});
}
const [{ value: reblogResults }, { value: favouriteResults }] =
await Promise.allSettled([
reblogIterator.current.next(),
favouriteIterator.current.next(),
]);
if (reblogResults.value?.length || favouriteResults.value?.length) {
const accounts = [];
if (reblogResults.value?.length) {
accounts.push(
...reblogResults.value.map((a) => {
a._types = ['reblog'];
return a;
}),
);
}
if (favouriteResults.value?.length) {
accounts.push(
...favouriteResults.value.map((a) => {
a._types = ['favourite'];
return a;
}),
);
}
return {
value: accounts,
done: reblogResults.done && favouriteResults.done,
};
}
return {
value: [],
done: true,
};
}
const menuInstanceRef = useRef();
const StatusMenuItems = (
<>
@ -620,7 +670,16 @@ function Status({
)}
{(!isSizeLarge || !!editedAt) && <MenuDivider />}
{isSizeLarge && (
<MenuItem onClick={() => setShowReactions(true)}>
<MenuItem
onClick={() => {
states.showGenericAccounts = {
heading: 'Boosted/Liked by…',
fetchAccounts: fetchBoostedLikedByAccounts,
instance,
showReactions: true,
};
}}
>
<Icon icon="react" />
<span>
Boosted/Liked by<span class="more-insignificant"></span>
@ -1759,22 +1818,6 @@ function Status({
/>
</Modal>
)}
{showReactions && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowReactions(false);
}
}}
>
<ReactionsModal
statusID={id}
instance={instance}
onClose={() => setShowReactions(false)}
/>
</Modal>
)}
</article>
);
}
@ -2046,160 +2089,6 @@ function EditedAtModal({
);
}
const REACTIONS_LIMIT = 80;
function ReactionsModal({ statusID, instance, onClose }) {
const { masto } = api({ instance });
const [uiState, setUIState] = useState('default');
const [accounts, setAccounts] = useState([]);
const [showMore, setShowMore] = useState(false);
const reblogIterator = useRef();
const favouriteIterator = useRef();
async function fetchAccounts(firstLoad) {
setShowMore(false);
setUIState('loading');
(async () => {
try {
if (firstLoad) {
reblogIterator.current = masto.v1.statuses
.$select(statusID)
.rebloggedBy.list({
limit: REACTIONS_LIMIT,
});
favouriteIterator.current = masto.v1.statuses
.$select(statusID)
.favouritedBy.list({
limit: REACTIONS_LIMIT,
});
}
const [{ value: reblogResults }, { value: favouriteResults }] =
await Promise.allSettled([
reblogIterator.current.next(),
favouriteIterator.current.next(),
]);
if (reblogResults.value?.length || favouriteResults.value?.length) {
if (reblogResults.value?.length) {
for (const account of reblogResults.value) {
const theAccount = accounts.find((a) => a.id === account.id);
if (!theAccount) {
accounts.push({
...account,
_types: ['reblog'],
});
} else {
theAccount._types.push('reblog');
}
}
}
if (favouriteResults.value?.length) {
for (const account of favouriteResults.value) {
const theAccount = accounts.find((a) => a.id === account.id);
if (!theAccount) {
accounts.push({
...account,
_types: ['favourite'],
});
} else {
theAccount._types.push('favourite');
}
}
}
setAccounts(accounts);
setShowMore(!reblogResults.done || !favouriteResults.done);
} else {
setShowMore(false);
}
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
}
useEffect(() => {
fetchAccounts(true);
}, []);
return (
<div id="reactions-container" class="sheet">
{!!onClose && (
<button type="button" class="sheet-close" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header>
<h2>Boosted/Liked by</h2>
</header>
<main>
{accounts.length > 0 ? (
<>
<ul class="reactions-list">
{accounts.map((account) => {
const { _types } = account;
return (
<li key={account.id + _types}>
<div class="reactions-block">
{_types.map((type) => (
<Icon
icon={
{
reblog: 'rocket',
favourite: 'heart',
}[type]
}
class={`${type}-icon`}
/>
))}
</div>
<AccountBlock account={account} instance={instance} />
</li>
);
})}
</ul>
{uiState === 'default' ? (
showMore ? (
<InView
onChange={(inView) => {
if (inView) {
fetchAccounts();
}
}}
>
<button
type="button"
class="plain block"
onClick={() => fetchAccounts()}
>
Show more&hellip;
</button>
</InView>
) : (
<p class="ui-state insignificant">The end.</p>
)
) : (
uiState === 'loading' && (
<p class="ui-state">
<Loader abrupt />
</p>
)
)}
</>
) : uiState === 'loading' ? (
<p class="ui-state">
<Loader abrupt />
</p>
) : uiState === 'error' ? (
<p class="ui-state">Unable to load accounts</p>
) : (
<p class="ui-state insignificant">No one yet.</p>
)}
</main>
</div>
);
}
function StatusButton({
checked,
count,

Wyświetl plik

@ -24,8 +24,12 @@
display: flex;
padding: 8px 16px;
gap: 8px;
align-items: center;
/* align-items: center; */
flex-grow: 1;
.account-block {
align-items: flex-start;
}
}
ul.link-list.hashtag-list {

Wyświetl plik

@ -14,6 +14,7 @@ import NavMenu from '../components/nav-menu';
import SearchForm from '../components/search-form';
import Status from '../components/status';
import { api } from '../utils/api';
import { fetchRelationships } from '../utils/relationships';
import shortenNumber from '../utils/shorten-number';
import useTitle from '../utils/useTitle';
@ -72,6 +73,18 @@ function Search(props) {
hashtags: setHashtagResults,
};
const [relationshipsMap, setRelationshipsMap] = useState({});
const loadRelationships = async (accounts) => {
if (!accounts?.length) return;
const relationships = await fetchRelationships(accounts, relationshipsMap);
if (relationships) {
setRelationshipsMap({
...relationshipsMap,
...relationships,
});
}
};
function loadResults(firstLoad) {
if (!firstLoad && !authenticated) {
// Search results pagination is only available to authenticated users
@ -119,6 +132,8 @@ function Search(props) {
offsetRef.current = 0;
setShowMore(false);
}
loadRelationships(results.accounts);
setUIState('default');
} catch (err) {
console.error(err);
@ -216,6 +231,7 @@ function Search(props) {
account={account}
instance={instance}
showStats
relationship={relationshipsMap[account.id]}
/>
</li>
))}

Wyświetl plik

@ -0,0 +1,37 @@
import { api } from './api';
import store from './store';
export async function fetchRelationships(accounts, relationshipsMap = {}) {
if (!accounts?.length) return;
const { masto } = api();
const currentAccount = store.session.get('currentAccount');
const uniqueAccountIds = accounts.reduce((acc, a) => {
// 1. Ignore duplicate accounts
// 2. Ignore accounts that are already inside relationshipsMap
// 3. Ignore currently logged in account
if (
!acc.includes(a.id) &&
!relationshipsMap[a.id] &&
a.id !== currentAccount
) {
acc.push(a.id);
}
return acc;
}, []);
try {
const relationships = await masto.v1.accounts.relationships.fetch({
id: uniqueAccountIds,
});
const newRelationshipsMap = relationships.reduce((acc, r) => {
acc[r.id] = r;
return acc;
}, {});
return newRelationshipsMap;
} catch (e) {
console.error(e);
// It's okay to fail
return null;
}
}