Skip to content

Commit

Permalink
Implementing recent unfollows (#513)
Browse files Browse the repository at this point in the history
* added unfollows but sql query doesnt execute locally (I think)

* removed some lines

* added check if unfollows exist

* first version unfollowers

* added unfollow type and more

* unfollows done? not tested

* minor fixes

---------

Co-authored-by: hellno <hellno@users.noreply.github.com>
  • Loading branch information
dragmakex and hellno authored Sep 12, 2024
1 parent 63c06f3 commit 691aa41
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 12 deletions.
7 changes: 4 additions & 3 deletions pages/analytics/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { getPlanLimitsForPlan } from '@/config/planLimits';
import { isPaidUser } from '@/stores/useUserStore';
import UpgradeFreePlanCard from '@/common/components/UpgradeFreePlanCard';
import { ArrowRightIcon } from '@heroicons/react/24/solid';
import RecentUnfollows from '@/common/components/Analytics/RecentUnfollows';

type FidToAnalyticsData = Record<string, AnalyticsData>;
const intervals = [Interval.d7, Interval.d30, Interval.d90];
Expand Down Expand Up @@ -260,7 +261,7 @@ export default function AnalyticsPage() {
<TabsList>
<TabsTrigger value="default">Top Casts</TabsTrigger>
<TabsTrigger value="followers">Top Followers (beta)</TabsTrigger>
<TabsTrigger value="unfollows">Unfollows (soon)</TabsTrigger>
<TabsTrigger value="unfollows">Recent Unfollows</TabsTrigger>
</TabsList>
</div>
<TabsContent value="default" className="max-w-2xl">
Expand All @@ -277,9 +278,9 @@ export default function AnalyticsPage() {
</TabsContent>
<TabsContent value="unfollows" className="max-w-2xl">
<div className="my-4">
<h2 className="text-2xl font-bold">Unfollows</h2>
<h2 className="text-2xl font-bold">Recent Unfollows</h2>
</div>
<div>Coming soon...</div>
{analyticsData.unfollows && <RecentUnfollows fid={fid} unfollows={analyticsData.unfollows} />}
</TabsContent>
</Tabs>
</>
Expand Down
2 changes: 1 addition & 1 deletion pages/api/additionalProfileInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const [airstackSocialInfo, icebreakerSocialInfo, coordinapeAttestations] = await Promise.all([
getAirstackSocialInfoForFid(fid),
getIcebreakerSocialInfoForFid(fid),
addresses ? getCoordinapeInfoForAddresses(addresses.toString()) : {},
addresses ? getCoordinapeInfoForAddresses(addresses.toString()) : [],
]);
res.status(200).json({ airstackSocialInfo, icebreakerSocialInfo, coordinapeAttestations });
} catch (error) {
Expand Down
72 changes: 72 additions & 0 deletions src/common/components/Analytics/RecentUnfollows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { useAccountStore } from '@/stores/useAccountStore';
import Link from 'next/link';
import ProfileInfoContent from '../ProfileInfoContent';
import { getProfile, getProfileFetchIfNeeded } from '@/common/helpers/profileUtils';
import { useDataStore } from '@/stores/useDataStore';
import { Loading } from '../Loading';
import { UnfollowData } from '@/common/types/types';

const RECENT_UNFOLLOWERS_LIMIT = 12;
const APP_FID = process.env.NEXT_PUBLIC_APP_FID!;

type RecentUnfollowsProps = {
fid: number;
unfollows: UnfollowData[];
};

const RecentUnfollows = ({ fid, unfollows }: RecentUnfollowsProps) => {
const [isLoading, setIsLoading] = useState(false);
const dataStore = useDataStore.getState();
const viewerFid = Number(
useAccountStore((state) => state.accounts[state.selectedAccountIdx]?.platformAccountId) || APP_FID
);

useEffect(() => {
const getData = async () => {
setIsLoading(true);
try {
const recentUnfollows = unfollows
.filter((unfollow) => unfollow.target_fid !== undefined)
.slice(0, RECENT_UNFOLLOWERS_LIMIT);

recentUnfollows.forEach((unfollow) =>
getProfileFetchIfNeeded({
fid: unfollow.target_fid.toString(),
viewerFid: viewerFid.toString(),
})
);
} catch (e) {
console.error('Error fetching recent unfollows:', e);
} finally {
setIsLoading(false);
}
};
if (fid && unfollows.length > 0) {
getData();
}
}, [fid, unfollows]);

const profiles = useMemo(
() => unfollows.map((unfollow) => getProfile(dataStore, undefined, unfollow.target_fid.toString())).filter(Boolean),
[dataStore, unfollows]
);

return (
<Card className="h-fit py-8 px-4">
<CardContent className="items-start grid gap-8 grid-cols-2 grid-flow-row">
{isLoading && <Loading />}
{profiles.map((profile) => (
<div key={`recent-unfollower-${profile.fid}`} className="flex items-center">
<Link href={`/profile/${profile?.username}`} prefetch={false} className="w-full text-left">
<ProfileInfoContent profile={profile} hideBio />
</Link>
</div>
))}
</CardContent>
</Card>
);
};

export default RecentUnfollows;
14 changes: 8 additions & 6 deletions src/common/components/Analytics/TopFollowers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Loading } from '../Loading';

const TOP_FOLLOWERS_LIMIT = 12;
const APP_FID = process.env.NEXT_PUBLIC_APP_FID!;

type TopFollowersProps = {
fid: number;
};
Expand All @@ -32,8 +33,9 @@ const TopFollowers = ({ fid }: TopFollowersProps) => {
.fetchRelevantFollowers(fid, viewerFid)
.then((response) => response.all_relevant_followers_dehydrated.map((follower) => follower.user?.fid));

setTopFollowerFids(fids.filter((fid) => fid !== undefined).slice(0, TOP_FOLLOWERS_LIMIT));
fids.forEach((fid) =>
const topFids = fids.filter((fid) => fid !== undefined).slice(0, TOP_FOLLOWERS_LIMIT);
setTopFollowerFids(topFids);
topFids.forEach((fid) =>
getProfileFetchIfNeeded({
fid: fid?.toString(),
viewerFid: viewerFid.toString(),
Expand All @@ -50,10 +52,10 @@ const TopFollowers = ({ fid }: TopFollowersProps) => {
}
}, [fid]);

const profiles = useMemo(() => {
const followers = topFollowerFids.map((fid) => getProfile(dataStore, undefined, fid.toString()));
return followers.filter((follower) => follower !== undefined);
}, [dataStore, topFollowerFids]);
const profiles = useMemo(
() => topFollowerFids.map((fid) => getProfile(dataStore, undefined, fid.toString())).filter(Boolean),
[dataStore, topFollowerFids]
);

return (
<Card className="h-fit py-8 px-4">
Expand Down
3 changes: 3 additions & 0 deletions src/common/helpers/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { AggregatedAnalytics } from '../types/types';
import { UTCDate } from '@date-fns/utc';

export const fillMissingDaysBetweenDates = (data: AggregatedAnalytics[], startDate: Date, endDate: Date) => {
if (!data || data.length === 0) {
return [];
}
const filledData: AggregatedAnalytics[] = [];
const currentDate = new UTCDate(startDate);
const utcEndDate = new UTCDate(endDate);
Expand Down
2 changes: 1 addition & 1 deletion src/common/helpers/coordinapeAttestations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const fetchAttestations = async (addresses: string[]): Promise<RawAttestationDat

export async function getCoordinapeInfoForAddresses(addressesString: string): Promise<CoordinapeAttestation[]> {
if (!addressesString || !addressesString.length) {
return [];
return {};
}

const addresses = addressesString.split(',');
Expand Down
6 changes: 6 additions & 0 deletions src/common/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export type CastData = {
recast_count: string;
};

export type UnfollowData = {
target_fid: number;
deleted_at: string;
};

export type CombinedActivityData = {
overview: OverviewAnalytics;
aggregated: AggregatedAnalytics[];
Expand All @@ -31,6 +36,7 @@ export type AnalyticsData = {
reactions: CombinedActivityData;
casts: CombinedActivityData;
topCasts: CastData[];
unfollows: UnfollowData[];
};

export type AnalyticsKey = 'follows' | 'casts' | 'reactions';
Expand Down
18 changes: 18 additions & 0 deletions supabase/functions/_shared/queryHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { sql } from 'kysely';
import { NumberToBytesErrorType } from 'viem';

export function buildAnalyticsQuery(
tableName: string,
Expand Down Expand Up @@ -77,6 +78,23 @@ export function getTopCasts(fid: number, limit: number = 30) {
`;
}

export function getRecentUnfollows(fid: number, limit: number = 50) {
return sql`
SELECT
target_fid,
deleted_at
FROM
links
WHERE
fid = ${fid}
AND deleted_at IS NOT NULL
AND deleted_at >= NOW() - INTERVAL '90 days'
ORDER BY
deleted_at DESC
LIMIT ${limit};
`;
}

export const formatResponseSection = (data: any) => ({
aggregated: data.aggregated,
overview: {
Expand Down
11 changes: 10 additions & 1 deletion supabase/functions/create-analytics-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import 'jsr:@supabase/functions-js/edge-runtime.d.ts';
import { createClient } from '@supabase/supabase-js';
import * as Sentry from 'https://deno.land/x/sentry/index.mjs';
import { Database } from '../_shared/db.ts';
import { buildAnalyticsQuery, formatResponseSection, getTopCasts } from '../_shared/queryHelpers.ts';
import {
buildAnalyticsQuery,
formatResponseSection,
getRecentUnfollows,
getTopCasts,
} from '../_shared/queryHelpers.ts';
import { corsHeaders } from '../_shared/cors.ts';
import { Kysely, PostgresAdapter, PostgresDialect, PostgresIntrospector, PostgresQueryCompiler, sql } from 'kysely';
// import { Client } from "postgres";
Expand Down Expand Up @@ -119,11 +124,15 @@ Deno.serve(async (req) => {
const topCastsQuery = getTopCasts(fid);
const topCasts = (await topCastsQuery.execute(db))?.rows;

const unfollowsQuery = getRecentUnfollows(fid);
const unfollows = (await unfollowsQuery.execute(db))?.rows;

const res = {
follows: formatResponseSection(links),
reactions: formatResponseSection(reactions),
casts: formatResponseSection(casts),
topCasts: topCasts,
unfollows: unfollows,
};

const { error: upsertError } = await supabaseClient.from('analytics').upsert(
Expand Down

0 comments on commit 691aa41

Please sign in to comment.