Fix profile tabs appearance

This commit is contained in:
Bojan Mojsilovic 2023-09-27 17:11:09 +02:00
parent b777c791c8
commit 718ed55996
7 changed files with 319 additions and 222 deletions

View File

@ -38,14 +38,6 @@ const Note: Component<{ note: PrimalNote, id?: string }> = (props) => {
const [openCustomZap, setOpenCustomZap] = createSignal(false);
createEffect(() => {
const n = props.note;
console.log('NOTE: ', n);
console.log('REPLY: ', n.replyTo);
console.log('------------------------')
})
return (
<A
id={props.id}

View File

@ -34,7 +34,7 @@
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;
background: var(--accent-1);
transition: all 250ms;
}
@ -87,3 +87,8 @@
height: auto;
}
}
.separator {
display: block;
width: 24px;
}

View File

@ -1,12 +1,15 @@
import { useIntl } from "@cookbook/solid-intl";
import { Tabs } from "@kobalte/core";
import { Component, For, Match, onMount, Switch } from "solid-js";
import { Component, createEffect, createSignal, For, Match, onMount, Show, Switch } from "solid-js";
import { createStore } from "solid-js/store";
import { profileContactListPage } from "../../constants";
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 { PrimalUser } from "../../types/primal";
import Loader from "../Loader/Loader";
import Note from "../Note/Note";
import Paginator from "../Paginator/Paginator";
@ -17,6 +20,7 @@ import styles from "./ProfileTabs.module.scss";
const ProfileTabs: Component<{
id?: string,
profile: PrimalUser | undefined,
setProfile?: (pk: string) => void,
}> = (props) => {
@ -26,9 +30,8 @@ const ProfileTabs: Component<{
const addToAllowlist = async () => {
const pk = profile?.profileKey;
const setP = props.setProfile;
if (pk && setP) {
account?.actions.addToAllowlist(pk, () => { setP(pk) });
if (pk) {
account?.actions.addToAllowlist(pk);
}
};
@ -46,13 +49,12 @@ const ProfileTabs: Component<{
const unMuteProfile = () => {
const pk = profile?.profileKey;
const setP = props.setProfile;
if (!account || !pk || !setP) {
if (!account || !pk) {
return;
}
account.actions.removeFromMuteList(pk, () => setP(pk));
account.actions.removeFromMuteList(pk);
};
const onContactAction = (remove: boolean, pubkey: string) => {
@ -64,221 +66,305 @@ const ProfileTabs: Component<{
}
};
const contacts = () => {
const cts = [...(profile?.contacts || [])];
const [contactsOffset, setContactsOffset] = createSignal(0);
const [contacts, setContacts] = createStore<PrimalUser[]>([]);
createEffect(() => {
if (!profile || profile.isFetchingContacts) {
return;
}
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;
const aFollowers: number = profile.profileStats[a.pubkey] || 0;
const bFollowers: number = profile.profileStats[b.pubkey] || 0;
return bFollowers >= aFollowers ? 1 : -1;
});
return cts;
setContacts((cs) => [ ...cs, ...(cts.slice(contactsOffset(), contactsOffset() + profileContactListPage))]);
});
const loadMoreFollows = () => {
setContactsOffset(contactsOffset() + profileContactListPage);
}
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 [followersOffset, setFollowersOffset] = createSignal(0);
const [followers, setFollowers] = createStore<PrimalUser[]>([]);
const c = bFollowers >= aFollowers ? 1 : -1;
createEffect(() => {
if (!profile || profile.isFetchingFollowers) {
return;
}
return c;
const cts = [...(profile.followers || [])];
cts.sort((a, b) => {
const aFollowers: number = profile.profileStats[a.pubkey] || 0;
const bFollowers: number = profile.profileStats[b.pubkey] || 0;
return bFollowers >= aFollowers ? 1 : -1;
});
return fls;
setFollowers((fs) => [ ...fs, ...(cts.slice(followersOffset(), followersOffset() + profileContactListPage))]);
});
const loadMoreFollowers = () => {
setFollowersOffset(followersOffset() + profileContactListPage);
}
const onChangeValue = (value: string) => {
if (!props.profile) return;
switch(value) {
case 'notes':
profile?.notes.length === 0 &&profile?.actions.fetchNotes(props.profile.pubkey);
break;
case 'replies':
profile?.replies.length === 0 && profile?.actions.fetchReplies(props.profile.pubkey);
break;
case 'follows':
profile?.contacts.length === 0 && profile?.actions.fetchContactList(props.profile.pubkey);
break;
case 'followers':
profile?.followers.length === 0 && profile?.actions.fetchFollowerList(props.profile.pubkey);
break;
}
};
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?.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?.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 />
<Show when={profile && props.profile && profile.fetchedUserStats}>
<Tabs.Root onChange={onChangeValue}>
<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>
}>
<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 class={styles.statName}>
{intl.formatMessage(t.stats.notes)}
</div>
</Match>
<Match when={isFiltered()}>
<div class={styles.mutedProfile}>
{intl.formatMessage(t.isFiltered)}
<button
onClick={addToAllowlist}
>
{intl.formatMessage(tActions.addToAllowlist)}
</button>
</div>
</Tabs.Trigger>
<div class={styles.separator}></div>
<Tabs.Trigger class={styles.profileTab} value="replies">
<div class={styles.stat}>
<div class={styles.statNumber}>
{humanizeNumber(profile?.userStats?.reply_count || 0)}
</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 class={styles.statName}>
{intl.formatMessage(t.stats.replies)}
</div>
</Match>
<Match when={profile && profile.notes.length > 0}>
<For each={profile?.notes}>
{note => (
<Note note={note} />
)}
</div>
</Tabs.Trigger>
<div class={styles.separator}></div>
<Tabs.Trigger class={styles.profileTab} value="follows">
<div class={styles.stat}>
<div class={styles.statNumber}>
{humanizeNumber(profile?.userStats?.follows_count || 0)}
</div>
<div class={styles.statName}>
{intl.formatMessage(t.stats.follow)}
</div>
</div>
</Tabs.Trigger>
<div class={styles.separator}></div>
<Tabs.Trigger class={styles.profileTab} value="followers">
<div class={styles.stat}>
<div class={styles.statNumber}>
{humanizeNumber(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">
<div class={styles.profileNotes}>
<Show
when={!profile?.isFetchingContacts}
fallback={
<div style="margin-top: 40px;">
<Loader />
</div>
}
>
<For each={contacts} fallback={
<div class={styles.mutedProfile}>
{intl.formatMessage(
t.noFollows,
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
)}
</div>
}>
{contact =>
<div>
<ProfileContact
profile={contact}
profileStats={profile?.profileStats[contact.pubkey]}
postAction={onContactAction}
/>
</div>}
</For>
<Paginator loadNextPage={() => {
profile?.actions.fetchNextPage();
}}/>
</Match>
</Switch>
</div>
</Tabs.Content>
<Paginator loadNextPage={loadMoreFollows}/>
</Show>
</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} />
)}
<Tabs.Content class={styles.tabContent} value="followers">
<div class={styles.profileNotes}>
<Show
when={!profile?.isFetchingFollowers}
fallback={
<div style="margin-top: 40px;">
<Loader />
</div>
}
>
<For each={followers} fallback={
<div class={styles.mutedProfile}>
{intl.formatMessage(
t.noFollowers,
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
)}
</div>
}>
{follower =>
<div>
<ProfileContact
profile={follower}
profileStats={profile?.profileStats[follower.pubkey]}
postAction={onContactAction}
/>
</div>
}
</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>
<Paginator loadNextPage={loadMoreFollowers}/>
</Show>
</div>
</Tabs.Content>
</Tabs.Root>
</Show>
);
}

