Add all the relationships
This commit is contained in:
parent
c16532d4c2
commit
8ce720f305
14 changed files with 350 additions and 242 deletions
|
@ -1578,6 +1578,13 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
.tag.danger {
|
.tag.danger {
|
||||||
background-color: var(--red-color);
|
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 */
|
/* MENU POPUP */
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
.account-block-acct {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.account-block:hover b {
|
.account-block:hover b {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -13,44 +17,54 @@
|
||||||
color: var(--bg-faded-color);
|
color: var(--bg-faded-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-block .short-desc {
|
.account-block .verified-field {
|
||||||
max-height: 1.2em; /* just in case clamping ain't working */
|
display: inline-flex;
|
||||||
}
|
align-items: baseline;
|
||||||
.account-block .short-desc,
|
gap: 2px;
|
||||||
.account-block .short-desc > * {
|
|
||||||
|
* {
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
line-clamp: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.account-block .short-desc > * + * {
|
|
||||||
display: none;
|
a {
|
||||||
}
|
|
||||||
.account-block .short-desc * {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
color: inherit;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
color: color-mix(
|
||||||
|
in lch,
|
||||||
|
var(--green-color) 20%,
|
||||||
|
var(--text-insignificant-color) 80%
|
||||||
|
) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-block .verified-field {
|
.icon {
|
||||||
color: var(--green-color);
|
color: var(--green-color);
|
||||||
display: inline-flex;
|
transform: translateY(1px);
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
}
|
||||||
.account-block .verified-field .icon {
|
|
||||||
}
|
.invisible {
|
||||||
.account-block .verified-field .invisible {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.ellipsis:after {
|
||||||
|
content: '…';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.account-block .account-block-stats {
|
.account-block .account-block-stats {
|
||||||
|
line-height: 1.25;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
}
|
display: flex;
|
||||||
.account-block .account-block-stats a {
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 4px;
|
||||||
|
|
||||||
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import './account-block.css';
|
||||||
// import { useNavigate } from 'react-router-dom';
|
// import { useNavigate } from 'react-router-dom';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
@ -22,6 +23,8 @@ function AccountBlock({
|
||||||
showStats = false,
|
showStats = false,
|
||||||
accountInstance,
|
accountInstance,
|
||||||
hideDisplayName = false,
|
hideDisplayName = false,
|
||||||
|
relationship = {},
|
||||||
|
excludeRelationshipAttrs = [],
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
|
@ -53,6 +56,7 @@ function AccountBlock({
|
||||||
fields,
|
fields,
|
||||||
note,
|
note,
|
||||||
group,
|
group,
|
||||||
|
followersCount,
|
||||||
} = account;
|
} = account;
|
||||||
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
|
let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct];
|
||||||
if (accountInstance) {
|
if (accountInstance) {
|
||||||
|
@ -61,6 +65,17 @@ function AccountBlock({
|
||||||
|
|
||||||
const verifiedField = fields?.find((f) => !!f.verifiedAt && !!f.value);
|
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 (
|
return (
|
||||||
<a
|
<a
|
||||||
class="account-block"
|
class="account-block"
|
||||||
|
@ -97,9 +112,8 @@ function AccountBlock({
|
||||||
) : (
|
) : (
|
||||||
<b>{username}</b>
|
<b>{username}</b>
|
||||||
)}
|
)}
|
||||||
<br />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}{' '}
|
||||||
<span class="account-block-acct">
|
<span class="account-block-acct">
|
||||||
@{acct1}
|
@{acct1}
|
||||||
<wbr />
|
<wbr />
|
||||||
|
@ -124,28 +138,44 @@ function AccountBlock({
|
||||||
)}
|
)}
|
||||||
{showStats && (
|
{showStats && (
|
||||||
<div class="account-block-stats">
|
<div class="account-block-stats">
|
||||||
<div
|
|
||||||
class="short-desc"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: enhanceContent(note, { emojis }),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{bot && (
|
{bot && (
|
||||||
<>
|
<>
|
||||||
<span class="tag">
|
<span class="tag collapsed">
|
||||||
<Icon icon="bot" /> Automated
|
<Icon icon="bot" /> Automated
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!!group && (
|
{!!group && (
|
||||||
<>
|
<>
|
||||||
<span class="tag">
|
<span class="tag collapsed">
|
||||||
<Icon icon="group" /> Group
|
<Icon icon="group" /> Group
|
||||||
</span>
|
</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 && (
|
{!!verifiedField && (
|
||||||
<span class="verified-field ib">
|
<span class="verified-field">
|
||||||
<Icon icon="check-circle" size="s" />{' '}
|
<Icon icon="check-circle" size="s" />{' '}
|
||||||
<span
|
<span
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|
|
@ -177,6 +177,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-container .account-block .account-block-acct {
|
.account-container .account-block .account-block-acct {
|
||||||
|
display: block;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -604,6 +604,8 @@ function AccountInfo({
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
heading: 'Followers',
|
heading: 'Followers',
|
||||||
fetchAccounts: fetchFollowers,
|
fetchAccounts: fetchFollowers,
|
||||||
|
instance,
|
||||||
|
excludeRelationshipAttrs: ['followedBy'],
|
||||||
};
|
};
|
||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
|
@ -637,6 +639,8 @@ function AccountInfo({
|
||||||
states.showGenericAccounts = {
|
states.showGenericAccounts = {
|
||||||
heading: 'Following',
|
heading: 'Following',
|
||||||
fetchAccounts: fetchFollowing,
|
fetchAccounts: fetchFollowing,
|
||||||
|
instance,
|
||||||
|
excludeRelationshipAttrs: ['following'],
|
||||||
};
|
};
|
||||||
}, 0);
|
}, 0);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#generic-accounts-container {
|
#generic-accounts-container {
|
||||||
.accounts-list {
|
.accounts-list {
|
||||||
|
--list-gap: 16px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
@ -7,29 +8,46 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
column-gap: 1.5em;
|
column-gap: 1.5em;
|
||||||
row-gap: 16px;
|
row-gap: var(--list-gap);
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-basis: 16em;
|
flex-basis: 16em;
|
||||||
align-items: center;
|
/* align-items: center; */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
gap: 8px;
|
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 {
|
.account-block-acct {
|
||||||
font-size: 80%;
|
font-size: 0.9em;
|
||||||
color: var(--text-insignificant-color);
|
color: var(--text-insignificant-color);
|
||||||
display: block;
|
/* display: block; */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactions-block {
|
.reactions-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: center;
|
/* align-self: center; */
|
||||||
|
|
||||||
.favourite-icon {
|
.favourite-icon {
|
||||||
color: var(--favourite-color);
|
color: var(--favourite-color);
|
||||||
|
@ -38,5 +56,21 @@
|
||||||
.reblog-icon {
|
.reblog-icon {
|
||||||
color: var(--reblog-color);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import { fetchRelationships } from '../utils/relationships';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import useLocationChange from '../utils/useLocationChange';
|
import useLocationChange from '../utils/useLocationChange';
|
||||||
|
|
||||||
|
@ -11,8 +13,15 @@ import AccountBlock from './account-block';
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Loader from './loader';
|
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 snapStates = useSnapshot(states);
|
||||||
|
``;
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
@ -31,6 +40,20 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
showReactions,
|
showReactions,
|
||||||
} = snapStates.showGenericAccounts;
|
} = 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) => {
|
const loadAccounts = (firstLoad) => {
|
||||||
if (!fetchAccounts) return;
|
if (!fetchAccounts) return;
|
||||||
if (firstLoad) setAccounts([]);
|
if (firstLoad) setAccounts([]);
|
||||||
|
@ -40,11 +63,41 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
const { done, value } = await fetchAccounts(firstLoad);
|
const { done, value } = await fetchAccounts(firstLoad);
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
if (firstLoad) {
|
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 {
|
} else {
|
||||||
setAccounts((prev) => [...prev, ...value]);
|
theAccount._types.push(...account._types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAccounts(accounts);
|
||||||
|
} else {
|
||||||
|
// 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);
|
setShowMore(!done);
|
||||||
|
|
||||||
|
loadRelationships(value);
|
||||||
} else {
|
} else {
|
||||||
setShowMore(false);
|
setShowMore(false);
|
||||||
}
|
}
|
||||||
|
@ -60,6 +113,7 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (staticAccounts?.length > 0) {
|
if (staticAccounts?.length > 0) {
|
||||||
setAccounts(staticAccounts);
|
setAccounts(staticAccounts);
|
||||||
|
loadRelationships(staticAccounts);
|
||||||
} else {
|
} else {
|
||||||
loadAccounts(true);
|
loadAccounts(true);
|
||||||
firstLoad.current = false;
|
firstLoad.current = false;
|
||||||
|
@ -87,8 +141,11 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
{accounts.length > 0 ? (
|
{accounts.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<ul class="accounts-list">
|
<ul class="accounts-list">
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => {
|
||||||
<li key={account.id + (account._types || '')}>
|
const relationship = relationshipsMap[account.id];
|
||||||
|
const key = `${account.id}-${account._types?.length || ''}`;
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
{showReactions && account._types?.length > 0 && (
|
{showReactions && account._types?.length > 0 && (
|
||||||
<div class="reactions-block">
|
<div class="reactions-block">
|
||||||
{account._types.map((type) => (
|
{account._types.map((type) => (
|
||||||
|
@ -104,9 +161,17 @@ export default function GenericAccounts({ onClose = () => {} }) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<AccountBlock account={account} />
|
<div class="account-relationships">
|
||||||
|
<AccountBlock
|
||||||
|
account={account}
|
||||||
|
showStats
|
||||||
|
relationship={relationship}
|
||||||
|
excludeRelationshipAttrs={excludeRelationshipAttrs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
{uiState === 'default' ? (
|
{uiState === 'default' ? (
|
||||||
showMore ? (
|
showMore ? (
|
||||||
|
|
|
@ -176,6 +176,10 @@ export default function Modals() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GenericAccounts
|
<GenericAccounts
|
||||||
|
instance={snapStates.showGenericAccounts.instance}
|
||||||
|
excludeRelationshipAttrs={
|
||||||
|
snapStates.showGenericAccounts.excludeRelationshipAttrs
|
||||||
|
}
|
||||||
onClose={() => (states.showGenericAccounts = false)}
|
onClose={() => (states.showGenericAccounts = false)}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -233,6 +233,7 @@ function NavMenu(props) {
|
||||||
id: 'mute',
|
id: 'mute',
|
||||||
heading: 'Muted users',
|
heading: 'Muted users',
|
||||||
fetchAccounts: fetchMutes,
|
fetchAccounts: fetchMutes,
|
||||||
|
excludeRelationshipAttrs: ['muting'],
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -244,6 +245,7 @@ function NavMenu(props) {
|
||||||
id: 'block',
|
id: 'block',
|
||||||
heading: 'Blocked users',
|
heading: 'Blocked users',
|
||||||
fetchAccounts: fetchBlocks,
|
fetchAccounts: fetchBlocks,
|
||||||
|
excludeRelationshipAttrs: ['blocking'],
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -158,6 +158,7 @@ function Notification({
|
||||||
heading: genericAccountsHeading,
|
heading: genericAccountsHeading,
|
||||||
accounts: _accounts,
|
accounts: _accounts,
|
||||||
showReactions: type === 'favourite+reblog',
|
showReactions: type === 'favourite+reblog',
|
||||||
|
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,8 @@ const isIOS =
|
||||||
window.ontouchstart !== undefined &&
|
window.ontouchstart !== undefined &&
|
||||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||||
|
|
||||||
|
const REACTIONS_LIMIT = 80;
|
||||||
|
|
||||||
function Status({
|
function Status({
|
||||||
statusID,
|
statusID,
|
||||||
status,
|
status,
|
||||||
|
@ -380,7 +382,6 @@ function Status({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
const [showReactions, setShowReactions] = useState(false);
|
|
||||||
|
|
||||||
const spoilerContentRef = useTruncated();
|
const spoilerContentRef = useTruncated();
|
||||||
const contentRef = useTruncated();
|
const contentRef = useTruncated();
|
||||||
|
@ -560,6 +561,55 @@ function Status({
|
||||||
(l) => language === l || localeMatch([language], [l]),
|
(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 menuInstanceRef = useRef();
|
||||||
const StatusMenuItems = (
|
const StatusMenuItems = (
|
||||||
<>
|
<>
|
||||||
|
@ -620,7 +670,16 @@ function Status({
|
||||||
)}
|
)}
|
||||||
{(!isSizeLarge || !!editedAt) && <MenuDivider />}
|
{(!isSizeLarge || !!editedAt) && <MenuDivider />}
|
||||||
{isSizeLarge && (
|
{isSizeLarge && (
|
||||||
<MenuItem onClick={() => setShowReactions(true)}>
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
states.showGenericAccounts = {
|
||||||
|
heading: 'Boosted/Liked by…',
|
||||||
|
fetchAccounts: fetchBoostedLikedByAccounts,
|
||||||
|
instance,
|
||||||
|
showReactions: true,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon icon="react" />
|
<Icon icon="react" />
|
||||||
<span>
|
<span>
|
||||||
Boosted/Liked by<span class="more-insignificant">…</span>
|
Boosted/Liked by<span class="more-insignificant">…</span>
|
||||||
|
@ -1759,22 +1818,6 @@ function Status({
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
{showReactions && (
|
|
||||||
<Modal
|
|
||||||
class="light"
|
|
||||||
onClick={(e) => {
|
|
||||||
if (e.target === e.currentTarget) {
|
|
||||||
setShowReactions(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReactionsModal
|
|
||||||
statusID={id}
|
|
||||||
instance={instance}
|
|
||||||
onClose={() => setShowReactions(false)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</article>
|
</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…
|
|
||||||
</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({
|
function StatusButton({
|
||||||
checked,
|
checked,
|
||||||
count,
|
count,
|
||||||
|
|
|
@ -24,8 +24,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
/* align-items: center; */
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.account-block {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.link-list.hashtag-list {
|
ul.link-list.hashtag-list {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import NavMenu from '../components/nav-menu';
|
||||||
import SearchForm from '../components/search-form';
|
import SearchForm from '../components/search-form';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { fetchRelationships } from '../utils/relationships';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -72,6 +73,18 @@ function Search(props) {
|
||||||
hashtags: setHashtagResults,
|
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) {
|
function loadResults(firstLoad) {
|
||||||
if (!firstLoad && !authenticated) {
|
if (!firstLoad && !authenticated) {
|
||||||
// Search results pagination is only available to authenticated users
|
// Search results pagination is only available to authenticated users
|
||||||
|
@ -119,6 +132,8 @@ function Search(props) {
|
||||||
offsetRef.current = 0;
|
offsetRef.current = 0;
|
||||||
setShowMore(false);
|
setShowMore(false);
|
||||||
}
|
}
|
||||||
|
loadRelationships(results.accounts);
|
||||||
|
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -216,6 +231,7 @@ function Search(props) {
|
||||||
account={account}
|
account={account}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
showStats
|
showStats
|
||||||
|
relationship={relationshipsMap[account.id]}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
37
src/utils/relationships.js
Normal file
37
src/utils/relationships.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue