Merge pull request #90 from plebbit/development

Development
This commit is contained in:
plebeius.eth
2023-11-30 21:41:15 +01:00
committed by GitHub
15 changed files with 252 additions and 110 deletions

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 B

View File

@@ -18,7 +18,7 @@
.userDropdownButton {
background: none no-repeat scroll center right;
background-image: url("/public/assets/buttons/droparrowgray.gif");
background-image: var(--blue-dropdown-arrow);
padding-right: 21px;
margin-right: -5px;
cursor: pointer;
@@ -75,5 +75,32 @@
}
.selectedTextButton {
color: var(--text-secondary);
color: var(--green);
}
.dropdown {
float: left;
cursor: pointer;
position: relative;
}
.dropChoices {
top: 16px;
position: absolute;
left: 0px;
border: 1px solid var(--border-text);
background-color: var(--background);
color: var(--text-primary) ;
white-space: nowrap;
line-height: normal;
}
.dropdownChoice {
cursor: pointer;
padding: 2px 3px 1px 3px;
display: block;
}
.dropdownChoice:hover {
background-color: var(--background-primary);
}

View File

@@ -22,9 +22,11 @@ const AccountBar = () => {
const searchBarRef = useRef<HTMLDivElement>(null);
const searchBarButtonRef = useRef<HTMLDivElement>(null);
const [accountSelectVisible, setAccountSelectVisible] = useState(false);
const toggleAccountSelectVisible = () => setAccountSelectVisible(!accountSelectVisible);
const accountSelectRef = useRef<HTMLDivElement>(null);
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
const toggleAccountDropdown = () => setIsAccountDropdownOpen(!isAccountDropdownOpen);
const accountDropdownRef = useRef<HTMLDivElement>(null);
const accountDropdownChoicesRef = useRef<HTMLDivElement>(null);
const accountDropdownClass = isAccountDropdownOpen ? styles.visible : styles.hidden;
const accountSelectButtonRef = useRef<HTMLDivElement>(null);
let submitLink;
@@ -41,17 +43,22 @@ const AccountBar = () => {
const isOutsideSearchBar =
searchBarRef.current && !searchBarRef.current.contains(target) && searchBarButtonRef.current && !searchBarButtonRef.current.contains(target);
const isOutsideAccountSelect =
accountSelectRef.current && !accountSelectRef.current.contains(target) && accountSelectButtonRef.current && !accountSelectButtonRef.current.contains(target);
const isOutsideAccountDropdown =
accountDropdownRef.current &&
!accountDropdownRef.current.contains(target) &&
accountDropdownChoicesRef.current &&
!accountDropdownChoicesRef.current.contains(target);
const isOutsideAccountSelectButton = accountSelectButtonRef.current && !accountSelectButtonRef.current.contains(target);
if (isOutsideAccountSelectButton && isOutsideAccountDropdown) {
setIsAccountDropdownOpen(false);
}
if (isOutsideSearchBar) {
setSearchVisible(false);
}
if (isOutsideAccountSelect) {
setAccountSelectVisible(false);
}
},
[searchBarRef, accountSelectRef],
[searchBarRef, accountSelectButtonRef, accountDropdownRef, accountDropdownChoicesRef],
);
useEffect(() => {
@@ -61,39 +68,31 @@ const AccountBar = () => {
};
}, [handleClickOutside]);
const accountsOptions = accounts.map((account) => (
<option key={account?.id} value={account?.name}>
u/{account?.author?.shortAddress?.toLowerCase?.().substring(0, 8) || ''}
</option>
const accountDropdownOptions = accounts.map((account, index) => (
<span key={index} className={styles.dropdownChoice} onClick={() => setActiveAccount(account?.name)}>
{`u/${account?.author?.shortAddress}`}
</span>
));
accountsOptions[accountsOptions.length] = (
<option key='create' value='createAccount'>
accountDropdownOptions.push(
<Link key='create' to='#' className={styles.dropdownChoice} onClick={() => createAccount()}>
+create
</option>
</Link>,
);
const onAccountSelectChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === 'createAccount') {
createAccount();
} else {
setActiveAccount(e.target.value);
}
};
return (
<div className={styles.content}>
<span className={styles.user}>
<Link to='/user.eth' onClick={(e) => e.preventDefault()}>
{account?.author?.shortAddress}
</Link>
<span className={styles.userDropdownButton} ref={accountSelectButtonRef} onClick={toggleAccountSelectVisible} />
{accountSelectVisible && (
<span className={styles.accountSelect} ref={accountSelectRef}>
<select className={styles.select} onChange={onAccountSelectChange} value={account?.name}>
{accountsOptions}
</select>
</span>
<span className={styles.userDropdownButton} ref={accountSelectButtonRef} onClick={toggleAccountDropdown} />
{isAccountDropdownOpen && (
<div className={`${styles.dropdown} ${accountDropdownClass}`} ref={accountDropdownRef}>
<div className={`${styles.dropChoices} ${styles.accountDropChoices}`} ref={accountDropdownChoicesRef}>
{accountDropdownOptions}
</div>
</div>
)}
</span>
<span className={styles.submitButton}>

View File

@@ -118,13 +118,13 @@
.tabMenu li a {
padding: 2px 6px 0 6px;
text-decoration: none;
color: var(--text-secondary);
color: var(--green);
background-color: var(--background);
border-bottom: 1px solid var(--background);
}
.tabMenu li .selected {
color: var(--text-secondary);
color: var(--green);
background-color: var(--background);
border: 1px solid var(--border-primary);
border-bottom: 2px solid var(--background);

View File

@@ -13,8 +13,8 @@
}
.stampPending {
color: var(--label-pending);
border-color: var(--label-pending);
color: var(--yellow);
border-color: var(--yellow);
}
.stampFailed {

View File

@@ -94,12 +94,12 @@
}
.pinnedLink {
color: var(--text-secondary) !important;
color: var(--green) !important;
font-weight: bold !important;
}
.announcement {
color: var(--text-secondary) !important;
color: var(--green) !important;
}
.domain {
@@ -171,10 +171,10 @@
animation-iteration-count: 1;
}
.moderator {
color: var(--text-secondary) !important;
.moderator, .admin, .owner {
color: var(--green) !important;
}
.admin, .owner {
color: var(--red) !important;
.displayName {
color: var(--text-primary);
}

View File

@@ -120,16 +120,20 @@ const Post = ({ post, index }: PostProps) => {
)}
<p className={styles.tagline}>
{t('post_submitted')} {getFormattedTimeAgo(timestamp)} {t('post_by')}{' '}
{post?.author?.displayName && (
<span className={`${styles.displayName} ${moderatorClass}`}>{post?.author?.displayName} </span>
)}
<Link className={`${styles.authorAddressWrapper} ${moderatorClass}`} to={`u/${shortAuthorAddress}`} onClick={(e) => e.preventDefault()}>
<span className={styles.authorAddressHidden}>u/{post?.author?.shortAddress || shortAuthorAddress}</span>
<span className={`${styles.authorAddressVisible} ${authorAddressChanged && styles.authorAddressChanged}`}>u/{shortAuthorAddress}</span>
</Link>
{(isAuthorOwner || isAuthorAdmin || isAuthorModerator) && (
<span>
{' '}[
<span className={moderatorClass} title={subplebbit?.roles?.[post.author.address]?.role}>
{(isAuthorOwner && 'O') || (isAuthorAdmin && 'A') || (isAuthorModerator && 'M')}
</span>
{' '}
[
<span className={moderatorClass} title={subplebbit?.roles?.[post.author.address]?.role}>
{(isAuthorOwner && 'O') || (isAuthorAdmin && 'A') || (isAuthorModerator && 'M')}
</span>
]
</span>
)}
@@ -140,11 +144,9 @@ const Post = ({ post, index }: PostProps) => {
{' '}
p/{subplebbit?.shortAddress || subplebbitAddress}
</Link>
</>
)}
{pinned && (
<span className={styles.announcement}> - {t('announcement')}</span>
</>
)}
{pinned && <span className={styles.announcement}> - {t('announcement')}</span>}
</p>
<PostTools cid={cid} failed={state === 'failed'} replyCount={totalReplyCount} subplebbitAddress={subplebbitAddress} />
</div>

View File

@@ -125,10 +125,6 @@
color: var(--text-info);
}
.moderator {
color: var(--text-secondary) !important;
}
.admin, .owner {
color: var(--red) !important;
.moderator, .admin, .owner {
color: var(--green) !important;
}

View File

@@ -71,7 +71,7 @@ const ReplyMedia = ({ commentMediaInfo, content, expanded, hasThumbnail, link, l
const Reply = ({ reply, depth }: ReplyProps) => {
const { cid, content, downvoteCount, flair, link, linkHeight, linkWidth, removed, spoiler, subplebbitAddress, timestamp, upvoteCount } = reply || {};
const subplebbit = useSubplebbit({subplebbitAddress});
const subplebbit = useSubplebbit({ subplebbitAddress });
const isAuthorOwner = subplebbit?.roles?.[reply.author.address]?.role === 'owner';
const isAuthorAdmin = subplebbit?.roles?.[reply.author.address]?.role === 'admin';
@@ -147,6 +147,9 @@ const Reply = ({ reply, depth }: ReplyProps) => {
<div className={styles.entry}>
<p className={styles.tagline}>
<span className={styles.expand}>[]</span>
{reply?.author?.displayName && (
<span className={`${styles.author} ${moderatorClass}`}>{reply?.author?.displayName} </span>
)}
<Link
to={`/u/${shortAuthorAddress}`}
onClick={(e) => {
@@ -154,14 +157,14 @@ const Reply = ({ reply, depth }: ReplyProps) => {
}}
className={`${styles.author} ${moderatorClass}`}
>
{shortAuthorAddress}
{reply?.author?.displayName ? `u/${shortAuthorAddress}` : shortAuthorAddress}
</Link>
{(isAuthorOwner || isAuthorAdmin || isAuthorModerator) && (
<span className={styles.moderatorBrackets}>
[
<span className={moderatorClass} title={subplebbit?.roles?.[reply.author.address]?.role}>
{(isAuthorOwner && 'O') || (isAuthorAdmin && 'A') || (isAuthorModerator && 'M')}
</span>
<span className={moderatorClass} title={subplebbit?.roles?.[reply.author.address]?.role}>
{(isAuthorOwner && 'O') || (isAuthorAdmin && 'A') || (isAuthorModerator && 'M')}
</span>
]{' '}
</span>
)}

View File

@@ -48,11 +48,7 @@ const Sidebar = ({ address, cid, createdAt, description, downvoteCount = 0, role
const rulesList = (
<div className={styles.rules}>
<strong>Rules</strong>
<ol className={styles.rulesList}>
{rules?.map((rule, index) => (
<li key={index}>{rule}</li>
))}
</ol>
<ol className={styles.rulesList}>{rules?.map((rule, index) => <li key={index}>{rule}</li>)}</ol>
</div>
);

View File

@@ -17,12 +17,15 @@
.dropdown {
float: left;
padding-left: 5px;
cursor: pointer;
display: inline;
position: relative;
}
.subsDropdown {
padding-left: 5px;
}
.dropChoices {
top: 18px;
margin-top: 0;
@@ -106,7 +109,7 @@
}
.srList .srBar li .selected {
color: var(--text-secondary) !important;
color: var(--green) !important;
font-weight: bold;
}

View File

@@ -94,7 +94,7 @@ const TopBar = () => {
return (
<div className={styles.headerArea}>
<div className={styles.widthClip}>
<div className={styles.dropdown} ref={subsDropdownRef} onClick={toggleSubsDropdown}>
<div className={`${styles.dropdown} ${styles.subsDropdown}`} ref={subsDropdownRef} onClick={toggleSubsDropdown}>
<span className={styles.selectedTitle}>{t('topbar_my_subs')}</span>
<div className={`${styles.dropChoices} ${styles.subsDropChoices} ${subsDropdownClass}`} ref={subsDropdownChoicesRef}>
{subscriptions?.map((subscription: string, index: number) => (

View File

@@ -13,7 +13,7 @@
--text-primary: #369;
--button-border-primary: #c4dbf1;
--button-border-primary-hover: #879eb4;
--text-secondary: #228822;
--green: #228822;
--text-info: #888;
--border-primary: #5f99cf;
--border-text: gray;
@@ -27,13 +27,14 @@
--play-button-hover: url("/public/assets/buttons/play-button-hover.png");
--close-button: url("/public/assets/buttons/close-button.png");
--close-button-hover: url("/public/assets/buttons/close-button-hover.png");
--label-pending: goldenrod;
--yellow: goldenrod;
--red: red;
--button-large: url('/public/assets/buttons/button-large.png');
--button-large-hover: url('/public/assets/buttons/button-large-hover.png');
--button-large-nub: url('/public/assets/buttons/button-large-nub.png');
--button-large-hover-nub: url('/public/assets/buttons/button-large-nub-hover.png');
--box-shadow: inset 0px 1px 1px rgba(0,0,0,0.3),0px 1px 0px rgba(255,255,255,0.6);
--blue-dropdown-arrow: url("/public/assets/buttons/droparrowblue.gif");
}
:root .dark {
@@ -51,7 +52,7 @@
--text-primary: #c7c7c7;
--button-border-primary: #1f1f1f;
--button-border-primary-hover: #3e3e3e;
--text-secondary: #228822;
--green: #228822;
--text-info: #757575;
--border-primary: #1f1f1f;
--border-text: #3e3e3e;
@@ -65,11 +66,12 @@
--play-button-hover: url("/public/assets/buttons/play-button-hover.png");
--close-button: url("/public/assets/buttons/close-button-dark.png");
--close-button-hover: url("/public/assets/buttons/close-button-hover.png");
--label-pending: rgb(200, 171, 0);
--yellow: rgb(200, 171, 0);
--red: rgb(200, 0, 0);
--button-large: url('/public/assets/buttons/button-large-dark.png');
--button-large-hover: url('/public/assets/buttons/button-large-hover-dark.png');
--button-large-nub: url('/public/assets/buttons/button-large-nub-dark.png');
--button-large-hover-nub: url('/public/assets/buttons/button-large-nub-hover-dark.png');
--box-shadow: none;
--blue-dropdown-arrow: url("/public/assets/buttons/droparrowgray.gif");
}

View File

@@ -60,12 +60,22 @@
padding-left: 5px;
font-style: italic;
text-transform: capitalize;
color: var(--text-secondary);
color: var(--green);
}
button {
.checkCryptoAddress {
margin-top: 5px;
margin-left: -5px;
}
.categorySettings button {
text-transform: capitalize;
margin-left: 5px;
cursor: pointer;
}
.categorySettings select {
cursor: pointer;
}
.accountButtons button {
@@ -102,8 +112,8 @@ button {
max-height: 60vh;
}
input {
width: 35vw !important;
.categorySettings input {
width: 35vw;
}
}
@@ -113,4 +123,16 @@ button {
position: relative;
bottom: 2px;
margin-bottom: -2px;
}
.resolvedMessageSuccess {
color: var(--green);
}
.resolvedMessageFailed {
color: var(--red);
}
.resolvedMessageResolving {
color: var(--yellow);
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createAccount, deleteAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
import { createAccount, deleteAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts, useResolvedAuthorAddress } from '@plebbit/plebbit-react-hooks';
import stringify from 'json-stringify-pretty-compact';
import useTheme from '../../hooks/use-theme';
import styles from './settings.module.css';
@@ -44,31 +44,120 @@ const ThemeSettings = () => {
};
const ProfileSettings = () => {
const cryptoAddressInfo = () => {
alert('A crypto address is more readable than a long string of characters. It can be used to send you crypto directly.\n\nChange your account address to an ENS name you own: in your ENS name page on ens.domains, click on "Records", "Edit Records", "Add record", add "plebbit-author-address" as record name, add your full address as value (you can copy it from your account data) and save.');
const account = useAccount();
const [username, setUsername] = useState(account?.author.displayName || '');
const [savedUsername, setSavedUsername] = useState(false);
useEffect(() => {
if (savedUsername) {
setTimeout(() => {
setSavedUsername(false);
}, 2000);
}
}, [savedUsername]);
const saveUsername = async () => {
try {
await setAccount({ ...account, author: { ...account?.author, displayName: username } });
} catch (error) {
if (error instanceof Error) {
alert(error.message);
console.log(error);
} else {
console.error('An unknown error occurred:', error);
}
}
setSavedUsername(true);
};
const [cryptoAddress, setCryptoAddress] = useState(account?.author.displayName || '');
const [savedCryptoAddress, setSavedCryptoAddress] = useState(false);
const [checkCryptoAddress, setCheckCryptoAddress] = useState(false);
const author = { ...account?.author, address: cryptoAddress };
const { state, error, chainProvider } = useResolvedAuthorAddress({ author, cache: false });
const resolvedAddressInfoMessageClass = `${state === 'succeeded' ? styles.resolvedMessageSuccess : state === 'failed' ? styles.resolvedMessageFailed : state === 'resolving' ? styles.resolvedMessageResolving : ''}`
const resolvedAddressInfoMessage = useMemo(() => {
if (state === 'succeeded') {
return 'crypto address resolved successfully';
} else if (state === 'failed') {
if (error instanceof Error) {
return `failed to resolve crypto address, ${error.message}`;
} else {
return 'cannot resolve crypto address';
}
} else if (state === 'resolving') {
return `resolving from ${chainProvider?.urls}`;
} else {
return '';
}
}, [state, error, chainProvider]);
const cryptoAddressInfo = () => {
alert(
'Change your account address to an ENS name you own: in your ENS name page on ens.domains, click on "Records", "Edit Records", "Add record", add "plebbit-author-address" as record name, add your full address as value (you can copy it from your account data) and save.',
);
};
const saveCryptoAddress = async () => {
if (state !== 'succeeded') {
alert('Cannot save crypto address, it is not resolved');
return;
}
try {
await setAccount({ ...account, author: { ...account?.author, address: cryptoAddress } });
} catch (error) {
if (error instanceof Error) {
alert(error.message);
console.log(error);
} else {
console.error('An unknown error occurred:', error);
}
}
setSavedCryptoAddress(true);
}
useEffect(() => {
if (savedCryptoAddress) {
setTimeout(() => {
setSavedCryptoAddress(false);
}, 2000);
}
}, [savedCryptoAddress]);
return (
<span className={styles.categorySettings}>
<span className={styles.settingTitle}>username</span>
<span className={styles.settingTitle}>display name</span>
<div className={styles.usernameInput}>
<input type='text' placeholder='My Name' />
<button className={styles.button}>save</button>
<span className={styles.saved}>Saved.</span>
<input type='text' placeholder='My Name' value={username} onChange={(e) => setUsername(e.target.value)} />
<button className={styles.button} onClick={saveUsername}>
save
</button>
{savedUsername && <span className={styles.saved}>Saved.</span>}
</div>
<div className={styles.cryptoAddressSetting}>
<span className={styles.settingTitle}>crypto address</span>
<button className={styles.infoButton} onClick={cryptoAddressInfo}>?</button>
<button className={styles.infoButton} onClick={cryptoAddressInfo}>
?
</button>
<div className={styles.usernameInput}>
<input type='text' placeholder='address.eth' />
<button className={styles.button}>save</button>
<button className={styles.button}>check</button>
<span className={styles.saved}>Saved.</span>
<input type='text' placeholder='address.eth' onChange={(e) => setCryptoAddress(e.target.value)} />
<button className={styles.button} onClick={saveCryptoAddress}>save</button>
{savedCryptoAddress && <span className={styles.saved}>Saved.</span>}
</div>
<div className={styles.checkCryptoAddress}>
<button className={styles.button} onClick={() => setCheckCryptoAddress(true)}>check</button>{' '}
{checkCryptoAddress ? (
<span className={resolvedAddressInfoMessageClass}>{resolvedAddressInfoMessage}</span>
) : 'if the crypto address is resolved p2p'}
</div>
<div></div>
</div>
</span>
);
}
};
const AccountSettings = () => {
const account = useAccount();
@@ -76,17 +165,17 @@ const AccountSettings = () => {
const [text, setText] = useState('');
const [switchToLastAccount, setSwitchToLastAccount] = useState(false);
const accountJson = useMemo(() => stringify({account: {...account, plebbit: undefined, karma: undefined, unreadNotificationCount: undefined}}), [account])
const accountJson = useMemo(() => stringify({ account: { ...account, plebbit: undefined, karma: undefined, unreadNotificationCount: undefined } }), [account]);
const accountsOptions = accounts.map((account) => (
<option key={account?.id} value={account?.name}>
u/{account?.author?.shortAddress?.toLowerCase?.().substring(0, 8) || ''}
u/{account?.author?.shortAddress}
</option>
));
useEffect(() => {
setText(accountJson)
}, [accountJson])
setText(accountJson);
}, [accountJson]);
useEffect(() => {
if (switchToLastAccount && accounts.length > 0) {
@@ -108,25 +197,25 @@ const AccountSettings = () => {
}
}
setSwitchToLastAccount(true);
}
};
const _deleteAccount = (accountName: string) => {
if (!accountName) {
return
return;
} else if (window.confirm(`Are you sure you want to delete ${accountName}?`)) {
deleteAccount(accountName);
setSwitchToLastAccount(true);
} else {
return
return;
}
}
};
const saveAccount = async () => {
try {
const newAccount = JSON.parse(text).account
const newAccount = JSON.parse(text).account;
// force keeping the same id, makes it easier to copy paste
await setAccount({...newAccount, id: account?.id})
alert(`Saved ${newAccount.name}`)
await setAccount({ ...newAccount, id: account?.id });
alert(`Saved ${newAccount.name}`);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
@@ -135,14 +224,14 @@ const AccountSettings = () => {
console.error('An unknown error occurred:', error);
}
}
}
};
const _importAccount = async () => {
try {
const newAccount = JSON.parse(text)
await importAccount(text)
const newAccount = JSON.parse(text);
await importAccount(text);
setSwitchToLastAccount(true);
alert(`Imported ${newAccount.account?.name}`)
alert(`Imported ${newAccount.account?.name}`);
} catch (error) {
if (error instanceof Error) {
alert(error.message);
@@ -151,16 +240,19 @@ const AccountSettings = () => {
console.error('An unknown error occurred:', error);
}
}
}
};
return (
<span className={styles.categorySettings}>
<div className={styles.accountAddress}>
<select value={account?.name} onChange={(e) => setActiveAccount(e.target.value)}>{accountsOptions}</select> is the current account
<select value={account?.name} onChange={(e) => setActiveAccount(e.target.value)}>
{accountsOptions}
</select>{' '}
is the current account
</div>
<span className={styles.settingTitle}>account data</span>
<div className={styles.accountData}>
<textarea className={styles.textarea} value={text} onChange={(e) => setText(e.target.value)} autoCorrect="off" autoComplete='off' spellCheck='false' />
<textarea className={styles.textarea} value={text} onChange={(e) => setText(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' />
<div className={styles.accountButtons}>
<div>
<button onClick={saveAccount}>Save</button> or <button onClick={() => setText(accountJson)}>Reset</button> changes
@@ -169,7 +261,7 @@ const AccountSettings = () => {
<button onClick={() => _deleteAccount(account?.name)}>Delete</button> this account
</div>
<div>
<button onClick={_importAccount}>Import</button> another account by pasting its data above
<button onClick={_importAccount}>Import</button> other account data
</div>
<div>
<button onClick={_createAccount}>Create</button> a new account