Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"@tiptap/react": "^3.14.0",
"@tiptap/starter-kit": "^3.14.0",
"check-password-strength": "^2.0.10",
"cmdk": "^1.0.0",
"fetch-event-stream": "^0.1.6",
"graphql-ws": "^5.5.5",
"jotai": "^2.12.2",
Expand Down
25 changes: 22 additions & 3 deletions packages/shared/src/components/GrowthBookProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,34 @@ export const GrowthBookProvider = ({
);

useEffect(() => {
if (gb && experimentation?.features) {
if (!gb) {
return;
}

if (experimentation?.features) {
const currentFeats = gb.getFeatures?.();
// Do not update when the features are already set
if (!currentFeats || !Object.keys(currentFeats).length) {
gb.setFeatures?.(experimentation.features);
setReady(true);
}
setReady(true);
return;
}

// Boot resolved but features couldn't be decrypted (typically a dev env
// without a matching `NEXT_PUBLIC_EXPERIMENTATION_KEY`). Seed GrowthBook
// with an empty feature set so its own `ready` flag flips to true and
// consumers like MainLayout — which short-circuit to `null` while
// `growthbook.ready` is false — render with default flag values
// instead of leaving the app stuck on a blank page.
if (experimentation) {
const currentFeats = gb.getFeatures?.();
if (!currentFeats || !Object.keys(currentFeats).length) {
gb.setFeatures?.({});
}
setReady(true);
}
}, [experimentation?.features, gb]);
}, [experimentation, experimentation?.features, gb]);

