Skip to content
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
14 changes: 11 additions & 3 deletions packages/shared/src/components/feeds/UnifiedMobileFeedNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import classNames from 'classnames';
import Link from '../utilities/Link';
import { PlusIcon } from '../icons';
import { useAuthContext } from '../../contexts/AuthContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { useFeeds } from '../../hooks';
import { useSortedFeeds } from '../../hooks/feed/useSortedFeeds';
import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
Expand Down Expand Up @@ -38,6 +39,10 @@ const chipInactiveClass =
function UnifiedMobileFeedNav(): ReactElement {
const router = useRouter();
const { isLoggedIn } = useAuthContext();
const { optOutAchievements, optOutLevelSystem, optOutQuestSystem } =
useSettingsContext();
const shouldHideGameCenter =
optOutAchievements && optOutLevelSystem && optOutQuestSystem;
const { feeds } = useFeeds();
const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed();
const sortedFeeds = useSortedFeeds({ edges: feeds?.edges });
Expand Down Expand Up @@ -173,13 +178,15 @@ function UnifiedMobileFeedNav(): ReactElement {
href: `${webappUrl}?openModal=hottakes`,
group: 'rest',
},
{
);
if (!shouldHideGameCenter) {
list.push({
id: 'gamecenter',
label: 'Game Center',
href: `${webappUrl}game-center`,
group: 'rest',
},
);
});
}
}

return list;
Expand All @@ -191,6 +198,7 @@ function UnifiedMobileFeedNav(): ReactElement {
router.pathname,
defaultFeedId,
personalizedTags,
shouldHideGameCenter,
]);

const activeId = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { UserAchievement } from '../../graphql/user/achievements';
import { AchievementType } from '../../graphql/user/achievements';

const mockUseAuthContext = jest.fn();
const mockUseSettingsContext = jest.fn();
const mockUseConditionalFeature = jest.fn();
const mockUseProfileAchievements = jest.fn();
const mockUseTrackedAchievement = jest.fn();
Expand All @@ -15,6 +16,10 @@ jest.mock('../../contexts/AuthContext', () => ({
useAuthContext: () => mockUseAuthContext(),
}));

jest.mock('../../contexts/SettingsContext', () => ({
useSettingsContext: () => mockUseSettingsContext(),
}));

