From 209f7a3fad70f777f2b8fdec958b2db3666dc2e7 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sat, 19 Aug 2023 16:41:25 +0300 Subject: [PATCH] settings functional components --- src/js/views/settings/Account.tsx | 175 ++++++++++--------- src/js/views/settings/Content.tsx | 149 ++++++++--------- src/js/views/settings/Dev.tsx | 107 ++++++------ src/js/views/settings/SettingsContent.tsx | 34 ++-- src/js/views/settings/SettingsMenu.tsx | 68 ++++---- src/js/views/settings/SocialNetwork.tsx | 194 ++++++++++------------ 6 files changed, 345 insertions(+), 382 deletions(-) diff --git a/src/js/views/settings/Account.tsx b/src/js/views/settings/Account.tsx index 37f23914..569d1d54 100644 --- a/src/js/views/settings/Account.tsx +++ b/src/js/views/settings/Account.tsx @@ -1,7 +1,9 @@ import { nip19 } from 'nostr-tools'; +import { useCallback, useState } from 'preact/hooks'; import { route } from 'preact-router'; -import Component from '../../BaseComponent'; +import Show from '@/components/helpers/Show.tsx'; + import Copy from '../../components/buttons/Copy'; import Events from '../../nostr/Events'; import Key from '../../nostr/Key'; @@ -10,22 +12,24 @@ import { translate as t } from '../../translations/Translation.mjs'; import Helpers from '../../utils/Helpers.tsx'; import { ExistingAccountLogin } from '../Login'; -export default class Account extends Component { - onLogoutClick(hasPriv) { +const Account = () => { + const [showSwitchAccount, setShowSwitchAccount] = useState(false); + + const onLogoutClick = useCallback((hasPriv) => { if (hasPriv) { route('/logout'); // confirmation screen } else { Session.logOut(); } - } + }, []); - async onExtensionLoginClick(e) { + const onExtensionLoginClick = async (e) => { e.preventDefault(); const rpub = await window.nostr.getPublicKey(); Key.login({ rpub }); - } + }; - deleteAccount() { + const deleteAccount = useCallback(() => { if (confirm(`${t('delete_account')}?`)) { Events.publish({ kind: 0, @@ -39,90 +43,79 @@ export default class Account extends Component { Session.logOut(); }, 1000); } + }, []); + + const myPrivHex = Key.getPrivKey(); + let myPriv32; + if (myPrivHex) { + myPriv32 = nip19.nsecEncode(myPrivHex); } + const myPub = Key.getPubKey(); + const myNpub = nip19.npubEncode(myPub); + const hasPriv = !!Key.getPrivKey(); - render() { - const myPrivHex = Key.getPrivKey(); - let myPriv32; - if (myPrivHex) { - // eslint-disable-next-line no-undef - myPriv32 = nip19.nsecEncode(myPrivHex); - } - const myPub = Key.getPubKey(); - // eslint-disable-next-line no-undef - const myNpub = nip19.npubEncode(myPub); + return ( +
+

{t('account')}

+ +

+ {t('save_backup_of_privkey_first')} {t('otherwise_cant_log_in_again')} +

+
+
+ + +
+ +

+ +

+

+ + {t('nostr_extension_login')} + +

+
+

{t('public_key')}

+

+ {myNpub} +

+
+ + +
+

{t('private_key')}

+
+ + <> + + + + + +

{t('private_key_not_present_good')}

+
+
+ +

{t('private_key_warning')}

+
+ +

{t('delete_account')}

+

+ +

+
+
+ ); +}; - const hasPriv = !!Key.getPrivKey(); - return ( - <> -
-

{t('account')}

- {hasPriv ? ( -

- {t('save_backup_of_privkey_first')} {t('otherwise_cant_log_in_again')} -

- ) : null} -
- - -
- {this.state.showSwitchAccount && ( - <> -

- -

-

- this.onExtensionLoginClick(e)}> - {t('nostr_extension_login')} - -

- - )} - -

{t('public_key')}

-

- {myNpub} -

-
- - -
-

{t('private_key')}

-
- {myPrivHex ? ( - <> - - - - ) : ( -

{t('private_key_not_present_good')}

- )} -
- {myPrivHex ?

{t('private_key_warning')}

: ''} - - {Helpers.isStandalone() ? ( - <> -

{t('delete_account')}

-

- -

- - ) : null} -
- - ); - } -} +export default Account; diff --git a/src/js/views/settings/Content.tsx b/src/js/views/settings/Content.tsx index beee3dc8..26bafb04 100644 --- a/src/js/views/settings/Content.tsx +++ b/src/js/views/settings/Content.tsx @@ -1,86 +1,79 @@ -import Component from '../../BaseComponent'; +import { useEffect, useState } from 'react'; + import localState from '../../LocalState'; import { translate as t } from '../../translations/Translation.mjs'; -export default class Content extends Component { - constructor() { - super(); - this.state = { - settings: {}, - }; - } +export default function Content() { + const [settings, setSettings] = useState({}); - componentDidMount() { - localState.get('settings').on(this.inject()); - } + useEffect(() => { + const unsubscribe = localState.get('settings').on((newSettings) => { + setSettings(newSettings); + }); + return () => unsubscribe(); + }, []); - render() { - const noteSettings = [ - // { setting: 'enableMarkdown', label: 'Markdown' }, - { setting: 'loadReactions', label: 'Replies and reactions' }, - { setting: 'showLikes', label: 'Likes' }, - { setting: 'showZaps', label: 'Zaps' }, - { setting: 'showReposts', label: 'Reposts' }, - ]; - // TODO get these from the embeds themselves - const mediaSettings = [ - { setting: 'enableImages', label: 'Images' }, - { setting: 'enableAudio', label: 'Audio' }, - { setting: 'enableVideo', label: 'Videos' }, - { setting: 'autoplayVideos', label: 'Autoplay videos' }, - { setting: 'enableAppleMusic', label: 'Apple Music' }, - { setting: 'enableInstagram', label: 'Instagram' }, - { setting: 'enableSoundCloud', label: 'SoundCloud' }, - { setting: 'enableSpotify', label: 'Spotify' }, - { setting: 'enableTidal', label: 'Tidal' }, - { setting: 'enableTiktok', label: 'TikTok' }, - { setting: 'enableTwitch', label: 'Twitch' }, - { setting: 'enableTwitter', label: 'Twitter' }, - { setting: 'enableYoutube', label: 'YouTube' }, - { setting: 'enableWavlake', label: 'Wavlake' }, - ]; - return ( - <> -
-

{t('content')}

+ const noteSettings = [ + // { setting: 'enableMarkdown', label: 'Markdown' }, + { setting: 'loadReactions', label: 'Replies and reactions' }, + { setting: 'showLikes', label: 'Likes' }, + { setting: 'showZaps', label: 'Zaps' }, + { setting: 'showReposts', label: 'Reposts' }, + ]; -

{t('notes')}

- {noteSettings.map(({ setting, label }) => ( -

- - localState - .get('settings') - .get(setting) - .put(!(this.state.settings[setting] !== false)) - } - id={setting} - /> - -

- ))} + const mediaSettings = [ + { setting: 'enableImages', label: 'Images' }, + { setting: 'enableAudio', label: 'Audio' }, + { setting: 'enableVideo', label: 'Videos' }, + { setting: 'autoplayVideos', label: 'Autoplay videos' }, + { setting: 'enableAppleMusic', label: 'Apple Music' }, + { setting: 'enableInstagram', label: 'Instagram' }, + { setting: 'enableSoundCloud', label: 'SoundCloud' }, + { setting: 'enableSpotify', label: 'Spotify' }, + { setting: 'enableTidal', label: 'Tidal' }, + { setting: 'enableTiktok', label: 'TikTok' }, + { setting: 'enableTwitch', label: 'Twitch' }, + { setting: 'enableTwitter', label: 'Twitter' }, + { setting: 'enableYoutube', label: 'YouTube' }, + { setting: 'enableWavlake', label: 'Wavlake' }, + ]; -

{t('media')}

- {mediaSettings.map(({ setting, label }) => ( -

- - localState - .get('settings') - .get(setting) - .put(!(this.state.settings[setting] !== false)) - } - id={setting} - /> - -

- ))} -
- - ); - } + const handleChange = (setting) => { + localState + .get('settings') + .get(setting) + .put(!(settings[setting] !== false)); + }; + + return ( +
+

{t('content')}

+ +

{t('notes')}

+ {noteSettings.map(({ setting, label }) => ( +

+ handleChange(setting)} + id={setting} + /> + +

+ ))} + +

{t('media')}

+ {mediaSettings.map(({ setting, label }) => ( +

+ handleChange(setting)} + id={setting} + /> + +

+ ))} +
+ ); } diff --git a/src/js/views/settings/Dev.tsx b/src/js/views/settings/Dev.tsx index 499340fb..55048430 100644 --- a/src/js/views/settings/Dev.tsx +++ b/src/js/views/settings/Dev.tsx @@ -1,61 +1,58 @@ -import Component from '../../BaseComponent'; +import { useEffect, useState } from 'react'; + import localState from '../../LocalState'; -export default class DevSettings extends Component { - render() { - const renderCheckbox = (key, label, defaultValue) => ( -

- { - const checked = (e.target as HTMLInputElement).checked; - localState.get('dev').get(key).put(checked); - }} - /> - -

- ); - // TODO reset button +export default function DevSettings() { + const [state, setState] = useState({}); - const checkboxes = [ - { - key: 'logSubscriptions', - label: 'Log RelayPool subscriptions', - defaultValue: false, - }, - { - key: 'indexedDbSave', - label: 'Save events to IndexedDB', - defaultValue: true, - }, - { - key: 'indexedDbLoad', - label: 'Load events from IndexedDB', - defaultValue: true, - }, - { - key: 'askEventsFromRelays', - label: 'Ask events from relays', - defaultValue: true, - }, - ]; + useEffect(() => { + const unsubscribe = localState.get('dev').on((data) => setState(data)); + return () => unsubscribe(); + }, []); - return ( - <> -
-

Developer

-

Settings intended for Iris developers.

- {checkboxes.map(({ key, label, defaultValue }) => - renderCheckbox(key, label, defaultValue), - )} -
- - ); - } + const renderCheckbox = (key, label, defaultValue) => ( +

+ { + const checked = (e.target as HTMLInputElement).checked; + localState.get('dev').get(key).put(checked); + }} + /> + +

