Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GAL-5440] add Suggested Profiles in curated Feed web #2429

Merged
merged 12 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/web/src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { graphql, useFragment } from 'react-relay';
import styled from 'styled-components';

import GalleryLink from '~/components/core/GalleryLink/GalleryLink';
import IconContainer from '~/components/core/IconContainer';
import IconContainer, { IconSize } from '~/components/core/IconContainer';
import Tooltip from '~/components/Tooltip/Tooltip';
import { BadgeFragment$key } from '~/generated/BadgeFragment.graphql';
import TopActivityBadgeIcon from '~/icons/TopActivityBadgeIcon';
Expand All @@ -16,9 +16,10 @@ import { getUrlForCommunityDangerously } from '~/utils/getCommunityUrl';
type Props = {
badgeRef: BadgeFragment$key;
eventContext: GalleryElementTrackingProps['eventContext'];
size?: IconSize;
};

export default function Badge({ badgeRef, eventContext }: Props) {
export default function Badge({ badgeRef, eventContext, size = 'md' as IconSize }: Props) {
const [showTooltip, setShowTooltip] = useState(false);

const badge = useFragment(
Expand Down Expand Up @@ -88,7 +89,7 @@ export default function Badge({ badgeRef, eventContext }: Props) {
<>
<StyledTooltip text={name || ''} showTooltip={showTooltip} />
<IconContainer
size="md"
size={size}
variant="default"
icon={
<StyledBadge
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/Feed/CuratedFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default function CuratedFeed({ queryRef }: Props) {
loadNextPage={loadNextPage}
hasNext={hasPrevious}
feedMode={'WORLDWIDE'}
showSuggestedProfiles
/>
);
}
112 changes: 100 additions & 12 deletions apps/web/src/components/Feed/FeedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import {
WindowScroller,
} from 'react-virtualized';
import { MeasuredCellParent } from 'react-virtualized/dist/es/CellMeasurer';
import { FragmentRefs } from 'relay-runtime';
import { v4 as uuid } from 'uuid';

import FeedSuggestedProfileSection from '~/components/Feed/FeedSuggestedProfileSection';
import { FeedMode } from '~/components/Feed/types';
import { FeedListEventDataFragment$key } from '~/generated/FeedListEventDataFragment.graphql';
import { FeedListFragment$key } from '~/generated/FeedListFragment.graphql';
import { useIsMobileOrMobileLargeWindowWidth } from '~/hooks/useWindowSize';

import FeedEventItem from './FeedEventItem';
import { PostItemWithBoundary as PostItem } from './PostItem';
Expand All @@ -23,6 +27,7 @@ type Props = {
queryRef: FeedListFragment$key;
feedEventRefs: FeedListEventDataFragment$key;
feedMode?: FeedMode;
showSuggestedProfiles?: boolean;
};

export default function FeedList({
Expand All @@ -31,12 +36,21 @@ export default function FeedList({
hasNext,
queryRef,
feedMode,
showSuggestedProfiles = false,
}: Props) {
const query = useFragment(
graphql`
fragment FeedListFragment on Query {
viewer {
... on Viewer {
user {
dbid
}
}
}
...PostItemWithErrorBoundaryQueryFragment
...FeedEventItemWithErrorBoundaryQueryFragment
...FeedSuggestedProfileSectionWithBoundaryFragment
}
`,
queryRef
Expand All @@ -59,11 +73,34 @@ export default function FeedList({
feedEventRefs
);

const isLoggedIn = useMemo(() => Boolean(query.viewer?.user?.dbid), [query.viewer?.user?.dbid]);
const isMobileOrMobileLargeWindowWidth = useIsMobileOrMobileLargeWindowWidth();
const suggestedProfileSectionHeight = isMobileOrMobileLargeWindowWidth ? 320 : 360;

// insert suggested profiles in between posts if showSuggestedProfiles is true
const finalFeedData = useMemo(() => {
const suggestedProfileSectionIdx = 8;
if (showSuggestedProfiles && isLoggedIn && feedData?.length >= suggestedProfileSectionIdx) {
const suggestedProfileSectionData = {
__typename: 'SuggestedProfileSection',
dbid: uuid(),
};

const insertAt = feedData.length - suggestedProfileSectionIdx;
return [
...feedData.slice(0, insertAt),
suggestedProfileSectionData,
...feedData.slice(insertAt),
];
}
return feedData;
}, [feedData, showSuggestedProfiles, isLoggedIn]);

// Keep the current feed data in a ref so we can access it below in the
// CellMeasurerCache's keyMapper without having to create a new cache
// every time the feed data changes.
const feedDataRef = useRef(feedData);
feedDataRef.current = feedData;
const feedDataRef = useRef(finalFeedData);
feedDataRef.current = finalFeedData;

const measurerCache = useMemo(() => {
return new CellMeasurerCache({
Expand All @@ -83,8 +120,8 @@ export default function FeedList({

// Function responsible for tracking the loaded state of each row.
const isRowLoaded = useCallback(
({ index }: { index: number }) => !hasNext || Boolean(feedData[index]),
[feedData, hasNext]
({ index }: { index: number }) => !hasNext || Boolean(finalFeedData[index]),
[finalFeedData, hasNext]
);

const virtualizedListRef = useRef<List | null>(null);
Expand Down Expand Up @@ -114,7 +151,7 @@ export default function FeedList({
return <div />;
}
// graphql returns the oldest event at the top of the list, so display in opposite order
const content = feedData[feedData.length - index - 1];
const content = finalFeedData[finalFeedData.length - index - 1];

// Better safe than sorry :)
if (!content) {
Expand All @@ -136,7 +173,7 @@ export default function FeedList({
<PostItem
onPotentialLayoutShift={handlePotentialLayoutShift}
index={index}
eventRef={content}
eventRef={content as FeedContentType}
key={content.dbid}
queryRef={query}
measure={measure}
Expand Down Expand Up @@ -168,7 +205,7 @@ export default function FeedList({
// to re-evaluate the height of the item to keep the virtualization good.
onPotentialLayoutShift={handlePotentialLayoutShift}
index={index}
eventRef={content}
eventRef={content as FeedContentType}
key={content.dbid}
queryRef={query}
feedMode={feedMode}
Expand All @@ -179,9 +216,28 @@ export default function FeedList({
);
}

if (content.__typename === 'SuggestedProfileSection') {
return (
<CellMeasurer
cache={measurerCache}
columnIndex={0}
rowIndex={index}
key={key}
parent={parent}
>
{({ registerChild }) => (
// @ts-expect-error: this is the suggested usage of registerChild
<div ref={registerChild} style={style} key={key}>
<FeedSuggestedProfileSection queryRef={query} />
</div>
)}
</CellMeasurer>
);
}

return null;
},
[feedData, feedMode, handlePotentialLayoutShift, isRowLoaded, measurerCache, query]
[finalFeedData, feedMode, handlePotentialLayoutShift, isRowLoaded, measurerCache, query]
);

const [, setIsLoading] = useState(false);
Expand All @@ -196,10 +252,31 @@ export default function FeedList({
function recalculateHeightsWhenEventsChange() {
virtualizedListRef.current?.recomputeRowHeights();
},
[feedData, measurerCache]
[finalFeedData, measurerCache]
);

const rowCount = hasNext ? feedData.length + 1 : feedData.length;
const rowCount = hasNext ? finalFeedData.length + 1 : finalFeedData.length;

const rowHeight = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work figuring this out!

({ index }: { index: number }) => {
if (!finalFeedData) {
return DEFAULT_ROW_HEIGHT;
}

// Determine the actual data index based on reverse order logic used in rowRenderer
const dataIndex = finalFeedData.length - index - 1;
const item = finalFeedData[dataIndex];

// Return static height for SuggestedProfileSection
if (item?.__typename === 'SuggestedProfileSection') {
return suggestedProfileSectionHeight;
}

// Return dynamic height for other types of content
return measurerCache.rowHeight({ index });
},
[finalFeedData, suggestedProfileSectionHeight, measurerCache]
);

return (
<WindowScroller>
Expand All @@ -224,8 +301,8 @@ export default function FeedList({
width={width}
height={height}
rowRenderer={rowRenderer}
rowCount={feedData.length}
rowHeight={measurerCache.rowHeight}
rowCount={finalFeedData.length}
rowHeight={rowHeight}
scrollTop={scrollTop}
overscanRowCount={2}
onRowsRendered={onRowsRendered}
Expand All @@ -247,3 +324,14 @@ export default function FeedList({
</WindowScroller>
);
}

const DEFAULT_ROW_HEIGHT = 100;

type FeedContentType = {
readonly __typename: string;
readonly dbid?: string | undefined;
readonly ' $fragmentSpreads': FragmentRefs<
'FeedEventItemWithErrorBoundaryFragment' | 'PostItemWithErrorBoundaryFragment'
>;
readonly ' $fragmentType': 'FeedListEventDataFragment';
};
Loading
Loading