mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(settings): add avatar setting
This commit is contained in:
@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Comment, useComment } from '@plebbit/plebbit-react-hooks';
|
||||
import { useFloating, offset, shift, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||
import { useFloating, offset, size, autoUpdate, Placement } from '@floating-ui/react';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import { isAllView } from '../../lib/utils/view-utils';
|
||||
|
||||
@@ -130,6 +130,7 @@ const AccountSettings = () => {
|
||||
await importAccount(fileContent);
|
||||
setSwitchToLastAccount(true);
|
||||
alert(`Imported ${newAccount.account?.name}`);
|
||||
window.location.reload();
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
.avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border: 1px solid #aaa;
|
||||
background-color: var(--avatar-background-color, white);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.avatarPreview {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.emptyAvatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
color: var(--avatar-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatarSettingsForm {
|
||||
padding: 10px 0 15px 10px;
|
||||
}
|
||||
|
||||
.avatarSettingsForm input {
|
||||
width: 200px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.avatarSettingInput input {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.avatarSettingInput a {
|
||||
color: var(--link-primary);
|
||||
}
|
||||
|
||||
.avatarSettingInput a {
|
||||
color: var(--post-link-text-color);
|
||||
text-decoration: var(--post-content-link-text-decoration);
|
||||
}
|
||||
|
||||
.avatarSettingInput a:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.settingTitle {
|
||||
font-style: italic;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.copyMessage {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.pasteSignature span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pasteSignature button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.state {
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
max-width: 300px;
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
}
|
||||
|
||||
.copyMessage a {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.copyMessage a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { setAccount, useAccount, useAuthorAvatar } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './avatar-settings.module.css';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import LoadingEllipsis from '../../loading-ellipsis';
|
||||
|
||||
const AvatarPreview = ({ avatar }: any) => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount();
|
||||
let author = useMemo(() => ({ ...account?.author, avatar }), [account, avatar]);
|
||||
|
||||
const { imageUrl, state, error } = useAuthorAvatar({ author });
|
||||
|
||||
// if avatar already set, and user hasn't typed anything yet, preview already set author
|
||||
if (account?.author?.avatar && !avatar?.chainTicker && !avatar?.address && !avatar?.id && !avatar?.signature) {
|
||||
author = account.author;
|
||||
}
|
||||
|
||||
// not enough data to preview yet
|
||||
if (!author?.avatar?.address && !author?.avatar?.signature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateText = state !== 'succeeded' ? <LoadingEllipsis string={state} /> : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.avatar}>
|
||||
{imageUrl && state !== 'initializing' ? <img src={imageUrl} alt='' /> : <span className={styles.emptyAvatar}>{t('none')}</span>}
|
||||
</div>
|
||||
{state !== 'succeeded' && account?.author?.avatar && (
|
||||
<div className={styles.state}>
|
||||
{stateText} {error?.message}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AvatarSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount();
|
||||
const authorAddress = account?.author?.address;
|
||||
const [chainTicker, setChainTicker] = useState(account?.author?.avatar?.chainTicker);
|
||||
const [tokenAddress, setTokenAddress] = useState(account?.author?.avatar?.address);
|
||||
const [tokenId, setTokenId] = useState(account?.author?.avatar?.id);
|
||||
const [timestamp, setTimestamp] = useState(account?.author?.avatar?.timestamp);
|
||||
const [signature, setSignature] = useState(account?.author?.avatar?.signature?.signature);
|
||||
|
||||
const getNftMessageToSign = (authorAddress: string, timestamp: number, tokenAddress: string, tokenId: string) => {
|
||||
let messageToSign: any = {};
|
||||
// the property names must be in this order for the signature to match
|
||||
// insert props one at a time otherwise babel/webpack will reorder
|
||||
messageToSign.domainSeparator = 'plebbit-author-avatar';
|
||||
messageToSign.authorAddress = authorAddress;
|
||||
messageToSign.timestamp = timestamp;
|
||||
messageToSign.tokenAddress = tokenAddress;
|
||||
messageToSign.tokenId = String(tokenId); // must be a type string, not number
|
||||
// use plain JSON so the user can read what he's signing
|
||||
messageToSign = JSON.stringify(messageToSign);
|
||||
return messageToSign;
|
||||
};
|
||||
|
||||
const [hasCopied, setHasCopied] = useState(false);
|
||||
|
||||
const copyMessageToSign = () => {
|
||||
if (!chainTicker) {
|
||||
return alert(t('missing_chain_ticker'));
|
||||
}
|
||||
if (!tokenAddress) {
|
||||
return alert(t('missing_token_address'));
|
||||
}
|
||||
if (!tokenId) {
|
||||
return alert(t('missing_token_id'));
|
||||
}
|
||||
const newTimestamp = Math.floor(Date.now() / 1000);
|
||||
const messageToSign = getNftMessageToSign(authorAddress, newTimestamp, tokenAddress, tokenId);
|
||||
// update timestamp every time the user gets a new message to sign
|
||||
setTimestamp(newTimestamp);
|
||||
navigator.clipboard.writeText(messageToSign);
|
||||
setHasCopied(true);
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// how to resolve and verify NFT signatures https://github.com/plebbit/plebbit-js/blob/master/docs/nft.md
|
||||
const avatar = {
|
||||
chainTicker: chainTicker?.toLowerCase() || account?.author?.avatar?.chainTicker,
|
||||
timestamp,
|
||||
address: tokenAddress || account?.author?.avatar?.address,
|
||||
id: tokenId || account?.author?.avatar?.id,
|
||||
signature: {
|
||||
signature: signature || account?.author?.avatar?.signature?.signature,
|
||||
type: 'eip191',
|
||||
},
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (!chainTicker) {
|
||||
return alert(t('missing_chain_ticker'));
|
||||
}
|
||||
if (!tokenAddress) {
|
||||
return alert(t('missing_token_address'));
|
||||
}
|
||||
if (!tokenId) {
|
||||
return alert(t('missing_token_id'));
|
||||
}
|
||||
if (!signature) {
|
||||
return alert(t('missing_signature'));
|
||||
}
|
||||
setAccount({ ...account, author: { ...account?.author, avatar } });
|
||||
alert(`saved`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.avatarSettings}>
|
||||
<AvatarPreview avatar={avatar} />
|
||||
<div className={styles.avatarSettingsForm}>
|
||||
<div className={styles.avatarSettingInput}>
|
||||
<span className={styles.settingTitle}>{t('chain_ticker')}</span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='eth/sol/avax'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck='false'
|
||||
defaultValue={account?.author?.avatar?.chainTicker}
|
||||
onChange={(e) => setChainTicker(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.avatarSettingInput}>
|
||||
<span className={styles.settingTitle}>
|
||||
<Trans
|
||||
i18nKey='token_address_whitelist'
|
||||
shouldUnescape={true}
|
||||
components={{
|
||||
1: (
|
||||
<Link
|
||||
to='https://github.com/plebbit/plebbit-react-hooks/blob/557cc3f40b5933a00553ed9c0bc310d2cd7a3b52/src/hooks/authors/author-avatars.ts#L133'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='0x...'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck='false'
|
||||
defaultValue={account?.author?.avatar?.address}
|
||||
onChange={(e) => setTokenAddress(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.avatarSettingInput}>
|
||||
<span className={styles.settingTitle}>{t('token_id')}</span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='Token ID'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck='false'
|
||||
defaultValue={account?.author?.avatar?.id}
|
||||
onChange={(e) => setTokenId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.copyMessage}>
|
||||
<Trans
|
||||
i18nKey='copy_message_etherscan'
|
||||
values={{ copy: hasCopied ? t('copied') : t('copy') }}
|
||||
components={{
|
||||
1: <button onClick={copyMessageToSign} />,
|
||||
// eslint-disable-next-line
|
||||
2: <a href='https://etherscan.io/verifiedSignatures' target='_blank' rel='noopener noreferrer' />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.pasteSignature}>
|
||||
<span className={styles.settingTitle}>{t('paste_signature')}</span>
|
||||
<input
|
||||
type='text'
|
||||
placeholder='0x...'
|
||||
autoCorrect='off'
|
||||
autoComplete='off'
|
||||
spellCheck='false'
|
||||
defaultValue={account?.author?.avatar?.signature?.signature}
|
||||
onChange={(e) => setSignature(e.target.value)}
|
||||
/>
|
||||
<button onClick={save}>{t('save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarSettings;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './avatar-settings';
|
||||
@@ -193,6 +193,7 @@ const PlebbitOptions = () => {
|
||||
},
|
||||
});
|
||||
alert('Options saved.');
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
alert('Error saving options: ' + e.message);
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
padding: 2px 4px 3px;
|
||||
}
|
||||
|
||||
.settingsModal input[type="text"]:focus, .settingsModal textarea:focus {
|
||||
border: var(--reply-modal-field-input-border-focus, revert);
|
||||
}
|
||||
|
||||
.settingsModal {
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './settings-modal.module.css';
|
||||
import AccountSettings from './account-settings';
|
||||
import AvatarSettings from './avatar-settings';
|
||||
import BlockedAddressesSetting from './blocked-addresses-setting';
|
||||
import CryptoAddressSetting from './crypto-address-setting';
|
||||
import CryptoWalletsSetting from './crypto-wallets-setting';
|
||||
@@ -19,6 +20,7 @@ const SettingsModal = () => {
|
||||
|
||||
const [showInterfaceSettings, setShowInterfaceSettings] = useState(false);
|
||||
const [showAccountSettings, setShowAccountSettings] = useState(false);
|
||||
const [showAvatarSettings, setShowAvatarSettings] = useState(false);
|
||||
const [showCryptoAddressSetting, setShowCryptoAddressSetting] = useState(false);
|
||||
const [showCryptoWalletSettings, setShowCryptoWalletSettings] = useState(false);
|
||||
const [showBlockedAddressesSetting, setShowBlockedAddressesSetting] = useState(false);
|
||||
@@ -30,6 +32,7 @@ const SettingsModal = () => {
|
||||
setExpandAll(newExpandState);
|
||||
setShowInterfaceSettings(newExpandState);
|
||||
setShowAccountSettings(newExpandState);
|
||||
setShowAvatarSettings(newExpandState);
|
||||
setShowCryptoAddressSetting(newExpandState);
|
||||
setShowCryptoWalletSettings(newExpandState);
|
||||
setShowBlockedAddressesSetting(newExpandState);
|
||||
@@ -61,6 +64,13 @@ const SettingsModal = () => {
|
||||
</label>
|
||||
</div>
|
||||
{showAccountSettings && <AccountSettings />}
|
||||
<div className={`${styles.setting} ${styles.category}`}>
|
||||
<label onClick={() => setShowAvatarSettings(!showAvatarSettings)}>
|
||||
<span className={showAvatarSettings ? styles.hideButton : styles.showButton} />
|
||||
{t('avatar')}
|
||||
</label>
|
||||
</div>
|
||||
{showAvatarSettings && <AvatarSettings />}
|
||||
<div className={`${styles.setting} ${styles.category}`}>
|
||||
<label onClick={() => setShowCryptoAddressSetting(!showCryptoAddressSetting)}>
|
||||
<span className={showCryptoAddressSetting ? styles.hideButton : styles.showButton} />
|
||||
|
||||
@@ -26,6 +26,7 @@ const SubplebbitDescription = ({ avatarUrl, createdAt, description, shortAddress
|
||||
author: { displayName: `## ${t('board_mods')}` },
|
||||
content: isInAllView ? multisubMetadata?.description : description,
|
||||
link: avatarUrl,
|
||||
replyCount: 0,
|
||||
title: t('welcome_to_board', {
|
||||
board: isInAllView ? multisubMetadata?.title : title || `p/${shortAddress}`,
|
||||
interpolation: { escapeValue: false },
|
||||
|
||||
@@ -17,6 +17,7 @@ const SubplebbitRules = ({ subplebbitAddress, createdAt, rules }: RulesPostProps
|
||||
timestamp: createdAt,
|
||||
author: { displayName: `## ${t('board_mods')}` },
|
||||
content,
|
||||
replyCount: 0,
|
||||
title: _.capitalize(t('rules')),
|
||||
pinned: true,
|
||||
locked: true,
|
||||
|
||||
Reference in New Issue
Block a user