mirror of
https://github.com/irislib/iris-messenger.git
synced 2024-09-20 01:56:33 +00:00
profilecard component
This commit is contained in:
parent
c96bda4f8d
commit
93110f262f
@ -665,7 +665,8 @@ export default {
|
||||
);
|
||||
});
|
||||
|
||||
s = s.map((x) => (typeof x === 'string' ? x.trim() : x));
|
||||
// remove leading and trailing newlines
|
||||
s = s.map((x) => (typeof x === 'string' ? x.replace(/^\n+|\n+$/g, '') : x));
|
||||
|
||||
return s;
|
||||
},
|
||||
|
@ -5,10 +5,9 @@ import Show from './helpers/Show';
|
||||
|
||||
type Props = {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
const Dropdown = ({ children, small }: Props) => {
|
||||
const Dropdown = ({ children }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const toggle = (e: MouseEvent, newOpenState: boolean) => {
|
||||
@ -45,9 +44,7 @@ const Dropdown = ({ children, small }: Props) => {
|
||||
onMouseEnter={(e) => toggle(e, true)}
|
||||
onMouseLeave={(e) => toggle(e, false)}
|
||||
>
|
||||
<button className={`dropbtn btn btn-circle ${small && 'btn-sm'} btn-ghost text-neutral-500`}>
|
||||
…
|
||||
</button>
|
||||
<button className="dropbtn btn btn-circle btn-sm btn-ghost text-neutral-500">…</button>
|
||||
<Show when={open}>
|
||||
<div className="absolute z-10 p-2 flex flex-col gap-2 right-0 w-56 rounded-md shadow-lg bg-black border-neutral-500 border-2">
|
||||
{children}
|
||||
|
@ -104,7 +104,7 @@ const EventDropdown = (props: EventDropdownProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown small>
|
||||
<Dropdown>
|
||||
<Copy className="btn btn-sm" key={`${id!}copy_link`} text={t('copy_link')} copyStr={url} />
|
||||
<Copy
|
||||
className="btn btn-sm"
|
||||
|
@ -1,55 +1,141 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { route } from 'preact-router';
|
||||
|
||||
import Block from '../../components/buttons/Block';
|
||||
import Report from '../../components/buttons/Report';
|
||||
import localState from '../../LocalState';
|
||||
import Events from '../../nostr/Events';
|
||||
import Key from '../../nostr/Key';
|
||||
import PubSub from '../../nostr/PubSub';
|
||||
import SocialNetwork from '../../nostr/SocialNetwork';
|
||||
import { translate as t } from '../../translations/Translation.mjs';
|
||||
import Copy from '../buttons/Copy';
|
||||
import Follow from '../buttons/Follow';
|
||||
import Dropdown from '../Dropdown';
|
||||
import Show from '../helpers/Show';
|
||||
import QRModal from '../modal/QRModal';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import Name from './Name';
|
||||
import ProfilePicture from './ProfilePicture';
|
||||
|
||||
interface Props {
|
||||
hexPub: string;
|
||||
npub: string;
|
||||
banner: boolean;
|
||||
picture: any;
|
||||
blocked: boolean;
|
||||
profilePictureError: boolean;
|
||||
loggedIn: boolean;
|
||||
isMyProfile: boolean;
|
||||
showQR: boolean;
|
||||
name: string;
|
||||
nip05: string;
|
||||
about: string;
|
||||
followedUserCount: number;
|
||||
followerCount: number;
|
||||
}
|
||||
|
||||
const ProfileCard = (props: Props) => {
|
||||
const {
|
||||
hexPub,
|
||||
npub,
|
||||
banner,
|
||||
picture,
|
||||
blocked,
|
||||
profilePictureError,
|
||||
loggedIn,
|
||||
isMyProfile,
|
||||
showQR,
|
||||
name,
|
||||
nip05,
|
||||
about,
|
||||
followedUserCount,
|
||||
followerCount,
|
||||
} = props;
|
||||
const ProfileCard = (props: { hexPub: string; npub: string }) => {
|
||||
const { hexPub, npub } = props;
|
||||
const [profile, setProfile] = useState<any>({});
|
||||
const [loggedIn, setLoggedIn] = useState<boolean>(false);
|
||||
const [nostrAddress, setNostrAddress] = useState<string>('');
|
||||
const [rawDataJson, setRawDataJson] = useState<string>('');
|
||||
const [showQRState, setShowQRState] = useState<boolean>(showQR);
|
||||
const history = useHistory();
|
||||
const [showQrCode, setShowQrCode] = useState<boolean>(false);
|
||||
const [isMyProfile, setIsMyProfile] = useState<boolean>(false);
|
||||
const [blocked, setBlocked] = useState<boolean>(false);
|
||||
const [followedUserCount, setFollowedUserCount] = useState<number>(0);
|
||||
const [followerCount, setFollowerCount] = useState<number>(0);
|
||||
|
||||
async function viewAs(event) {
|
||||
event.preventDefault();
|
||||
route('/');
|
||||
Key.login({ rpub: hexPub });
|
||||
}
|
||||
|
||||
const getNostrProfile = useCallback((address, nostrAddress) => {
|
||||
const subscriptions = [] as any[];
|
||||
subscriptions.push(
|
||||
PubSub.subscribe(
|
||||
{
|
||||
authors: [address],
|
||||
kinds: [0, 3],
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
);
|
||||
fetch(`https://eu.rbr.bio/${address}/info.json`).then((res) => {
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
res.json().then((json) => {
|
||||
if (json) {
|
||||
setFollowedUserCount(json.following?.length || followedUserCount);
|
||||
setFollowerCount(json.followerCount || followerCount);
|
||||
}
|
||||
});
|
||||
});
|
||||
const setFollowCounts = () => {
|
||||
if (address) {
|
||||
setFollowedUserCount(
|
||||
Math.max(SocialNetwork.followedByUser.get(address)?.size ?? 0, followedUserCount),
|
||||
);
|
||||
setFollowerCount(
|
||||
Math.max(SocialNetwork.followersByUser.get(address)?.size ?? 0, followerCount),
|
||||
);
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
subscriptions.push(SocialNetwork.getFollowersByUser(address, setFollowCounts));
|
||||
subscriptions.push(SocialNetwork.getFollowedByUser(address, setFollowCounts));
|
||||
}, 1000); // this causes social graph recursive loading, so let some other stuff like feed load first
|
||||
subscriptions.push(
|
||||
SocialNetwork.getProfile(
|
||||
address,
|
||||
(profile) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const isIrisAddress = nostrAddress && nostrAddress.endsWith('@iris.to');
|
||||
if (!isIrisAddress && profile.nip05 && profile.nip05valid) {
|
||||
// replace url and history entry with iris.to/${profile.nip05} or if nip is user@iris.to, just iris.to/${user}
|
||||
// TODO don't replace if at /likes or /replies
|
||||
const nip05 = profile.nip05;
|
||||
const nip05Parts = nip05.split('@');
|
||||
const nip05User = nip05Parts[0];
|
||||
const nip05Domain = nip05Parts[1];
|
||||
let newUrl;
|
||||
if (nip05Domain === 'iris.to') {
|
||||
if (nip05User === '_') {
|
||||
newUrl = 'iris';
|
||||
} else {
|
||||
newUrl = nip05User;
|
||||
}
|
||||
} else {
|
||||
if (nip05User === '_') {
|
||||
newUrl = nip05Domain;
|
||||
} else {
|
||||
newUrl = nip05;
|
||||
}
|
||||
}
|
||||
setNostrAddress(newUrl);
|
||||
// replace part before first slash with new url
|
||||
newUrl = window.location.pathname.replace(/[^/]+/, newUrl);
|
||||
const previousState = window.history.state;
|
||||
window.history.replaceState(previousState, '', newUrl);
|
||||
}
|
||||
|
||||
let lightning = profile.lud16 || profile.lud06;
|
||||
if (lightning && !lightning.startsWith('lightning:')) {
|
||||
lightning = 'lightning:' + lightning;
|
||||
}
|
||||
|
||||
let website =
|
||||
profile.website &&
|
||||
(profile.website.match(/^https?:\/\//) ? profile.website : 'http://' + profile.website);
|
||||
// remove trailing slash
|
||||
if (website && website.endsWith('/')) {
|
||||
website = website.slice(0, -1);
|
||||
}
|
||||
|
||||
setProfile(profile);
|
||||
},
|
||||
true,
|
||||
),
|
||||
);
|
||||
return () => {
|
||||
subscriptions.forEach((unsub) => unsub());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const rawDataArray = [];
|
||||
const rawDataArray = [] as Event[];
|
||||
const profileEvent = Events.db.findOne({
|
||||
kind: 0,
|
||||
pubkey: hexPub,
|
||||
@ -59,57 +145,103 @@ const ProfileCard = (props: Props) => {
|
||||
pubkey: hexPub,
|
||||
});
|
||||
if (profileEvent) {
|
||||
delete profileEvent.$loki;
|
||||
rawDataArray.push(profileEvent);
|
||||
}
|
||||
if (followEvent) {
|
||||
delete followEvent.$loki;
|
||||
rawDataArray.push(followEvent);
|
||||
}
|
||||
setRawDataJson(JSON.stringify(rawDataArray, null, 2));
|
||||
setIsMyProfile(hexPub === Key.getPubKey());
|
||||
getNostrProfile(hexPub, nostrAddress);
|
||||
const unsubLoggedIn = localState.get('loggedIn').on((loggedIn) => {
|
||||
setLoggedIn(loggedIn);
|
||||
});
|
||||
const unsubBlocked = SocialNetwork.getBlockedUsers((blockedUsers) => {
|
||||
setBlocked(blockedUsers.has(hexPub));
|
||||
});
|
||||
return () => {
|
||||
unsubBlocked();
|
||||
unsubLoggedIn();
|
||||
};
|
||||
}, [hexPub]);
|
||||
|
||||
const profilePicture =
|
||||
!blocked && !profilePictureError ? (
|
||||
<ProfilePicture
|
||||
key={`${hexPub}picture`}
|
||||
picture={picture}
|
||||
onError={() => /* Handle error here */ null}
|
||||
/>
|
||||
) : (
|
||||
<Avatar key={`${npub}avatar`} str={npub} hidePicture={true} width={128} />
|
||||
);
|
||||
const onClickHandler = () =>
|
||||
!loggedIn && /* Call localState.get('showLoginModal').put(true) here */ null;
|
||||
const profilePicture = !blocked ? (
|
||||
<ProfilePicture
|
||||
key={`${hexPub}picture`}
|
||||
picture={profile.picture}
|
||||
onError={() => /* Handle error here */ null}
|
||||
/>
|
||||
) : (
|
||||
<Avatar key={`${npub}avatar`} str={npub} hidePicture={true} width={128} />
|
||||
);
|
||||
const onClickHandler = () => !loggedIn && localState.get('showLoginModal').put(true);
|
||||
|
||||
return (
|
||||
<div key={`${hexPub}details`}>
|
||||
<div className="mb-2 mx-2 md:px-2 md:mx-0 flex flex-col gap-2">
|
||||
<div className="flex flex-row">
|
||||
<div className={banner ? '-mt-20' : ''}>{profilePicture}</div>
|
||||
<div className={profile.banner ? '-mt-20' : ''}>{profilePicture}</div>
|
||||
<div className="flex-1 justify-end flex">
|
||||
<div onClick={onClickHandler}>
|
||||
<Show when={isMyProfile}>
|
||||
<button
|
||||
className="btn btn-sm btn-neutral"
|
||||
onClick={() => loggedIn && history.push('/profile/edit')}
|
||||
>
|
||||
<a className="btn btn-sm btn-neutral" href="/profile/edit">
|
||||
{t('edit_profile')}
|
||||
</button>
|
||||
</a>
|
||||
</Show>
|
||||
<Show when={!isMyProfile}>
|
||||
<Follow key={`${hexPub}follow`} id={hexPub} />
|
||||
{/* More stuff to add here */}
|
||||
<button
|
||||
className="btn btn-neutral btn-sm"
|
||||
onClick={() => loggedIn && route(`/chat/${npub}`)}
|
||||
>
|
||||
{t('send_message')}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<div className="profile-actions">
|
||||
<Dropdown>{/* Add more things here */}</Dropdown>
|
||||
<Dropdown>
|
||||
<Copy
|
||||
className="btn btn-sm"
|
||||
key={`${hexPub}copyLink`}
|
||||
text={t('copy_link')}
|
||||
copyStr={window.location.href}
|
||||
/>
|
||||
<Copy
|
||||
className="btn btn-sm"
|
||||
key={`${hexPub}copyNpub`}
|
||||
text={t('copy_user_ID')}
|
||||
copyStr={npub}
|
||||
/>
|
||||
<button className="btn btn-sm" onClick={() => setShowQrCode(true)}>
|
||||
{t('show_qr_code')}
|
||||
</button>
|
||||
<Copy
|
||||
className="btn btn-sm"
|
||||
key={`${hexPub}copyData`}
|
||||
text={t('copy_raw_data')}
|
||||
copyStr={rawDataJson}
|
||||
/>
|
||||
<Show when={!isMyProfile && !Key.getPrivKey()}>
|
||||
<button className="btn btn-sm" onClick={viewAs}>
|
||||
{t('view_as') + ' '}
|
||||
<Name pub={hexPub} hideBadge={true} />
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={!isMyProfile}>
|
||||
<>
|
||||
<Block className="btn btn-sm" id={hexPub} />
|
||||
<Report className="btn btn-sm" id={hexPub} />
|
||||
</>
|
||||
</Show>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* More code here */}
|
||||
</div>
|
||||
{showQRState && <QRModal pub={hexPub} onClose={() => setShowQRState(false)} />}
|
||||
<Show when={showQrCode}>
|
||||
<QRModal pub={hexPub} onClose={() => setShowQrCode(false)} />
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -10,15 +10,12 @@ import ProfileCard from '../components/user/ProfileCard';
|
||||
import Helpers from '../Helpers';
|
||||
import localState from '../LocalState';
|
||||
import Key from '../nostr/Key';
|
||||
import PubSub from '../nostr/PubSub';
|
||||
import SocialNetwork from '../nostr/SocialNetwork';
|
||||
import { translate as t } from '../translations/Translation.mjs';
|
||||
|
||||
import View from './View';
|
||||
|
||||
class Profile extends View {
|
||||
followedUsers: Set<string>;
|
||||
followers: Set<string>;
|
||||
subscriptions: any[];
|
||||
unsub: any;
|
||||
|
||||
@ -28,14 +25,12 @@ class Profile extends View {
|
||||
followedUserCount: 0,
|
||||
followerCount: 0,
|
||||
};
|
||||
this.followedUsers = new Set();
|
||||
this.followers = new Set();
|
||||
this.id = 'profile';
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
getNotification() {
|
||||
if (this.state.noFollowers && this.followers.has(Key.getPubKey())) {
|
||||
if (this.state.noFollowers /* && this.followers.has(Key.getPubKey()) */) {
|
||||
return (
|
||||
<div className="msg">
|
||||
<div className="msg-content">
|
||||
@ -172,7 +167,7 @@ class Profile extends View {
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
</Helmet>
|
||||
<ProfileCard />
|
||||
<ProfileCard npub={this.state.npub} hexPub={this.state.hexPub} />
|
||||
<Show when={!blocked}>
|
||||
{this.renderTabs()}
|
||||
{this.renderTab()}
|
||||
@ -182,138 +177,6 @@ class Profile extends View {
|
||||
);
|
||||
}
|
||||
|
||||
getNostrProfile(address, nostrAddress) {
|
||||
this.unsub = PubSub.subscribe(
|
||||
{
|
||||
authors: [address],
|
||||
kinds: [0, 3],
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
fetch(`https://eu.rbr.bio/${address}/info.json`).then((res) => {
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
res.json().then((json) => {
|
||||
if (json) {
|
||||
this.setState({
|
||||
followerCount: json.followerCount || this.state.followerCount,
|
||||
followedUserCount: json.following?.length || this.state.followedUserCount,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
const setFollowCounts = () => {
|
||||
address &&
|
||||
this.setState({
|
||||
followedUserCount: Math.max(
|
||||
SocialNetwork.followedByUser.get(address)?.size ?? 0,
|
||||
this.state.followedUserCount,
|
||||
),
|
||||
followerCount: Math.max(
|
||||
SocialNetwork.followersByUser.get(address)?.size ?? 0,
|
||||
this.state.followerCount,
|
||||
),
|
||||
});
|
||||
};
|
||||
setTimeout(() => {
|
||||
this.subscriptions.push(SocialNetwork.getFollowersByUser(address, setFollowCounts));
|
||||
this.subscriptions.push(SocialNetwork.getFollowedByUser(address, setFollowCounts));
|
||||
}, 1000); // this causes social graph recursive loading, so let some other stuff like feed load first
|
||||
const unsub = SocialNetwork.getProfile(
|
||||
address,
|
||||
(profile) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const isIrisAddress = nostrAddress && nostrAddress.endsWith('@iris.to');
|
||||
if (!isIrisAddress && profile.nip05 && profile.nip05valid) {
|
||||
// replace url and history entry with iris.to/${profile.nip05} or if nip is user@iris.to, just iris.to/${user}
|
||||
// TODO don't replace if at /likes or /replies
|
||||
const nip05 = profile.nip05;
|
||||
const nip05Parts = nip05.split('@');
|
||||
const nip05User = nip05Parts[0];
|
||||
const nip05Domain = nip05Parts[1];
|
||||
let newUrl;
|
||||
if (nip05Domain === 'iris.to') {
|
||||
if (nip05User === '_') {
|
||||
newUrl = 'iris';
|
||||
} else {
|
||||
newUrl = nip05User;
|
||||
}
|
||||
} else {
|
||||
if (nip05User === '_') {
|
||||
newUrl = nip05Domain;
|
||||
} else {
|
||||
newUrl = nip05;
|
||||
}
|
||||
}
|
||||
this.setState({ nostrAddress: newUrl });
|
||||
// replace part before first slash with new url
|
||||
newUrl = window.location.pathname.replace(/[^/]+/, newUrl);
|
||||
const previousState = window.history.state;
|
||||
window.history.replaceState(previousState, '', newUrl);
|
||||
}
|
||||
|
||||
let lightning = profile.lud16 || profile.lud06;
|
||||
if (lightning && !lightning.startsWith('lightning:')) {
|
||||
lightning = 'lightning:' + lightning;
|
||||
}
|
||||
|
||||
let website =
|
||||
profile.website &&
|
||||
(profile.website.match(/^https?:\/\//) ? profile.website : 'http://' + profile.website);
|
||||
// remove trailing slash
|
||||
if (website && website.endsWith('/')) {
|
||||
website = website.slice(0, -1);
|
||||
}
|
||||
|
||||
let banner;
|
||||
|
||||
try {
|
||||
banner = profile.banner && new URL(profile.banner).toString();
|
||||
banner = isSafeOrigin(banner)
|
||||
? banner
|
||||
: `https://imgproxy.iris.to/insecure/plain/${banner}`;
|
||||
} catch (e) {
|
||||
console.log('Invalid banner URL', profile.banner);
|
||||
}
|
||||
|
||||
// profile may contain arbitrary fields, so be careful what you pass to setState
|
||||
this.setState({
|
||||
name: profile.name,
|
||||
display_name: profile.display_name,
|
||||
about: Helpers.highlightText(profile.about),
|
||||
picture: profile.picture,
|
||||
nip05: profile.nip05valid && profile.nip05,
|
||||
lightning,
|
||||
website: website,
|
||||
banner,
|
||||
profile,
|
||||
});
|
||||
},
|
||||
true,
|
||||
);
|
||||
this.subscriptions.push(unsub);
|
||||
}
|
||||
|
||||
loadProfile(hexPub: string, nostrAddress?: string) {
|
||||
const isMyProfile = hexPub === Key.getPubKey();
|
||||
localState.get('isMyProfile').put(isMyProfile);
|
||||
this.setState({ isMyProfile });
|
||||
this.followedUsers = new Set();
|
||||
this.followers = new Set();
|
||||
localState.get('noFollowers').on(this.inject());
|
||||
this.getNostrProfile(hexPub, nostrAddress);
|
||||
this.subscriptions.push(
|
||||
SocialNetwork.getBlockedUsers((blockedUsers) => {
|
||||
this.setState({ blocked: blockedUsers.has(hexPub) });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
this.unsub?.();
|
||||
@ -330,6 +193,30 @@ class Profile extends View {
|
||||
}
|
||||
}
|
||||
|
||||
loadProfile(hexPub: string) {
|
||||
const isMyProfile = hexPub === Key.getPubKey();
|
||||
localState.get('isMyProfile').put(isMyProfile);
|
||||
localState.get('noFollowers').on(this.inject());
|
||||
this.subscriptions.push(
|
||||
SocialNetwork.getProfile(hexPub, (profile) => {
|
||||
let banner;
|
||||
|
||||
try {
|
||||
banner = profile.banner && new URL(profile.banner).toString();
|
||||
if (!banner) {
|
||||
return;
|
||||
}
|
||||
banner = isSafeOrigin(banner)
|
||||
? banner
|
||||
: `https://imgproxy.iris.to/insecure/plain/${banner}`;
|
||||
this.setState({ banner });
|
||||
} catch (e) {
|
||||
console.log('Invalid banner URL', profile.banner);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.restoreScrollPosition();
|
||||
const pub = this.props.id;
|
||||
@ -356,7 +243,7 @@ class Profile extends View {
|
||||
const npub = Key.toNostrBech32Address(pubKey, 'npub');
|
||||
if (npub && npub !== pubKey) {
|
||||
this.setState({ npub, hexPub: pubKey });
|
||||
this.loadProfile(pubKey, nostrAddress);
|
||||
this.loadProfile(pubKey);
|
||||
}
|
||||
} else {
|
||||
this.setState({ notFound: true });
|
||||
|
Loading…
Reference in New Issue
Block a user