jest.mock('../../hooks/useConditionalFeature', () => ({
useConditionalFeature: () => mockUseConditionalFeature(),
}));
Expand Down Expand Up @@ -122,6 +127,7 @@ const defaultProfileAchievementsHook = {
beforeEach(() => {
jest.clearAllMocks();
mockUseAuthContext.mockReturnValue({ user: { id: 'u1' } });
mockUseSettingsContext.mockReturnValue({ optOutAchievements: false });
mockUseConditionalFeature.mockReturnValue({ value: true, isLoading: false });
mockUseProfileAchievements.mockReturnValue(defaultProfileAchievementsHook);
mockUseTrackedAchievement.mockReturnValue(defaultTrackedAchievementHook);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import { MedalBadgeIcon } from '../icons';
import { AlertColor, AlertDot } from '../AlertDot';
import { useAuthContext } from '../../contexts/AuthContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { useConditionalFeature } from '../../hooks/useConditionalFeature';
import { useProfileAchievements } from '../../hooks/profile/useProfileAchievements';
import { useTrackedAchievement } from '../../hooks/profile/useTrackedAchievement';
Expand Down Expand Up @@ -44,6 +45,7 @@ function AchievementIcon({
export function AchievementTrackerButton(): ReactElement | null {
const { openModal, closeModal } = useLazyModal();
const { user } = useAuthContext();
const { optOutAchievements } = useSettingsContext();
const isLaptop = useViewSize(ViewSize.Laptop);
const {
value: isAchievementTrackingWidgetEnabled,
Expand Down Expand Up @@ -129,7 +131,7 @@ export function AchievementTrackerButton(): ReactElement | null {
});
};

if (!user || isAchievementTrackingWidgetLoading) {
if (!user || isAchievementTrackingWidgetLoading || optOutAchievements) {
return null;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/shared/src/components/modals/BootPopups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactElement } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useLazyModal } from '../../hooks/useLazyModal';
import { useAuthContext } from '../../contexts/AuthContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { useActions, useBoot } from '../../hooks';
import { ActionType } from '../../graphql/actions';
import { LazyModal } from './common/types';
Expand Down Expand Up @@ -63,6 +64,7 @@ export const BootPopups = (): ReactElement => {
const { checkHasCompleted, isActionsFetched } = useActions();
const { openModal } = useLazyModal();
const { user, isValidRegion } = useAuthContext();
const { optOutAchievements } = useSettingsContext();
const { updateUserProfile } = useProfileForm();
const {
alerts,
Expand Down Expand Up @@ -304,7 +306,11 @@ export const BootPopups = (): ReactElement => {
* Bypasses the one-per-day queue so users see their unlock right away.
*/
useEffect(() => {
if (!alerts?.showAchievementUnlock || !isActionsFetched) {
if (
!alerts?.showAchievementUnlock ||
!isActionsFetched ||
optOutAchievements
) {
return;
}

Expand All @@ -329,6 +335,7 @@ export const BootPopups = (): ReactElement => {
alerts?.showAchievementUnlock,
checkHasCompleted,
isActionsFetched,
optOutAchievements,
updateAlerts,
]);

Expand Down
42 changes: 29 additions & 13 deletions packages/shared/src/components/profile/ProfileSettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
import { ProfileSection } from '../ProfileMenu/ProfileSection';
import { LogoutReason } from '../../lib/user';
import { logout, useAuthContext } from '../../contexts/AuthContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import type { WithClassNameProps } from '../utilities';
import { HorizontalSeparator } from '../utilities';
import { useFeatureTheme } from '../../hooks/utils/useFeatureTheme';
Expand Down Expand Up @@ -84,6 +85,10 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
const { openModal } = useLazyModal();
const { logEvent } = useLogContext();
const { user } = useAuthContext();
const { optOutAchievements, optOutLevelSystem, optOutQuestSystem } =
useSettingsContext();
const shouldHideGameCenter =
optOutAchievements && optOutLevelSystem && optOutQuestSystem;

const items = useMemo(
() =>
Expand Down Expand Up @@ -207,12 +212,14 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
playground: {
title: 'Gamification',
items: {
gameCenter: {
title: 'Game Center',
icon: JoystickIcon,
href: `${webappUrl}game-center`,
external: true,
},
...(!shouldHideGameCenter && {
gameCenter: {
title: 'Game Center',
icon: JoystickIcon,
href: `${webappUrl}game-center`,
external: true,
},
}),
gamification: {
title: 'Feature visibility',
icon: EyeIcon,
Expand All @@ -223,12 +230,14 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
icon: HotIcon,
href: `${settingsUrl}/customization/streaks`,
},
achievements: {
title: 'Achievements',
icon: MedalBadgeIcon,
href: `${webappUrl}${user?.username}/achievements`,
external: true,
},
...(!optOutAchievements && {
achievements: {
title: 'Achievements',
icon: MedalBadgeIcon,
href: `${webappUrl}${user?.username}/achievements`,
external: true,
},
}),
hotTakes: {
title: 'Hot Takes',
icon: HotIcon,
Expand Down Expand Up @@ -337,7 +346,14 @@ const useAccountPageItems = ({ onClose }: { onClose?: () => void } = {}) => {
},
},
}),
[logEvent, onClose, openModal, user?.username],
[
logEvent,
onClose,
openModal,
user?.username,
optOutAchievements,
shouldHideGameCenter,
],
);

return { items };
Expand Down
33 changes: 33 additions & 0 deletions packages/shared/src/contexts/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export interface SettingsContextData extends Omit<RemoteSettings, 'theme'> {
toggleOptOutReadingStreak: () => Promise<void>;
toggleOptOutLevelSystem: () => Promise<void>;
toggleOptOutQuestSystem: () => Promise<void>;
toggleOptOutAchievements: () => Promise<void>;
toggleOptOutCompanion: () => Promise<void>;
isGamificationEnabled: boolean;
toggleAllGamification: () => Promise<void>;
toggleAutoDismissNotifications: () => Promise<void>;
toggleShowFeedbackButton: () => Promise<void>;
loadedSettings: boolean;
Expand Down Expand Up @@ -130,6 +133,7 @@ const defaultSettings: RemoteSettings = {
optOutReadingStreak: false,
optOutLevelSystem: false,
optOutQuestSystem: false,
optOutAchievements: false,
// Companion is opt-in: it injects a side panel into every article page,
// which only some users want. Default to off so new users see a clean
// feed and can flip the toggle in the customize sidebar's Widgets
Expand Down Expand Up @@ -271,6 +275,35 @@ export const SettingsContextProvider = ({
...settings,
optOutQuestSystem: !settings.optOutQuestSystem,
}),
toggleOptOutAchievements: () =>
setSettings({
...settings,
optOutAchievements: !settings.optOutAchievements,
}),
isGamificationEnabled:
!settings.optOutReadingStreak ||
!settings.optOutLevelSystem ||
!settings.optOutQuestSystem ||
!settings.optOutAchievements,
toggleAllGamification: () => {
const anyEnabled =
!settings.optOutReadingStreak ||
!settings.optOutLevelSystem ||
!settings.optOutQuestSystem ||
!settings.optOutAchievements;
if (anyEnabled && !settings.optOutReadingStreak) {
unsubscribePersonalizedDigest({
type: UserPersonalizedDigestType.StreakReminder,
});
}
return setSettings({
...settings,
optOutReadingStreak: anyEnabled,
optOutLevelSystem: anyEnabled,
optOutQuestSystem: anyEnabled,
optOutAchievements: anyEnabled,
});
},
toggleOptOutCompanion: () =>
setSettings({
...settings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query';
import { startOfTomorrow, subDays, subMonths } from 'date-fns';
import dynamic from 'next/dynamic';
import { useAuthContext } from '../../../../contexts/AuthContext';
import { useSettingsContext } from '../../../../contexts/SettingsContext';
import { ActiveOrRecomendedSquads } from './ActiveOrRecomendedSquads';
import type { ProfileReadingData, ProfileV2 } from '../../../../graphql/users';
import { USER_READING_HISTORY_QUERY } from '../../../../graphql/users';
Expand Down Expand Up @@ -54,6 +55,7 @@ export function ProfileWidgets({
className,
}: ProfileWidgetsProps): ReactElement {
const { user: loggedUser, tokenRefreshed } = useAuthContext();
const { optOutAchievements } = useSettingsContext();
const { showIndicator: showProfileCompletion } =
useProfileCompletionIndicator();
const { isPreviewMode, isOwner, togglePreview } = useProfilePreview(user);
Expand Down Expand Up @@ -137,13 +139,19 @@ export function ProfileWidgets({
mostReadTags={readingHistory?.userMostReadTags}
isLoading={isReadingHistoryLoading}
/>
{shouldShowTrackingWidget && <AchievementTrackingWidget user={user} />}
{shouldShowTrackingWidget && !optOutAchievements && (
<AchievementTrackingWidget user={user} />
)}
{(isOwner || squads.length > 0) && (
<ActiveOrRecomendedSquads userId={user.id} squads={squads} />
)}
<BadgesAndAwards user={user} />
<AchievementsWidget user={user} />
<AchievementSyncPromptCheck user={user} />
{!optOutAchievements && (
<>
<AchievementsWidget user={user} />
<AchievementSyncPromptCheck user={user} />
</>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ const readHistoryToTooltip = (
};

export interface ReadingOverviewProps {
readHistory: UserReadHistory[];
readHistory?: UserReadHistory[];
before: Date;
after: Date;
streak: UserStreak;
mostReadTags: MostReadTag[];
streak?: UserStreak;
mostReadTags?: MostReadTag[];
isLoading?: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactElement } from 'react';
import React from 'react';
import classNames from 'classnames';
import type { PublicProfile } from '../../../../lib/user';
import { useSettingsContext } from '../../../../contexts/SettingsContext';
import { useShowcaseAchievements } from '../../../../hooks/profile/useShowcaseAchievements';
import { useProfilePreview } from '../../../../hooks/profile/useProfilePreview';
import { useProfileAchievements } from '../../../../hooks/profile/useProfileAchievements';
Expand Down Expand Up @@ -35,10 +36,15 @@ export function ProfileAchievementShowcase({
user,
}: ProfileAchievementShowcaseProps): ReactElement | null {
const { isOwner } = useProfilePreview(user);
const { optOutAchievements } = useSettingsContext();
const { showcaseAchievements } = useShowcaseAchievements(user);
const { achievements } = useProfileAchievements(user);
const { openModal } = useLazyModal();

if (optOutAchievements) {
return null;
}

const hasShowcase = showcaseAchievements.length > 0;

const unlockedAchievements =
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/graphql/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export type RemoteSettings = {
optOutReadingStreak: boolean;
optOutLevelSystem: boolean;
optOutQuestSystem: boolean;
optOutAchievements: boolean;
optOutCompanion: boolean;
autoDismissNotifications: boolean;
sortCommentsBy: SortCommentsBy;
Expand Down
18 changes: 16 additions & 2 deletions packages/webapp/pages/[userId]/achievements.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactElement } from 'react';
import React, { useContext } from 'react';
import React, { useContext, useEffect } from 'react';
import { useRouter } from 'next/router';
import { NextSeo } from 'next-seo';
import type { NextSeoProps } from 'next-seo/lib/types';
import GoBackHeaderMobile from '@dailydotdev/shared/src/components/post/GoBackHeaderMobile';
Expand All @@ -9,6 +10,7 @@ import {
} from '@dailydotdev/shared/src/components/typography/Typography';
import { ProfileAchievements } from '@dailydotdev/shared/src/features/profile/components/achievements/ProfileAchievements';
import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext';
import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext';
import type { ProfileLayoutProps } from '../../components/layouts/ProfileLayout';
import {
getLayout as getProfileLayout,
Expand All @@ -24,10 +26,22 @@ export const getStaticPaths = getProfileStaticPaths;
const ProfileAchievementsPage = ({
user,
noindex,
}: ProfileLayoutProps): ReactElement => {
}: ProfileLayoutProps): ReactElement | null => {
const { user: loggedUser } = useContext(AuthContext);
const { optOutAchievements } = useSettingsContext();
const router = useRouter();
const isSameUser = user && loggedUser?.id === user.id;

useEffect(() => {
if (optOutAchievements && user?.username) {
router.replace(`/${user.username}`);
}
}, [optOutAchievements, user?.username, router]);

if (optOutAchievements || !user) {
return null;
}

const seo: NextSeoProps = {
...getProfileSeoDefaults(
user,
Expand Down
Loading
Loading