View File

@ -299,3 +299,5 @@ export const defaultContentModeration: ContentModeration[] = [
export const algoNpub ='npub1tkpg9lyfgy83c4mgrgkrhzl90t732ekzvt73m6658xva88g5rj6qy6ntw4';
export const specialAlgos = ['primal_spam', 'primal_nsfw'];
export const profileContactListPage = 50;

View File

@ -55,6 +55,7 @@ export type ProfileContextStore = {
reply_count: number,
time_joined: number,
},
fetchedUserStats: boolean,
knownProfiles: VanityProfiles,
notes: PrimalNote[],
replies: PrimalNote[],
@ -172,11 +173,11 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
// ACTIONS --------------------------------------
const addContact = (pubkey: string, source: PrimalUser[]) => {
const newContact = source.find(c => c.pubkey === pubkey);
const addContact = (pubkey: string, source: PrimalUser[]) => {
const newContact = source.find(c => c.pubkey === pubkey);
newContact && updateStore('contacts', store.contacts.length, reconcile(newContact));
};
newContact && updateStore('contacts', store.contacts.length, reconcile(newContact));
};
const removeContact = (pubkey: string) => {
const newContacts = store.contacts.filter(c => c.pubkey !== pubkey);
@ -248,19 +249,19 @@ const addContact = (pubkey: string, source: PrimalUser[]) => {
}
});
updateStore('isFetchingContacts', () => true);
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);
console.log('FETCHING FOLLOWERS DONE: ', store.isFetchingFollowers)
unsubProfiles();
return;
}
@ -289,6 +290,10 @@ const addContact = (pubkey: string, source: PrimalUser[]) => {
}
});
updateStore('isFetchingFollowers', () => true);
console.log('FETCHING FOLLOWERS: ', store.isFetchingFollowers)
getProfileFollowerList(pubkey, subIdProfiles);
};
@ -327,7 +332,7 @@ const addContact = (pubkey: string, source: PrimalUser[]) => {
return;
}
updateStore('isFetching', () => true);
updateStore('isFetchingReplies', () => true);
updateStore('page', () => ({ messages: [], users: {}, postStats: {} }));
getUserFeed(account?.publicKey, pubkey, `profile_replies_${APP_ID}`, 'replies', until, limit);
}
@ -752,6 +757,7 @@ const addContact = (pubkey: string, source: PrimalUser[]) => {
updateStore('filterReason', () => null);
updateStore('userProfile', () => undefined);
updateStore('userStats', () => ({ ...emptyStats }));
updateStore('fetchedUserStats', () => false);
getUserProfileInfo(profileKey, account?.publicKey, `profile_info_${APP_ID}`);
getProfileScoredNotes(profileKey, account?.publicKey, `profile_scored_${APP_ID}`, 10);
@ -831,6 +837,7 @@ const addContact = (pubkey: string, source: PrimalUser[]) => {
const stats = JSON.parse(content.content);
updateStore('userStats', () => ({ ...stats }));
updateStore('fetchedUserStats', () => true);
return;
}
}

View File

@ -90,13 +90,8 @@ const Profile: Component = () => {
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(() => {
@ -606,7 +601,7 @@ const Profile: Component = () => {
</div>
<ProfileTabs setProfile={setProfile} />
<ProfileTabs setProfile={setProfile} profile={profile?.userProfile}/>
<ConfirmModal
open={confirmReportUser()}

View File

@ -857,6 +857,16 @@ export const profile = {
defaultMessage: '{name} hasn\'t posted any notes',
description: 'Label indicating that the profile has no notes',
},
noFollowers: {
id: 'profile.noFollowers',
defaultMessage: '{name} has no followers',
description: 'Label indicating that the profile has no followers',
},
noFollows: {
id: 'profile.noFollows',
defaultMessage: 'No one is following {name}',
description: 'Label indicating that the profile has no followers',
},
};
export const search = {