diff --git a/src/views/settings/avatar-settings/avatar-settings.module.css b/src/views/settings/avatar-settings/avatar-settings.module.css
new file mode 100644
index 00000000..cf018486
--- /dev/null
+++ b/src/views/settings/avatar-settings/avatar-settings.module.css
@@ -0,0 +1,63 @@
+.avatar {
+ width: 70px;
+ height: 70px;
+ border: 1px solid var(--border-text);
+}
+
+.avatar img {
+ width: 70px;
+ height: 70px;
+}
+
+.emptyAvatar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ height: 100%;
+ cursor: pointer;
+}
+
+.avatarSettingsForm {
+ padding-top: 10px;
+}
+
+.avatarSettingsForm input {
+ margin-bottom: 10px;
+ width: 200px;
+ padding: 2px;
+ box-shadow: var(--box-shadow-input);
+}
+
+.avatarSettingInput input {
+ display: block;
+}
+
+.settingTitle {
+ font-style: italic;
+ text-transform: lowercase;
+}
+
+.copyMessage {
+ padding-bottom: 10px;
+}
+
+.pasteSignature span {
+ display: block;
+}
+
+.pasteSignature button {
+ margin-left: 5px;
+}
+
+.state {
+ padding-top: 10px;
+}
+
+.copyMessage a {
+ color: var(--text-primary);
+}
+
+.copyMessage a:hover {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/src/views/settings/avatar-settings/avatar-settings.tsx b/src/views/settings/avatar-settings/avatar-settings.tsx
new file mode 100644
index 00000000..de99d812
--- /dev/null
+++ b/src/views/settings/avatar-settings/avatar-settings.tsx
@@ -0,0 +1,195 @@
+import { useEffect, useMemo, useState } from 'react';
+import { setAccount, useAccount, useAuthorAvatar } from '@plebbit/plebbit-react-hooks';
+import { useTranslation } from 'react-i18next';
+import styles from './avatar-settings.module.css';
+
+interface AvatarSettingsProps {
+ areSettingsShown?: boolean;
+ avatar?: any;
+ showSettings?: () => void;
+}
+
+const AvatarPreview = ({ avatar, showSettings, areSettingsShown }: AvatarSettingsProps) => {
+ 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' ? `${state}...` : undefined;
+
+ return (
+ <>
+
+ {imageUrl && state !== 'initializing' ? (
+

+ ) : (
+
+ {areSettingsShown ? '–' + t('hide') : '+' + t('add')}
+
+ )}
+
+
+ {stateText} {error?.message}
+
+ >
+ );
+};
+
+const AvatarSettings = () => {
+ const [showSettings, setShowSettings] = useState(false);
+
+ 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);
+ useEffect(() => {
+ if (hasCopied) {
+ setTimeout(() => setHasCopied(false), 2000);
+ }
+ }, [hasCopied]);
+
+ const copyMessageToSign = () => {
+ if (!chainTicker) {
+ return alert('missing chain ticker');
+ }
+ if (!tokenAddress) {
+ return alert('missing token address');
+ }
+ if (!tokenId) {
+ return alert('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);
+ };
+
+ // 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('missing chain ticker');
+ }
+ if (!tokenAddress) {
+ return alert('missing token address');
+ }
+ if (!tokenId) {
+ return alert('missing token id');
+ }
+ if (!signature) {
+ return alert('missing signature');
+ }
+ setAccount({ ...account, author: { ...account?.author, avatar } });
+ alert(`saved`);
+ };
+
+ return (
+
+
setShowSettings(!showSettings)} areSettingsShown={showSettings} />
+ {showSettings && (
+
+
+ chain ticker
+ setChainTicker(e.target.value)}
+ />
+
+
+ token address
+ setTokenAddress(e.target.value)}
+ />
+
+
+ token id
+ setTokenId(e.target.value)}
+ />
+
+
+
message to sign on{' '}
+
+ etherscan
+
+
+
+ paste signature
+ setSignature(e.target.value)}
+ />
+
+
+
+ )}
+
+ );
+};
+
+export default AvatarSettings;
diff --git a/src/views/settings/avatar-settings/index.ts b/src/views/settings/avatar-settings/index.ts
new file mode 100644
index 00000000..5e1715cd
--- /dev/null
+++ b/src/views/settings/avatar-settings/index.ts
@@ -0,0 +1 @@
+export { default } from './avatar-settings';
diff --git a/src/views/settings/settings.module.css b/src/views/settings/settings.module.css
index 2f41691d..b8d4d04c 100644
--- a/src/views/settings/settings.module.css
+++ b/src/views/settings/settings.module.css
@@ -75,26 +75,6 @@
text-decoration: underline;
}
-.avatar {
- width: 70px;
- height: 70px;
- border: 1px solid var(--border-text);
-}
-
-.avatar img {
- width: 70px;
- height: 70px;
-}
-
-.emptyAvatar {
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- height: 100%;
- cursor: pointer;
-}
-
.usernameInput input {
width: 200px;
padding: 2px;
diff --git a/src/views/settings/settings.tsx b/src/views/settings/settings.tsx
index 63100174..c38dbf33 100644
--- a/src/views/settings/settings.tsx
+++ b/src/views/settings/settings.tsx
@@ -1,9 +1,10 @@
import { useEffect, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
-import { setAccount, useAccount, useAuthorAvatar } from '@plebbit/plebbit-react-hooks';
+import { setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
import styles from './settings.module.css';
import AccountSettings from './account-settings';
import AddressSettings from './address-settings';
+import AvatarSettings from './avatar-settings';
import useTheme from '../../hooks/use-theme';
import packageJson from '../../../package.json';
import _ from 'lodash';
@@ -106,18 +107,6 @@ const ThemeSettings = () => {
);
};
-const AvatarSettings = () => {
- const { t } = useTranslation();
- const account = useAccount();
- const { imageUrl } = useAuthorAvatar({ author: account?.author });
-
- return (
-
-
{imageUrl ?

:
+{t('add')}}
-
- );
-};
-
const DisplayNameSetting = () => {
const { t } = useTranslation();
const account = useAccount();
@@ -233,7 +222,7 @@ const Settings = () => {