Skip to content
76 changes: 59 additions & 17 deletions src/client/entries/drawing-post/DrawingPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ type DrawingState = 'unsolved' | 'guessing' | 'solved' | 'skipped' | 'author';
export function DrawingPost() {
const postData = getPostData<DrawingPostData>();
const currentPostId = context.postId;
const contextLoid =
(context as typeof context & { loid?: string | null }).loid ?? undefined;
const profileInput = {
postId: currentPostId,
...(contextLoid ? { loid: contextLoid } : {}),
};
const { error: showErrorToast, success } = useToastHelpers();

// If postData is missing, try to trigger migration via API
Expand Down Expand Up @@ -64,8 +70,20 @@ export function DrawingPost() {
const [showConfetti, setShowConfetti] = useState(false);
const lastShownPointsRef = useRef<number | null>(null);
const { data: userProfile } = trpc.app.user.getProfile.useQuery(
{ postId: currentPostId },
{ enabled: true }
profileInput,
{
enabled: true,
}
);
const { data: anonymousDrawingStatus } = trpc.app.guess.getStatus.useQuery(
{
postId: currentPostId,
...(contextLoid ? { loid: contextLoid } : {}),
},
{
enabled: !context.userId && !!effectivePostData,
refetchOnWindowFocus: false,
}
);
const queryClient = useQueryClient();
const submitGuess = trpc.app.guess.submit.useMutation({
Expand All @@ -77,7 +95,7 @@ export function DrawingPost() {

// Invalidate user profile to update score
void queryClient.invalidateQueries({
queryKey: ['pixelary', 'user', 'profile', { postId: variables.postId }],
queryKey: ['pixelary', 'user', 'profile', profileInput],
});

// Invalidate leaderboard
Expand All @@ -97,7 +115,7 @@ export function DrawingPost() {
onSuccess: (_: unknown, variables: { postId: string }) => {
// Invalidate user profile to update skipped status
void queryClient.invalidateQueries({
queryKey: ['pixelary', 'user', 'profile', { postId: variables.postId }],
queryKey: ['pixelary', 'user', 'profile', profileInput],
});

// Invalidate post data to update skip count
Expand Down Expand Up @@ -141,21 +159,41 @@ export function DrawingPost() {

// Update state based on user's interaction with this post
useEffect(() => {
if (userProfile && effectivePostData) {
if (isAuthor) {
setCurrentState('author');
if (!effectivePostData) {
return;
}

if (isAuthor) {
setCurrentState('author');
return;
}

if (context.userId) {
if (!userProfile) {
return;
}

// Check logged-in user's server state
if (userProfile.skipped) {
setCurrentState('skipped');
} else if (userProfile.solved) {
setCurrentState('solved');
} else {
// Check user's server state
if (userProfile.skipped) {
setCurrentState('skipped');
} else if (userProfile.solved) {
setCurrentState('solved');
} else {
setCurrentState('unsolved');
}
setCurrentState('unsolved');
}
return;
}
}, [userProfile, effectivePostData, isAuthor]);

if (anonymousDrawingStatus) {
if (anonymousDrawingStatus.skipped) {
setCurrentState('skipped');
} else if (anonymousDrawingStatus.solved) {
setCurrentState('solved');
} else {
setCurrentState('unsolved');
}
}
}, [userProfile, anonymousDrawingStatus, effectivePostData, isAuthor]);

// Clear earned points when transitioning away from solved state
useEffect(() => {
Expand Down Expand Up @@ -239,6 +277,7 @@ export function DrawingPost() {
const result = await submitGuess.mutateAsync({
postId: currentPostId,
guess,
...(!context.userId && contextLoid ? { loid: contextLoid } : {}),
});

// Only change state after server confirms
Expand All @@ -261,7 +300,10 @@ export function DrawingPost() {
// currentPostId is always present for drawing posts

try {
await skipPost.mutateAsync({ postId: currentPostId });
await skipPost.mutateAsync({
postId: currentPostId,
...(!context.userId && contextLoid ? { loid: contextLoid } : {}),
});
setCurrentState('skipped');
} catch (err) {
showErrorToast('Failed to skip post. Please try again.', {
Expand Down
32 changes: 21 additions & 11 deletions src/client/entries/drawing-post/_components/ResultsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { PostGuesses } from '@shared/schema/pixelary';
import { useTelemetry } from '@client/hooks/useTelemetry';
import {
requestExpandedMode,
showLoginPrompt,
addWebViewModeListener,
removeWebViewModeListener,
navigateTo,
Expand Down Expand Up @@ -41,6 +42,7 @@ export function ResultsView({
postId,
}: ResultsViewProps) {
const [isLightboxOpen, setIsLightboxOpen] = useState(false);
const isLoggedIn = Boolean(context.userId);
const { success } = useToastHelpers();
const { track } = useTelemetry();
const utils = trpc.useUtils();
Expand Down Expand Up @@ -198,26 +200,34 @@ export function ResultsView({
})}
</div>
{/* Secondary CTA */}
<CyclingMessage
messages={[
'See comments for more',
`Draw for ${AUTHOR_REWARD_SUBMIT} points`,
'Join r/Pixelary today',
]}
/>
{isLoggedIn ? (
<CyclingMessage
messages={[
'See comments for more',
`Draw for ${AUTHOR_REWARD_SUBMIT} points`,
'Join r/Pixelary today',
]}
/>
) : (
<Text>log in to save your rewards</Text>
)}
{/* Primary CTA */}
<Button
onClick={async (e) => {
onClick={(e) => {
if (!isLoggedIn) {
showLoginPrompt();
return;
}
try {
await requestExpandedMode(e, 'editor');
requestExpandedMode(e, 'editor');
} catch (error) {
console.error('Could not enter expanded mode:', error);
}
}}
size="large"
telemetryEvent="click_draw_something"
telemetryEvent={isLoggedIn ? 'click_draw_something' : 'click_log_in'}
>
DRAW SOMETHING
{isLoggedIn ? 'DRAW SOMETHING' : 'LOG IN'}
</Button>
{/* Lightbox */}
<Lightbox
Expand Down
11 changes: 8 additions & 3 deletions src/client/entries/editor/_context/EditorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export function EditorContextProvider(props: ProviderProps) {
tournamentPostId,
tournamentWord,
} = props;
const contextLoid =
(context as typeof context & { loid?: string | null }).loid ?? undefined;

// flow
const flow = useEditorFlow({
Expand All @@ -84,9 +86,12 @@ export function EditorContextProvider(props: ProviderProps) {
});

// queries
const { data: userProfile } = trpc.app.user.getProfile.useQuery(undefined, {
staleTime: 30000,
});
const { data: userProfile } = trpc.app.user.getProfile.useQuery(
contextLoid ? { loid: contextLoid } : undefined,
{
staleTime: 30000,
}
);
const { data: effectiveBonuses } =
trpc.app.rewards.getEffectiveBonuses.useQuery(undefined, {
enabled: !!context.userId,
Expand Down
19 changes: 14 additions & 5 deletions src/client/entries/pinned-post/PinnedPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MyRewards } from './_components/MyRewards';
import { LevelDetails } from './_components/LevelDetails';
import { Menu } from './_components/Menu';
import { trpc } from '@client/trpc/client';
import { context } from '@devvit/web/client';

type Page =
| 'menu'
Expand All @@ -16,11 +17,15 @@ type Page =
export function PinnedPost() {
const [page, setPage] = useState<Page>('menu');
const utils = trpc.useUtils();
const isLoggedIn = Boolean(context.userId);
const contextLoid =
(context as typeof context & { loid?: string | null }).loid ?? undefined;

// Prefetch drawings optimistically for maximum performance
trpc.app.user.getMyArtPage.useQuery(
{ limit: 20 },
{
enabled: isLoggedIn,
staleTime: 60000, // Cache for 1 minute
refetchOnWindowFocus: false, // Don't refetch on window focus
}
Expand All @@ -36,11 +41,15 @@ export function PinnedPost() {
);

// Warm profile and bonuses so downstream views benefit from cache
trpc.app.user.getProfile.useQuery(undefined, {
staleTime: 60000,
refetchOnWindowFocus: false,
});
trpc.app.user.getProfile.useQuery(
contextLoid ? { loid: contextLoid } : undefined,
{
staleTime: 60000,
refetchOnWindowFocus: false,
}
);
trpc.app.rewards.getEffectiveBonuses.useQuery(undefined, {
enabled: isLoggedIn,
staleTime: 10000,
refetchOnWindowFocus: false,
});
Expand All @@ -50,7 +59,7 @@ export function PinnedPost() {
}

function goToPage(page: Page) {
if (page === 'my-rewards') {
if (page === 'my-rewards' && isLoggedIn) {
// Proactively load inventory/effects to avoid flashes in the modal
void utils.app.rewards.getInventory.prefetch();
void utils.app.rewards.getActiveEffects.prefetch();
Expand Down
12 changes: 9 additions & 3 deletions src/client/entries/pinned-post/_components/LevelDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,29 @@ import { IconButton } from '@components/IconButton';
import { trpc } from '@client/trpc/client';
import { useTelemetry } from '@client/hooks/useTelemetry';
import { getRewardsByLevel, getRewardLabel } from '@shared/rewards';
import { context } from '@devvit/web/client';

type LevelDetailsProps = {
onClose: () => void;
};

export function LevelDetails({ onClose }: LevelDetailsProps) {
const { track } = useTelemetry();
const contextLoid =
(context as typeof context & { loid?: string | null }).loid ?? undefined;

// Track level details view on mount
useEffect(() => {
void track('view_level_details');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Get user profile to show their actual progress
const { data: userProfile } = trpc.app.user.getProfile.useQuery(undefined, {
enabled: true,
});
const { data: userProfile } = trpc.app.user.getProfile.useQuery(
contextLoid ? { loid: contextLoid } : undefined,
{
enabled: true,
}
);

const [currentLevelRank, setCurrentLevelRank] = useState<number>(1);
const [displayProgress, setDisplayProgress] = useState<number>(0);
Expand Down
17 changes: 12 additions & 5 deletions src/client/entries/pinned-post/_components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type MenuProps = {
export function Menu(props: MenuProps) {
const { onMyDrawings, onLeaderboard, onHowToPlay, onLevelClick } = props;
const isLoggedIn = Boolean(context.userId);
const contextLoid =
(context as typeof context & { loid?: string | null }).loid ?? null;

// Telemetry
const { track } = useTelemetry();
Expand All @@ -36,9 +38,12 @@ export function Menu(props: MenuProps) {
}, []);

// Grab data
const { data: userProfile } = trpc.app.user.getProfile.useQuery(undefined, {
enabled: true,
});
const { data: userProfile } = trpc.app.user.getProfile.useQuery(
contextLoid ? { loid: contextLoid } : undefined,
{
enabled: true,
}
);

// Check if user is admin
const { data: isUserAdmin } = trpc.app.user.isAdmin.useQuery(undefined, {
Expand All @@ -47,11 +52,13 @@ export function Menu(props: MenuProps) {

// Warm editor-related caches as soon as the menu is visible
useEffect(() => {
void utils.app.user.getProfile.prefetch();
void utils.app.user.getProfile.prefetch(
contextLoid ? { loid: contextLoid } : undefined
);
void utils.app.rewards.getEffectiveBonuses.prefetch();
void utils.app.user.colors.getRecent.prefetch();
void utils.app.dictionary.getCandidates.prefetch();
}, [utils]);
}, [contextLoid, utils]);

// Get progress percentage from user profile
const progressPercentage = userProfile?.levelProgressPercentage ?? 0;
Expand Down
6 changes: 5 additions & 1 deletion src/client/entries/pinned-post/_components/MyRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ export function MyRewards({ onClose }: MyRewardsProps) {
void track('view_my_rewards');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const contextLoid =
(context as typeof context & { loid?: string | null }).loid ?? undefined;

// Get user profile data
const { data: userProfile } = trpc.app.user.getProfile.useQuery();
const { data: userProfile } = trpc.app.user.getProfile.useQuery(
contextLoid ? { loid: contextLoid } : undefined
);

const userLevel = userProfile?.level ?? 1;
const allRewards = getAllRewards();
Expand Down
7 changes: 6 additions & 1 deletion src/server/core/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const REDIS_KEYS = {

// Progression system
scores: () => 'scores',
scoresGuest: () => 'scores:guest',

// Flair templates
flairTemplates: {
Expand Down Expand Up @@ -104,13 +105,17 @@ export const REDIS_KEYS = {
userArtItem: (userId: T2, compositeId: string) =>
`user:art:item:${userId}:${compositeId}`, // HASH snapshot for listing hydration
// Rate limit keys
rateGuess: (userId: T2) => `rate:guess:${userId}`,
rateGuess: (userId: string) => `rate:guess:${userId}`,
rateVote: (userId: T2) => `rate:vote:${userId}`,
rateSubmit: (userId: T2) => `rate:submit:${userId}`,

// Migration
migrationLock: (postId: T3) => `migration:drawing:${postId}`,
migrationMarker: (postId: T3) => `migrated:drawing:${postId}`,
guestProgressMigrationMarker: (postId: T3, guestId: string, userId: T2) =>
`migration:guest_progress:${postId}:${guestId}:${userId}`,
guestScoreMigrationMarker: (guestId: string, userId: T2) =>
`migration:guest_score:${guestId}:${userId}`,
};

const MODERATOR_STATUS_TTL = 10 * 24 * 60 * 60; // 10 days.
Expand Down
Loading