From d17e3bc6a4eedd36583763c58cf0b70720d181b2 Mon Sep 17 00:00:00 2001 From: Wesley Finck Date: Thu, 19 Mar 2026 14:11:53 -0700 Subject: [PATCH] filter feed by activity type (behind feature flag) --- .../components/feedControls/FeedControls.tsx | 52 ++++++++++++++++++- .../myFeedContainer/MyFeedContainer.tsx | 13 ++++- src/webapp/features/feeds/lib/dal.ts | 6 ++- src/webapp/features/feeds/lib/feedKeys.ts | 36 +++++++++---- .../feeds/lib/queries/useFollowingFeed.tsx | 11 +++- .../feeds/lib/queries/useGemsFeed.tsx | 11 +++- .../feeds/lib/queries/useGlobalFeed.tsx | 11 +++- 7 files changed, 121 insertions(+), 19 deletions(-) diff --git a/src/webapp/features/feeds/components/feedControls/FeedControls.tsx b/src/webapp/features/feeds/components/feedControls/FeedControls.tsx index 67f34039..8ce28023 100644 --- a/src/webapp/features/feeds/components/feedControls/FeedControls.tsx +++ b/src/webapp/features/feeds/components/feedControls/FeedControls.tsx @@ -10,7 +10,7 @@ import { } from '@mantine/core'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; -import { ActivitySource, UrlType } from '@semble/types'; +import { ActivitySource, UrlType, ActivityType } from '@semble/types'; import { useOptimistic, useState, useTransition } from 'react'; import { FaSeedling } from 'react-icons/fa6'; import { IoMdCheckmark } from 'react-icons/io'; @@ -19,6 +19,7 @@ import SembleLogo from '@/assets/semble-logo.svg'; import { getUrlTypeIcon } from '@/lib/utils/icon'; import { upperFirst } from '@mantine/hooks'; import { MdFilterList } from 'react-icons/md'; +import { useFeatureFlags } from '@/lib/clientFeatureFlags'; const sourceOptions = [ { value: null, label: 'All', icon: null }, @@ -43,13 +44,22 @@ const feedOptions = [ { value: 'following' as const, label: 'Following' }, ]; +const activityTypeOptions = [ + { value: ActivityType.CARD_COLLECTED, label: 'Card Collected' }, + { value: ActivityType.CONNECTION_CREATED, label: 'Card Connected' }, +]; + export default function FeedControls() { const router = useRouter(); const searchParams = useSearchParams(); + const { data: featureFlags } = useFeatureFlags(); const sourceFromUrl = searchParams.get('source') as ActivitySource | null; const feedFromUrl = (searchParams.get('feed') as 'global' | 'following') || 'global'; const typeFromUrl = searchParams.get('type') as UrlType | null; + const activityTypesFromUrl = searchParams.getAll('activityTypes') as + | ActivityType[] + | []; const [optimisticSource, setOptimisticSource] = useOptimistic(sourceFromUrl); @@ -59,6 +69,8 @@ export default function FeedControls() { const [optimisticType, setOptimisticType] = useOptimistic( typeFromUrl, ); + const [optimisticActivityTypes, setOptimisticActivityTypes] = + useOptimistic(activityTypesFromUrl); const [typePopoverOpened, setTypePopoverOpened] = useState(false); @@ -120,6 +132,24 @@ export default function FeedControls() { setTypePopoverOpened(false); }; + const handleActivityTypeToggle = (activityType: ActivityType) => { + startTransition(() => { + const currentTypes = optimisticActivityTypes; + const isSelected = currentTypes.includes(activityType); + const nextTypes = isSelected + ? currentTypes.filter((t) => t !== activityType) + : [...currentTypes, activityType]; + + setOptimisticActivityTypes(nextTypes); + + const params = new URLSearchParams(searchParams.toString()); + params.delete('activityTypes'); + nextTypes.forEach((type) => params.append('activityTypes', type)); + + router.push(`?${params.toString()}`, { scroll: false }); + }); + }; + const SelectedTypeIcon = optimisticType === null ? MdFilterList : getUrlTypeIcon(optimisticType); @@ -216,6 +246,26 @@ export default function FeedControls() { + + {featureFlags?.connections && ( + <> + Activity Type + {activityTypeOptions.map((option) => ( + handleActivityTypeToggle(option.value)} + rightSection={ + optimisticActivityTypes.includes(option.value) ? ( + + ) : null + } + closeMenuOnClick={false} + > + {option.label} + + ))} + + )} diff --git a/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx b/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx index 45d596a0..e7b28cf1 100644 --- a/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx +++ b/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx @@ -16,7 +16,7 @@ import MyFeedContainerSkeleton from './Skeleton.MyFeedContainer'; import MyFeedContainerError from './Error.MyFeedContainer'; import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; import RefetchButton from '@/components/navigation/refetchButton/RefetchButton'; -import { UrlType, ActivitySource } from '@semble/types'; +import { UrlType, ActivitySource, ActivityType } from '@semble/types'; import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import { CardSaveSource } from '@/features/analytics/types'; import { useState, useEffect } from 'react'; @@ -29,13 +29,24 @@ export default function MyFeedContainer() { const selectedFeed = (searchParams.get('feed') as 'global' | 'following') || 'global'; + // Parse activityTypes from URL params (can be multiple) + const activityTypesParam = searchParams.getAll('activityTypes'); + const selectedActivityTypes = + activityTypesParam.length > 0 + ? (activityTypesParam.filter((type) => + Object.values(ActivityType).includes(type as ActivityType), + ) as ActivityType[]) + : undefined; + const globalFeed = useGlobalFeed({ urlType: selectedUrlType, source: selectedSource, + activityTypes: selectedActivityTypes, }); const followingFeed = useFollowingFeed({ urlType: selectedUrlType, source: selectedSource, + activityTypes: selectedActivityTypes, enabled: selectedFeed === 'following', }); diff --git a/src/webapp/features/feeds/lib/dal.ts b/src/webapp/features/feeds/lib/dal.ts index 5a8c2d14..859b2cb5 100644 --- a/src/webapp/features/feeds/lib/dal.ts +++ b/src/webapp/features/feeds/lib/dal.ts @@ -1,5 +1,5 @@ import { createSembleClient } from '@/services/client.apiClient'; -import { UrlType, ActivitySource } from '@semble/types'; +import { UrlType, ActivitySource, ActivityType } from '@semble/types'; import { cache } from 'react'; import { verifySessionOnClient } from '@/lib/auth/dal'; @@ -8,6 +8,7 @@ interface PageParams { limit?: number; urlType?: UrlType; source?: ActivitySource; + activityTypes?: ActivityType[]; } export const getGlobalFeed = cache(async (params?: PageParams) => { @@ -17,6 +18,7 @@ export const getGlobalFeed = cache(async (params?: PageParams) => { limit: params?.limit, urlType: params?.urlType, source: params?.source, + activityTypes: params?.activityTypes, }); return response; @@ -29,6 +31,7 @@ export const getGemsActivityFeed = cache(async (params?: PageParams) => { limit: params?.limit, urlType: params?.urlType, source: params?.source, + activityTypes: params?.activityTypes, }); return response; @@ -44,6 +47,7 @@ export const getFollowingFeed = cache(async (params?: PageParams) => { limit: params?.limit, urlType: params?.urlType, source: params?.source, + activityTypes: params?.activityTypes, }); return response; diff --git a/src/webapp/features/feeds/lib/feedKeys.ts b/src/webapp/features/feeds/lib/feedKeys.ts index e5f8db08..aa90b84b 100644 --- a/src/webapp/features/feeds/lib/feedKeys.ts +++ b/src/webapp/features/feeds/lib/feedKeys.ts @@ -1,24 +1,40 @@ -import { UrlType, ActivitySource } from '@semble/types'; +import { UrlType, ActivitySource, ActivityType } from '@semble/types'; export const feedKeys = { all: () => ['feeds'] as const, - infinite: (limit?: number, urlType?: UrlType, source?: ActivitySource) => [ - ...feedKeys.all(), - 'infinite', - limit, - urlType, - source, - ], + infinite: ( + limit?: number, + urlType?: UrlType, + source?: ActivitySource, + activityTypes?: ActivityType[], + ) => [...feedKeys.all(), 'infinite', limit, urlType, source, activityTypes], gems: () => [...feedKeys.all(), 'gems'] as const, gemsInfinite: ( limit?: number, urlType?: UrlType, source?: ActivitySource, - ) => [...feedKeys.gems(), [...feedKeys.infinite()], urlType, limit, source], + activityTypes?: ActivityType[], + ) => [ + ...feedKeys.gems(), + [...feedKeys.infinite()], + urlType, + limit, + source, + activityTypes, + ], following: () => [...feedKeys.all(), 'following'] as const, followingInfinite: ( limit?: number, urlType?: UrlType, source?: ActivitySource, - ) => [...feedKeys.following(), 'infinite', limit, urlType, source] as const, + activityTypes?: ActivityType[], + ) => + [ + ...feedKeys.following(), + 'infinite', + limit, + urlType, + source, + activityTypes, + ] as const, }; diff --git a/src/webapp/features/feeds/lib/queries/useFollowingFeed.tsx b/src/webapp/features/feeds/lib/queries/useFollowingFeed.tsx index 4aeff237..fdfd42a7 100644 --- a/src/webapp/features/feeds/lib/queries/useFollowingFeed.tsx +++ b/src/webapp/features/feeds/lib/queries/useFollowingFeed.tsx @@ -1,12 +1,13 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { getFollowingFeed } from '../dal'; import { feedKeys } from '../feedKeys'; -import { UrlType, ActivitySource } from '@semble/types'; +import { UrlType, ActivitySource, ActivityType } from '@semble/types'; interface Props { limit?: number; urlType?: UrlType; source?: ActivitySource; + activityTypes?: ActivityType[]; enabled?: boolean; } @@ -15,7 +16,12 @@ export default function useFollowingFeed(props?: Props) { const enabled = props?.enabled ?? true; const query = useInfiniteQuery({ - queryKey: feedKeys.followingInfinite(limit, props?.urlType, props?.source), + queryKey: feedKeys.followingInfinite( + limit, + props?.urlType, + props?.source, + props?.activityTypes, + ), staleTime: 10000, initialPageParam: 1, refetchOnWindowFocus: false, @@ -26,6 +32,7 @@ export default function useFollowingFeed(props?: Props) { page: pageParam, urlType: props?.urlType, source: props?.source, + activityTypes: props?.activityTypes, }); }, getNextPageParam: (lastPage) => { diff --git a/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx b/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx index 904d141a..9175a93c 100644 --- a/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx +++ b/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx @@ -1,19 +1,25 @@ import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { getGemsActivityFeed } from '../dal'; import { feedKeys } from '../feedKeys'; -import { UrlType, ActivitySource } from '@semble/types'; +import { UrlType, ActivitySource, ActivityType } from '@semble/types'; interface Props { limit?: number; urlType?: UrlType; source?: ActivitySource; + activityTypes?: ActivityType[]; } export default function useGemsFeed(props?: Props) { const limit = props?.limit ?? 15; const query = useSuspenseInfiniteQuery({ - queryKey: feedKeys.gemsInfinite(limit, props?.urlType, props?.source), + queryKey: feedKeys.gemsInfinite( + limit, + props?.urlType, + props?.source, + props?.activityTypes, + ), staleTime: 10000, initialPageParam: 1, refetchOnWindowFocus: false, @@ -23,6 +29,7 @@ export default function useGemsFeed(props?: Props) { page: pageParam, urlType: props?.urlType, source: props?.source, + activityTypes: props?.activityTypes, }); }, getNextPageParam: (lastPage) => { diff --git a/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx b/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx index ef17bf95..27f6321e 100644 --- a/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx +++ b/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx @@ -1,19 +1,25 @@ import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { getGlobalFeed } from '../dal'; import { feedKeys } from '../feedKeys'; -import { UrlType, ActivitySource } from '@semble/types'; +import { UrlType, ActivitySource, ActivityType } from '@semble/types'; interface Props { limit?: number; urlType?: UrlType; source?: ActivitySource; + activityTypes?: ActivityType[]; } export default function useGlobalFeed(props?: Props) { const limit = props?.limit ?? 15; const query = useSuspenseInfiniteQuery({ - queryKey: feedKeys.infinite(limit, props?.urlType, props?.source), + queryKey: feedKeys.infinite( + limit, + props?.urlType, + props?.source, + props?.activityTypes, + ), staleTime: 10000, initialPageParam: 1, refetchOnWindowFocus: false, @@ -23,6 +29,7 @@ export default function useGlobalFeed(props?: Props) { page: pageParam, urlType: props?.urlType, source: props?.source, + activityTypes: props?.activityTypes, }); }, getNextPageParam: (lastPage) => { -- 2.51.2