useEffect(() => {
callback.current = async (experiment, result) => {
Expand Down
7 changes: 6 additions & 1 deletion packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import usePlusEntry from '../hooks/usePlusEntry';
import { SearchProvider } from '../contexts/search/SearchContext';
import { FeedbackWidget } from './feedback';
import { isExtension } from '../lib/func';
import { SpotlightProvider } from './spotlight/SpotlightContext';
import { SpotlightHost } from './spotlight/SpotlightHost';

const GoBackHeaderMobile = dynamic(
() =>
Expand Down Expand Up @@ -189,6 +191,7 @@ function MainLayoutComponent({
<BootPopups />
<SmartComposerHotkey />
<SmartComposerDevToggle />
<SpotlightHost />
<StreakMilestonePopup />
{plusEntryAnnouncementBar && (
<PlusMobileEntryBanner
Expand Down Expand Up @@ -234,7 +237,9 @@ function MainLayoutComponent({
const MainLayout = (props: MainLayoutProps): ReactElement => (
<ActiveFeedNameContextProvider>
<SearchProvider>
<MainLayoutComponent {...props} />
<SpotlightProvider>
<MainLayoutComponent {...props} />
</SpotlightProvider>
</SearchProvider>
</ActiveFeedNameContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import classNames from 'classnames';
import type { ReactElement } from 'react';
import React, { useContext } from 'react';
import { useRouter } from 'next/router';
import type { SearchSuggestion } from '../../../../graphql/search';
import { SearchProviderEnum } from '../../../../graphql/search';
import { useSearchProviderSuggestions } from '../../../../hooks/search';
import { LogEvent, Origin, TargetType } from '../../../../lib/log';
import { useLogContext } from '../../../../contexts/LogContext';
import { webappUrl } from '../../../../lib/constants';
import { Image } from '../../../image/Image';
import { FeedSettingsEditContext } from '../FeedSettingsEditContext';
import { FollowButton } from '../../../contentPreference/FollowButton';
import { ContentPreferenceType } from '../../../../graphql/contentPreference';
import { CopyType } from '../../../sources/SourceActions/SourceActionsFollow';

interface SuggestionsListProps {
query: string;
title: string;
className?: string;
showFollow?: boolean;
}

const SuggestionRow = ({
suggestion,
onClick,
imageRoundedClass,
contentPreferenceType,
showFollow,
}: {
suggestion: SearchSuggestion;
onClick: () => void;
imageRoundedClass: string;
contentPreferenceType: ContentPreferenceType;
showFollow?: boolean;
}): ReactElement => {
const feedSettingsEditContext = useContext(FeedSettingsEditContext);
const feed = feedSettingsEditContext?.feed;
return (
<button
type="button"
onClick={onClick}
className="flex w-full items-center gap-2 overflow-hidden rounded-12 p-2 hover:bg-surface-float focus:bg-surface-float laptop:text-text-tertiary"
>
<Image
loading="lazy"
src={suggestion.image}
alt={`${suggestion.title} logo`}
className={classNames('size-7', imageRoundedClass)}
/>
<div className="flex flex-1 flex-col items-start">
<span className="flex-shrink overflow-hidden overflow-ellipsis whitespace-nowrap font-bold text-text-primary typo-subhead">
{suggestion.title}
</span>
<span className="text-text-quarternary typo-footnote">
@{suggestion.subtitle}
</span>
</div>
{!!showFollow && (
<FollowButton
feedId={feed?.id}
entityId={suggestion.id ?? ''}
type={contentPreferenceType}
status={suggestion.contentPreference?.status}
entityName={`@${suggestion.subtitle}`}
showSubscribe={false}
copyType={CopyType.Custom}
/>
)}
</button>
);
};

const SectionHeader = ({ title }: { title: string }): ReactElement => (
<div className="relative my-2 flex items-center justify-start gap-2">
<hr className="w-2 border-border-subtlest-tertiary" />
<span className="relative inline-flex font-bold typo-footnote">
{title}
</span>
<hr className="flex-1 border-border-subtlest-tertiary" />
</div>
);

export const SourceSearchSuggestions = ({
query,
title,
className,
showFollow,
}: SuggestionsListProps): ReactElement | null => {
const feedSettingsEditContext = useContext(FeedSettingsEditContext);
const feed = feedSettingsEditContext?.feed;
const router = useRouter();
const { logEvent } = useLogContext();

const { suggestions } = useSearchProviderSuggestions({
provider: SearchProviderEnum.Sources,
query,
limit: 3,
includeContentPreference: true,
feedId: feed?.id,
});

if (!suggestions?.hits?.length) {
return null;
}

const onSuggestionClick = (suggestion: SearchSuggestion) => {
const source = suggestion.subtitle?.toLowerCase() || suggestion.id;
logEvent({
event_name: LogEvent.Click,
target_type: TargetType.SearchRecommendation,
target_id: source,
feed_item_title: source,
extra: JSON.stringify({
origin: Origin.HomePage,
provider: SearchProviderEnum.Sources,
}),
});
router.push(`${webappUrl}sources/${source}`);
};

return (
<div className={classNames(className, 'flex flex-col')}>
<SectionHeader title={title} />
{suggestions.hits.map((suggestion) => (
<SuggestionRow
key={suggestion.title}
suggestion={suggestion}
showFollow={showFollow}
imageRoundedClass="rounded-full"
contentPreferenceType={ContentPreferenceType.Source}
onClick={() => onSuggestionClick(suggestion)}
/>
))}
</div>
);
};

export const UserSearchSuggestions = ({
query,
title,
className,
showFollow,
}: SuggestionsListProps): ReactElement | null => {
const feedSettingsEditContext = useContext(FeedSettingsEditContext);
const feed = feedSettingsEditContext?.feed;
const router = useRouter();
const { logEvent } = useLogContext();

const { suggestions } = useSearchProviderSuggestions({
provider: SearchProviderEnum.Users,
query,
limit: 3,
includeContentPreference: true,
feedId: feed?.id,
});

if (!suggestions?.hits?.length) {
return null;
}

const onSuggestionClick = (suggestion: SearchSuggestion) => {
const user = suggestion.id || suggestion.subtitle?.toLowerCase() || '';
logEvent({
event_name: LogEvent.Click,
target_type: TargetType.SearchRecommendation,
target_id: user,
feed_item_title: user,
extra: JSON.stringify({
origin: Origin.HomePage,
provider: SearchProviderEnum.Users,
}),
});
router.push(`${webappUrl}${user}`);
};

return (
<div className={classNames(className, 'flex flex-col')}>
<SectionHeader title={title} />
{suggestions.hits.map((suggestion) => (
<SuggestionRow
key={suggestion.title}
suggestion={suggestion}
showFollow={showFollow}
imageRoundedClass="rounded-8"
contentPreferenceType={ContentPreferenceType.User}
onClick={() => onSuggestionClick(suggestion)}
/>
))}
</div>
);
};
Loading
Loading