diff --git a/src/js/components/user/Avatar.tsx b/src/js/components/user/Avatar.tsx index d5e2cd229..fa018e00a 100644 --- a/src/js/components/user/Avatar.tsx +++ b/src/js/components/user/Avatar.tsx @@ -52,7 +52,7 @@ const MyAvatar: React.FC = (props) => { return () => unsub?.(); } - }, [hex, props.str, props.width]); + }, [hex, props.str]); const width = props.width; const isActive = ['online', 'active'].includes(activity || ''); diff --git a/src/js/nostr/Session.ts b/src/js/nostr/Session.ts index 0295df31d..10fb34316 100644 --- a/src/js/nostr/Session.ts +++ b/src/js/nostr/Session.ts @@ -149,6 +149,7 @@ const Session = { }, init: function (options: any) { Key.getOrCreate(options); + localState.get('isMyProfile').put(false); localState.get('loggedIn').on(() => this.onLoggedIn()); Helpers.showConsoleWarning(); }, diff --git a/src/js/views/feeds/Global.tsx b/src/js/views/feeds/Global.tsx index 62d5b2e29..36e6d547a 100644 --- a/src/js/views/feeds/Global.tsx +++ b/src/js/views/feeds/Global.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import CreateNoteForm from '@/components/create/CreateNoteForm'; import FeedComponent from '@/components/feed/Feed'; import OnboardingNotification from '@/components/onboarding/OnboardingNotification'; -import { getEventReplyingTo } from '@/nostr/utils'; +import { getEventReplyingTo, isRepost } from '@/nostr/utils'; import { translate as t } from '@/translations/Translation.mjs'; import { RouteProps } from '@/views/types.ts'; import View from '@/views/View.tsx'; @@ -14,7 +14,7 @@ const Global: React.FC = () => { { name: t('posts'), filter: { kinds: [1, 6], limit: 10 }, - filterFn: (event) => !getEventReplyingTo(event), + filterFn: (event) => !getEventReplyingTo(event) || isRepost(event), eventProps: { showRepliedMsg: true }, }, { diff --git a/src/js/views/feeds/Home.tsx b/src/js/views/feeds/Home.tsx index 7fa8847b2..d570364b9 100644 --- a/src/js/views/feeds/Home.tsx +++ b/src/js/views/feeds/Home.tsx @@ -5,7 +5,7 @@ import FeedComponent from '@/components/feed/Feed'; import Show from '@/components/helpers/Show'; import OnboardingNotification from '@/components/onboarding/OnboardingNotification'; import Key from '@/nostr/Key'; -import { getEventReplyingTo } from '@/nostr/utils'; +import { getEventReplyingTo, isRepost } from '@/nostr/utils'; import { translate as t } from '@/translations/Translation.mjs'; import { ID, STR } from '@/utils/UniqueIds'; import { RouteProps } from '@/views/types.ts'; @@ -40,7 +40,7 @@ const Home: React.FC = () => { { name: t('posts'), filter: { kinds: [1, 6], authors: followedUsers, limit: 10 }, - filterFn: (event) => !getEventReplyingTo(event), + filterFn: (event) => !getEventReplyingTo(event) || isRepost(event), eventProps: { showRepliedMsg: true }, }, { diff --git a/src/js/views/profile/Profile.tsx b/src/js/views/profile/Profile.tsx index aff531a4a..7a97e7535 100644 --- a/src/js/views/profile/Profile.tsx +++ b/src/js/views/profile/Profile.tsx @@ -1,191 +1,165 @@ +import { useMemo } from 'react'; +import { useEffect, useState } from 'preact/hooks'; import { route } from 'preact-router'; -import BaseComponent from '@/BaseComponent.ts'; import SimpleImageModal from '@/components/modal/Image.tsx'; -import { getEventReplyingTo } from '@/nostr/utils.ts'; +import { getEventReplyingTo, isRepost } from '@/nostr/utils.ts'; import ProfileHelmet from '@/views/profile/Helmet.tsx'; import Feed from '../../components/feed/Feed.tsx'; import Show from '../../components/helpers/Show.tsx'; import { isSafeOrigin } from '../../components/SafeImg.tsx'; import ProfileCard from '../../components/user/ProfileCard.tsx'; -import localState from '../../LocalState.ts'; +import { useLocalState } from '../../LocalState.ts'; import Key from '../../nostr/Key.ts'; import SocialNetwork from '../../nostr/SocialNetwork.ts'; import { translate as t } from '../../translations/Translation.mjs'; import View from '../View.tsx'; -class Profile extends BaseComponent { - subscriptions: any[]; - unsub: any; - state = { - hexPub: '', - npub: '', - name: '', - display_name: '', - profile: {} as any, - banner: '', - fullBanner: '', - picture: '', - website: '', - lightning: '', - blocked: false, - bannerModalOpen: false, - notFound: false, - }; - - constructor() { - super(); - this.subscriptions = []; - } +function Profile(props) { + const [blocked, setBlocked] = useState(false); + const [hexPub, setHexPub] = useState(''); + const [npub, setNpub] = useState(''); + const [profile, setProfile] = useState({} as any); + const [banner, setBanner] = useState(''); + const [bannerModalOpen, setBannerModalOpen] = useState(false); + const setIsMyProfile = useLocalState('isMyProfile', false)[1]; - render() { - const { - hexPub, - display_name, - name, - profile, - banner, - picture, - blocked, - bannerModalOpen, - fullBanner, - } = this.state; - - if (!hexPub) { - return
; - } - - const title = display_name || name || 'Profile'; - const ogTitle = `${title} | Iris`; - const description = `Latest posts by ${display_name || name || 'user'}. ${ - profile?.about || '' - }`; - const setBannerModalOpen = (bannerModalOpen) => this.setState({ bannerModalOpen }); - - return ( - - -
setBannerModalOpen(true)} - >
- - setBannerModalOpen(false)} /> - -
-
- - - - !getEventReplyingTo(event), - eventProps: { showRepliedMsg: true }, - }, - { - name: t('posts_and_replies'), - filter: { authors: [this.state.hexPub], kinds: [1, 6], limit: 5 }, - eventProps: { showRepliedMsg: true, fullWidth: false }, - }, - { - name: t('likes'), - filter: { authors: [this.state.hexPub], kinds: [7], limit: 5 }, - }, - ]} - /> - -
-
- ); - } - - componentWillUnmount() { - super.componentWillUnmount(); - this.unsub?.(); - localState.get('isMyProfile').put(false); - } + useEffect(() => { + const pub = props.id; + const npubComputed = Key.toNostrBech32Address(pub, 'npub'); - componentDidUpdate(_prevProps, prevState) { - if (!prevState.name && this.state.name) { - this.unsub?.(); - setTimeout(() => { - // important for SEO: prerenderReady is false until page content is loaded - window.prerenderReady = true; - }, 1000); // give feed a sec to load - } - } - - loadProfile(hexPub: string) { - const isMyProfile = hexPub === Key.getPubKey(); - localState.get('isMyProfile').put(isMyProfile); - this.subscriptions.push( - SocialNetwork.getProfile(hexPub, (profile) => { - let banner, fullBanner; - - try { - banner = profile.banner && new URL(profile.banner).toString(); - if (!banner) { - return; - } - fullBanner = banner; - banner = isSafeOrigin(banner) - ? banner - : `https://imgproxy.iris.to/insecure/rs:fit:948:948/plain/${banner}`; - this.setState({ banner, fullBanner }); - } catch (e) { - console.log('Invalid banner URL', profile.banner); - } - }), - ); - } - - componentDidMount() { - const pub = this.props.id; - const npub = Key.toNostrBech32Address(pub, 'npub'); - //localState.get('loggedIn').on(this.inject()); - if (npub && npub !== pub) { - route(`/${npub}`, true); + if (npubComputed && npubComputed !== pub) { + route(`/${npubComputed}`, true); return; } - const hexPub = Key.toNostrHexAddress(pub); - if (!hexPub) { - // id is not a nostr address, but maybe it's a username + + const hexPubComputed = Key.toNostrHexAddress(pub) || ''; + if (!hexPubComputed) { let nostrAddress = pub; if (!nostrAddress.match(/.+@.+\..+/)) { - // domain name? if (nostrAddress.match(/.+\..+/)) { nostrAddress = '_@' + nostrAddress; } else { nostrAddress = nostrAddress + '@iris.to'; } } + Key.getPubKeyByNip05Address(nostrAddress).then((pubKey) => { if (pubKey) { - const npub = Key.toNostrBech32Address(pubKey, 'npub'); - if (npub && npub !== pubKey) { - this.setState({ npub, hexPub: pubKey }); - this.loadProfile(pubKey); + const npubComputed = Key.toNostrBech32Address(pubKey, 'npub'); + if (npubComputed && npubComputed !== pubKey) { + setNpub(npubComputed); + setHexPub(pubKey); + loadProfile(pubKey); } } else { - this.setState({ notFound: true }); + setNpub(''); // To indicate not found } }); + } + + setHexPub(hexPubComputed); + setNpub(Key.toNostrBech32Address(hexPubComputed, 'npub') || ''); + loadProfile(hexPubComputed); + setTimeout(() => { + window.prerenderReady = true; + }, 1000); + return () => { + setIsMyProfile(false); + }; + }, [props.id]); + + const filterOptions = useMemo(() => { + return [ + { + name: t('posts'), + filter: { authors: [hexPub], kinds: [1, 6], limit: 10 }, + filterFn: (event) => !getEventReplyingTo(event) || isRepost(event), + eventProps: { showRepliedMsg: true }, + }, + { + name: t('posts_and_replies'), + filter: { authors: [hexPub], kinds: [1, 6], limit: 5 }, + eventProps: { showRepliedMsg: true, fullWidth: false }, + }, + { + name: t('likes'), + filter: { authors: [hexPub], kinds: [7], limit: 5 }, + }, + ]; + }, [hexPub]); + + const loadProfile = (hexPub) => { + if (!hexPub) { return; } - this.setState({ hexPub, npub: Key.toNostrBech32Address(hexPub, 'npub') }); - this.loadProfile(hexPub); + const isMyProfile = hexPub === Key.getPubKey(); + setIsMyProfile(isMyProfile); + SocialNetwork.getBlockedUsers((blockedUsers) => { + setBlocked(blockedUsers.has(hexPub)); + }); + + SocialNetwork.getProfile(hexPub, (profileData) => { + if (!profileData) { + return; + } + setProfile(profileData); + let bannerURL; + + try { + bannerURL = profileData.banner && new URL(profileData.banner).toString(); + if (!bannerURL) { + return; + } + + bannerURL = isSafeOrigin(bannerURL) + ? bannerURL + : `https://imgproxy.iris.to/insecure/rs:fit:948:948/plain/${bannerURL}`; + + setBanner(bannerURL); + } catch (e) { + console.log('Invalid banner URL', profileData.banner); + } + }); + }; + + if (!hexPub) { + return
; } + + const title = profile.display_name || profile.name || 'Profile'; + const ogTitle = `${title} | Iris`; + const description = `Latest posts by ${profile.display_name || profile.name || 'user'}. ${ + profile.about || '' + }`; + + return ( + + +
setBannerModalOpen(true)} + >
+ + setBannerModalOpen(false)} /> + +
+
+ + + + + +
+
+ ); } export default Profile;