Skip to content

Commit b4195fe

Browse files
new icon quests (#3793)
1 parent 7b4aa98 commit b4195fe

4 files changed

Lines changed: 233 additions & 33 deletions

File tree

__tests__/quests.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ query QuestDashboard {
9696
questDashboard {
9797
currentStreak
9898
longestStreak
99+
hasNewQuestRotations
99100
daily {
100101
regular {
101102
rotationId
@@ -126,6 +127,22 @@ query QuestDashboard {
126127
}
127128
`;
128129

130+
const QUEST_NEW_ROTATIONS_QUERY = `
131+
query QuestDashboard {
132+
questDashboard {
133+
hasNewQuestRotations
134+
}
135+
}
136+
`;
137+
138+
const MARK_QUEST_ROTATIONS_VIEWED_MUTATION = `
139+
mutation MarkQuestRotationsViewed {
140+
markQuestRotationsViewed {
141+
_
142+
}
143+
}
144+
`;
145+
129146
const QUEST_STREAK_QUERY = `
130147
query QuestDashboard {
131148
questDashboard {
@@ -743,6 +760,89 @@ describe('claimQuestReward mutation', () => {
743760
});
744761

745762
describe('questDashboard query', () => {
763+
it('should expose when there are unseen active quest rotations', async () => {
764+
const now = new Date();
765+
loggedUser = questUserId;
766+
767+
await saveFixtures(con, User, [{ id: questUserId }]);
768+
await saveFixtures(con, Quest, [
769+
{
770+
id: questId,
771+
name: 'Fresh quest',
772+
description: 'New quest',
773+
type: QuestType.Daily,
774+
eventType: QuestEventType.PostUpvote,
775+
criteria: {
776+
targetCount: 1,
777+
},
778+
active: true,
779+
},
780+
]);
781+
await saveFixtures(con, QuestRotation, [
782+
{
783+
id: questRotationId,
784+
questId,
785+
type: QuestType.Daily,
786+
plusOnly: false,
787+
slot: 1,
788+
periodStart: new Date(now.getTime() - 60 * 60 * 1000),
789+
periodEnd: new Date(now.getTime() + 60 * 60 * 1000),
790+
},
791+
]);
792+
793+
const res = await client.query(QUEST_NEW_ROTATIONS_QUERY);
794+
795+
expect(res.errors).toBeUndefined();
796+
expect(res.data.questDashboard.hasNewQuestRotations).toBe(true);
797+
});
798+
799+
it('should clear new quest rotations after marking them viewed', async () => {
800+
const now = new Date();
801+
loggedUser = questUserId;
802+
803+
await saveFixtures(con, User, [{ id: questUserId }]);
804+
await saveFixtures(con, Quest, [
805+
{
806+
id: questId,
807+
name: 'Fresh quest',
808+
description: 'New quest',
809+
type: QuestType.Daily,
810+
eventType: QuestEventType.PostUpvote,
811+
criteria: {
812+
targetCount: 1,
813+
},
814+
active: true,
815+
},
816+
]);
817+
await saveFixtures(con, QuestRotation, [
818+
{
819+
id: questRotationId,
820+
questId,
821+
type: QuestType.Daily,
822+
plusOnly: false,
823+
slot: 1,
824+
periodStart: new Date(now.getTime() - 60 * 60 * 1000),
825+
periodEnd: new Date(now.getTime() + 60 * 60 * 1000),
826+
},
827+
]);
828+
829+
const markRes = await client.mutate(MARK_QUEST_ROTATIONS_VIEWED_MUTATION);
830+
831+
expect(markRes.errors).toBeUndefined();
832+
expect(markRes.data.markQuestRotationsViewed._).toBe(true);
833+
834+
const dashboardRes = await client.query(QUEST_NEW_ROTATIONS_QUERY);
835+
836+
expect(dashboardRes.errors).toBeUndefined();
837+
expect(dashboardRes.data.questDashboard.hasNewQuestRotations).toBe(false);
838+
839+
const profile = await con
840+
.getRepository(UserQuestProfile)
841+
.findOneByOrFail({ userId: questUserId });
842+
843+
expect(profile.lastViewedQuestRotationsAt).toBeTruthy();
844+
});
845+
746846
it('should return the current quest streak when consecutive quest days end yesterday', async () => {
747847
loggedUser = questUserId;
748848

src/entity/user/UserQuestProfile.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export class UserQuestProfile {
2525
@Column({ type: 'integer', default: 0 })
2626
totalXp: number;
2727

28+
@Column({ type: 'timestamp', nullable: true })
29+
lastViewedQuestRotationsAt: Date | null;
30+
2831
@ManyToOne('User', {
2932
lazy: true,
3033
onDelete: 'CASCADE',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class QuestRotationSeenAt1776100000000 implements MigrationInterface {
4+
name = 'QuestRotationSeenAt1776100000000';
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(/* sql */ `
8+
ALTER TABLE "user_quest_profile"
9+
ADD COLUMN "lastViewedQuestRotationsAt" TIMESTAMP
10+
`);
11+
}
12+
13+
public async down(queryRunner: QueryRunner): Promise<void> {
14+
await queryRunner.query(/* sql */ `
15+
ALTER TABLE "user_quest_profile"
16+
DROP COLUMN "lastViewedQuestRotationsAt"
17+
`);
18+
}
19+
}

src/schema/quests.ts

Lines changed: 111 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type GQLQuestDashboard = {
5858
level: GQLQuestLevel;
5959
currentStreak: number;
6060
longestStreak: number;
61+
hasNewQuestRotations: boolean;
6162
daily: GQLQuestBucket;
6263
weekly: GQLQuestBucket;
6364
milestone: GQLUserQuest[];
@@ -259,6 +260,30 @@ const getQuestStreaks = async ({
259260
};
260261
};
261262

263+
const getLatestActiveQuestRotationCreatedAt = async ({
264+
con,
265+
now,
266+
}: {
267+
con: EntityManager;
268+
now: Date;
269+
}): Promise<Date | null> => {
270+
const result = await con
271+
.getRepository(QuestRotation)
272+
.createQueryBuilder('rotation')
273+
.select('MAX(rotation."createdAt")', 'latestCreatedAt')
274+
.where('rotation."periodStart" <= :now', { now })
275+
.andWhere('rotation."periodEnd" > :now', { now })
276+
.getRawOne<{ latestCreatedAt: Date | string | null }>();
277+
278+
if (!result?.latestCreatedAt) {
279+
return null;
280+
}
281+
282+
const latestCreatedAt = new Date(result.latestCreatedAt);
283+
284+
return Number.isNaN(latestCreatedAt.getTime()) ? null : latestCreatedAt;
285+
};
286+
262287
const getQuestDashboard = async ({
263288
con,
264289
userId,
@@ -270,50 +295,86 @@ const getQuestDashboard = async ({
270295
isPlus: boolean;
271296
now: Date;
272297
}): Promise<GQLQuestDashboard> => {
273-
const [profile, dailyQuests, weeklyQuests, milestoneQuests, streaks] =
274-
await Promise.all([
275-
con.getRepository(UserQuestProfile).findOne({
276-
where: {
277-
userId,
278-
},
279-
}),
280-
getCurrentUserQuestsByType({
281-
con,
282-
userId,
283-
type: QuestType.Daily,
284-
isPlus,
285-
now,
286-
}),
287-
getCurrentUserQuestsByType({
288-
con,
289-
userId,
290-
type: QuestType.Weekly,
291-
isPlus,
292-
now,
293-
}),
294-
getCurrentUserQuestsByType({
295-
con,
296-
userId,
297-
type: QuestType.Milestone,
298-
isPlus,
299-
now,
300-
}),
301-
getQuestStreaks({
302-
con,
298+
const [
299+
profile,
300+
dailyQuests,
301+
weeklyQuests,
302+
milestoneQuests,
303+
streaks,
304+
latestActiveQuestRotationCreatedAt,
305+
] = await Promise.all([
306+
con.getRepository(UserQuestProfile).findOne({
307+
where: {
303308
userId,
304-
now,
305-
}),
306-
]);
309+
},
310+
}),
311+
getCurrentUserQuestsByType({
312+
con,
313+
userId,
314+
type: QuestType.Daily,
315+
isPlus,
316+
now,
317+
}),
318+
getCurrentUserQuestsByType({
319+
con,
320+
userId,
321+
type: QuestType.Weekly,
322+
isPlus,
323+
now,
324+
}),
325+
getCurrentUserQuestsByType({
326+
con,
327+
userId,
328+
type: QuestType.Milestone,
329+
isPlus,
330+
now,
331+
}),
332+
getQuestStreaks({
333+
con,
334+
userId,
335+
now,
336+
}),
337+
getLatestActiveQuestRotationCreatedAt({
338+
con,
339+
now,
340+
}),
341+
]);
307342

308343
return {
309344
level: getQuestLevelState(profile?.totalXp ?? 0),
310345
...streaks,
346+
hasNewQuestRotations:
347+
!!latestActiveQuestRotationCreatedAt &&
348+
(!profile?.lastViewedQuestRotationsAt ||
349+
profile.lastViewedQuestRotationsAt <
350+
latestActiveQuestRotationCreatedAt),
311351
daily: toQuestBucket(dailyQuests),
312352
weekly: toQuestBucket(weeklyQuests),
313353
milestone: milestoneQuests,
314354
};
315355
};
316356

357+
const saveQuestRotationsViewedAt = async ({
358+
con,
359+
userId,
360+
now,
361+
}: {
362+
con: EntityManager;
363+
userId: string;
364+
now: Date;
365+
}): Promise<void> => {
366+
await con
367+
.createQueryBuilder()
368+
.insert()
369+
.into(UserQuestProfile)
370+
.values({
371+
userId,
372+
lastViewedQuestRotationsAt: now,
373+
})
374+
.orUpdate(['lastViewedQuestRotationsAt'], ['userId'])
375+
.execute();
376+
};
377+
317378
const getClaimQuestRewardPayload = async ({
318379
con,
319380
userId,
@@ -596,6 +657,7 @@ export const typeDefs = /* GraphQL */ `
596657
level: QuestLevel!
597658
currentStreak: Int!
598659
longestStreak: Int!
660+
hasNewQuestRotations: Boolean!
599661
daily: QuestBucket!
600662
weekly: QuestBucket!
601663
milestone: [UserQuest!]!
@@ -633,6 +695,7 @@ export const typeDefs = /* GraphQL */ `
633695
634696
extend type Mutation {
635697
claimQuestReward(userQuestId: ID!): ClaimQuestRewardPayload! @auth
698+
markQuestRotationsViewed: EmptyResponse! @auth
636699
trackQuestEvent(eventType: ClientQuestEventType!): EmptyResponse! @auth
637700
}
638701
@@ -697,6 +760,21 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
697760

698761
return dashboard;
699762
},
763+
markQuestRotationsViewed: async (
764+
_,
765+
__,
766+
ctx: AuthContext,
767+
): Promise<GQLEmptyResponse> => {
768+
await saveQuestRotationsViewedAt({
769+
con: ctx.con.manager,
770+
userId: ctx.userId,
771+
now: new Date(),
772+
});
773+
774+
return {
775+
_: true,
776+
};
777+
},
700778
trackQuestEvent: async (
701779
_,
702780
{ eventType }: { eventType: QuestEventType },

0 commit comments

Comments
 (0)