2023-03-11 09:05:56 +03:00
|
|
|
import './account-info.css';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-04-05 18:30:26 +03:00
|
|
|
import { Menu, MenuDivider, MenuItem, SubMenu } from '@szhsin/react-menu';
|
|
|
|
import { useEffect, useReducer, useRef, useState } from 'preact/hooks';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
import { api } from '../utils/api';
|
2023-01-25 11:25:23 +03:00
|
|
|
import emojifyText from '../utils/emojify-text';
|
2022-12-10 14:16:11 +03:00
|
|
|
import enhanceContent from '../utils/enhance-content';
|
2023-03-28 20:12:59 +03:00
|
|
|
import getHTMLText from '../utils/getHTMLText';
|
2023-01-31 14:31:25 +03:00
|
|
|
import handleContentLinks from '../utils/handle-content-links';
|
2023-03-01 15:07:22 +03:00
|
|
|
import niceDateTime from '../utils/nice-date-time';
|
2022-12-10 12:14:48 +03:00
|
|
|
import shortenNumber from '../utils/shorten-number';
|
2023-03-18 11:24:04 +03:00
|
|
|
import showToast from '../utils/show-toast';
|
2023-02-02 05:30:16 +03:00
|
|
|
import states, { hideAllModals } from '../utils/states';
|
2022-12-10 14:15:30 +03:00
|
|
|
import store from '../utils/store';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-02-12 14:29:03 +03:00
|
|
|
import AccountBlock from './account-block';
|
2022-12-10 12:14:48 +03:00
|
|
|
import Avatar from './avatar';
|
2022-12-19 12:02:47 +03:00
|
|
|
import Icon from './icon';
|
2023-01-29 18:37:13 +03:00
|
|
|
import Link from './link';
|
2023-04-05 18:30:26 +03:00
|
|
|
import ListAddEdit from './list-add-edit';
|
|
|
|
import Loader from './loader';
|
2023-03-28 20:12:59 +03:00
|
|
|
import Modal from './modal';
|
|
|
|
import TranslationBlock from './translation-block';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-03-18 11:24:04 +03:00
|
|
|
const MUTE_DURATIONS = [
|
|
|
|
1000 * 60 * 5, // 5 minutes
|
|
|
|
1000 * 60 * 30, // 30 minutes
|
|
|
|
1000 * 60 * 60, // 1 hour
|
|
|
|
1000 * 60 * 60 * 6, // 6 hours
|
|
|
|
1000 * 60 * 60 * 24, // 1 day
|
|
|
|
1000 * 60 * 60 * 24 * 3, // 3 days
|
|
|
|
1000 * 60 * 60 * 24 * 7, // 1 week
|
|
|
|
0, // forever
|
|
|
|
];
|
|
|
|
const MUTE_DURATIONS_LABELS = {
|
|
|
|
0: 'Forever',
|
|
|
|
300_000: '5 minutes',
|
|
|
|
1_800_000: '30 minutes',
|
|
|
|
3_600_000: '1 hour',
|
|
|
|
21_600_000: '6 hours',
|
|
|
|
86_400_000: '1 day',
|
|
|
|
259_200_000: '3 days',
|
|
|
|
604_800_000: '1 week',
|
|
|
|
};
|
|
|
|
|
2023-03-11 09:05:56 +03:00
|
|
|
function AccountInfo({
|
|
|
|
account,
|
|
|
|
fetchAccount = () => {},
|
|
|
|
standalone,
|
|
|
|
instance,
|
|
|
|
authenticated,
|
|
|
|
}) {
|
2022-12-10 12:14:48 +03:00
|
|
|
const [uiState, setUIState] = useState('default');
|
|
|
|
const isString = typeof account === 'string';
|
|
|
|
const [info, setInfo] = useState(isString ? null : account);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-03-11 10:14:59 +03:00
|
|
|
if (!isString) {
|
|
|
|
setInfo(account);
|
|
|
|
return;
|
|
|
|
}
|
2023-03-11 09:05:56 +03:00
|
|
|
setUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const info = await fetchAccount();
|
2023-03-11 17:07:20 +03:00
|
|
|
states.accounts[`${info.id}@${instance}`] = info;
|
2023-03-11 09:05:56 +03:00
|
|
|
setInfo(info);
|
|
|
|
setUIState('default');
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setInfo(null);
|
|
|
|
setUIState('error');
|
|
|
|
}
|
|
|
|
})();
|
2023-03-11 10:14:59 +03:00
|
|
|
}, [isString, account, fetchAccount]);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
const {
|
|
|
|
acct,
|
|
|
|
avatar,
|
|
|
|
avatarStatic,
|
|
|
|
bot,
|
|
|
|
createdAt,
|
|
|
|
displayName,
|
|
|
|
emojis,
|
|
|
|
fields,
|
|
|
|
followersCount,
|
|
|
|
followingCount,
|
|
|
|
group,
|
2023-03-14 06:50:27 +03:00
|
|
|
// header,
|
|
|
|
// headerStatic,
|
2022-12-10 12:14:48 +03:00
|
|
|
id,
|
|
|
|
lastStatusAt,
|
|
|
|
locked,
|
|
|
|
note,
|
|
|
|
statusesCount,
|
|
|
|
url,
|
|
|
|
username,
|
|
|
|
} = info || {};
|
2023-03-14 06:50:27 +03:00
|
|
|
let headerIsAvatar = false;
|
|
|
|
let { header, headerStatic } = info || {};
|
|
|
|
if (!header || /missing\.png$/.test(header)) {
|
|
|
|
if (avatar && !/missing\.png$/.test(avatar)) {
|
|
|
|
header = avatar;
|
|
|
|
headerIsAvatar = true;
|
|
|
|
if (avatarStatic && !/missing\.png$/.test(avatarStatic)) {
|
|
|
|
headerStatic = avatarStatic;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
2023-03-11 09:05:56 +03:00
|
|
|
const [headerCornerColors, setHeaderCornerColors] = useState([]);
|
2023-02-22 08:28:01 +03:00
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
return (
|
|
|
|
<div
|
2023-03-11 09:05:56 +03:00
|
|
|
class={`account-container ${uiState === 'loading' ? 'skeleton' : ''}`}
|
|
|
|
style={{
|
|
|
|
'--header-color-1': headerCornerColors[0],
|
|
|
|
'--header-color-2': headerCornerColors[1],
|
|
|
|
'--header-color-3': headerCornerColors[2],
|
|
|
|
'--header-color-4': headerCornerColors[3],
|
|
|
|
}}
|
2022-12-10 12:14:48 +03:00
|
|
|
>
|
2023-01-26 06:26:24 +03:00
|
|
|
{uiState === 'error' && (
|
|
|
|
<div class="ui-state">
|
|
|
|
<p>Unable to load account.</p>
|
|
|
|
<p>
|
|
|
|
<a href={account} target="_blank">
|
|
|
|
Go to account page <Icon icon="external" />
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{uiState === 'loading' ? (
|
2022-12-10 12:14:48 +03:00
|
|
|
<>
|
|
|
|
<header>
|
2023-02-12 14:29:03 +03:00
|
|
|
<AccountBlock avatarSize="xxxl" skeleton />
|
2022-12-10 12:14:48 +03:00
|
|
|
</header>
|
2022-12-25 13:01:01 +03:00
|
|
|
<main>
|
|
|
|
<div class="note">
|
|
|
|
<p>████████ ███████</p>
|
|
|
|
<p>███████████████ ███████████████</p>
|
|
|
|
</div>
|
|
|
|
<p class="stats">
|
2023-03-11 10:17:42 +03:00
|
|
|
<span>
|
|
|
|
Posts
|
|
|
|
<br />
|
|
|
|
██
|
|
|
|
</span>
|
|
|
|
<span>
|
|
|
|
Following
|
|
|
|
<br />
|
|
|
|
██
|
|
|
|
</span>
|
|
|
|
<span>
|
|
|
|
Followers
|
|
|
|
<br />
|
|
|
|
██
|
|
|
|
</span>
|
2022-12-25 13:01:01 +03:00
|
|
|
</p>
|
|
|
|
</main>
|
2022-12-10 12:14:48 +03:00
|
|
|
</>
|
|
|
|
) : (
|
2023-01-26 06:26:24 +03:00
|
|
|
info && (
|
|
|
|
<>
|
2023-03-10 12:36:42 +03:00
|
|
|
{header && !/missing\.png$/.test(header) && (
|
2023-03-10 14:00:20 +03:00
|
|
|
<img
|
|
|
|
src={header}
|
|
|
|
alt=""
|
2023-03-14 06:50:27 +03:00
|
|
|
class={`header-banner ${
|
|
|
|
headerIsAvatar ? 'header-is-avatar' : ''
|
|
|
|
}`}
|
2023-03-10 14:00:20 +03:00
|
|
|
onError={(e) => {
|
2023-03-11 09:05:56 +03:00
|
|
|
if (e.target.crossOrigin) {
|
|
|
|
if (e.target.src !== headerStatic) {
|
|
|
|
e.target.src = headerStatic;
|
|
|
|
} else {
|
|
|
|
e.target.removeAttribute('crossorigin');
|
|
|
|
e.target.src = header;
|
|
|
|
}
|
|
|
|
} else if (e.target.src !== headerStatic) {
|
|
|
|
e.target.src = headerStatic;
|
|
|
|
} else {
|
|
|
|
e.target.remove();
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
crossOrigin="anonymous"
|
|
|
|
onLoad={(e) => {
|
|
|
|
try {
|
|
|
|
// Get color from four corners of image
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
canvas.width = e.target.width;
|
|
|
|
canvas.height = e.target.height;
|
|
|
|
ctx.drawImage(e.target, 0, 0);
|
2023-03-13 05:42:34 +03:00
|
|
|
// const colors = [
|
|
|
|
// ctx.getImageData(0, 0, 1, 1).data,
|
|
|
|
// ctx.getImageData(e.target.width - 1, 0, 1, 1).data,
|
|
|
|
// ctx.getImageData(0, e.target.height - 1, 1, 1).data,
|
|
|
|
// ctx.getImageData(
|
|
|
|
// e.target.width - 1,
|
|
|
|
// e.target.height - 1,
|
|
|
|
// 1,
|
|
|
|
// 1,
|
|
|
|
// ).data,
|
|
|
|
// ];
|
|
|
|
// Get 10x10 pixels from corners, get average color from each
|
|
|
|
const pixelDimension = 10;
|
2023-03-11 09:05:56 +03:00
|
|
|
const colors = [
|
2023-03-13 05:42:34 +03:00
|
|
|
ctx.getImageData(0, 0, pixelDimension, pixelDimension)
|
|
|
|
.data,
|
2023-03-11 09:05:56 +03:00
|
|
|
ctx.getImageData(
|
2023-03-13 05:42:34 +03:00
|
|
|
e.target.width - pixelDimension,
|
|
|
|
0,
|
|
|
|
pixelDimension,
|
|
|
|
pixelDimension,
|
2023-03-11 09:05:56 +03:00
|
|
|
).data,
|
2023-03-13 05:42:34 +03:00
|
|
|
ctx.getImageData(
|
|
|
|
0,
|
|
|
|
e.target.height - pixelDimension,
|
|
|
|
pixelDimension,
|
|
|
|
pixelDimension,
|
|
|
|
).data,
|
|
|
|
ctx.getImageData(
|
|
|
|
e.target.width - pixelDimension,
|
|
|
|
e.target.height - pixelDimension,
|
|
|
|
pixelDimension,
|
|
|
|
pixelDimension,
|
|
|
|
).data,
|
|
|
|
].map((data) => {
|
|
|
|
let r = 0;
|
|
|
|
let g = 0;
|
|
|
|
let b = 0;
|
|
|
|
let a = 0;
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
|
|
r += data[i];
|
|
|
|
g += data[i + 1];
|
|
|
|
b += data[i + 2];
|
|
|
|
a += data[i + 3];
|
|
|
|
}
|
|
|
|
const dataLength = data.length / 4;
|
|
|
|
return [
|
|
|
|
r / dataLength,
|
|
|
|
g / dataLength,
|
|
|
|
b / dataLength,
|
|
|
|
a / dataLength,
|
|
|
|
];
|
|
|
|
});
|
2023-03-11 09:05:56 +03:00
|
|
|
const rgbColors = colors.map((color) => {
|
2023-03-13 05:10:21 +03:00
|
|
|
const [r, g, b, a] = lightenRGB(color);
|
|
|
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
2023-03-11 09:05:56 +03:00
|
|
|
});
|
|
|
|
setHeaderCornerColors(rgbColors);
|
|
|
|
console.log({ colors, rgbColors });
|
|
|
|
} catch (e) {
|
|
|
|
// Silently fail
|
|
|
|
}
|
2023-03-10 14:00:20 +03:00
|
|
|
}}
|
|
|
|
/>
|
2023-03-10 12:36:42 +03:00
|
|
|
)}
|
2023-01-26 06:26:24 +03:00
|
|
|
<header>
|
2023-02-12 14:29:03 +03:00
|
|
|
<AccountBlock
|
|
|
|
account={info}
|
|
|
|
instance={instance}
|
|
|
|
avatarSize="xxxl"
|
2023-03-11 09:05:56 +03:00
|
|
|
external={standalone}
|
|
|
|
internal={!standalone}
|
2023-02-12 14:29:03 +03:00
|
|
|
/>
|
2023-01-26 06:26:24 +03:00
|
|
|
</header>
|
|
|
|
<main tabIndex="-1">
|
|
|
|
{bot && (
|
|
|
|
<>
|
|
|
|
<span class="tag">
|
|
|
|
<Icon icon="bot" /> Automated
|
|
|
|
</span>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
<div
|
|
|
|
class="note"
|
2023-02-05 19:17:19 +03:00
|
|
|
onClick={handleContentLinks({
|
|
|
|
instance,
|
|
|
|
})}
|
2023-01-26 06:26:24 +03:00
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
__html: enhanceContent(note, { emojis }),
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{fields?.length > 0 && (
|
|
|
|
<div class="profile-metadata">
|
|
|
|
{fields.map(({ name, value, verifiedAt }) => (
|
|
|
|
<div
|
|
|
|
class={`profile-field ${
|
|
|
|
verifiedAt ? 'profile-verified' : ''
|
|
|
|
}`}
|
|
|
|
key={name}
|
|
|
|
>
|
|
|
|
<b>
|
|
|
|
<span
|
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
__html: emojifyText(name, emojis),
|
|
|
|
}}
|
|
|
|
/>{' '}
|
|
|
|
{!!verifiedAt && <Icon icon="check-circle" size="s" />}
|
|
|
|
</b>
|
|
|
|
<p
|
2023-01-25 11:25:23 +03:00
|
|
|
dangerouslySetInnerHTML={{
|
2023-01-26 06:26:24 +03:00
|
|
|
__html: enhanceContent(value, { emojis }),
|
2023-01-25 11:25:23 +03:00
|
|
|
}}
|
2023-01-26 06:26:24 +03:00
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<p class="stats">
|
2023-03-12 05:34:58 +03:00
|
|
|
{standalone ? (
|
|
|
|
<span>
|
|
|
|
Posts
|
|
|
|
<br />
|
|
|
|
<b title={statusesCount}>
|
|
|
|
{shortenNumber(statusesCount)}
|
|
|
|
</b>{' '}
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
<Link
|
|
|
|
to={instance ? `/${instance}/a/${id}` : `/a/${id}`}
|
|
|
|
onClick={() => {
|
|
|
|
hideAllModals();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Posts
|
|
|
|
<br />
|
|
|
|
<b title={statusesCount}>
|
|
|
|
{shortenNumber(statusesCount)}
|
|
|
|
</b>{' '}
|
|
|
|
</Link>
|
|
|
|
)}
|
2023-01-26 06:26:24 +03:00
|
|
|
<span>
|
|
|
|
Following
|
2023-01-29 18:37:13 +03:00
|
|
|
<br />
|
|
|
|
<b title={followingCount}>
|
|
|
|
{shortenNumber(followingCount)}
|
|
|
|
</b>{' '}
|
2023-01-26 06:26:24 +03:00
|
|
|
</span>
|
|
|
|
<span>
|
|
|
|
Followers
|
2023-01-29 18:37:13 +03:00
|
|
|
<br />
|
|
|
|
<b title={followersCount}>
|
|
|
|
{shortenNumber(followersCount)}
|
|
|
|
</b>{' '}
|
2023-01-26 06:26:24 +03:00
|
|
|
</span>
|
|
|
|
{!!createdAt && (
|
|
|
|
<span>
|
2023-01-29 18:37:13 +03:00
|
|
|
Joined
|
|
|
|
<br />
|
2023-01-26 06:26:24 +03:00
|
|
|
<b>
|
|
|
|
<time datetime={createdAt}>
|
2023-03-01 15:07:22 +03:00
|
|
|
{niceDateTime(createdAt, {
|
|
|
|
hideTime: true,
|
|
|
|
})}
|
2023-01-26 06:26:24 +03:00
|
|
|
</time>
|
|
|
|
</b>
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</p>
|
2023-02-21 09:19:50 +03:00
|
|
|
<RelatedActions
|
|
|
|
info={info}
|
|
|
|
instance={instance}
|
|
|
|
authenticated={authenticated}
|
|
|
|
/>
|
2023-01-26 06:26:24 +03:00
|
|
|
</main>
|
|
|
|
</>
|
|
|
|
)
|
2022-12-10 12:14:48 +03:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
2022-12-16 08:27:04 +03:00
|
|
|
}
|
|
|
|
|
2023-02-21 09:19:50 +03:00
|
|
|
function RelatedActions({ info, instance, authenticated }) {
|
|
|
|
if (!info) return null;
|
|
|
|
const {
|
|
|
|
masto: currentMasto,
|
|
|
|
instance: currentInstance,
|
|
|
|
authenticated: currentAuthenticated,
|
|
|
|
} = api();
|
|
|
|
const sameInstance = instance === currentInstance;
|
|
|
|
|
|
|
|
const [relationshipUIState, setRelationshipUIState] = useState('default');
|
|
|
|
const [relationship, setRelationship] = useState(null);
|
|
|
|
const [familiarFollowers, setFamiliarFollowers] = useState([]);
|
|
|
|
|
2023-03-28 20:12:59 +03:00
|
|
|
const { id, acct, url, username, locked, lastStatusAt, note, fields } = info;
|
2023-02-21 09:19:50 +03:00
|
|
|
const accountID = useRef(id);
|
|
|
|
|
|
|
|
const {
|
|
|
|
following,
|
|
|
|
showingReblogs,
|
|
|
|
notifying,
|
|
|
|
followedBy,
|
|
|
|
blocking,
|
|
|
|
blockedBy,
|
|
|
|
muting,
|
|
|
|
mutingNotifications,
|
|
|
|
requested,
|
|
|
|
domainBlocking,
|
|
|
|
endorsed,
|
|
|
|
} = relationship || {};
|
|
|
|
|
2023-03-18 12:04:47 +03:00
|
|
|
const [currentInfo, setCurrentInfo] = useState(null);
|
2023-03-18 15:40:16 +03:00
|
|
|
const [isSelf, setIsSelf] = useState(false);
|
2023-03-18 12:04:47 +03:00
|
|
|
|
2023-02-21 09:19:50 +03:00
|
|
|
useEffect(() => {
|
|
|
|
if (info) {
|
|
|
|
const currentAccount = store.session.get('currentAccount');
|
|
|
|
let currentID;
|
|
|
|
(async () => {
|
|
|
|
if (sameInstance && authenticated) {
|
|
|
|
currentID = id;
|
|
|
|
} else if (!sameInstance && currentAuthenticated) {
|
|
|
|
// Grab this account from my logged-in instance
|
|
|
|
const acctHasInstance = info.acct.includes('@');
|
|
|
|
try {
|
|
|
|
const results = await currentMasto.v2.search({
|
|
|
|
q: acctHasInstance ? info.acct : `${info.username}@${instance}`,
|
|
|
|
type: 'accounts',
|
|
|
|
limit: 1,
|
|
|
|
resolve: true,
|
|
|
|
});
|
|
|
|
console.log('🥏 Fetched account from logged-in instance', results);
|
2023-03-18 12:04:47 +03:00
|
|
|
if (results.accounts.length) {
|
|
|
|
currentID = results.accounts[0].id;
|
|
|
|
setCurrentInfo(results.accounts[0]);
|
|
|
|
}
|
2023-02-21 09:19:50 +03:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!currentID) return;
|
|
|
|
|
|
|
|
if (currentAccount === currentID) {
|
|
|
|
// It's myself!
|
2023-03-18 15:40:16 +03:00
|
|
|
setIsSelf(true);
|
2023-02-21 09:19:50 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
accountID.current = currentID;
|
|
|
|
|
|
|
|
setRelationshipUIState('loading');
|
|
|
|
setFamiliarFollowers([]);
|
|
|
|
|
|
|
|
const fetchRelationships = currentMasto.v1.accounts.fetchRelationships([
|
|
|
|
currentID,
|
|
|
|
]);
|
|
|
|
const fetchFamiliarFollowers =
|
|
|
|
currentMasto.v1.accounts.fetchFamiliarFollowers(currentID);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const relationships = await fetchRelationships;
|
|
|
|
console.log('fetched relationship', relationships);
|
|
|
|
if (relationships.length) {
|
|
|
|
const relationship = relationships[0];
|
|
|
|
setRelationship(relationship);
|
|
|
|
|
|
|
|
if (!relationship.following) {
|
|
|
|
try {
|
|
|
|
const followers = await fetchFamiliarFollowers;
|
|
|
|
console.log('fetched familiar followers', followers);
|
|
|
|
setFamiliarFollowers(followers[0].accounts.slice(0, 10));
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setRelationshipUIState('default');
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setRelationshipUIState('error');
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
}, [info, authenticated]);
|
|
|
|
|
2023-03-18 11:24:04 +03:00
|
|
|
const loading = relationshipUIState === 'loading';
|
2023-03-18 17:59:19 +03:00
|
|
|
const menuInstanceRef = useRef(null);
|
2023-03-18 11:24:04 +03:00
|
|
|
|
2023-03-28 20:12:59 +03:00
|
|
|
const [showTranslatedBio, setShowTranslatedBio] = useState(false);
|
2023-04-05 18:30:26 +03:00
|
|
|
const [showAddRemoveLists, setShowAddRemoveLists] = useState(false);
|
2023-03-28 20:12:59 +03:00
|
|
|
|
2023-02-21 09:19:50 +03:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{familiarFollowers?.length > 0 && (
|
|
|
|
<p class="common-followers">
|
|
|
|
Common followers{' '}
|
|
|
|
<span class="ib">
|
|
|
|
{familiarFollowers.map((follower) => (
|
|
|
|
<a
|
|
|
|
href={follower.url}
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
states.showAccount = {
|
|
|
|
account: follower,
|
|
|
|
instance,
|
|
|
|
};
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Avatar
|
|
|
|
url={follower.avatarStatic}
|
|
|
|
size="l"
|
|
|
|
alt={`${follower.displayName} @${follower.acct}`}
|
|
|
|
/>
|
|
|
|
</a>
|
|
|
|
))}
|
|
|
|
</span>
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
<p class="actions">
|
2023-03-17 13:58:10 +03:00
|
|
|
{followedBy ? (
|
|
|
|
<span class="tag">Following you</span>
|
2023-03-19 10:24:15 +03:00
|
|
|
) : !!lastStatusAt ? (
|
2023-03-17 13:58:10 +03:00
|
|
|
<span class="insignificant">
|
2023-03-19 10:24:15 +03:00
|
|
|
Last status:{' '}
|
|
|
|
{niceDateTime(lastStatusAt, {
|
|
|
|
hideTime: true,
|
|
|
|
})}
|
2023-03-17 13:58:10 +03:00
|
|
|
</span>
|
2023-03-19 10:24:15 +03:00
|
|
|
) : (
|
|
|
|
<span />
|
2023-03-17 13:58:10 +03:00
|
|
|
)}{' '}
|
2023-03-18 11:24:04 +03:00
|
|
|
<span class="buttons">
|
|
|
|
<Menu
|
2023-03-18 17:59:19 +03:00
|
|
|
instanceRef={menuInstanceRef}
|
2023-03-18 11:24:04 +03:00
|
|
|
portal={{
|
|
|
|
target: document.body,
|
|
|
|
}}
|
|
|
|
containerProps={{
|
|
|
|
style: {
|
|
|
|
// Higher than the backdrop
|
|
|
|
zIndex: 1001,
|
|
|
|
},
|
2023-03-22 09:11:38 +03:00
|
|
|
onClick: (e) => {
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
menuInstanceRef.current?.closeMenu?.();
|
|
|
|
}
|
2023-03-18 17:59:19 +03:00
|
|
|
},
|
2023-02-21 09:19:50 +03:00
|
|
|
}}
|
2023-03-18 11:24:04 +03:00
|
|
|
align="center"
|
|
|
|
position="anchor"
|
|
|
|
overflow="auto"
|
|
|
|
boundingBoxPadding="8 8 8 8"
|
|
|
|
menuButton={
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
title="More"
|
|
|
|
class="plain"
|
|
|
|
disabled={loading}
|
|
|
|
>
|
|
|
|
<Icon icon="more" size="l" alt="More" />
|
|
|
|
</button>
|
|
|
|
}
|
2023-02-21 09:19:50 +03:00
|
|
|
>
|
2023-03-18 15:40:16 +03:00
|
|
|
{currentAuthenticated && !isSelf && (
|
2023-02-21 09:19:50 +03:00
|
|
|
<>
|
2023-03-18 11:24:04 +03:00
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
states.showCompose = {
|
|
|
|
draftStatus: {
|
2023-03-18 12:04:47 +03:00
|
|
|
status: `@${currentInfo?.acct || acct} `,
|
2023-03-18 11:24:04 +03:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="at" />
|
|
|
|
<span>Mention @{username}</span>
|
|
|
|
</MenuItem>
|
2023-03-28 20:12:59 +03:00
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
setShowTranslatedBio(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="translate" />
|
|
|
|
<span>Translate bio</span>
|
|
|
|
</MenuItem>
|
2023-04-05 18:30:26 +03:00
|
|
|
{/* Add/remove from lists is only possible if following the account */}
|
|
|
|
{following && (
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
setShowAddRemoveLists(true);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="list" />
|
|
|
|
<span>Add/remove from Lists</span>
|
|
|
|
</MenuItem>
|
|
|
|
)}
|
2023-03-18 11:24:04 +03:00
|
|
|
<MenuDivider />
|
2023-02-21 09:19:50 +03:00
|
|
|
</>
|
2023-03-18 11:24:04 +03:00
|
|
|
)}
|
|
|
|
<MenuItem href={url} target="_blank">
|
|
|
|
<Icon icon="external" />
|
|
|
|
<small class="menu-double-lines">{niceAccountURL(url)}</small>
|
|
|
|
</MenuItem>
|
|
|
|
<div class="menu-horizontal">
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
// Copy url to clipboard
|
|
|
|
try {
|
|
|
|
navigator.clipboard.writeText(url);
|
|
|
|
showToast('Link copied');
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
showToast('Unable to copy link');
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="link" />
|
|
|
|
<span>Copy</span>
|
|
|
|
</MenuItem>
|
|
|
|
{navigator?.share &&
|
|
|
|
navigator?.canShare?.({
|
|
|
|
url,
|
|
|
|
}) && (
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
try {
|
|
|
|
navigator.share({
|
|
|
|
url,
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
alert("Sharing doesn't seem to work.");
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="share" />
|
|
|
|
<span>Share…</span>
|
|
|
|
</MenuItem>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
{!!relationship && (
|
2023-02-21 09:19:50 +03:00
|
|
|
<>
|
2023-03-18 11:24:04 +03:00
|
|
|
<MenuDivider />
|
|
|
|
{muting ? (
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
setRelationshipUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const newRelationship =
|
2023-03-18 12:04:47 +03:00
|
|
|
await currentMasto.v1.accounts.unmute(
|
|
|
|
currentInfo?.id || id,
|
|
|
|
);
|
2023-03-18 11:24:04 +03:00
|
|
|
console.log('unmuting', newRelationship);
|
|
|
|
setRelationship(newRelationship);
|
|
|
|
setRelationshipUIState('default');
|
|
|
|
showToast(`Unmuted @${username}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setRelationshipUIState('error');
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="unmute" />
|
|
|
|
<span>Unmute @{username}</span>
|
|
|
|
</MenuItem>
|
|
|
|
) : (
|
|
|
|
<SubMenu
|
|
|
|
openTrigger="clickOnly"
|
|
|
|
direction="bottom"
|
|
|
|
overflow="auto"
|
|
|
|
offsetX={-16}
|
|
|
|
label={
|
|
|
|
<>
|
|
|
|
<Icon icon="mute" />
|
|
|
|
<span class="menu-grow">Mute @{username}…</span>
|
2023-03-24 10:05:16 +03:00
|
|
|
<span
|
|
|
|
style={{
|
|
|
|
textOverflow: 'clip',
|
|
|
|
}}
|
|
|
|
>
|
2023-03-18 11:24:04 +03:00
|
|
|
<Icon icon="time" />
|
|
|
|
<Icon icon="chevron-right" />
|
|
|
|
</span>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<div class="menu-wrap">
|
|
|
|
{MUTE_DURATIONS.map((duration) => (
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
setRelationshipUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const newRelationship =
|
2023-03-18 12:04:47 +03:00
|
|
|
await currentMasto.v1.accounts.mute(
|
|
|
|
currentInfo?.id || id,
|
|
|
|
{
|
|
|
|
duration,
|
|
|
|
},
|
|
|
|
);
|
2023-03-18 11:24:04 +03:00
|
|
|
console.log('muting', newRelationship);
|
|
|
|
setRelationship(newRelationship);
|
|
|
|
setRelationshipUIState('default');
|
|
|
|
showToast(
|
|
|
|
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setRelationshipUIState('error');
|
|
|
|
showToast(`Unable to mute @${username}`);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{MUTE_DURATIONS_LABELS[duration]}
|
|
|
|
</MenuItem>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</SubMenu>
|
|
|
|
)}
|
|
|
|
<MenuItem
|
|
|
|
onClick={() => {
|
|
|
|
if (!blocking && !confirm(`Block @${username}?`)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setRelationshipUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
if (blocking) {
|
|
|
|
const newRelationship =
|
2023-03-18 12:04:47 +03:00
|
|
|
await currentMasto.v1.accounts.unblock(
|
|
|
|
currentInfo?.id || id,
|
|
|
|
);
|
2023-03-18 11:24:04 +03:00
|
|
|
console.log('unblocking', newRelationship);
|
|
|
|
setRelationship(newRelationship);
|
|
|
|
setRelationshipUIState('default');
|
|
|
|
showToast(`Unblocked @${username}`);
|
|
|
|
} else {
|
|
|
|
const newRelationship =
|
2023-03-18 12:04:47 +03:00
|
|
|
await currentMasto.v1.accounts.block(
|
|
|
|
currentInfo?.id || id,
|
|
|
|
);
|
2023-03-18 11:24:04 +03:00
|
|
|
console.log('blocking', newRelationship);
|
|
|
|
setRelationship(newRelationship);
|
|
|
|
setRelationshipUIState('default');
|
|
|
|
showToast(`Blocked @${username}`);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setRelationshipUIState('error');
|
|
|
|
if (blocking) {
|
|
|
|
showToast(`Unable to unblock @${username}`);
|
|
|
|
} else {
|
|
|
|
showToast(`Unable to block @${username}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{blocking ? (
|
|
|
|
<>
|
|
|
|
<Icon icon="unblock" />
|
|
|
|
<span>Unblock @{username}</span>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<Icon icon="block" />
|
|
|
|
<span>Block @{username}…</span>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</MenuItem>
|
|
|
|
{/* <MenuItem>
|
|
|
|
<Icon icon="flag" />
|
|
|
|
<span>Report @{username}…</span>
|
|
|
|
</MenuItem> */}
|
2023-02-21 09:19:50 +03:00
|
|
|
</>
|
|
|
|
)}
|
2023-03-18 11:24:04 +03:00
|
|
|
</Menu>
|
|
|
|
{!!relationship && (
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class={`${following || requested ? 'light swap' : ''}`}
|
|
|
|
data-swap-state={following || requested ? 'danger' : ''}
|
|
|
|
disabled={loading}
|
|
|
|
onClick={() => {
|
|
|
|
setRelationshipUIState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
let newRelationship;
|
|
|
|
|
|
|
|
if (following || requested) {
|
|
|
|
const yes = confirm(
|
|
|
|
requested
|
|
|
|
? 'Withdraw follow request?'
|
|
|
|
: `Unfollow @${info.acct || info.username}?`,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (yes) {
|
|
|
|
newRelationship =
|
|
|
|
await currentMasto.v1.accounts.unfollow(
|
|
|
|
accountID.current,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
newRelationship = await currentMasto.v1.accounts.follow(
|
|
|
|
accountID.current,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newRelationship) setRelationship(newRelationship);
|
|
|
|
setRelationshipUIState('default');
|
|
|
|
} catch (e) {
|
|
|
|
alert(e);
|
|
|
|
setRelationshipUIState('error');
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{following ? (
|
|
|
|
<>
|
|
|
|
<span>Following</span>
|
|
|
|
<span>Unfollow…</span>
|
|
|
|
</>
|
|
|
|
) : requested ? (
|
|
|
|
<>
|
|
|
|
<span>Requested</span>
|
|
|
|
<span>Withdraw…</span>
|
|
|
|
</>
|
|
|
|
) : locked ? (
|
|
|
|
<>
|
|
|
|
<Icon icon="lock" /> <span>Follow</span>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
'Follow'
|
|
|
|
)}
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</span>
|
2023-02-21 09:19:50 +03:00
|
|
|
</p>
|
2023-03-28 20:12:59 +03:00
|
|
|
{!!showTranslatedBio && (
|
|
|
|
<Modal
|
|
|
|
class="light"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
setShowTranslatedBio(false);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<TranslatedBioSheet note={note} fields={fields} />
|
|
|
|
</Modal>
|
|
|
|
)}
|
2023-04-05 18:30:26 +03:00
|
|
|
{!!showAddRemoveLists && (
|
|
|
|
<Modal
|
|
|
|
class="light"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
setShowAddRemoveLists(false);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<AddRemoveListsSheet accountID={accountID.current} />
|
|
|
|
</Modal>
|
|
|
|
)}
|
2023-02-21 09:19:50 +03:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-13 05:10:21 +03:00
|
|
|
// Apply more alpha if high luminence
|
|
|
|
function lightenRGB([r, g, b]) {
|
|
|
|
const luminence = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
|
|
console.log('luminence', luminence);
|
2023-03-13 05:42:34 +03:00
|
|
|
let alpha;
|
|
|
|
if (luminence >= 220) {
|
|
|
|
alpha = 1;
|
2023-03-13 09:24:53 +03:00
|
|
|
} else if (luminence <= 50) {
|
|
|
|
alpha = 0.1;
|
2023-03-13 05:42:34 +03:00
|
|
|
} else {
|
2023-03-13 09:24:53 +03:00
|
|
|
alpha = luminence / 255;
|
2023-03-13 05:42:34 +03:00
|
|
|
}
|
|
|
|
alpha = Math.min(1, alpha);
|
2023-03-13 05:10:21 +03:00
|
|
|
return [r, g, b, alpha];
|
|
|
|
}
|
|
|
|
|
2023-03-18 11:24:04 +03:00
|
|
|
function niceAccountURL(url) {
|
|
|
|
if (!url) return;
|
|
|
|
const urlObj = new URL(url);
|
|
|
|
const { host, pathname } = urlObj;
|
|
|
|
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<span class="more-insignificant">{host}/</span>
|
|
|
|
<wbr />
|
|
|
|
<span>{path}</span>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-28 20:12:59 +03:00
|
|
|
function TranslatedBioSheet({ note, fields }) {
|
|
|
|
const fieldsText =
|
|
|
|
fields
|
|
|
|
?.map(({ name, value }) => `${name}\n${getHTMLText(value)}`)
|
|
|
|
.join('\n\n') || '';
|
|
|
|
|
|
|
|
const text = getHTMLText(note) + (fieldsText ? `\n\n${fieldsText}` : '');
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div class="sheet">
|
|
|
|
<header>
|
|
|
|
<h2>Translated Bio</h2>
|
|
|
|
</header>
|
|
|
|
<main>
|
|
|
|
<p
|
|
|
|
style={{
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{text}
|
|
|
|
</p>
|
|
|
|
<TranslationBlock forceTranslate text={text} />
|
|
|
|
</main>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2023-04-05 18:30:26 +03:00
|
|
|
|
|
|
|
function AddRemoveListsSheet({ accountID }) {
|
|
|
|
const { masto } = api();
|
|
|
|
const [uiState, setUiState] = useState('default');
|
|
|
|
const [lists, setLists] = useState([]);
|
|
|
|
const [listsContainingAccount, setListsContainingAccount] = useState([]);
|
|
|
|
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setUiState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const lists = await masto.v1.lists.list();
|
|
|
|
const listsContainingAccount = await masto.v1.accounts.listLists(
|
|
|
|
accountID,
|
|
|
|
);
|
|
|
|
console.log({ lists, listsContainingAccount });
|
|
|
|
setLists(lists);
|
|
|
|
setListsContainingAccount(listsContainingAccount);
|
|
|
|
setUiState('default');
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setUiState('error');
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}, [reloadCount]);
|
|
|
|
|
|
|
|
const [showListAddEditModal, setShowListAddEditModal] = useState(false);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div class="sheet" id="list-add-remove-container">
|
|
|
|
<header>
|
|
|
|
<h2>Add/Remove from Lists</h2>
|
|
|
|
</header>
|
|
|
|
<main>
|
|
|
|
{lists.length > 0 ? (
|
|
|
|
<ul class="list-add-remove">
|
|
|
|
{lists.map((list) => {
|
|
|
|
const inList = listsContainingAccount.some(
|
|
|
|
(l) => l.id === list.id,
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
<li>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class={`light ${inList ? 'checked' : ''}`}
|
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
onClick={() => {
|
|
|
|
setUiState('loading');
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
if (inList) {
|
|
|
|
await masto.v1.lists.removeAccount(list.id, {
|
|
|
|
accountIds: [accountID],
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
await masto.v1.lists.addAccount(list.id, {
|
|
|
|
accountIds: [accountID],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
// setUiState('default');
|
|
|
|
reload();
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
setUiState('error');
|
|
|
|
alert(
|
|
|
|
inList
|
|
|
|
? 'Unable to remove from list.'
|
|
|
|
: 'Unable to add to list.',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Icon icon="check-circle" />
|
|
|
|
<span>{list.title}</span>
|
|
|
|
</button>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</ul>
|
|
|
|
) : uiState === 'loading' ? (
|
|
|
|
<p class="ui-state">
|
|
|
|
<Loader abrupt />
|
|
|
|
</p>
|
|
|
|
) : uiState === 'error' ? (
|
|
|
|
<p class="ui-state">Unable to load lists.</p>
|
|
|
|
) : (
|
|
|
|
<p class="ui-state">No lists.</p>
|
|
|
|
)}
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="plain2"
|
|
|
|
onClick={() => setShowListAddEditModal(true)}
|
|
|
|
disabled={uiState !== 'default'}
|
|
|
|
>
|
|
|
|
<Icon icon="plus" size="l" /> <span>New list</span>
|
|
|
|
</button>
|
|
|
|
</main>
|
|
|
|
{showListAddEditModal && (
|
|
|
|
<Modal
|
|
|
|
class="light"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
setShowListAddEditModal(false);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<ListAddEdit
|
|
|
|
list={showListAddEditModal?.list}
|
|
|
|
onClose={(result) => {
|
|
|
|
if (result.state === 'success') {
|
|
|
|
reload();
|
|
|
|
}
|
|
|
|
setShowListAddEditModal(false);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-03-11 09:05:56 +03:00
|
|
|
export default AccountInfo;
|