Skip to content

Commit

Permalink
For You front end
Browse files Browse the repository at this point in the history
  • Loading branch information
artlu99 committed Jun 25, 2024
1 parent f3e2383 commit 586ed85
Show file tree
Hide file tree
Showing 15 changed files with 297 additions and 15 deletions.
2 changes: 1 addition & 1 deletion src/api/followingFeed.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { FOLLOWING_FEED_PAGESIZE } from '@app/constants/pinataPagination';
import { listify, sift, unique } from 'radash';
import './mocks/mockornot';

const getTagsForCast = (allChannels: ChannelObject[], parent_url?: string): IHashTag[] => {
export const getTagsForCast = (allChannels: ChannelObject[], parent_url?: string): IHashTag[] => {
const maybeChannelObj = allChannels?.find((channel) => channel.url === parent_url);
const tags: IHashTag[] = maybeChannelObj
? [{ title: maybeChannelObj.name, id: maybeChannelObj.id, bgColor: 'info' }]
Expand Down
60 changes: 54 additions & 6 deletions src/api/forYouFeed.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { getBotOrNot } from '@app/api/botOrNot.api';
import { PagedCronFeed } from '@app/api/channelFeed.api';
import { getCuration } from '@app/api/curation.api';
import { FeedObject } from '@app/api/feed-types';
import { getTagsForCast } from '@app/api/followingFeed.api';
import { httpApi } from '@app/api/http.api';
import { ChannelObject } from '@app/api/warpcast-types';
import { FORYOU_FEED_PAGESIZE } from '@app/constants/neynarPagination';
import { sift, unique } from 'radash';
import './mocks/mockornot';

interface ForYouFeedRequest {
Expand All @@ -7,11 +15,51 @@ interface ForYouFeedRequest {
cursor?: string;
}

interface ForYouFeedResponse {
casts: {
object: 'cast';
}[];
export const getNeynarOpenrankForYouFeed = (forYouFeedRequestPayload: ForYouFeedRequest): Promise<FeedObject> =>
httpApi.post<FeedObject>('getForYouFeed', { ...forYouFeedRequestPayload }).then(({ data }) => data);

interface EnhancedForYouFeedRequest {
fid: number;
cursor?: string;
powerBadgeUsers: number[];
allChannels: ChannelObject[];
}
export const getNeynarOpenrankForYouFeed = (forYouFeedRequestPayload: ForYouFeedRequest): Promise<ForYouFeedResponse> =>
httpApi.post<ForYouFeedResponse>('getForYouFeed', { ...forYouFeedRequestPayload }).then(({ data }) => data);
export const getEnhancedForYouFeed = async (
homeFeedRequestPayload: EnhancedForYouFeedRequest,
): Promise<PagedCronFeed> => {
const { fid, cursor, powerBadgeUsers, allChannels } = homeFeedRequestPayload;

const forYouFeed = await getNeynarOpenrankForYouFeed({ fid: fid, limit: FORYOU_FEED_PAGESIZE, cursor });
const seenFids = sift(forYouFeed.casts.map((cast) => cast.author.fid).filter((fid) => fid !== null));
const seenHashes = unique(forYouFeed.casts.map((cast) => cast.hash));
const botOrNotResponse = await getBotOrNot({ fids: seenFids ?? [] });
const curation = await getCuration({ hashList: seenHashes ?? [] });

return {
...forYouFeed,
casts: forYouFeed.casts.map((castObject) => ({
...castObject,
amFollowing: true,

authorHasPowerBadge: powerBadgeUsers.find((fid) => fid === castObject.author.fid) !== undefined,
botOrNotResult: botOrNotResponse.fids.find((fid) => fid.fid === castObject.author.fid)?.result ?? {
label: '<unknown>',
summary: '<unknown>',
farcaptcha: false,
},
tags: getTagsForCast(allChannels, castObject.parent_url),
curation: {
upvotes: curation.results.filter(
(result) =>
result.votedFid === castObject.author.fid && result.hash === castObject.hash && result.action === 'upvote',
),
downvotes: curation.results.filter(
(result) =>
result.votedFid === castObject.author.fid &&
result.hash === castObject.hash &&
result.action === 'downvote',
),
},
})),
};
};
187 changes: 187 additions & 0 deletions src/components/apps/forYouFeed/ForYouFeed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { EnhancedCastObject } from '@app/api/channelFeed.api';
import { getEnhancedForYouFeed } from '@app/api/forYouFeed.api';
import { Cast } from '@app/components/apps/cast/Cast';
import { AdvertFeed } from '@app/components/apps/channelFeed/AdvertFeed/AdvertFeed';
import { BaseEmpty } from '@app/components/common/BaseEmpty/BaseEmpty';
import { BaseFeed } from '@app/components/common/BaseFeed/BaseFeed';
import { useAppSelector } from '@app/hooks/reduxHooks';
import { allChannelsQuery, allPowerBadgeUsersQuery, followingByFidQuery } from '@app/queries/queries';
import { useZustand } from '@app/store/zustand';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';

interface ForYouFeedProps {
fid: number;
}
export const ForYouFeed: React.FC<ForYouFeedProps> = ({ fid }) => {
const [allPowerBadgeUsers, setAllPowerBadgeUsers] = useState<number[]>([]);
const [casts, setCasts] = useState<EnhancedCastObject[]>([]);
const [nextCursor, setNextCursor] = useState<string | undefined>();
const [hasMore, setHasMore] = useState<boolean>(true);
const [loaded, setLoaded] = useState<boolean>(false);
const {
setNumCasts,
setNumCuratedChannelsCasts,
setNumFarcaptchas,
setNumUpvotes,
setNumDownvotes,
setNumCastsWithUpvotes,
setNumCastsWithDownvotes,
setNumCastsAboveThreshold,
setNumCastsAfterFiltering,
selectedLabels,
} = useZustand();

const signalToNoiseState = useAppSelector((state) => state.signalToNoise);
const showOnlyCuratedChannels = signalToNoiseState.showOnlyCuratedChannels;
const showOnlyFarcaptcha = signalToNoiseState.showOnlyFarcaptcha;
const onlyShowUpvoted = signalToNoiseState.onlyShowUpvoted;
const hideDownvoted = signalToNoiseState.hideDownvoted;
const onlyShowRatioAboveThreshold = signalToNoiseState.onlyShowRatioAboveThreshold;
const ratioThreshold = signalToNoiseState.ratioThreshold;

const chQuery = useQuery(allChannelsQuery());
const memodChannelData = useMemo(() => {
if (chQuery.isLoading || chQuery.error) return null;
return chQuery?.data?.result?.channels ?? [];
}, [chQuery.isLoading, chQuery.error, chQuery.data]);

const ffQuery = useQuery(followingByFidQuery(fid));
const memodFfData = useMemo(() => {
if (ffQuery.isLoading || ffQuery.error) return null;
return (ffQuery.data?.result?.users ?? []).map((u) => Number(u.fid));
}, [ffQuery.isLoading, ffQuery.error, ffQuery.data]);

const pbQuery = useQuery(allPowerBadgeUsersQuery());
const memodPbData = useMemo(() => {
if (pbQuery.isLoading || pbQuery.error) return null;
return pbQuery.data;
}, [pbQuery.isLoading, pbQuery.error, pbQuery.data]);

useEffect(() => {
const allPowerBadgeUsers = memodPbData?.result.fids ?? [];
setAllPowerBadgeUsers(allPowerBadgeUsers);
}, [memodPbData]);

useEffect(() => {
setCasts([]);

getEnhancedForYouFeed({
fid: fid,
powerBadgeUsers: allPowerBadgeUsers,
allChannels: memodChannelData ?? [],
})
.then((res) => {
setCasts(res.casts);
setNextCursor(res.next?.cursor);
setHasMore(!!res.next?.cursor);
})
.finally(() => {
setLoaded(true);
});
}, [fid, allPowerBadgeUsers, memodChannelData, memodFfData]);

useEffect(() => {
setNumCasts(casts.length);
setNumCuratedChannelsCasts(casts.filter((c) => c.tags.length > 1).length);
setNumFarcaptchas(casts.filter((c) => c.botOrNotResult.farcaptcha).length);
setNumUpvotes(casts.reduce((acc, c) => acc + c.curation.upvotes.length, 0));
setNumDownvotes(casts.reduce((acc, c) => acc + c.curation.downvotes.length, 0));
setNumCastsWithUpvotes(casts.filter((c) => c.curation.upvotes.length > 0).length);
setNumCastsWithDownvotes(casts.filter((c) => c.curation.downvotes.length > 0).length);
setNumCastsAboveThreshold(
casts.filter((c) => c.curation.upvotes.length / (c.curation.downvotes.length + 0.00001) > ratioThreshold).length,
);
}, [
casts,
ratioThreshold,
setNumCasts,
setNumCuratedChannelsCasts,
setNumCastsAboveThreshold,
setNumCastsWithDownvotes,
setNumCastsWithUpvotes,
setNumDownvotes,
setNumFarcaptchas,
setNumUpvotes,
]);

const next = () =>
getEnhancedForYouFeed({
fid: fid,
cursor: nextCursor,
powerBadgeUsers: allPowerBadgeUsers,
allChannels: memodChannelData ?? [],
}).then((newCasts) => {
setNextCursor(newCasts.next?.cursor);
setCasts(casts.concat(newCasts.casts));
});

const filteredCastsList = useMemo(() => {
const filteredCasts = casts
.filter((c) => !showOnlyCuratedChannels || c.tags.length > 1)
.filter((c) => !showOnlyFarcaptcha || c.botOrNotResult.farcaptcha)
.filter((c) => !onlyShowUpvoted || c.curation.upvotes.length > 0)
.filter((c) => (hideDownvoted ? c.curation.downvotes.length < 1 : true))
.filter((c) =>
onlyShowRatioAboveThreshold
? c.curation.upvotes.length / (c.curation.downvotes.length + 0.00001) > ratioThreshold
: true,
)
.filter((c) =>
selectedLabels.length === 0 ? true : selectedLabels.includes(c.botOrNotResult.label ?? 'missing-label'),
)
.map((post, index) => (
<Cast
level={0}
key={`${post.hash}-top-${index}`}
castHash={post.hash}
title={' '}
description={post.text}
date={post.timestamp}
embeds={post.embeds ?? []}
displayName={post.author.display_name}
fid={post.author.fid}
fname={post.author.username ?? post.author.display_name ?? post.author.fid.toString() ?? '<unknown>'}
avatar={post.author.pfp_url}
parentHash={post.parent_hash}
threadHash={post.thread_hash}
parentUrl={post.parent_url}
replies={post.replies.count}
recasts={post.reactions.recasts_count}
recastooors={post.reactions.recasts.map((r) => r.fid)}
likes={post.reactions.likes_count}
likooors={post.reactions.likes.map((l) => l.fid)}
tags={post.tags}
curation={post.curation}
hasPowerBadge={post.authorHasPowerBadge}
botOrNotResult={post.botOrNotResult}
/>
));
setNumCastsAfterFiltering(filteredCasts.length);
return filteredCasts;
}, [
casts,
setNumCastsAfterFiltering,
showOnlyCuratedChannels,
showOnlyFarcaptcha,
onlyShowUpvoted,
hideDownvoted,
onlyShowRatioAboveThreshold,
ratioThreshold,
selectedLabels,
]);

return (
<AdvertFeed casts={casts}>
{({ castList }) =>
castList?.length || !loaded ? (
<BaseFeed next={next} hasMore={hasMore}>
{filteredCastsList}
</BaseFeed>
) : (
<BaseEmpty />
)
}
</AdvertFeed>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import styled from 'styled-components';
export const FeedSettings: React.FC = () => {
const { pathname } = useLocation();
const isHomeFeed = pathname.startsWith('/home');
const isForYouFeed = pathname.startsWith('/foryou');
const { t } = useTranslation();

const {
Expand Down Expand Up @@ -83,7 +84,7 @@ export const FeedSettings: React.FC = () => {
<br />
{numCastsAfterFiltering} Casts after filtering
<BaseDivider />
{isHomeFeed ? (
{isHomeFeed || isForYouFeed ? (
<>
<SwitchContainer>
<span>{t('Only Curated Channels')}</span> {numCuratedChannelsCasts}
Expand Down
15 changes: 12 additions & 3 deletions src/components/header/layouts/DesktopHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ interface DesktopHeaderProps {
export const DesktopHeader: React.FC<DesktopHeaderProps> = ({ isTwoColumnsLayout }) => {
const { pathname } = useLocation();
const isHomeFeed = pathname.startsWith('/home');
const isForYouFeed = pathname.startsWith('/foryou');
const isChannelFeed = pathname.startsWith('/~/channel/');
const isDecentBookmarksPage = pathname.startsWith('/external/decent-bookmarks');
const isVotePage = pathname.startsWith('/votes');

const leftSide = isTwoColumnsLayout ? (
<S.SearchColumn xl={16} xxl={17}>
<BaseRow justify="space-between">
{(isHomeFeed || isChannelFeed) && (
{(isHomeFeed || isForYouFeed || isChannelFeed) && (
<BaseCol>
<HeaderSignalToNoise />
</BaseCol>
Expand All @@ -39,7 +40,7 @@ export const DesktopHeader: React.FC<DesktopHeaderProps> = ({ isTwoColumnsLayout
</S.SearchColumn>
) : (
<>
{(isHomeFeed || isChannelFeed) && (
{(isHomeFeed || isForYouFeed || isChannelFeed) && (
<>
<BaseCol>
<HeaderSignalToNoise />
Expand All @@ -55,7 +56,15 @@ export const DesktopHeader: React.FC<DesktopHeaderProps> = ({ isTwoColumnsLayout
</BaseCol>
)}
<BaseCol>
{isChannelFeed ? <ChannelLogo /> : isHomeFeed ? <S.CCAButton /> : isVotePage ? <S.CCAButton /> : <S.FCButton />}
{isChannelFeed ? (
<ChannelLogo />
) : isHomeFeed || isForYouFeed ? (
<S.CCAButton />
) : isVotePage ? (
<S.CCAButton />
) : (
<S.FCButton />
)}
</BaseCol>
</>
);
Expand Down
5 changes: 3 additions & 2 deletions src/components/header/layouts/MobileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const MobileHeader: React.FC<MobileHeaderProps> = ({ toggleSider, isSider

const { pathname } = useLocation();
const isHomeFeed = pathname.startsWith('/home');
const isForYouFeed = pathname.startsWith('/foryou');
const isChannelFeed = pathname.startsWith('/~/channel/');
const isVotePage = pathname.startsWith('/votes');

Expand All @@ -30,7 +31,7 @@ export const MobileHeader: React.FC<MobileHeaderProps> = ({ toggleSider, isSider

<BaseCol>
<BaseRow align="middle" justify="space-between">
{(isHomeFeed || isChannelFeed) && (
{(isHomeFeed || isForYouFeed || isChannelFeed) && (
<>
<BaseCol>
<ZenModeDropdown />
Expand All @@ -44,7 +45,7 @@ export const MobileHeader: React.FC<MobileHeaderProps> = ({ toggleSider, isSider
<BaseCol>
{isChannelFeed ? (
<ChannelLogo />
) : isHomeFeed ? (
) : isHomeFeed || isForYouFeed ? (
<S.CCAButton />
) : isVotePage ? (
<S.CCAButton />
Expand Down
4 changes: 2 additions & 2 deletions src/components/layouts/main/sider/SiderMenu/SiderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const SiderMenu: React.FC<SiderContentProps> = ({ setCollapsed }) => {
);

const sidebarNavigation = [
sidebarNavigationPre[0],
...sidebarNavigationPre.slice(0, 2),
{
...channelsSkeleton,
children: [
Expand All @@ -82,7 +82,7 @@ const SiderMenu: React.FC<SiderContentProps> = ({ setCollapsed }) => {
),
],
},
...sidebarNavigationPre.slice(1),
...sidebarNavigationPre.slice(2),
];

const sidebarNavFlat = sidebarNavigation.reduce(
Expand Down
7 changes: 7 additions & 0 deletions src/components/layouts/main/sider/sidebarNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
HomeOutlined,
LayoutOutlined,
LineChartOutlined,
UserOutlined,
} from '@ant-design/icons';
import { ThumbsUpIcon } from 'lucide-react';

Expand All @@ -28,6 +29,12 @@ export const sidebarNavigation: SidebarNavigationItem[] = [
icon: <HomeOutlined />,
url: '/home',
},
{
title: 'sidebar.foryou',
key: 'foryou',
icon: <UserOutlined />,
url: '/foryou',
},
{
title: 'sidebar.curated',
key: 'curated',
Expand Down
Loading

0 comments on commit 586ed85

Please sign in to comment.