this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add all the relationships

+350 -242
+7
src/app.css
··· 1578 1578 .tag.danger { 1579 1579 background-color: var(--red-color); 1580 1580 } 1581 + .tag.minimal { 1582 + margin: 0; 1583 + color: var(--text-insignificant-color); 1584 + background-color: var(--bg-faded-color); 1585 + text-shadow: 0 1px var(--bg-color); 1586 + line-height: 1; 1587 + } 1581 1588 1582 1589 /* MENU POPUP */ 1583 1590
+45 -31
src/components/account-block.css
··· 4 4 gap: 8px; 5 5 color: var(--text-color); 6 6 text-decoration: none; 7 + 8 + .account-block-acct { 9 + display: inline-block; 10 + } 7 11 } 8 12 .account-block:hover b { 9 13 text-decoration: underline; ··· 13 17 color: var(--bg-faded-color); 14 18 } 15 19 16 - .account-block .short-desc { 17 - max-height: 1.2em; /* just in case clamping ain't working */ 18 - } 19 - .account-block .short-desc, 20 - .account-block .short-desc > * { 21 - display: -webkit-box; 22 - -webkit-line-clamp: 1; 23 - -webkit-box-orient: vertical; 24 - overflow: hidden; 25 - } 26 - .account-block .short-desc > * + * { 27 - display: none; 28 - } 29 - .account-block .short-desc * { 30 - margin: 0; 31 - padding: 0; 32 - color: inherit; 33 - pointer-events: none; 34 - } 35 - 36 20 .account-block .verified-field { 37 - color: var(--green-color); 38 21 display: inline-flex; 39 - align-items: center; 22 + align-items: baseline; 40 23 gap: 2px; 41 - } 42 - .account-block .verified-field .icon { 43 - } 44 - .account-block .verified-field .invisible { 45 - display: none; 24 + 25 + * { 26 + -webkit-box-orient: vertical; 27 + display: -webkit-box; 28 + -webkit-line-clamp: 1; 29 + line-clamp: 1; 30 + text-overflow: ellipsis; 31 + overflow: hidden; 32 + } 33 + 34 + a { 35 + pointer-events: none; 36 + color: color-mix( 37 + in lch, 38 + var(--green-color) 20%, 39 + var(--text-insignificant-color) 80% 40 + ) !important; 41 + } 42 + 43 + .icon { 44 + color: var(--green-color); 45 + transform: translateY(1px); 46 + } 47 + 48 + .invisible { 49 + display: none; 50 + } 51 + .ellipsis:after { 52 + content: '…'; 53 + } 46 54 } 47 55 48 56 .account-block .account-block-stats { 57 + line-height: 1.25; 49 58 margin-top: 2px; 50 59 font-size: 0.9em; 51 60 color: var(--text-insignificant-color); 52 - } 53 - .account-block .account-block-stats a { 54 - color: inherit; 55 - text-decoration: none; 61 + display: flex; 62 + flex-wrap: wrap; 63 + align-items: center; 64 + column-gap: 4px; 65 + 66 + a { 67 + color: inherit; 68 + text-decoration: none; 69 + } 56 70 }
+41 -11
src/components/account-block.jsx
··· 3 3 // import { useNavigate } from 'react-router-dom'; 4 4 import enhanceContent from '../utils/enhance-content'; 5 5 import niceDateTime from '../utils/nice-date-time'; 6 + import shortenNumber from '../utils/shorten-number'; 6 7 import states from '../utils/states'; 7 8 8 9 import Avatar from './avatar'; ··· 22 23 showStats = false, 23 24 accountInstance, 24 25 hideDisplayName = false, 26 + relationship = {}, 27 + excludeRelationshipAttrs = [], 25 28 }) { 26 29 if (skeleton) { 27 30 return ( ··· 53 56 fields, 54 57 note, 55 58 group, 59 + followersCount, 56 60 } = account; 57 61 let [_, acct1, acct2] = acct.match(/([^@]+)(@.+)/i) || [, acct]; 58 62 if (accountInstance) { ··· 61 65 62 66 const verifiedField = fields?.find((f) => !!f.verifiedAt && !!f.value); 63 67 68 + const excludedRelationship = {}; 69 + for (const r in relationship) { 70 + if (!excludeRelationshipAttrs.includes(r)) { 71 + excludedRelationship[r] = relationship[r]; 72 + } 73 + } 74 + const hasRelationship = 75 + excludedRelationship.following || 76 + excludedRelationship.followedBy || 77 + excludedRelationship.requested; 78 + 64 79 return ( 65 80 <a 66 81 class="account-block" ··· 97 112 ) : ( 98 113 <b>{username}</b> 99 114 )} 100 - <br /> 101 115 </> 102 - )} 116 + )}{' '} 103 117 <span class="account-block-acct"> 104 118 @{acct1} 105 119 <wbr /> ··· 124 138 )} 125 139 {showStats && ( 126 140 <div class="account-block-stats"> 127 - <div 128 - class="short-desc" 129 - dangerouslySetInnerHTML={{ 130 - __html: enhanceContent(note, { emojis }), 131 - }} 132 - /> 133 141 {bot && ( 134 142 <> 135 - <span class="tag"> 143 + <span class="tag collapsed"> 136 144 <Icon icon="bot" /> Automated 137 145 </span> 138 146 </> 139 147 )} 140 148 {!!group && ( 141 149 <> 142 - <span class="tag"> 150 + <span class="tag collapsed"> 143 151 <Icon icon="group" /> Group 144 152 </span> 145 153 </> 146 154 )} 155 + {hasRelationship && ( 156 + <div key={relationship.id} class="shazam-container-horizontal"> 157 + <div class="shazam-container-inner"> 158 + {excludedRelationship.following && 159 + excludedRelationship.followedBy ? ( 160 + <span class="tag minimal">Mutual</span> 161 + ) : excludedRelationship.requested ? ( 162 + <span class="tag minimal">Requested</span> 163 + ) : excludedRelationship.following ? ( 164 + <span class="tag minimal">Following</span> 165 + ) : excludedRelationship.followedBy ? ( 166 + <span class="tag minimal">Follows you</span> 167 + ) : null} 168 + </div> 169 + </div> 170 + )} 171 + {!!followersCount && ( 172 + <span class="ib"> 173 + {shortenNumber(followersCount)}{' '} 174 + {followersCount === 1 ? 'follower' : 'followers'} 175 + </span> 176 + )} 147 177 {!!verifiedField && ( 148 - <span class="verified-field ib"> 178 + <span class="verified-field"> 149 179 <Icon icon="check-circle" size="s" />{' '} 150 180 <span 151 181 dangerouslySetInnerHTML={{
+1
src/components/account-info.css
··· 177 177 } 178 178 179 179 .account-container .account-block .account-block-acct { 180 + display: block; 180 181 opacity: 0.7; 181 182 } 182 183
+4
src/components/account-info.jsx
··· 604 604 states.showGenericAccounts = { 605 605 heading: 'Followers', 606 606 fetchAccounts: fetchFollowers, 607 + instance, 608 + excludeRelationshipAttrs: ['followedBy'], 607 609 }; 608 610 }, 0); 609 611 }} ··· 637 639 states.showGenericAccounts = { 638 640 heading: 'Following', 639 641 fetchAccounts: fetchFollowing, 642 + instance, 643 + excludeRelationshipAttrs: ['following'], 640 644 }; 641 645 }, 0); 642 646 }}
+39 -5
src/components/generic-accounts.css
··· 1 1 #generic-accounts-container { 2 2 .accounts-list { 3 + --list-gap: 16px; 3 4 list-style: none; 4 5 margin: 0; 5 6 padding: 8px 0; ··· 7 8 flex-wrap: wrap; 8 9 flex-direction: row; 9 10 column-gap: 1.5em; 10 - row-gap: 16px; 11 + row-gap: var(--list-gap); 11 12 12 13 li { 13 14 display: flex; 14 15 flex-grow: 1; 15 16 flex-basis: 16em; 16 - align-items: center; 17 + /* align-items: center; */ 17 18 margin: 0; 18 19 padding: 0; 19 20 gap: 8px; 21 + 22 + position: relative; 23 + 24 + &:before { 25 + content: ''; 26 + display: block; 27 + border-top: var(--hairline-width) solid var(--divider-color); 28 + position: absolute; 29 + bottom: calc(-1 * var(--list-gap) / 2); 30 + left: 40px; 31 + right: 0; 32 + } 33 + 34 + &:has(.reactions-block):before { 35 + /* avatar + reactions + gap */ 36 + left: calc(40px + 16px + 8px); 37 + } 20 38 } 21 39 22 40 .account-block-acct { 23 - font-size: 80%; 41 + font-size: 0.9em; 24 42 color: var(--text-insignificant-color); 25 - display: block; 43 + /* display: block; */ 26 44 } 27 45 } 28 46 29 47 .reactions-block { 30 48 display: flex; 31 49 flex-direction: column; 32 - align-self: center; 50 + /* align-self: center; */ 33 51 34 52 .favourite-icon { 35 53 color: var(--favourite-color); ··· 38 56 .reblog-icon { 39 57 color: var(--reblog-color); 40 58 } 59 + 60 + > .icon:only-child { 61 + margin-top: 8px; /* half of icon dimension */ 62 + } 63 + } 64 + 65 + .account-relationships { 66 + flex-grow: 1; 67 + 68 + .tag { 69 + animation: appear 0.3s ease-out; 70 + } 71 + } 72 + 73 + .account-block { 74 + align-items: flex-start; 41 75 } 42 76 }
+87 -22
src/components/generic-accounts.jsx
··· 4 4 import { InView } from 'react-intersection-observer'; 5 5 import { useSnapshot } from 'valtio'; 6 6 7 + import { api } from '../utils/api'; 8 + import { fetchRelationships } from '../utils/relationships'; 7 9 import states from '../utils/states'; 8 10 import useLocationChange from '../utils/useLocationChange'; 9 11 ··· 11 13 import Icon from './icon'; 12 14 import Loader from './loader'; 13 15 14 - export default function GenericAccounts({ onClose = () => {} }) { 16 + export default function GenericAccounts({ 17 + instance, 18 + excludeRelationshipAttrs = [], 19 + onClose = () => {}, 20 + }) { 21 + const { masto, instance: currentInstance } = api(); 22 + const isCurrentInstance = instance ? instance === currentInstance : true; 15 23 const snapStates = useSnapshot(states); 24 + ``; 16 25 const [uiState, setUIState] = useState('default'); 17 26 const [accounts, setAccounts] = useState([]); 18 27 const [showMore, setShowMore] = useState(false); ··· 31 40 showReactions, 32 41 } = snapStates.showGenericAccounts; 33 42 43 + const [relationshipsMap, setRelationshipsMap] = useState({}); 44 + 45 + const loadRelationships = async (accounts) => { 46 + if (!accounts?.length) return; 47 + if (!isCurrentInstance) return; 48 + const relationships = await fetchRelationships(accounts, relationshipsMap); 49 + if (relationships) { 50 + setRelationshipsMap({ 51 + ...relationshipsMap, 52 + ...relationships, 53 + }); 54 + } 55 + }; 56 + 34 57 const loadAccounts = (firstLoad) => { 35 58 if (!fetchAccounts) return; 36 59 if (firstLoad) setAccounts([]); ··· 40 63 const { done, value } = await fetchAccounts(firstLoad); 41 64 if (Array.isArray(value)) { 42 65 if (firstLoad) { 43 - setAccounts(value); 66 + const accounts = []; 67 + for (let i = 0; i < value.length; i++) { 68 + const account = value[i]; 69 + const theAccount = accounts.find( 70 + (a, j) => a.id === account.id && i !== j, 71 + ); 72 + if (!theAccount) { 73 + accounts.push({ 74 + _types: [], 75 + ...account, 76 + }); 77 + } else { 78 + theAccount._types.push(...account._types); 79 + } 80 + } 81 + setAccounts(accounts); 44 82 } else { 45 - setAccounts((prev) => [...prev, ...value]); 83 + // setAccounts((prev) => [...prev, ...value]); 84 + // Merge accounts by id and _types 85 + setAccounts((prev) => { 86 + const newAccounts = prev; 87 + for (const account of value) { 88 + const theAccount = newAccounts.find((a) => a.id === account.id); 89 + if (!theAccount) { 90 + newAccounts.push(account); 91 + } else { 92 + theAccount._types.push(...account._types); 93 + } 94 + } 95 + return newAccounts; 96 + }); 46 97 } 47 98 setShowMore(!done); 99 + 100 + loadRelationships(value); 48 101 } else { 49 102 setShowMore(false); 50 103 } ··· 60 113 useEffect(() => { 61 114 if (staticAccounts?.length > 0) { 62 115 setAccounts(staticAccounts); 116 + loadRelationships(staticAccounts); 63 117 } else { 64 118 loadAccounts(true); 65 119 firstLoad.current = false; ··· 87 141 {accounts.length > 0 ? ( 88 142 <> 89 143 <ul class="accounts-list"> 90 - {accounts.map((account) => ( 91 - <li key={account.id + (account._types || '')}> 92 - {showReactions && account._types?.length > 0 && ( 93 - <div class="reactions-block"> 94 - {account._types.map((type) => ( 95 - <Icon 96 - icon={ 97 - { 98 - reblog: 'rocket', 99 - favourite: 'heart', 100 - }[type] 101 - } 102 - class={`${type}-icon`} 103 - /> 104 - ))} 144 + {accounts.map((account) => { 145 + const relationship = relationshipsMap[account.id]; 146 + const key = `${account.id}-${account._types?.length || ''}`; 147 + return ( 148 + <li key={key}> 149 + {showReactions && account._types?.length > 0 && ( 150 + <div class="reactions-block"> 151 + {account._types.map((type) => ( 152 + <Icon 153 + icon={ 154 + { 155 + reblog: 'rocket', 156 + favourite: 'heart', 157 + }[type] 158 + } 159 + class={`${type}-icon`} 160 + /> 161 + ))} 162 + </div> 163 + )} 164 + <div class="account-relationships"> 165 + <AccountBlock 166 + account={account} 167 + showStats 168 + relationship={relationship} 169 + excludeRelationshipAttrs={excludeRelationshipAttrs} 170 + /> 105 171 </div> 106 - )} 107 - <AccountBlock account={account} /> 108 - </li> 109 - ))} 172 + </li> 173 + ); 174 + })} 110 175 </ul> 111 176 {uiState === 'default' ? ( 112 177 showMore ? (
+4
src/components/modals.jsx
··· 176 176 }} 177 177 > 178 178 <GenericAccounts 179 + instance={snapStates.showGenericAccounts.instance} 180 + excludeRelationshipAttrs={ 181 + snapStates.showGenericAccounts.excludeRelationshipAttrs 182 + } 179 183 onClose={() => (states.showGenericAccounts = false)} 180 184 /> 181 185 </Modal>
+2
src/components/nav-menu.jsx
··· 233 233 id: 'mute', 234 234 heading: 'Muted users', 235 235 fetchAccounts: fetchMutes, 236 + excludeRelationshipAttrs: ['muting'], 236 237 }; 237 238 }} 238 239 > ··· 244 245 id: 'block', 245 246 heading: 'Blocked users', 246 247 fetchAccounts: fetchBlocks, 248 + excludeRelationshipAttrs: ['blocking'], 247 249 }; 248 250 }} 249 251 >
+1
src/components/notification.jsx
··· 158 158 heading: genericAccountsHeading, 159 159 accounts: _accounts, 160 160 showReactions: type === 'favourite+reblog', 161 + excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [], 161 162 }; 162 163 }; 163 164
+61 -172
src/components/status.jsx
··· 88 88 window.ontouchstart !== undefined && 89 89 /iPad|iPhone|iPod/.test(navigator.userAgent); 90 90 91 + const REACTIONS_LIMIT = 80; 92 + 91 93 function Status({ 92 94 statusID, 93 95 status, ··· 380 382 ]); 381 383 382 384 const [showEdited, setShowEdited] = useState(false); 383 - const [showReactions, setShowReactions] = useState(false); 384 385 385 386 const spoilerContentRef = useTruncated(); 386 387 const contentRef = useTruncated(); ··· 560 561 (l) => language === l || localeMatch([language], [l]), 561 562 ); 562 563 564 + const reblogIterator = useRef(); 565 + const favouriteIterator = useRef(); 566 + async function fetchBoostedLikedByAccounts(firstLoad) { 567 + if (firstLoad) { 568 + reblogIterator.current = masto.v1.statuses 569 + .$select(statusID) 570 + .rebloggedBy.list({ 571 + limit: REACTIONS_LIMIT, 572 + }); 573 + favouriteIterator.current = masto.v1.statuses 574 + .$select(statusID) 575 + .favouritedBy.list({ 576 + limit: REACTIONS_LIMIT, 577 + }); 578 + } 579 + const [{ value: reblogResults }, { value: favouriteResults }] = 580 + await Promise.allSettled([ 581 + reblogIterator.current.next(), 582 + favouriteIterator.current.next(), 583 + ]); 584 + if (reblogResults.value?.length || favouriteResults.value?.length) { 585 + const accounts = []; 586 + if (reblogResults.value?.length) { 587 + accounts.push( 588 + ...reblogResults.value.map((a) => { 589 + a._types = ['reblog']; 590 + return a; 591 + }), 592 + ); 593 + } 594 + if (favouriteResults.value?.length) { 595 + accounts.push( 596 + ...favouriteResults.value.map((a) => { 597 + a._types = ['favourite']; 598 + return a; 599 + }), 600 + ); 601 + } 602 + return { 603 + value: accounts, 604 + done: reblogResults.done && favouriteResults.done, 605 + }; 606 + } 607 + return { 608 + value: [], 609 + done: true, 610 + }; 611 + } 612 + 563 613 const menuInstanceRef = useRef(); 564 614 const StatusMenuItems = ( 565 615 <> ··· 620 670 )} 621 671 {(!isSizeLarge || !!editedAt) && <MenuDivider />} 622 672 {isSizeLarge && ( 623 - <MenuItem onClick={() => setShowReactions(true)}> 673 + <MenuItem 674 + onClick={() => { 675 + states.showGenericAccounts = { 676 + heading: 'Boosted/Liked by…', 677 + fetchAccounts: fetchBoostedLikedByAccounts, 678 + instance, 679 + showReactions: true, 680 + }; 681 + }} 682 + > 624 683 <Icon icon="react" /> 625 684 <span> 626 685 Boosted/Liked by<span class="more-insignificant">…</span> ··· 1759 1818 /> 1760 1819 </Modal> 1761 1820 )} 1762 - {showReactions && ( 1763 - <Modal 1764 - class="light" 1765 - onClick={(e) => { 1766 - if (e.target === e.currentTarget) { 1767 - setShowReactions(false); 1768 - } 1769 - }} 1770 - > 1771 - <ReactionsModal 1772 - statusID={id} 1773 - instance={instance} 1774 - onClose={() => setShowReactions(false)} 1775 - /> 1776 - </Modal> 1777 - )} 1778 1821 </article> 1779 1822 ); 1780 1823 } ··· 2040 2083 ); 2041 2084 })} 2042 2085 </ol> 2043 - )} 2044 - </main> 2045 - </div> 2046 - ); 2047 - } 2048 - 2049 - const REACTIONS_LIMIT = 80; 2050 - function ReactionsModal({ statusID, instance, onClose }) { 2051 - const { masto } = api({ instance }); 2052 - const [uiState, setUIState] = useState('default'); 2053 - const [accounts, setAccounts] = useState([]); 2054 - const [showMore, setShowMore] = useState(false); 2055 - 2056 - const reblogIterator = useRef(); 2057 - const favouriteIterator = useRef(); 2058 - 2059 - async function fetchAccounts(firstLoad) { 2060 - setShowMore(false); 2061 - setUIState('loading'); 2062 - (async () => { 2063 - try { 2064 - if (firstLoad) { 2065 - reblogIterator.current = masto.v1.statuses 2066 - .$select(statusID) 2067 - .rebloggedBy.list({ 2068 - limit: REACTIONS_LIMIT, 2069 - }); 2070 - favouriteIterator.current = masto.v1.statuses 2071 - .$select(statusID) 2072 - .favouritedBy.list({ 2073 - limit: REACTIONS_LIMIT, 2074 - }); 2075 - } 2076 - const [{ value: reblogResults }, { value: favouriteResults }] = 2077 - await Promise.allSettled([ 2078 - reblogIterator.current.next(), 2079 - favouriteIterator.current.next(), 2080 - ]); 2081 - if (reblogResults.value?.length || favouriteResults.value?.length) { 2082 - if (reblogResults.value?.length) { 2083 - for (const account of reblogResults.value) { 2084 - const theAccount = accounts.find((a) => a.id === account.id); 2085 - if (!theAccount) { 2086 - accounts.push({ 2087 - ...account, 2088 - _types: ['reblog'], 2089 - }); 2090 - } else { 2091 - theAccount._types.push('reblog'); 2092 - } 2093 - } 2094 - } 2095 - if (favouriteResults.value?.length) { 2096 - for (const account of favouriteResults.value) { 2097 - const theAccount = accounts.find((a) => a.id === account.id); 2098 - if (!theAccount) { 2099 - accounts.push({ 2100 - ...account, 2101 - _types: ['favourite'], 2102 - }); 2103 - } else { 2104 - theAccount._types.push('favourite'); 2105 - } 2106 - } 2107 - } 2108 - setAccounts(accounts); 2109 - setShowMore(!reblogResults.done || !favouriteResults.done); 2110 - } else { 2111 - setShowMore(false); 2112 - } 2113 - setUIState('default'); 2114 - } catch (e) { 2115 - console.error(e); 2116 - setUIState('error'); 2117 - } 2118 - })(); 2119 - } 2120 - 2121 - useEffect(() => { 2122 - fetchAccounts(true); 2123 - }, []); 2124 - 2125 - return ( 2126 - <div id="reactions-container" class="sheet"> 2127 - {!!onClose && ( 2128 - <button type="button" class="sheet-close" onClick={onClose}> 2129 - <Icon icon="x" /> 2130 - </button> 2131 - )} 2132 - <header> 2133 - <h2>Boosted/Liked by…</h2> 2134 - </header> 2135 - <main> 2136 - {accounts.length > 0 ? ( 2137 - <> 2138 - <ul class="reactions-list"> 2139 - {accounts.map((account) => { 2140 - const { _types } = account; 2141 - return ( 2142 - <li key={account.id + _types}> 2143 - <div class="reactions-block"> 2144 - {_types.map((type) => ( 2145 - <Icon 2146 - icon={ 2147 - { 2148 - reblog: 'rocket', 2149 - favourite: 'heart', 2150 - }[type] 2151 - } 2152 - class={`${type}-icon`} 2153 - /> 2154 - ))} 2155 - </div> 2156 - <AccountBlock account={account} instance={instance} /> 2157 - </li> 2158 - ); 2159 - })} 2160 - </ul> 2161 - {uiState === 'default' ? ( 2162 - showMore ? ( 2163 - <InView 2164 - onChange={(inView) => { 2165 - if (inView) { 2166 - fetchAccounts(); 2167 - } 2168 - }} 2169 - > 2170 - <button 2171 - type="button" 2172 - class="plain block" 2173 - onClick={() => fetchAccounts()} 2174 - > 2175 - Show more&hellip; 2176 - </button> 2177 - </InView> 2178 - ) : ( 2179 - <p class="ui-state insignificant">The end.</p> 2180 - ) 2181 - ) : ( 2182 - uiState === 'loading' && ( 2183 - <p class="ui-state"> 2184 - <Loader abrupt /> 2185 - </p> 2186 - ) 2187 - )} 2188 - </> 2189 - ) : uiState === 'loading' ? ( 2190 - <p class="ui-state"> 2191 - <Loader abrupt /> 2192 - </p> 2193 - ) : uiState === 'error' ? ( 2194 - <p class="ui-state">Unable to load accounts</p> 2195 - ) : ( 2196 - <p class="ui-state insignificant">No one yet.</p> 2197 2086 )} 2198 2087 </main> 2199 2088 </div>
+5 -1
src/pages/search.css
··· 24 24 display: flex; 25 25 padding: 8px 16px; 26 26 gap: 8px; 27 - align-items: center; 27 + /* align-items: center; */ 28 28 flex-grow: 1; 29 + 30 + .account-block { 31 + align-items: flex-start; 32 + } 29 33 } 30 34 31 35 ul.link-list.hashtag-list {
+16
src/pages/search.jsx
··· 14 14 import SearchForm from '../components/search-form'; 15 15 import Status from '../components/status'; 16 16 import { api } from '../utils/api'; 17 + import { fetchRelationships } from '../utils/relationships'; 17 18 import shortenNumber from '../utils/shorten-number'; 18 19 import useTitle from '../utils/useTitle'; 19 20 ··· 72 73 hashtags: setHashtagResults, 73 74 }; 74 75 76 + const [relationshipsMap, setRelationshipsMap] = useState({}); 77 + const loadRelationships = async (accounts) => { 78 + if (!accounts?.length) return; 79 + const relationships = await fetchRelationships(accounts, relationshipsMap); 80 + if (relationships) { 81 + setRelationshipsMap({ 82 + ...relationshipsMap, 83 + ...relationships, 84 + }); 85 + } 86 + }; 87 + 75 88 function loadResults(firstLoad) { 76 89 if (!firstLoad && !authenticated) { 77 90 // Search results pagination is only available to authenticated users ··· 119 132 offsetRef.current = 0; 120 133 setShowMore(false); 121 134 } 135 + loadRelationships(results.accounts); 136 + 122 137 setUIState('default'); 123 138 } catch (err) { 124 139 console.error(err); ··· 216 231 account={account} 217 232 instance={instance} 218 233 showStats 234 + relationship={relationshipsMap[account.id]} 219 235 /> 220 236 </li> 221 237 ))}
+37
src/utils/relationships.js
··· 1 + import { api } from './api'; 2 + import store from './store'; 3 + 4 + export async function fetchRelationships(accounts, relationshipsMap = {}) { 5 + if (!accounts?.length) return; 6 + const { masto } = api(); 7 + 8 + const currentAccount = store.session.get('currentAccount'); 9 + const uniqueAccountIds = accounts.reduce((acc, a) => { 10 + // 1. Ignore duplicate accounts 11 + // 2. Ignore accounts that are already inside relationshipsMap 12 + // 3. Ignore currently logged in account 13 + if ( 14 + !acc.includes(a.id) && 15 + !relationshipsMap[a.id] && 16 + a.id !== currentAccount 17 + ) { 18 + acc.push(a.id); 19 + } 20 + return acc; 21 + }, []); 22 + 23 + try { 24 + const relationships = await masto.v1.accounts.relationships.fetch({ 25 + id: uniqueAccountIds, 26 + }); 27 + const newRelationshipsMap = relationships.reduce((acc, r) => { 28 + acc[r.id] = r; 29 + return acc; 30 + }, {}); 31 + return newRelationshipsMap; 32 + } catch (e) { 33 + console.error(e); 34 + // It's okay to fail 35 + return null; 36 + } 37 + }