From 3842ba5e9d7913294e47ec1e6b4b25d9d9fed965 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Sat, 19 Aug 2023 18:42:44 +0300 Subject: [PATCH] more fn comps --- .eslintrc.cjs | 1 + src/js/components/buttons/Block.tsx | 119 ++++++------ src/js/components/buttons/Follow.tsx | 124 ++++++------- src/js/components/buttons/Report.tsx | 86 ++++++--- src/js/views/About.tsx | 178 +++++++++--------- src/js/views/EditProfile.tsx | 261 +++++++++++++-------------- src/js/views/Subscribe.tsx | 126 +++++++------ src/js/views/types.ts | 3 + 8 files changed, 441 insertions(+), 457 deletions(-) create mode 100644 src/js/views/types.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 8c97db59..753fbe7d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -18,6 +18,7 @@ module.exports = { plugins: ['simple-import-sort', '@typescript-eslint'], rules: { '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', 'simple-import-sort/imports': [ 'error', { diff --git a/src/js/components/buttons/Block.tsx b/src/js/components/buttons/Block.tsx index 07b20be4..42ef2c23 100644 --- a/src/js/components/buttons/Block.tsx +++ b/src/js/components/buttons/Block.tsx @@ -1,4 +1,5 @@ -import Component from '../../BaseComponent'; +import { useEffect, useState } from 'react'; + import Key from '../../nostr/Key'; import SocialNetwork from '../../nostr/SocialNetwork'; import { translate as t } from '../../translations/Translation.mjs'; @@ -11,77 +12,61 @@ type Props = { onClick?: (e) => void; }; -class Block extends Component { - key: string; - cls?: string; - actionDone: string; - action: string; - activeClass: string; - hoverAction: string; +const Block = ({ id, showName, className, onClick }: Props) => { + const cls = 'block-btn'; + const key = 'blocked'; + const activeClass = 'blocked'; + const action = t('block'); + const actionDone = t('blocked'); + const hoverAction = t('unblock'); - constructor() { - super(); - this.cls = 'block-btn'; - this.key = 'blocked'; - this.activeClass = 'blocked'; - this.action = t('block'); - this.actionDone = t('blocked'); - this.hoverAction = t('unblock'); - this.state = { ...this.state, hover: false }; - } + const [hover, setHover] = useState(false); + const [isBlocked, setIsBlocked] = useState(false); - handleMouseEnter = () => { - this.setState({ hover: true }); - }; - - handleMouseLeave = () => { - this.setState({ hover: false }); - }; - - onClick(e) { - e.preventDefault(); - const newValue = !this.state[this.key]; - const hex = Key.toNostrHexAddress(this.props.id); - hex && SocialNetwork.block(hex, newValue); - this.props.onClick?.(e); - } - - componentDidMount() { + useEffect(() => { SocialNetwork.getBlockedUsers((blocks) => { - const blocked = blocks?.has(Key.toNostrHexAddress(this.props.id) as string); - this.setState({ blocked }); + const blocked = blocks?.has(Key.toNostrHexAddress(id) as string); + setIsBlocked(!!blocked); }); + }, [id]); + + const handleMouseEnter = () => { + setHover(true); + }; + + const handleMouseLeave = () => { + setHover(false); + }; + + const onButtonClick = (e) => { + e.preventDefault(); + const newValue = !isBlocked; + const hex = Key.toNostrHexAddress(id); + hex && SocialNetwork.block(hex, newValue); + onClick?.(e); + }; + + let buttonText; + if (isBlocked && hover) { + buttonText = hoverAction; + } else if (isBlocked && !hover) { + buttonText = actionDone; + } else { + buttonText = action; } - render() { - const isBlocked = this.state[this.key]; - const isHovering = this.state.hover; - - let buttonText; - - if (isBlocked && isHovering) { - buttonText = this.hoverAction; - } else if (isBlocked && !isHovering) { - buttonText = this.actionDone; - } else { - buttonText = this.action; - } - - return ( - - ); - } -} + return ( + + ); +}; export default Block; diff --git a/src/js/components/buttons/Follow.tsx b/src/js/components/buttons/Follow.tsx index ef0db13b..0af0ba8d 100644 --- a/src/js/components/buttons/Follow.tsx +++ b/src/js/components/buttons/Follow.tsx @@ -1,4 +1,5 @@ -import Component from '../../BaseComponent'; +import { useEffect, useState } from 'react'; + import Key from '../../nostr/Key'; import SocialNetwork from '../../nostr/SocialNetwork'; import { translate as t } from '../../translations/Translation.mjs'; @@ -8,82 +9,69 @@ type Props = { className?: string; }; -class Follow extends Component { - key: string; - cls?: string; - actionDone: string; - action: string; - activeClass: string; - hoverAction: string; +const Follow = ({ id, className }: Props) => { + const key = 'follow'; + const activeClass = 'following'; + const action = t('follow_btn'); + const actionDone = t('following_btn'); + const hoverAction = t('unfollow_btn'); - constructor() { - super(); - this.key = 'follow'; - this.activeClass = 'following'; - this.action = t('follow_btn'); - this.actionDone = t('following_btn'); - this.hoverAction = t('unfollow_btn'); - this.state = { ...this.state, hover: false }; - } + const [hover, setHover] = useState(false); + const [isFollowed, setIsFollowed] = useState(false); - handleMouseEnter = () => { - this.setState({ hover: true }); - }; - - handleMouseLeave = () => { - this.setState({ hover: false }); - }; - - onClick(e) { - e.preventDefault(); - const newValue = !this.state[this.key]; - const hex = Key.toNostrHexAddress(this.props.id); - if (!hex) return; - if (this.key === 'follow') { - SocialNetwork.setFollowed(hex, newValue); - return; - } - if (this.key === 'block') { - SocialNetwork.setBlocked(hex, newValue); - } - } - - componentDidMount() { - if (this.key === 'follow') { + useEffect(() => { + if (key === 'follow') { SocialNetwork.getFollowedByUser(Key.getPubKey(), (follows) => { - const hex = Key.toNostrHexAddress(this.props.id); + const hex = Key.toNostrHexAddress(id); const follow = hex && follows?.has(hex); - this.setState({ follow }); + setIsFollowed(!!follow); }); + } + }, [id]); + + const handleMouseEnter = () => { + setHover(true); + }; + + const handleMouseLeave = () => { + setHover(false); + }; + + const onClick = (e) => { + e.preventDefault(); + const newValue = !isFollowed; + const hex = Key.toNostrHexAddress(id); + if (!hex) return; + if (key === 'follow') { + SocialNetwork.setFollowed(hex, newValue); + setIsFollowed(newValue); return; } - } - - render() { - const isFollowed = this.state[this.key]; - const isHovering = this.state.hover; - - let buttonText; - - if (isFollowed && isHovering) { - buttonText = this.hoverAction; - } else if (isFollowed && !isHovering) { - buttonText = this.actionDone; - } else { - buttonText = this.action; + if (key === 'block') { + SocialNetwork.setBlocked(hex, newValue); + setIsFollowed(newValue); } + }; - return ( - - ); + let buttonText; + if (isFollowed && hover) { + buttonText = hoverAction; + } else if (isFollowed && !hover) { + buttonText = actionDone; + } else { + buttonText = action; } -} + + return ( + + ); +}; export default Follow; diff --git a/src/js/components/buttons/Report.tsx b/src/js/components/buttons/Report.tsx index 076ac44b..6778130a 100644 --- a/src/js/components/buttons/Report.tsx +++ b/src/js/components/buttons/Report.tsx @@ -1,36 +1,74 @@ +import { useEffect, useState } from 'react'; + import Key from '../../nostr/Key'; import SocialNetwork from '../../nostr/SocialNetwork'; import { translate as t } from '../../translations/Translation.mjs'; +import Name from '../user/Name'; -import Block from './Block'; +type Props = { + id: string; + showName?: boolean; + className?: string; + onClick?: (e) => void; +}; -class Report extends Block { - constructor() { - super(); - this.cls = 'block'; - this.key = 'reported'; - this.activeClass = 'blocked'; - this.action = t('report_public'); - this.actionDone = t('reported'); - this.hoverAction = t('unreport'); - } +const Report = ({ id, showName = false, className, onClick }: Props) => { + const cls = 'block'; // changed this from 'block-btn' to 'block' + const key = 'reported'; // key updated for reporting + const activeClass = 'blocked'; // activeClass remains the same + const action = t('report_public'); // changed to report_public + const actionDone = t('reported'); // changed to reported + const hoverAction = t('unreport'); // changed to unreport - onClick(e) { - e.preventDefault(); - const newValue = !this.state[this.key]; - if (confirm(newValue ? 'Publicly report this user?' : 'Unreport user?')) { - const hex = Key.toNostrHexAddress(this.props.id); - hex && SocialNetwork.flag(hex, newValue); - } - } + const [hover, setHover] = useState(false); + const [isReported, setIsReported] = useState(false); - componentDidMount() { + useEffect(() => { SocialNetwork.getFlaggedUsers((flags) => { - const hex = Key.toNostrHexAddress(this.props.id); - const reported = hex && flags?.has(hex); - this.setState({ reported }); + const reported = flags?.has(Key.toNostrHexAddress(id) as string); + setIsReported(!!reported); }); + }, [id]); + + const handleMouseEnter = () => { + setHover(true); + }; + + const handleMouseLeave = () => { + setHover(false); + }; + + const onButtonClick = (e) => { + e.preventDefault(); + const newValue = !isReported; + if (window.confirm(newValue ? 'Publicly report this user?' : 'Unreport user?')) { + const hex = Key.toNostrHexAddress(id); + hex && SocialNetwork.flag(hex, newValue); + onClick?.(e); + } + }; + + let buttonText; + if (isReported && hover) { + buttonText = hoverAction; + } else if (isReported && !hover) { + buttonText = actionDone; + } else { + buttonText = action; } -} + + return ( + + ); +}; export default Report; diff --git a/src/js/views/About.tsx b/src/js/views/About.tsx index 95bac50d..e708b6ba 100644 --- a/src/js/views/About.tsx +++ b/src/js/views/About.tsx @@ -1,6 +1,8 @@ import { Link } from 'preact-router'; -import Component from '../BaseComponent'; +import Show from '@/components/helpers/Show.tsx'; +import { RouteProps } from '@/views/types.ts'; + import Follow from '../components/buttons/Follow'; import Header from '../components/Header'; import Avatar from '../components/user/Avatar'; @@ -10,110 +12,98 @@ import Helpers from '../utils/Helpers.tsx'; const IRIS_INFO_ACCOUNT = 'npub1wnwwcv0a8wx0m9stck34ajlwhzuua68ts8mw3kjvspn42dcfyjxs4n95l8'; -class About extends Component { - render() { - return ( - <> -
-
-
-

{t('about')}

-

Iris is like the social networking apps we're used to, but better:

+const About: React.FC = () => ( + <> +
+
+
+

{t('about')}

+

Iris is like the social networking apps we're used to, but better:

+
    +
  • + Accessible. No phone number or signup is required. Just type in your name or + alias and go! +
  • +
  • + Secure. It's open source. You can verify that your data stays safe. +
  • +
  • + Always available. It works offline-first and is not dependent on any single + centrally managed server. Users can even connect directly to each other. +
  • +
+ + +

Versions

+

  • - Accessible. No phone number or signup is required. Just type in your name or - alias and go! + + iris.to + {' '} + (web)
  • - Secure. It's open source. You can verify that your data stays safe. + + Desktop + {' '} + (macOS, Windows, Linux)
  • - Always available. It works offline-first and is not dependent on any single - centrally managed server. Users can even connect directly to each other. + + iOS + +
  • +
  • + + Android + {' '} + ( + + apk + + )
+

+
- {!Helpers.isStandalone() && ( - <> -

Versions

-

-

-

- - )} +

Iris docs

+

+ Visit Iris docs for features, explanations and + troubleshooting. +

-

Iris docs

-

- Visit Iris docs for features, explanations and - troubleshooting. -

+

Privacy

+

{t('application_security_warning')}

-

Privacy

-

{t('application_security_warning')}

- -

Follow

-
- - - - - -
- -

- Telegram channel. -

- -

- Released under MIT license. Code:{' '} - Github. -

-
-
+

Follow

+
+ + + + +
- - ); - } -} + +

+ Telegram channel. +

+ +

+ Released under MIT license. Code:{' '} + Github. +

+
+
+
+ +); export default About; diff --git a/src/js/views/EditProfile.tsx b/src/js/views/EditProfile.tsx index 547dbfcd..94ef2293 100644 --- a/src/js/views/EditProfile.tsx +++ b/src/js/views/EditProfile.tsx @@ -1,7 +1,9 @@ import debounce from 'lodash/debounce'; +import { useEffect, useState } from 'preact/hooks'; import { route } from 'preact-router'; -import Component from '../BaseComponent'; +import { RouteProps } from '@/views/types.ts'; + import Upload from '../components/buttons/Upload'; import Header from '../components/Header'; import SafeImg from '../components/SafeImg'; @@ -16,172 +18,153 @@ const explainers = { nip05: 'Nostr address (nip05)', }; -export default class EditProfile extends Component { - constructor(props) { - super(props); - this.state = { - profile: {}, - newFieldName: '', - newFieldValue: '', - edited: false, - }; - } +const EditProfile: React.FC = () => { + const [profile, setProfile] = useState({}); + const [newFieldName, setNewFieldName] = useState(''); + const [newFieldValue, setNewFieldValue] = useState(''); + const [edited, setEdited] = useState(false); - componentDidMount() { - SocialNetwork.getProfile(Key.getPubKey(), (p) => { - if (!this.state.edited && Object.keys(this.state.profile).length === 0) { + useEffect(() => { + return SocialNetwork.getProfile(Key.getPubKey(), (p) => { + if (!edited && Object.keys(profile).length === 0) { delete p['created_at']; - this.setState({ - profile: p, - }); + setProfile(p); } }); - } + }, [profile, edited]); - saveOnChange = debounce(() => { - const profile = this.state.profile; - Object.keys(profile).forEach((key) => { - if (typeof profile[key] === 'string') { - profile[key] = profile[key].trim(); + const saveOnChange = debounce(() => { + const trimmedProfile = { ...profile }; + Object.keys(trimmedProfile).forEach((key) => { + if (typeof trimmedProfile[key] === 'string') { + trimmedProfile[key] = trimmedProfile[key].trim(); } }); - SocialNetwork.setMetadata(profile); + SocialNetwork.setMetadata(trimmedProfile); }, 2000); - setProfileAttribute = (key, value) => { + const setProfileAttribute = (key, value) => { key = key.trim(); - const profile = Object.assign({}, this.state.profile); + const updatedProfile = { ...profile }; if (value) { - profile[key] = value; + updatedProfile[key] = value; } else { - delete profile[key]; + delete updatedProfile[key]; } - this.setState({ profile, edited: true }); - this.saveOnChange(); + setProfile(updatedProfile); + setEdited(true); + saveOnChange(); }; - handleSubmit = (event) => { + const handleSubmit = (event) => { event.preventDefault(); - SocialNetwork.setMetadata(this.state.profile); + SocialNetwork.setMetadata(profile); const myPub = Key.toNostrBech32Address(Key.getPubKey(), 'npub'); route('/' + myPub); }; - handleAddField = (event) => { + const handleAddField = (event) => { event.preventDefault(); - const fieldName = this.state.newFieldName; - const fieldValue = this.state.newFieldValue; - if (fieldName && fieldValue) { - this.setProfileAttribute(fieldName, fieldValue); - this.setState({ newFieldName: '', newFieldValue: '' }); - SocialNetwork.setMetadata(this.state.profile); + if (newFieldName && newFieldValue) { + setProfileAttribute(newFieldName, newFieldValue); + setNewFieldName(''); + setNewFieldValue(''); + SocialNetwork.setMetadata(profile); } }; - render() { - const fields = ['name', 'picture', 'about', 'banner', 'website', 'lud16', 'nip05']; - // add other possible fields from profile - Object.keys(this.state.profile).forEach((key) => { - if (!fields.includes(key)) { - fields.push(key); - } - }); + const fields = ['name', 'picture', 'about', 'banner', 'website', 'lud16', 'nip05']; + Object.keys(profile).forEach((key) => { + if (!fields.includes(key)) { + fields.push(key); + } + }); - return ( - <> -
-
-
-

{t('edit_profile')}

-
this.handleSubmit(e)}> - {fields.map((field) => { - const val = this.state.profile[field]; - const isString = typeof val === 'string' || typeof val === 'undefined'; - return ( -

- -
- - isString && - this.setProfileAttribute(field, (e.target as HTMLInputElement).value) - } - /> - {field === 'lud16' && !val && ( + return ( + <> +

+
+
+

{t('edit_profile')}

+ + {fields.map((field) => { + const val = profile[field]; + const isString = typeof val === 'string' || typeof val === 'undefined'; + return ( +

+ +
+ isString && setProfileAttribute(field, e.target.value)} + /> + {field === 'lud16' && !val && ( +

+ {t('install_lightning_wallet_prompt')} +

+ )} + {(field === 'picture' || field === 'banner') && ( + <>

- {t('install_lightning_wallet_prompt')} + setProfileAttribute(field, url)} />

- )} - {field === 'picture' || field === 'banner' ? ( - <> + {val && (

- this.setProfileAttribute(field, url)} /> +

- {val && ( -

- -

- )} - - ) : null} -

- ); - })} -

- -

- + )} + + )} +

+ ); + })} +

+ +

+ -

Add new field

-
this.handleAddField(e)}> -

- -
- - this.setState({ - newFieldName: (e.target as HTMLInputElement).value, - }) - } - /> -

-

- -
- - this.setState({ - newFieldValue: (e.target as HTMLInputElement).value, - }) - } - /> -

-

- -

-
-
+

Add new field

+
+

+ +
+ setNewFieldName(e.target.value)} + /> +

+

+ +
+ setNewFieldValue(e.target.value)} + /> +

+

+ +

+
- - ); - } -} +
+ + ); +}; + +export default EditProfile; diff --git a/src/js/views/Subscribe.tsx b/src/js/views/Subscribe.tsx index 67794941..fa36a474 100644 --- a/src/js/views/Subscribe.tsx +++ b/src/js/views/Subscribe.tsx @@ -1,77 +1,73 @@ -import Component from '../BaseComponent'; +import { RouteProps } from '@/views/types.ts'; + import Header from '../components/Header'; import { translate as t } from '../translations/Translation.mjs'; -class Subscribe extends Component { - render() { - return ( - <> -
-
-
-

{t('subscribe')}

-

Iris Supporter

-

Support open source development and get extra features!

-

-

    -
  • Iris Supporter Badge
  • -
  • Purple checkmark on Iris
  • -
  • High-quality automatic translations (via deepl.com)
  • -
  • Iris Supporters' private group chat
  • - {/* +const Subscribe: React.FC = () => ( + <> +
    +
    +
    +

    {t('subscribe')}

    +

    Iris Supporter

    +

    Support open source development and get extra features!

    +

    +

      +
    • Iris Supporter Badge
    • +
    • Purple checkmark on Iris
    • +
    • Iris Supporters' private group chat
    • + {/* :D
    • Email-DM bridge for your Iris address
    • Bitcoin Lightning proxy for your Iris address
    • Custom themes for your profile page
    • Profile view statistics
    • */} -
    • More features to come!
    • -
    -

    -

    - - -

    -

    - - -

    -

    - -

    +
  • More features to come!
  • +
+

+

+ + +

+

+ + +

+

+ +

-

Iris Titan

-

- True Mighty Titan status. Lifetime Iris Purple access, plus: -

    -
  • Iris Titan Badge
  • -
  • Iris Titans private group chat
  • -
  • Priority support
  • -
-

-

- 1000 € one-time payment. -

-

- -

-
-
-
-
- - ); - } -} +

Iris Titan

+

+ True Mighty Titan status. Lifetime Iris Purple access, plus: +

    +
  • Iris Titan Badge
  • +
  • Iris Titans private group chat
  • +
  • Priority support
  • +
+

+

+ 1000 € one-time payment. +

+

+ +

+
+
+
+
+ +); export default Subscribe; diff --git a/src/js/views/types.ts b/src/js/views/types.ts new file mode 100644 index 00000000..4979deab --- /dev/null +++ b/src/js/views/types.ts @@ -0,0 +1,3 @@ +export type RouteProps = { + path: string; +};