+ ); - componentDidMount() { - localState.get('dev').on(this.sub((data) => this.setState(data))); - } + const checkboxes = [ + { + key: 'logSubscriptions', + label: 'Log RelayPool subscriptions', + defaultValue: false, + }, + { + key: 'indexedDbSave', + label: 'Save events to IndexedDB', + defaultValue: true, + }, + { + key: 'indexedDbLoad', + label: 'Load events from IndexedDB', + defaultValue: true, + }, + { + key: 'askEventsFromRelays', + label: 'Ask events from relays', + defaultValue: true, + }, + ]; + + return ( +
+

Developer

+

Settings intended for Iris developers.

+ {checkboxes.map(({ key, label, defaultValue }) => renderCheckbox(key, label, defaultValue))} +
+ ); } diff --git a/src/js/views/settings/SettingsContent.tsx b/src/js/views/settings/SettingsContent.tsx index bc8f359c..19a1cd93 100644 --- a/src/js/views/settings/SettingsContent.tsx +++ b/src/js/views/settings/SettingsContent.tsx @@ -1,9 +1,7 @@ -import Component from '../../BaseComponent'; - import Account from './Account.js'; import Appearance from './Appearance'; import Backup from './Backup'; -import Content from './Content'; +import ContentPage from './Content'; import Dev from './Dev'; import IrisAccount from './IrisAccount.js'; import Language from './Language'; @@ -11,14 +9,13 @@ import Network from './Network.js'; import Payments from './Payments'; import SocialNetwork from './SocialNetwork'; -export default class SettingsContent extends Component { - content = ''; - pages = { +const SettingsContent = (props) => { + const pages = { account: Account, network: Network, appearance: Appearance, language: Language, - content: Content, + content: ContentPage, payments: Payments, backup: Backup, social_network: SocialNetwork, @@ -26,16 +23,13 @@ export default class SettingsContent extends Component { dev: Dev, }; - constructor() { - super(); - this.content = 'home'; - } - render() { - const Content = this.pages[this.props.id] || this.pages.account; - return ( -
- -
- ); - } -} + const SelectedContent = pages[props.id] || pages.account; + + return ( +
+ +
+ ); +}; + +export default SettingsContent; diff --git a/src/js/views/settings/SettingsMenu.tsx b/src/js/views/settings/SettingsMenu.tsx index 63830fa4..ce3f84a0 100644 --- a/src/js/views/settings/SettingsMenu.tsx +++ b/src/js/views/settings/SettingsMenu.tsx @@ -1,7 +1,6 @@ import { LanguageIcon } from '@heroicons/react/24/solid'; import { route } from 'preact-router'; -import Component from '../../BaseComponent'; import Show from '../../components/helpers/Show'; import localState from '../../LocalState'; import { translate as t } from '../../translations/Translation.mjs'; @@ -22,41 +21,42 @@ if (['iris.to', 'beta.iris.to', 'localhost'].includes(window.location.hostname)) SETTINGS.iris_account = 'iris.to'; } -export default class SettingsMenu extends Component { - menuLinkClicked(url, e) { +const SettingsMenu = (props) => { + const activePage = props.activePage || 'account'; + + const menuLinkClicked = (url, e) => { e.preventDefault(); localState.get('toggleSettingsMenu').put(false); localState.get('scrollUp').put(true); route(`/settings/${url}`); - } + }; - render() { - const activePage = this.props.activePage || 'account'; - return ( -
- {Object.keys(SETTINGS).map((page) => { - if (!SETTINGS[page]) return; - return ( - 640 ? 'active' : '' - }`} - onClick={(e) => this.menuLinkClicked(page, e)} - key={page} - > - {t(SETTINGS[page])} - - - - - ); - })} -
- ); - } -} + return ( +
+ {Object.keys(SETTINGS).map((page) => { + if (!SETTINGS[page]) return null; + return ( + 640 ? 'active' : '' + }`} + onClick={(e) => menuLinkClicked(page, e)} + key={page} + > + {t(SETTINGS[page])} + + + + + ); + })} +
+ ); +}; + +export default SettingsMenu; diff --git a/src/js/views/settings/SocialNetwork.tsx b/src/js/views/settings/SocialNetwork.tsx index 797fb6c4..03f18d97 100644 --- a/src/js/views/settings/SocialNetwork.tsx +++ b/src/js/views/settings/SocialNetwork.tsx @@ -1,7 +1,6 @@ -import { ChangeEvent } from 'react'; +import { useEffect, useState } from 'preact/hooks'; import { Link } from 'preact-router'; -import Component from '../../BaseComponent'; import Name from '../../components/user/Name'; import localState from '../../LocalState'; import Events from '../../nostr/Events'; @@ -9,109 +8,96 @@ import Key from '../../nostr/Key'; import SocialNetwork from '../../nostr/SocialNetwork'; import { translate as t } from '../../translations/Translation.mjs'; -export default class SocialNetworkSettings extends Component { - private refreshInterval: any; - constructor() { - super(); - this.state = { - blockedUsers: [] as string[], - globalFilter: {}, - }; - } - render() { - let hasBlockedUsers = false; - const blockedUsers = this.state.blockedUsers.map((user) => { - const bech32 = Key.toNostrBech32Address(user, 'npub'); - if (bech32) { - hasBlockedUsers = true; - return ( -
- - - -
- ); - } - }); +const SocialNetworkSettings = () => { + const [blockedUsers, setBlockedUsers] = useState([]); + const [globalFilter, setGlobalFilter] = useState({}); + const [showBlockedUsers, setShowBlockedUsers] = useState(false); + let refreshInterval: any; - const followDistances = Array.from(SocialNetwork.usersByFollowDistance.entries()).slice(1); + const handleFilterChange = (e) => { + const value = parseInt(e.target?.value); + localState.get('globalFilter').get('maxFollowDistance').put(value); + }; - return ( - <> -
-

{t('social_network')}

-

Stored on your device

-

Total size: {followDistances.reduce((a, b) => a + b[1].size, 0)} users

-

Depth: {followDistances.length} degrees of separation

- {followDistances.sort().map((distance) => ( -
- {distance[0] || t('unknown')}: {distance[1].size} users -
- ))} -

Filter incoming events by follow distance:

- -

Minimum number of followers at maximum follow distance:

- ) => { - const target = e.target as HTMLInputElement; - localState - .get('globalFilter') - .get('minFollowersAtMaxDistance') - .put(parseInt(target.value)); - }} - /> -

{t('blocked_users')}

- {!hasBlockedUsers && t('none')} -

- { - e.preventDefault(); - this.setState({ - showBlockedUsers: !this.state.showBlockedUsers, - }); - }} - > - {this.state.showBlockedUsers ? t('hide') : t('show')} {blockedUsers.length} - -

- {this.state.showBlockedUsers && blockedUsers} -
- - ); - } - componentDidMount() { - SocialNetwork.getBlockedUsers((blockedUsers) => { - this.setState({ blockedUsers: Array.from(blockedUsers) }); - }); - localState.get('globalFilter').on(this.inject()); - this.refreshInterval = setInterval(() => { - this.forceUpdate(); + const handleMinFollowersChange = (e) => { + const value = parseInt(e.target?.value); + localState.get('globalFilter').get('minFollowersAtMaxDistance').put(value); + }; + + useEffect(() => { + SocialNetwork.getBlockedUsers((set) => setBlockedUsers(Array.from(set))); + localState.get('globalFilter').on(setGlobalFilter); + refreshInterval = setInterval(() => { + // We might need some actual updating logic here }, 1000); - } - componentWillUnmount() { - clearInterval(this.refreshInterval); - } -} + return () => { + clearInterval(refreshInterval); + }; + }, []); + + const hasBlockedUsers = blockedUsers.some((user) => Key.toNostrBech32Address(user, 'npub')); + const renderedBlockedUsers = blockedUsers.map((user) => { + const bech32 = Key.toNostrBech32Address(user, 'npub'); + if (bech32) { + return ( +
+ + + +
+ ); + } + return null; + }); + + const followDistances = Array.from(SocialNetwork.usersByFollowDistance.entries()).slice(1); + + return ( +
+

{t('social_network')}

+

Stored on your device

+

Total size: {followDistances.reduce((a, b) => a + b[1].size, 0)} users

+

Depth: {followDistances.length} degrees of separation

+ {followDistances.sort().map((distance) => ( +
+ {distance[0] || t('unknown')}: {distance[1].size} users +
+ ))} +

Filter incoming events by follow distance:

+ +

Minimum number of followers at maximum follow distance:

+ +

{t('blocked_users')}

+ {!hasBlockedUsers && t('none')} +

+ { + e.preventDefault(); + setShowBlockedUsers(!showBlockedUsers); + }} + > + {showBlockedUsers ? t('hide') : t('show')} {renderedBlockedUsers.length} + +

+ {showBlockedUsers && renderedBlockedUsers} +
+ ); +}; + +export default SocialNetworkSettings;