mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-29 08:21:15 +00:00
Add Profile tabs
This commit is contained in:
parent
39d70b9dc1
commit
12826817f2
@ -2,14 +2,19 @@ import { useIntl } from '@cookbook/solid-intl';
|
||||
import { Component, Show } from 'solid-js';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { account as t } from '../../translations';
|
||||
import { account as t, actions } from '../../translations';
|
||||
import { PrimalUser } from '../../types/primal';
|
||||
import { useToastContext } from '../Toaster/Toaster';
|
||||
|
||||
import styles from './FollowButton.module.scss';
|
||||
|
||||
|
||||
const FollowButton: Component<{ person: PrimalUser | undefined, large?: boolean, id?: string }> = (props) => {
|
||||
const FollowButton: Component<{
|
||||
person: PrimalUser | undefined,
|
||||
large?: boolean,
|
||||
id?: string,
|
||||
postAction?: (remove: boolean, pubkey: string) => void,
|
||||
}> = (props) => {
|
||||
|
||||
const toast = useToastContext()
|
||||
const account = useAccountContext();
|
||||
@ -32,7 +37,7 @@ const FollowButton: Component<{ person: PrimalUser | undefined, large?: boolean,
|
||||
account.actions.removeFollow :
|
||||
account.actions.addFollow;
|
||||
|
||||
action(props.person.pubkey);
|
||||
action(props.person.pubkey, props.postAction);
|
||||
}
|
||||
|
||||
const klass = () => {
|
||||
@ -42,7 +47,7 @@ const FollowButton: Component<{ person: PrimalUser | undefined, large?: boolean,
|
||||
return (
|
||||
<Show when={props.person}>
|
||||
<div id={props.id} class={klass()}>
|
||||
<button onClick={onFollow} >
|
||||
<button onClick={onFollow} disabled={account?.followInProgress === props.person?.pubkey}>
|
||||
<Show
|
||||
when={isFollowed()}
|
||||
fallback={intl.formatMessage(t.follow)}
|
||||
|
53
src/components/ProfileContact/ProfileContact.module.scss
Normal file
53
src/components/ProfileContact/ProfileContact.module.scss
Normal file
@ -0,0 +1,53 @@
|
||||
.profileContact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
border-bottom: 1px solid var(--background-input);
|
||||
padding-block: 12px;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-grow: 8;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
|
||||
.profileInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.name {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.nip05 {
|
||||
color: var(--text-tertiary-2);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
.number {
|
||||
color: var(--text-primary);
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--text-tertiary-2);
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
src/components/ProfileContact/ProfileContact.tsx
Normal file
64
src/components/ProfileContact/ProfileContact.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { Component, For, Show } from 'solid-js';
|
||||
import {
|
||||
PrimalNote,
|
||||
PrimalUser
|
||||
} from '../../types/primal';
|
||||
|
||||
import styles from './ProfileContact.module.scss';
|
||||
import SmallNote from '../SmallNote/SmallNote';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { nip05Verification, userName } from '../../stores/profile';
|
||||
import { profile, profile as t } from '../../translations';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import FollowButton from '../FollowButton/FollowButton';
|
||||
import { A } from '@solidjs/router';
|
||||
import { humanizeNumber } from '../../lib/stats';
|
||||
|
||||
|
||||
const ProfileContact: Component<{
|
||||
profile: PrimalUser | undefined,
|
||||
profileStats: any,
|
||||
postAction?: (remove: boolean, pubkey: string) => void,
|
||||
id?: string,
|
||||
}> = (props) => {
|
||||
|
||||
return (
|
||||
<div id={props.id} class={styles.profileContact}>
|
||||
<Avatar src={props.profile?.picture} size="sm" />
|
||||
|
||||
<A href={`/p/${props.profile?.npub}`} class={styles.info}>
|
||||
<div class={styles.profileInfo}>
|
||||
<div class={styles.name}>{userName(props.profile)}</div>
|
||||
<div class={styles.nip05}>
|
||||
<Show when={props.profile?.nip05}>
|
||||
<span
|
||||
class={styles.verifiedBy}
|
||||
title={props.profile?.nip05}
|
||||
>
|
||||
{nip05Verification(props.profile)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.profileStats}>
|
||||
<div class={styles.stats}>
|
||||
<div class={styles.number}>
|
||||
{humanizeNumber(props.profileStats)}
|
||||
</div>
|
||||
<div class={styles.label}>
|
||||
followers
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</A>
|
||||
|
||||
<div class={styles.action}>
|
||||
<FollowButton person={props.profile} postAction={props.postAction} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ProfileContact);
|
64
src/components/ProfileTabs/ProfileTabs.module.scss
Normal file
64
src/components/ProfileTabs/ProfileTabs.module.scss
Normal file
@ -0,0 +1,64 @@
|
||||
.profileTabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
padding-inline: 8px;
|
||||
background-color: var(--background-card);
|
||||
width: 100%;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.profileTab {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-inline: 14px;
|
||||
padding-block: 2px;
|
||||
border: none;
|
||||
background: none;
|
||||
width: fit-content;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profileTabIndicator {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
background: linear-gradient(0deg, #CA079F 0%, #CA079F 100%), linear-gradient(134deg, #EF404A 0%, #5B12A4 100%), #121212;
|
||||
transition: all 250ms;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.statNumber {
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.statName {
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
color: var(--text-tertiary-2);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
|
||||
.profileNotes {
|
||||
position: relative;
|
||||
}
|
284
src/components/ProfileTabs/ProfileTabs.tsx
Normal file
284
src/components/ProfileTabs/ProfileTabs.tsx
Normal file
@ -0,0 +1,284 @@
|
||||
import { useIntl } from "@cookbook/solid-intl";
|
||||
import { Tabs } from "@kobalte/core";
|
||||
import { Component, For, Match, onMount, Switch } from "solid-js";
|
||||
import { useAccountContext } from "../../contexts/AccountContext";
|
||||
import { useProfileContext } from "../../contexts/ProfileContext";
|
||||
import { hookForDev } from "../../lib/devTools";
|
||||
import { humanizeNumber } from "../../lib/stats";
|
||||
import { userName } from "../../stores/profile";
|
||||
import { profile as t, actions as tActions } from "../../translations";
|
||||
import Loader from "../Loader/Loader";
|
||||
import Note from "../Note/Note";
|
||||
import Paginator from "../Paginator/Paginator";
|
||||
import ProfileContact from "../ProfileContact/ProfileContact";
|
||||
|
||||
import styles from "./ProfileTabs.module.scss";
|
||||
|
||||
|
||||
const ProfileTabs: Component<{
|
||||
id?: string,
|
||||
setProfile?: (pk: string) => void,
|
||||
}> = (props) => {
|
||||
|
||||
const intl = useIntl();
|
||||
const profile = useProfileContext();
|
||||
const account = useAccountContext();
|
||||
|
||||
const addToAllowlist = async () => {
|
||||
const pk = profile?.profileKey;
|
||||
const setP = props.setProfile;
|
||||
if (pk && setP) {
|
||||
account?.actions.addToAllowlist(pk, () => { setP(pk) });
|
||||
}
|
||||
};
|
||||
|
||||
const isMuted = (pk: string | undefined, ignoreContentCheck = false) => {
|
||||
const isContentMuted = account?.mutelists.find(x => x.pubkey === account.publicKey)?.content;
|
||||
|
||||
return pk &&
|
||||
account?.muted.includes(pk) &&
|
||||
(ignoreContentCheck ? true : isContentMuted);
|
||||
};
|
||||
|
||||
const isFiltered = () => {
|
||||
return profile?.filterReason !== null;
|
||||
};
|
||||
|
||||
const unMuteProfile = () => {
|
||||
const pk = profile?.profileKey;
|
||||
const setP = props.setProfile;
|
||||
if (!account || !pk || !setP) {
|
||||
return;
|
||||
}
|
||||
|
||||
account.actions.removeFromMuteList(pk, () => setP(pk));
|
||||
};
|
||||
|
||||
const onContactAction = (remove: boolean, pubkey: string) => {
|
||||
if (remove) {
|
||||
profile?.actions.removeContact(pubkey);
|
||||
}
|
||||
else {
|
||||
profile?.actions.addContact(pubkey, profile.followers);
|
||||
}
|
||||
};
|
||||
|
||||
const contacts = () => {
|
||||
const cts = [...(profile?.contacts || [])];
|
||||
|
||||
cts.sort((a, b) => {
|
||||
const aFollowers = profile?.profileStats[a.pubkey] || 0;
|
||||
const bFollowers = profile?.profileStats[b.pubkey] || 0;
|
||||
|
||||
const c = bFollowers >= aFollowers ? 1 : -1;
|
||||
|
||||
return c;
|
||||
|
||||
});
|
||||
|
||||
return cts;
|
||||
}
|
||||
|
||||
const followers = () => {
|
||||
const fls = [...(profile?.followers || [])];
|
||||
|
||||
fls.sort((a, b) => {
|
||||
const aFollowers = profile?.profileStats[a.pubkey] || 0;
|
||||
const bFollowers = profile?.profileStats[b.pubkey] || 0;
|
||||
|
||||
const c = bFollowers >= aFollowers ? 1 : -1;
|
||||
|
||||
return c;
|
||||
|
||||
});
|
||||
|
||||
return fls;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs.Root aria-label="Main navigation">
|
||||
<Tabs.List class={styles.profileTabs}>
|
||||
<Tabs.Trigger class={styles.profileTab} value="notes">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.userStats?.note_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.notes)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Trigger class={styles.profileTab} value="replies">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.userStats?.reply_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.replies)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Trigger class={styles.profileTab} value="follows">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.contacts.length || profile?.userStats?.follows_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.follow)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Trigger class={styles.profileTab} value="followers">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.followers.length || profile?.userStats?.followers_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.followers)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Indicator class={styles.profileTabIndicator} />
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="notes">
|
||||
<div class={styles.profileNotes}>
|
||||
<Switch
|
||||
fallback={
|
||||
<div style="margin-top: 40px;">
|
||||
<Loader />
|
||||
</div>
|
||||
}>
|
||||
<Match when={isMuted(profile?.profileKey)}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.isMuted,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
<button
|
||||
onClick={unMuteProfile}
|
||||
>
|
||||
{intl.formatMessage(tActions.unmute)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isFiltered()}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(t.isFiltered)}
|
||||
<button
|
||||
onClick={addToAllowlist}
|
||||
>
|
||||
{intl.formatMessage(tActions.addToAllowlist)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.notes.length === 0 && !profile.isFetching}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.noNotes,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.notes.length > 0}>
|
||||
<For each={profile?.notes}>
|
||||
{note => (
|
||||
<Note note={note} />
|
||||
)}
|
||||
</For>
|
||||
<Paginator loadNextPage={() => {
|
||||
profile?.actions.fetchNextPage();
|
||||
}}/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="replies">
|
||||
<div class={styles.profileNotes}>
|
||||
<Switch
|
||||
fallback={
|
||||
<div style="margin-top: 40px;">
|
||||
<Loader />
|
||||
</div>
|
||||
}>
|
||||
<Match when={isMuted(profile?.profileKey)}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.isMuted,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
<button
|
||||
onClick={unMuteProfile}
|
||||
>
|
||||
{intl.formatMessage(tActions.unmute)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isFiltered()}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(t.isFiltered)}
|
||||
<button
|
||||
onClick={addToAllowlist}
|
||||
>
|
||||
{intl.formatMessage(tActions.addToAllowlist)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.replies.length === 0 && !profile.isFetchingReplies}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.noNotes,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.replies.length > 0}>
|
||||
<For each={profile?.replies}>
|
||||
{reply => (
|
||||
<Note note={reply} />
|
||||
)}
|
||||
</For>
|
||||
<Paginator loadNextPage={() => {
|
||||
profile?.actions.fetchNextRepliesPage();
|
||||
}}/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="follows">
|
||||
<For each={contacts()}>
|
||||
{contact =>
|
||||
<div>
|
||||
<ProfileContact
|
||||
profile={contact}
|
||||
profileStats={profile?.profileStats[contact.pubkey]}
|
||||
postAction={onContactAction}
|
||||
/>
|
||||
</div>}
|
||||
</For>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="followers">
|
||||
<For each={followers()}>
|
||||
{follower =>
|
||||
<div>
|
||||
<ProfileContact
|
||||
profile={follower}
|
||||
profileStats={profile?.profileStats[follower.pubkey]}
|
||||
postAction={onContactAction}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</For>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ProfileTabs);
|
@ -129,6 +129,7 @@ export enum Kind {
|
||||
ImportResponse = 10_000_127,
|
||||
LinkMetadata = 10_000_128,
|
||||
FilteringReason = 10_000_131,
|
||||
UserFollowerCounts= 10_000_133,
|
||||
}
|
||||
|
||||
export const relayConnectingTimeout = 1000;
|
||||
|
@ -2,7 +2,7 @@ import { createStore, unwrap } from "solid-js/store";
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
JSX,
|
||||
JSXElement,
|
||||
onCleanup,
|
||||
onMount,
|
||||
useContext
|
||||
@ -39,6 +39,7 @@ export type AccountContextStore = {
|
||||
showNewNoteForm: boolean,
|
||||
following: string[],
|
||||
followingSince: number,
|
||||
followInProgress: string,
|
||||
muted: string[],
|
||||
mutedPrivate: string,
|
||||
mutedSince: number,
|
||||
@ -57,8 +58,8 @@ export type AccountContextStore = {
|
||||
setActiveUser: (user: PrimalUser) => void,
|
||||
addLike: (note: PrimalNote) => Promise<boolean>,
|
||||
setPublicKey: (pubkey: string | undefined) => void,
|
||||
addFollow: (pubkey: string) => void,
|
||||
removeFollow: (pubkey: string) => void,
|
||||
addFollow: (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => void,
|
||||
removeFollow: (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => void,
|
||||
quoteNote: (noteId: string | undefined) => void,
|
||||
addToMuteList: (pubkey: string) => void,
|
||||
removeFromMuteList: (pubkey: string, then?: () => void) => void,
|
||||
@ -86,6 +87,7 @@ const initialData = {
|
||||
showNewNoteForm: false,
|
||||
following: [],
|
||||
followingSince: 0,
|
||||
followInProgress: '',
|
||||
muted: [],
|
||||
mutedPrivate: '',
|
||||
mutedSince: 0,
|
||||
@ -101,7 +103,7 @@ const initialData = {
|
||||
|
||||
export const AccountContext = createContext<AccountContextStore>();
|
||||
|
||||
export function AccountProvider(props: { children: number | boolean | Node | JSX.ArrayElement | JSX.FunctionElement | (string & {}) | null | undefined; }) {
|
||||
export function AccountProvider(props: { children: JSXElement }) {
|
||||
|
||||
let relayAtempts: Record<string, number> = {};
|
||||
const relayAtemptLimit = 10;
|
||||
@ -435,11 +437,13 @@ export function AccountProvider(props: { children: number | boolean | Node | JSX
|
||||
saveMuteList(store.publicKey, muted, content.content, mutedSince || 0);
|
||||
};
|
||||
|
||||
const addFollow = (pubkey: string) => {
|
||||
const addFollow = (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => {
|
||||
if (!store.publicKey || store.following.includes(pubkey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('followInProgress', () => pubkey);
|
||||
|
||||
const unsub = subscribeTo(`before_follow_${APP_ID}`, async (type, subId, content) => {
|
||||
if (type === 'EOSE') {
|
||||
|
||||
@ -458,9 +462,11 @@ export function AccountProvider(props: { children: number | boolean | Node | JSX
|
||||
updateStore('followingSince', () => date);
|
||||
updateStore('contactsTags', () => [...tags]);
|
||||
saveFollowing(store.publicKey, following, date);
|
||||
cb && cb(false, pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
updateStore('followInProgress', () => '');
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
@ -478,11 +484,13 @@ export function AccountProvider(props: { children: number | boolean | Node | JSX
|
||||
|
||||
}
|
||||
|
||||
const removeFollow = (pubkey: string) => {
|
||||
const removeFollow = (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => {
|
||||
if (!store.publicKey || !store.following.includes(pubkey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('followInProgress', () => pubkey);
|
||||
|
||||
const unsub = subscribeTo(`before_unfollow_${APP_ID}`, async (type, subId, content) => {
|
||||
if (type === 'EOSE') {
|
||||
if (store.following.includes(pubkey)) {
|
||||
@ -500,9 +508,11 @@ export function AccountProvider(props: { children: number | boolean | Node | JSX
|
||||
updateStore('followingSince', () => date);
|
||||
updateStore('contactsTags', () => [...tags]);
|
||||
saveFollowing(store.publicKey, following, date);
|
||||
cb && cb(true, pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
updateStore('followInProgress', () => '');
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { createStore, reconcile } from "solid-js/store";
|
||||
import { getEvents, getFutureUserFeed, getUserFeed } from "../lib/feed";
|
||||
import { convertToNotes, paginationPlan, parseEmptyReposts, sortByRecency, sortByScore } from "../stores/note";
|
||||
import { Kind } from "../constants";
|
||||
@ -34,12 +34,16 @@ import {
|
||||
import { APP_ID } from "../App";
|
||||
import { hexToNpub } from "../lib/keys";
|
||||
import {
|
||||
getProfileContactList,
|
||||
getProfileFollowerList,
|
||||
getProfileScoredNotes,
|
||||
getUserProfileInfo,
|
||||
getUserProfiles,
|
||||
isUserFollowing,
|
||||
} from "../lib/profile";
|
||||
import { useAccountContext } from "./AccountContext";
|
||||
import { setLinkPreviews } from "../lib/notes";
|
||||
import { subscribeTo } from "../sockets";
|
||||
|
||||
export type ProfileContextStore = {
|
||||
profileKey: string | undefined,
|
||||
@ -48,33 +52,56 @@ export type ProfileContextStore = {
|
||||
follows_count: number,
|
||||
followers_count: number,
|
||||
note_count: number,
|
||||
reply_count: number,
|
||||
time_joined: number,
|
||||
},
|
||||
knownProfiles: VanityProfiles,
|
||||
notes: PrimalNote[],
|
||||
replies: PrimalNote[],
|
||||
future: {
|
||||
notes: PrimalNote[],
|
||||
page: FeedPage,
|
||||
replies: PrimalNote[],
|
||||
repliesPage: FeedPage,
|
||||
reposts: Record<string, string> | undefined,
|
||||
},
|
||||
isProfileFollowing: boolean,
|
||||
isFetching: boolean,
|
||||
isFetchingReplies: boolean,
|
||||
page: FeedPage,
|
||||
repliesPage: FeedPage,
|
||||
reposts: Record<string, string> | undefined,
|
||||
lastNote: PrimalNote | undefined,
|
||||
lastReply: PrimalNote | undefined,
|
||||
following: string[],
|
||||
sidebar: FeedPage & { notes: PrimalNote[] },
|
||||
filterReason: { action: 'block' | 'allow', pubkey?: string, group?: string } | null,
|
||||
contacts: PrimalUser[],
|
||||
followers: PrimalUser[],
|
||||
isFetchingContacts: boolean,
|
||||
isFetchingFollowers: boolean,
|
||||
profileStats: Record<string, number>,
|
||||
actions: {
|
||||
saveNotes: (newNotes: PrimalNote[]) => void,
|
||||
clearNotes: () => void,
|
||||
saveReplies: (newNotes: PrimalNote[]) => void,
|
||||
clearReplies: () => void,
|
||||
fetchReplies: (noteId: string | undefined, until?: number) => void,
|
||||
fetchNextRepliesPage: () => void,
|
||||
fetchNotes: (noteId: string | undefined, until?: number) => void,
|
||||
fetchNextPage: () => void,
|
||||
updatePage: (content: NostrEventContent) => void,
|
||||
savePage: (page: FeedPage) => void,
|
||||
updateRepliesPage: (content: NostrEventContent) => void,
|
||||
saveRepliesPage: (page: FeedPage) => void,
|
||||
setProfileKey: (profileKey?: string) => void,
|
||||
refreshNotes: () => void,
|
||||
checkForNewNotes: (pubkey: string | undefined) => void,
|
||||
fetchContactList: (pubkey: string | undefined) => void,
|
||||
fetchFollowerList: (pubkey: string | undefined) => void,
|
||||
clearContacts: () => void,
|
||||
removeContact: (pubkey: string) => void,
|
||||
addContact: (pubkey: string, source: PrimalUser[]) => void,
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +109,7 @@ export const emptyStats = {
|
||||
follows_count: 0,
|
||||
followers_count: 0,
|
||||
note_count: 0,
|
||||
reply_count: 0,
|
||||
time_joined: 0,
|
||||
};
|
||||
|
||||
@ -91,13 +119,22 @@ export const initialData = {
|
||||
userStats: { ...emptyStats },
|
||||
knownProfiles: { names: {} },
|
||||
notes: [],
|
||||
replies: [],
|
||||
isFetching: false,
|
||||
isFetchingReplies: false,
|
||||
isProfileFollowing: false,
|
||||
page: { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} },
|
||||
repliesPage: { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} },
|
||||
reposts: {},
|
||||
lastNote: undefined,
|
||||
lastReply: undefined,
|
||||
following: [],
|
||||
filterReason: null,
|
||||
contacts: [],
|
||||
profileStats: {},
|
||||
isFetchingContacts: false,
|
||||
followers: [],
|
||||
isFetchingFollowers: false,
|
||||
sidebar: {
|
||||
messages: [],
|
||||
users: {},
|
||||
@ -107,6 +144,7 @@ export const initialData = {
|
||||
},
|
||||
future: {
|
||||
notes: [],
|
||||
replies: [],
|
||||
reposts: {},
|
||||
page: {
|
||||
messages: [],
|
||||
@ -115,6 +153,13 @@ export const initialData = {
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
},
|
||||
repliesPage: {
|
||||
messages: [],
|
||||
users: {},
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -127,6 +172,126 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
|
||||
// ACTIONS --------------------------------------
|
||||
|
||||
const addContact = (pubkey: string, source: PrimalUser[]) => {
|
||||
const newContact = source.find(c => c.pubkey === pubkey);
|
||||
|
||||
newContact && updateStore('contacts', store.contacts.length, reconcile(newContact));
|
||||
};
|
||||
|
||||
const removeContact = (pubkey: string) => {
|
||||
const newContacts = store.contacts.filter(c => c.pubkey !== pubkey);
|
||||
|
||||
updateStore('contacts', reconcile(newContacts));
|
||||
|
||||
};
|
||||
|
||||
const fetchContactList = (pubkey: string | undefined) => {
|
||||
if (!pubkey) return;
|
||||
|
||||
updateStore('isFetchingContacts', () => true);
|
||||
|
||||
const subIdContacts = `profile_contacts_${APP_ID}`;
|
||||
const subIdProfiles = `profile_contacts_2_${APP_ID}`;
|
||||
|
||||
let ids: string[] = [];
|
||||
|
||||
const unsubProfiles = subscribeTo(subIdProfiles, (type, _, content) => {
|
||||
if (type === 'EOSE') {
|
||||
updateStore('isFetchingContacts', () => false);
|
||||
unsubProfiles();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (content?.kind === Kind.Metadata) {
|
||||
let user = JSON.parse(content.content);
|
||||
|
||||
if (!user.displayName || typeof user.displayName === 'string' && user.displayName.trim().length === 0) {
|
||||
user.displayName = user.display_name;
|
||||
}
|
||||
user.pubkey = content.pubkey;
|
||||
user.npub = hexToNpub(content.pubkey);
|
||||
user.created_at = content.created_at;
|
||||
|
||||
updateStore('contacts', store.contacts.length, () => ({ ...user }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content?.kind === Kind.UserFollowerCounts) {
|
||||
const stats = JSON.parse(content.content);
|
||||
|
||||
updateStore('profileStats', () => ({ ...stats }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const unsubContacts = subscribeTo(subIdContacts, (type, _, content) => {
|
||||
|
||||
if (type === 'EOSE') {
|
||||
getUserProfiles(ids, subIdProfiles);
|
||||
unsubContacts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (content && content.kind === Kind.Contacts) {
|
||||
const tags = content.tags;
|
||||
|
||||
for (let i = 0;i<tags.length;i++) {
|
||||
const tag = tags[i];
|
||||
if (tag[0] === 'p') {
|
||||
ids.push(tag[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getProfileContactList(pubkey, subIdContacts);
|
||||
};
|
||||
|
||||
const fetchFollowerList = (pubkey: string | undefined) => {
|
||||
if (!pubkey) return;
|
||||
|
||||
updateStore('isFetchingFollowers', () => true);
|
||||
|
||||
const subIdProfiles = `profile_followers_2_${APP_ID}`;
|
||||
|
||||
const unsubProfiles = subscribeTo(subIdProfiles, (type, _, content) => {
|
||||
if (type === 'EOSE') {
|
||||
updateStore('isFetchingFollowers', () => false);
|
||||
unsubProfiles();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (content?.kind === Kind.Metadata) {
|
||||
let user = JSON.parse(content.content);
|
||||
|
||||
if (!user.displayName || typeof user.displayName === 'string' && user.displayName.trim().length === 0) {
|
||||
user.displayName = user.display_name;
|
||||
}
|
||||
user.pubkey = content.pubkey;
|
||||
user.npub = hexToNpub(content.pubkey);
|
||||
user.created_at = content.created_at;
|
||||
|
||||
updateStore('followers', store.followers.length, () => ({ ...user }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content?.kind === Kind.UserFollowerCounts) {
|
||||
const stats: Record<string, number> = JSON.parse(content.content);
|
||||
|
||||
updateStore('profileStats', () => ({ ...stats }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getProfileFollowerList(pubkey, subIdProfiles);
|
||||
};
|
||||
|
||||
const saveNotes = (newNotes: PrimalNote[], scope?: 'future') => {
|
||||
if (scope) {
|
||||
updateStore(scope, 'notes', (notes) => [ ...notes, ...newNotes ]);
|
||||
@ -137,6 +302,16 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
updateStore('isFetching', () => false);
|
||||
};
|
||||
|
||||
const saveReplies = (newNotes: PrimalNote[], scope?: 'future') => {
|
||||
if (scope) {
|
||||
updateStore(scope, 'replies', (notes) => [ ...notes, ...newNotes ]);
|
||||
loadFutureContent();
|
||||
return;
|
||||
}
|
||||
updateStore('replies', (notes) => [ ...notes, ...newNotes ]);
|
||||
updateStore('isFetchingReplies', () => false);
|
||||
};
|
||||
|
||||
const fetchNotes = (pubkey: string | undefined, until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
@ -144,7 +319,17 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
|
||||
updateStore('isFetching', () => true);
|
||||
updateStore('page', () => ({ messages: [], users: {}, postStats: {} }));
|
||||
getUserFeed(account?.publicKey, pubkey, `profile_feed_${APP_ID}`, until, limit);
|
||||
getUserFeed(account?.publicKey, pubkey, `profile_feed_${APP_ID}`, 'authored', until, limit);
|
||||
}
|
||||
|
||||
const fetchReplies = (pubkey: string | undefined, until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('isFetching', () => true);
|
||||
updateStore('page', () => ({ messages: [], users: {}, postStats: {} }));
|
||||
getUserFeed(account?.publicKey, pubkey, `profile_replies_${APP_ID}`, 'replies', until, limit);
|
||||
}
|
||||
|
||||
const clearNotes = () => {
|
||||
@ -161,6 +346,20 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const clearReplies = () => {
|
||||
updateStore('repliesPage', () => ({ messages: [], users: {}, postStats: {}, noteActions: {} }));
|
||||
updateStore('replies', () => []);
|
||||
updateStore('lastReply', () => undefined);
|
||||
};
|
||||
|
||||
const clearContacts = () => {
|
||||
updateStore('contacts', () => []);
|
||||
updateStore('followers', () => []);
|
||||
// @ts-ignore
|
||||
updateStore('profileStats', () => undefined);
|
||||
updateStore('profileStats', () => ({}));
|
||||
};
|
||||
|
||||
const fetchNextPage = () => {
|
||||
const lastNote = store.notes[store.notes.length - 1];
|
||||
|
||||
@ -183,9 +382,32 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchNextRepliesPage = () => {
|
||||
const lastReply = store.notes[store.notes.length - 1];
|
||||
|
||||
if (!lastReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('lastReply', () => ({ ...lastReply }));
|
||||
|
||||
const criteria = paginationPlan('latest');
|
||||
|
||||
const noteData: Record<string, any> = lastReply.repost ?
|
||||
lastReply.repost.note :
|
||||
lastReply.post;
|
||||
|
||||
const until = noteData[criteria];
|
||||
|
||||
if (until > 0 && store.profileKey) {
|
||||
fetchReplies(store.profileKey, until);
|
||||
}
|
||||
};
|
||||
|
||||
const clearFuture = () => {
|
||||
updateStore('future', () => ({
|
||||
notes: [],
|
||||
replies: [],
|
||||
reposts: {},
|
||||
page: {
|
||||
messages: [],
|
||||
@ -194,6 +416,13 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
},
|
||||
repliesPage: {
|
||||
messages: [],
|
||||
users: {},
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@ -348,6 +577,124 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
};
|
||||
|
||||
|
||||
const updateRepliesPage = (content: NostrEventContent, scope?: 'future') => {
|
||||
if (content.kind === Kind.Metadata) {
|
||||
const user = content as NostrUserContent;
|
||||
|
||||
if (scope) {
|
||||
updateStore(scope, 'repliesPage', 'users',
|
||||
() => ({ [user.pubkey]: { ...user } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('repliesPage', 'users',
|
||||
() => ({ [user.pubkey]: { ...user } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ([Kind.Text, Kind.Repost].includes(content.kind)) {
|
||||
const message = content as NostrNoteContent;
|
||||
const messageId = nip19.noteEncode(message.id);
|
||||
|
||||
if (scope) {
|
||||
const isFirstReply = message.kind === Kind.Text ?
|
||||
store.replies[0]?.post?.noteId === messageId :
|
||||
store.replies[0]?.repost?.note.noteId === messageId;
|
||||
|
||||
if (!isFirstReply) {
|
||||
updateStore(scope, 'repliesPage', 'messages',
|
||||
(msgs) => [ ...msgs, { ...message }]
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isLastReply = message.kind === Kind.Text ?
|
||||
store.lastReply?.post?.noteId === messageId :
|
||||
store.lastReply?.repost?.note.noteId === messageId;
|
||||
|
||||
if (!isLastReply) {
|
||||
updateStore('repliesPage', 'messages', messages => [ ...messages, message]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteStats) {
|
||||
const statistic = content as NostrStatsContent;
|
||||
const stat = JSON.parse(statistic.content);
|
||||
|
||||
if (scope) {
|
||||
updateStore(scope, 'repliesPage', 'postStats',
|
||||
(stats) => ({ ...stats, [stat.event_id]: { ...stat } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('repliesPage', 'postStats', () => ({ [stat.event_id]: stat }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.Mentions) {
|
||||
const mentionContent = content as NostrMentionContent;
|
||||
const mention = JSON.parse(mentionContent.content);
|
||||
|
||||
if (scope) {
|
||||
updateStore(scope, 'repliesPage', 'mentions',
|
||||
() => ({ [mention.id]: { ...mention } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
updateStore('repliesPage', 'mentions', () => ({ [mention.id]: { ...mention } }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteActions) {
|
||||
const noteActionContent = content as NostrNoteActionsContent;
|
||||
const noteActions = JSON.parse(noteActionContent.content) as NoteActions;
|
||||
|
||||
if (scope) {
|
||||
updateStore(scope, 'repliesPage', 'noteActions',
|
||||
() => ({ [noteActions.event_id]: { ...noteActions } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
updateStore('repliesPage', 'noteActions', () => ({ [noteActions.event_id]: noteActions }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.LinkMetadata) {
|
||||
const metadata = JSON.parse(content.content);
|
||||
|
||||
const data = metadata.resources[0];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = {
|
||||
url: data.url,
|
||||
title: data.md_title,
|
||||
description: data.md_description,
|
||||
mediaType: data.mimetype,
|
||||
contentType: data.mimetype,
|
||||
images: [data.md_image],
|
||||
favicons: [data.icon_url],
|
||||
};
|
||||
|
||||
setLinkPreviews(() => ({ [data.url]: preview }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const saveRepliesPage = (page: FeedPage, scope?: 'future') => {
|
||||
const newPosts = sortByRecency(convertToNotes(page));
|
||||
|
||||
saveReplies(newPosts, scope);
|
||||
};
|
||||
|
||||
|
||||
const updateSidebar = (content: NostrEventContent) => {
|
||||
if (content.kind === Kind.Metadata) {
|
||||
const user = content as NostrUserContent;
|
||||
@ -444,6 +791,18 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (subId === `profile_replies_${APP_ID}`) {
|
||||
if (type === 'EOSE') {
|
||||
saveRepliesPage(store.repliesPage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
updateRepliesPage(content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (subId === `profile_info_${APP_ID}`) {
|
||||
if (content?.kind === Kind.FilteringReason) {
|
||||
const reason: { action: 'block' | 'allow', pubkey?: string, group?: string } | null =
|
||||
@ -625,9 +984,20 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
fetchNextPage,
|
||||
updatePage,
|
||||
savePage,
|
||||
saveReplies,
|
||||
clearReplies,
|
||||
fetchReplies,
|
||||
fetchNextRepliesPage,
|
||||
updateRepliesPage,
|
||||
saveRepliesPage,
|
||||
setProfileKey,
|
||||
refreshNotes,
|
||||
checkForNewNotes,
|
||||
fetchContactList,
|
||||
fetchFollowerList,
|
||||
clearContacts,
|
||||
addContact,
|
||||
removeContact,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -62,14 +62,14 @@ export const getEvents = (user_pubkey: string | undefined, eventIds: string[], s
|
||||
|
||||
};
|
||||
|
||||
export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, until = 0, limit = 20) => {
|
||||
export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, notes: 'authored' | 'replies', until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = until === 0 ? 'since' : 'until';
|
||||
|
||||
let payload = { pubkey, limit, notes: 'authored', [start]: until } ;
|
||||
let payload = { pubkey, limit, notes, [start]: until } ;
|
||||
|
||||
if (user_pubkey) {
|
||||
payload.user_pubkey = user_pubkey;
|
||||
|
@ -60,6 +60,18 @@ export const getProfileContactList = (pubkey: string | undefined, subid: string,
|
||||
]));
|
||||
}
|
||||
|
||||
export const getProfileFollowerList = (pubkey: string | undefined, subid: string, extended = false) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["user_followers", { pubkey }]},
|
||||
]));
|
||||
}
|
||||
|
||||
export const getProfileMuteList = (pubkey: string | undefined, subid: string, extended?: boolean) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
|
@ -2,7 +2,6 @@
|
||||
position: relative;
|
||||
background-color: var(--background-card);
|
||||
padding-bottom: 20px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
|
@ -41,6 +41,7 @@ import PrimalMenu from '../components/PrimalMenu/PrimalMenu';
|
||||
import ConfirmModal from '../components/ConfirmModal/ConfirmModal';
|
||||
import { isAccountVerified, reportUser } from '../lib/profile';
|
||||
import { APP_ID } from '../App';
|
||||
import ProfileTabs from '../components/ProfileTabs/ProfileTabs';
|
||||
|
||||
const Profile: Component = () => {
|
||||
|
||||
@ -87,8 +88,15 @@ const Profile: Component = () => {
|
||||
|
||||
const setProfile = (hex: string | undefined) => {
|
||||
profile?.actions.setProfileKey(hex);
|
||||
|
||||
profile?.actions.clearNotes();
|
||||
profile?.actions.clearReplies();
|
||||
profile?.actions.clearContacts();
|
||||
|
||||
profile?.actions.fetchNotes(hex);
|
||||
profile?.actions.fetchReplies(hex);
|
||||
profile?.actions.fetchContactList(hex);
|
||||
profile?.actions.fetchFollowerList(hex);
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@ -596,86 +604,9 @@ const Profile: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.userStats}>
|
||||
<div class={styles.userStat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.userStats?.follows_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.follow)}
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.userStat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.userStats?.followers_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.followers)}
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.userStat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.userStats?.note_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.notes)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class={styles.userFeed}>
|
||||
<Switch
|
||||
fallback={
|
||||
<div style="margin-top: 40px;">
|
||||
<Loader />
|
||||
</div>
|
||||
}>
|
||||
<Match when={isMuted(profile?.profileKey)}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.isMuted,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
<button
|
||||
onClick={unMuteProfile}
|
||||
>
|
||||
{intl.formatMessage(tActions.unmute)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isFiltered()}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(t.isFiltered)}
|
||||
<button
|
||||
onClick={addToAllowlist}
|
||||
>
|
||||
{intl.formatMessage(tActions.addToAllowlist)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.notes.length === 0 && !profile.isFetching}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.noNotes,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.notes.length > 0}>
|
||||
<For each={profile?.notes}>
|
||||
{note => (
|
||||
<Note note={note} />
|
||||
)}
|
||||
</For>
|
||||
<Paginator loadNextPage={() => {
|
||||
profile?.actions.fetchNextPage();
|
||||
}}/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<ProfileTabs />
|
||||
|
||||
<ConfirmModal
|
||||
open={confirmReportUser()}
|
||||
|
@ -831,6 +831,11 @@ export const profile = {
|
||||
defaultMessage: 'Notes',
|
||||
description: 'Label for notes profile stat',
|
||||
},
|
||||
replies: {
|
||||
id: 'profile.stats.replies',
|
||||
defaultMessage: 'Replies',
|
||||
description: 'Label for replies profile stat',
|
||||
},
|
||||
},
|
||||
isMuted: {
|
||||
id: 'profile.isMuted',
|
||||
|
12
src/types/primal.d.ts
vendored
12
src/types/primal.d.ts
vendored
@ -172,6 +172,12 @@ export type NostrFilteringReason = {
|
||||
created_at?: number,
|
||||
};
|
||||
|
||||
export type NostrUserFollwerCounts = {
|
||||
kind: Kind.UserFollowerCounts,
|
||||
content: string,
|
||||
created_at?: number,
|
||||
};
|
||||
|
||||
export type NostrEventContent =
|
||||
NostrNoteContent |
|
||||
NostrUserContent |
|
||||
@ -195,7 +201,8 @@ export type NostrEventContent =
|
||||
NostrMediaInfo |
|
||||
NostrMediaUploaded |
|
||||
NostrLinkMetadata |
|
||||
NostrFilteringReason;
|
||||
NostrFilteringReason |
|
||||
NostrUserFollwerCounts;
|
||||
|
||||
export type NostrEvent = [
|
||||
type: "EVENT",
|
||||
@ -490,13 +497,12 @@ export type ContextChildren =
|
||||
boolean |
|
||||
Node |
|
||||
JSX.ArrayElement |
|
||||
JSX.FunctionElement |
|
||||
(string & {}) | null | undefined;
|
||||
|
||||
|
||||
export type PrimalTheme = { name: string, label: string, logo: string, dark?: boolean};
|
||||
|
||||
export type ChildrenProp = { children: number | boolean | Node | JSX.ArrayElement | JSX.FunctionElement | (string & {}) | null | undefined; };
|
||||
export type ChildrenProp = { children: number | boolean | Node | JSX.ArrayElement | (string & {}) | null | undefined; };
|
||||
|
||||
export type VanityProfiles = { names: Record<string, string> };
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user