mirror of
				https://github.com/monkeytypegame/monkeytype.git
				synced 2025-11-04 09:06:17 +08:00 
			
		
		
		
	Streak hour offset (#4357)
* backend flow to set hour offset * added frontend popup to set streak hour offset * displaying a message that the user can change offset displaying the actual offset if its already set * applying offset when checking streaks * added tests for the new offset updated old streak tests * defaulting to undefined * removing content if offset is already set * updated the way offset is displayed * also updating in the local snapshot
This commit is contained in:
		
							parent
							
								
									303e2b2628
								
							
						
					
					
						commit
						27b1a7fedb
					
				
					 18 changed files with 441 additions and 23 deletions
				
			
		| 
						 | 
				
			
			@ -615,22 +615,140 @@ describe("UserDal", () => {
 | 
			
		|||
  it("updateStreak should update streak", async () => {
 | 
			
		||||
    await UserDAL.addUser("testStack", "test email", "TestID");
 | 
			
		||||
 | 
			
		||||
    Date.now = jest.fn(() => 1662372000000);
 | 
			
		||||
    const testSteps = [
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 21:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 23:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/08 00:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/08 23:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/09 00:00:00 UTC",
 | 
			
		||||
        expectedStreak: 3,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/11 00:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const streak1 = await updateStreak("TestID", 1662372000000);
 | 
			
		||||
    for (const { date, expectedStreak } of testSteps) {
 | 
			
		||||
      const milis = new Date(date).getTime();
 | 
			
		||||
      Date.now = jest.fn(() => milis);
 | 
			
		||||
 | 
			
		||||
    await expect(streak1).toBe(1);
 | 
			
		||||
      const streak = await updateStreak("TestID", milis);
 | 
			
		||||
 | 
			
		||||
    Date.now = jest.fn(() => 1662458400000);
 | 
			
		||||
      await expect(streak).toBe(expectedStreak);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
    const streak2 = await updateStreak("TestID", 1662458400000);
 | 
			
		||||
  it("positive streak offset should award streak correctly", async () => {
 | 
			
		||||
    await UserDAL.addUser("testStack", "test email", "TestID");
 | 
			
		||||
 | 
			
		||||
    await expect(streak2).toBe(2);
 | 
			
		||||
    await UserDAL.setStreakHourOffset("TestID", 10);
 | 
			
		||||
 | 
			
		||||
    Date.now = jest.fn(() => 1999969721000000);
 | 
			
		||||
    const testSteps = [
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/06 21:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 01:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 09:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 10:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 23:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/08 00:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/08 01:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/08 09:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/08 10:00:00 UTC",
 | 
			
		||||
        expectedStreak: 3,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/10 10:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const streak3 = await updateStreak("TestID", 1999969721000);
 | 
			
		||||
    for (const { date, expectedStreak } of testSteps) {
 | 
			
		||||
      const milis = new Date(date).getTime();
 | 
			
		||||
      Date.now = jest.fn(() => milis);
 | 
			
		||||
 | 
			
		||||
    await expect(streak3).toBe(1);
 | 
			
		||||
      const streak = await updateStreak("TestID", milis);
 | 
			
		||||
 | 
			
		||||
      await expect(streak).toBe(expectedStreak);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("negative streak offset should award streak correctly", async () => {
 | 
			
		||||
    await UserDAL.addUser("testStack", "test email", "TestID");
 | 
			
		||||
 | 
			
		||||
    await UserDAL.setStreakHourOffset("TestID", -4);
 | 
			
		||||
 | 
			
		||||
    const testSteps = [
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/06 19:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/06 20:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 01:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 19:00:00 UTC",
 | 
			
		||||
        expectedStreak: 2,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/07 20:00:00 UTC",
 | 
			
		||||
        expectedStreak: 3,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        date: "2023/06/09 23:00:00 UTC",
 | 
			
		||||
        expectedStreak: 1,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const { date, expectedStreak } of testSteps) {
 | 
			
		||||
      const milis = new Date(date).getTime();
 | 
			
		||||
      Date.now = jest.fn(() => milis);
 | 
			
		||||
 | 
			
		||||
      const streak = await updateStreak("TestID", milis);
 | 
			
		||||
 | 
			
		||||
      await expect(streak).toBe(expectedStreak);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -832,6 +832,23 @@ export async function reportUser(
 | 
			
		|||
  return new MonkeyResponse("User reported");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function setStreakHourOffset(
 | 
			
		||||
  req: MonkeyTypes.Request
 | 
			
		||||
): Promise<MonkeyResponse> {
 | 
			
		||||
  const { uid } = req.ctx.decodedToken;
 | 
			
		||||
  const { hourOffset } = req.body;
 | 
			
		||||
 | 
			
		||||
  const user = await UserDAL.getUser(uid, "update user profile");
 | 
			
		||||
 | 
			
		||||
  if (user.streak?.hourOffset !== undefined) {
 | 
			
		||||
    throw new MonkeyError(403, "Streak hour offset already set");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await UserDAL.setStreakHourOffset(uid, hourOffset);
 | 
			
		||||
 | 
			
		||||
  return new MonkeyResponse("Streak hour offset set");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function toggleBan(
 | 
			
		||||
  req: MonkeyTypes.Request
 | 
			
		||||
): Promise<MonkeyResponse> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -419,6 +419,18 @@ router.get(
 | 
			
		|||
  asyncHandler(UserController.getStats)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
router.post(
 | 
			
		||||
  "/setStreakHourOffset",
 | 
			
		||||
  authenticateRequest(),
 | 
			
		||||
  RateLimit.setStreakHourOffset,
 | 
			
		||||
  validateRequest({
 | 
			
		||||
    body: {
 | 
			
		||||
      hourOffset: joi.number().min(-11).max(12).required(),
 | 
			
		||||
    },
 | 
			
		||||
  }),
 | 
			
		||||
  asyncHandler(UserController.setStreakHourOffset)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
router.get(
 | 
			
		||||
  "/favoriteQuotes",
 | 
			
		||||
  authenticateRequest(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -973,11 +973,12 @@ export async function updateStreak(
 | 
			
		|||
    lastResultTimestamp: user.streak?.lastResultTimestamp ?? 0,
 | 
			
		||||
    length: user.streak?.length ?? 0,
 | 
			
		||||
    maxLength: user.streak?.maxLength ?? 0,
 | 
			
		||||
    hourOffset: user.streak?.hourOffset ?? 0,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (isYesterday(streak.lastResultTimestamp)) {
 | 
			
		||||
  if (isYesterday(streak.lastResultTimestamp, streak.hourOffset)) {
 | 
			
		||||
    streak.length += 1;
 | 
			
		||||
  } else if (!isToday(streak.lastResultTimestamp)) {
 | 
			
		||||
  } else if (!isToday(streak.lastResultTimestamp, streak.hourOffset)) {
 | 
			
		||||
    Logger.logToDb("streak_lost", { streak }, uid);
 | 
			
		||||
    streak.length = 1;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -992,6 +993,21 @@ export async function updateStreak(
 | 
			
		|||
  return streak.length;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function setStreakHourOffset(
 | 
			
		||||
  uid: string,
 | 
			
		||||
  hourOffset: number
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
  await getUsersCollection().updateOne(
 | 
			
		||||
    { uid },
 | 
			
		||||
    {
 | 
			
		||||
      $set: {
 | 
			
		||||
        "streak.hourOffset": hourOffset,
 | 
			
		||||
        "streak.lastResultTimestamp": Date.now(),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
  
 | 
			
		||||
export async function setBanned(uid: string, banned: boolean): Promise<void> {
 | 
			
		||||
  if (banned) {
 | 
			
		||||
    await getUsersCollection().updateOne({ uid }, { $set: { banned: true } });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -299,6 +299,13 @@ export const userGet = rateLimit({
 | 
			
		|||
  handler: customHandler,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const setStreakHourOffset = rateLimit({
 | 
			
		||||
  windowMs: ONE_HOUR_MS,
 | 
			
		||||
  max: 5 * REQUEST_MULTIPLIER,
 | 
			
		||||
  keyGenerator: getKeyWithUid,
 | 
			
		||||
  handler: customHandler,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const userSignup = rateLimit({
 | 
			
		||||
  windowMs: 24 * ONE_HOUR_MS, // 1 day
 | 
			
		||||
  max: 2 * REQUEST_MULTIPLIER,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								backend/src/types/types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								backend/src/types/types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -199,6 +199,7 @@ declare namespace MonkeyTypes {
 | 
			
		|||
    lastResultTimestamp: number;
 | 
			
		||||
    length: number;
 | 
			
		||||
    maxLength: number;
 | 
			
		||||
    hourOffset?: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface UserInventory {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,10 +81,16 @@ export function padNumbers(
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MILISECONDS_IN_HOUR = 3600000;
 | 
			
		||||
export const MILLISECONDS_IN_DAY = 86400000;
 | 
			
		||||
 | 
			
		||||
export function getStartOfDayTimestamp(timestamp: number): number {
 | 
			
		||||
  return timestamp - (timestamp % MILLISECONDS_IN_DAY);
 | 
			
		||||
export function getStartOfDayTimestamp(
 | 
			
		||||
  timestamp: number,
 | 
			
		||||
  offsetMilis = 0
 | 
			
		||||
): number {
 | 
			
		||||
  return (
 | 
			
		||||
    timestamp - offsetMilis - ((timestamp - offsetMilis) % MILLISECONDS_IN_DAY)
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getCurrentDayTimestamp(): number {
 | 
			
		||||
| 
						 | 
				
			
			@ -165,16 +171,21 @@ export function getOrdinalNumberString(number: number): string {
 | 
			
		|||
  return `${number}${suffix}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isYesterday(timestamp: number): boolean {
 | 
			
		||||
  const yesterday = getStartOfDayTimestamp(Date.now() - MILLISECONDS_IN_DAY);
 | 
			
		||||
  const date = getStartOfDayTimestamp(timestamp);
 | 
			
		||||
export function isYesterday(timestamp: number, hourOffset = 0): boolean {
 | 
			
		||||
  const offsetMilis = hourOffset * MILISECONDS_IN_HOUR;
 | 
			
		||||
  const yesterday = getStartOfDayTimestamp(
 | 
			
		||||
    Date.now() - MILLISECONDS_IN_DAY,
 | 
			
		||||
    offsetMilis
 | 
			
		||||
  );
 | 
			
		||||
  const date = getStartOfDayTimestamp(timestamp, offsetMilis);
 | 
			
		||||
 | 
			
		||||
  return yesterday === date;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isToday(timestamp: number): boolean {
 | 
			
		||||
  const today = getStartOfDayTimestamp(Date.now());
 | 
			
		||||
  const date = getStartOfDayTimestamp(timestamp);
 | 
			
		||||
export function isToday(timestamp: number, hourOffset = 0): boolean {
 | 
			
		||||
  const offsetMilis = hourOffset * MILISECONDS_IN_HOUR;
 | 
			
		||||
  const today = getStartOfDayTimestamp(Date.now(), offsetMilis);
 | 
			
		||||
  const date = getStartOfDayTimestamp(timestamp, offsetMilis);
 | 
			
		||||
 | 
			
		||||
  return today === date;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1505,6 +1505,25 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#streakHourOffsetPopupWrapper {
 | 
			
		||||
  #streakHourOffsetPopup {
 | 
			
		||||
    .title {
 | 
			
		||||
      font-size: 1.5rem;
 | 
			
		||||
      color: var(--sub-color);
 | 
			
		||||
    }
 | 
			
		||||
    width: 400px;
 | 
			
		||||
    background: var(--bg-color);
 | 
			
		||||
    border-radius: var(--roundness);
 | 
			
		||||
    padding: 2rem;
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: 1rem;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    .red {
 | 
			
		||||
      color: var(--error-color);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#tagsWrapper,
 | 
			
		||||
#newResultFilterPresetPopupWrapper,
 | 
			
		||||
#editProfilePopupWrapper {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -255,4 +255,10 @@ export default class Users {
 | 
			
		|||
      payload: { email },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setStreakHourOffset(hourOffset: number): Ape.EndpointData {
 | 
			
		||||
    return await this.httpClient.post(`${BASE_PATH}/setStreakHourOffset`, {
 | 
			
		||||
      payload: { hourOffset },
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,4 +30,5 @@ export const defaultSnap: MonkeyTypes.Snapshot = {
 | 
			
		|||
  inboxUnreadSize: 0,
 | 
			
		||||
  streak: 0,
 | 
			
		||||
  maxStreak: 0,
 | 
			
		||||
  streakHourOffset: undefined,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -131,6 +131,7 @@ export async function initSnapshot(): Promise<
 | 
			
		|||
    snap.inboxUnreadSize = userData.inboxUnreadSize ?? 0;
 | 
			
		||||
    snap.streak = userData?.streak?.length ?? 0;
 | 
			
		||||
    snap.maxStreak = userData?.streak?.maxLength ?? 0;
 | 
			
		||||
    snap.streakHourOffset = userData?.streak?.hourOffset;
 | 
			
		||||
 | 
			
		||||
    if (userData.lbMemory?.time15 || userData.lbMemory?.time60) {
 | 
			
		||||
      //old memory format
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,19 +117,31 @@ export async function update(
 | 
			
		|||
    const lastResult = results?.[0];
 | 
			
		||||
 | 
			
		||||
    const dayInMilis = 1000 * 60 * 60 * 24;
 | 
			
		||||
    const milisOffset = (profile.streakHourOffset ?? 0) * 3600000;
 | 
			
		||||
    const timeDif = formatDistanceToNowStrict(
 | 
			
		||||
      Misc.getCurrentDayTimestamp() + dayInMilis
 | 
			
		||||
      Misc.getCurrentDayTimestamp() + dayInMilis + milisOffset
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (lastResult) {
 | 
			
		||||
      //check if the last result is from today
 | 
			
		||||
      const isToday = Misc.isToday(lastResult.timestamp);
 | 
			
		||||
 | 
			
		||||
      const offsetString = profile.streakHourOffset
 | 
			
		||||
        ? `(${profile.streakHourOffset > 0 ? "+" : ""}${
 | 
			
		||||
            profile.streakHourOffset
 | 
			
		||||
          } offset)`
 | 
			
		||||
        : "";
 | 
			
		||||
 | 
			
		||||
      if (isToday) {
 | 
			
		||||
        hoverText += `\nClaimed today: yes`;
 | 
			
		||||
        hoverText += `\nCome back in: ${timeDif}`;
 | 
			
		||||
        hoverText += `\nCome back in: ${timeDif} ${offsetString}`;
 | 
			
		||||
      } else {
 | 
			
		||||
        hoverText += `\nClaimed today: no`;
 | 
			
		||||
        hoverText += `\nStreak lost in: ${timeDif}`;
 | 
			
		||||
        hoverText += `\nStreak lost in: ${timeDif} ${offsetString}`;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (profile.streakHourOffset === undefined) {
 | 
			
		||||
        hoverText += `\n\nIf the streak reset time doesn't line up with your timezone, you can change it in Settings > Danger zone > Update streak hour offset.`;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import "./popups/support-popup";
 | 
			
		|||
import "./popups/contact-popup";
 | 
			
		||||
import "./popups/version-popup";
 | 
			
		||||
import "./popups/edit-preset-popup";
 | 
			
		||||
import "./popups/set-streak-hour-offset";
 | 
			
		||||
import "./popups/simple-popups";
 | 
			
		||||
import "./controllers/input-controller";
 | 
			
		||||
import "./ready";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										147
									
								
								frontend/src/ts/popups/set-streak-hour-offset.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								frontend/src/ts/popups/set-streak-hour-offset.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,147 @@
 | 
			
		|||
import Ape from "../ape";
 | 
			
		||||
// import * as DB from "../db";
 | 
			
		||||
import * as Notifications from "../elements/notifications";
 | 
			
		||||
import * as Loader from "../elements/loader";
 | 
			
		||||
// import * as Settings from "../pages/settings";
 | 
			
		||||
import * as ConnectionState from "../states/connection";
 | 
			
		||||
import * as Skeleton from "./skeleton";
 | 
			
		||||
import { isPopupVisible } from "../utils/misc";
 | 
			
		||||
import { getSnapshot, setSnapshot } from "../db";
 | 
			
		||||
 | 
			
		||||
const wrapperId = "streakHourOffsetPopupWrapper";
 | 
			
		||||
 | 
			
		||||
export function show(): void {
 | 
			
		||||
  if (!ConnectionState.get()) {
 | 
			
		||||
    Notifications.add("You are offline", 0, {
 | 
			
		||||
      duration: 2,
 | 
			
		||||
    });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Skeleton.append(wrapperId);
 | 
			
		||||
  if (!isPopupVisible(wrapperId)) {
 | 
			
		||||
    if (getSnapshot()?.streakHourOffset !== undefined) {
 | 
			
		||||
      $(`#${wrapperId} .text`).html(
 | 
			
		||||
        "You have already set your streak hour offset."
 | 
			
		||||
      );
 | 
			
		||||
      $(`#${wrapperId} input`).remove();
 | 
			
		||||
      $(`#${wrapperId} .preview`).remove();
 | 
			
		||||
      $(`#${wrapperId} .button`).remove();
 | 
			
		||||
    } else {
 | 
			
		||||
      updatePreview();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(`#${wrapperId}`)
 | 
			
		||||
      .stop(true, true)
 | 
			
		||||
      .css("opacity", 0)
 | 
			
		||||
      .removeClass("hidden")
 | 
			
		||||
      .animate({ opacity: 1 }, 125, () => {
 | 
			
		||||
        $(`#${wrapperId} input`).trigger("focus");
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updatePreview(): void {
 | 
			
		||||
  const inputValue = $(`#${wrapperId} input`).val() as number;
 | 
			
		||||
  const preview = $(`#${wrapperId} .preview`);
 | 
			
		||||
 | 
			
		||||
  const date = new Date();
 | 
			
		||||
  date.setUTCHours(0);
 | 
			
		||||
  date.setUTCMinutes(0);
 | 
			
		||||
  date.setUTCSeconds(0);
 | 
			
		||||
  date.setUTCMilliseconds(0);
 | 
			
		||||
 | 
			
		||||
  const newDate = new Date();
 | 
			
		||||
  newDate.setUTCHours(0);
 | 
			
		||||
  newDate.setUTCMinutes(0);
 | 
			
		||||
  newDate.setUTCSeconds(0);
 | 
			
		||||
  newDate.setUTCMilliseconds(0);
 | 
			
		||||
 | 
			
		||||
  newDate.setHours(newDate.getHours() - -1 * inputValue); //idk why, but it only works when i subtract (so i have to negate inputValue)
 | 
			
		||||
 | 
			
		||||
  preview.html(`
 | 
			
		||||
  
 | 
			
		||||
  Current local reset time: ${date.toLocaleTimeString()}<br>
 | 
			
		||||
  New local reset time: ${newDate.toLocaleTimeString()}
 | 
			
		||||
 | 
			
		||||
  `);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hide(): void {
 | 
			
		||||
  if (isPopupVisible(wrapperId)) {
 | 
			
		||||
    $(`#${wrapperId}`)
 | 
			
		||||
      .stop(true, true)
 | 
			
		||||
      .css("opacity", 1)
 | 
			
		||||
      .animate(
 | 
			
		||||
        {
 | 
			
		||||
          opacity: 0,
 | 
			
		||||
        },
 | 
			
		||||
        125,
 | 
			
		||||
        () => {
 | 
			
		||||
          $(`#${wrapperId}`).addClass("hidden");
 | 
			
		||||
          Skeleton.remove(wrapperId);
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function apply(): Promise<void> {
 | 
			
		||||
  const value = parseInt($(`#${wrapperId} input`).val() as string);
 | 
			
		||||
 | 
			
		||||
  if (isNaN(value)) {
 | 
			
		||||
    Notifications.add("Streak hour offset must be a number", 0);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (value < -11 || value > 12) {
 | 
			
		||||
    Notifications.add("Streak hour offset must be between -11 and 12", 0);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Loader.show();
 | 
			
		||||
 | 
			
		||||
  const response = await Ape.users.setStreakHourOffset(value);
 | 
			
		||||
  Loader.hide();
 | 
			
		||||
  if (response.status !== 200) {
 | 
			
		||||
    Notifications.add(
 | 
			
		||||
      "Failed to set streak hour offset: " + response.message,
 | 
			
		||||
      -1
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    Notifications.add("Streak hour offset set", 1);
 | 
			
		||||
    const snap = getSnapshot() as MonkeyTypes.Snapshot;
 | 
			
		||||
    snap.streakHourOffset = value;
 | 
			
		||||
    setSnapshot(snap);
 | 
			
		||||
    hide();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(`#${wrapperId}`).on("mousedown", (e) => {
 | 
			
		||||
  if ($(e.target).attr("id") === wrapperId) {
 | 
			
		||||
    hide();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(`#${wrapperId} .button`).on("click", () => {
 | 
			
		||||
  apply();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(`#${wrapperId} input`).on("keypress", (e) => {
 | 
			
		||||
  if (e.key === "Enter") {
 | 
			
		||||
    apply();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(".pageSettings .section.setStreakHourOffset").on(
 | 
			
		||||
  "click",
 | 
			
		||||
  "#setStreakHourOffset",
 | 
			
		||||
  () => {
 | 
			
		||||
    show();
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
$(`#${wrapperId} input`).on("input", () => {
 | 
			
		||||
  updatePreview();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Skeleton.save(wrapperId);
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/src/ts/types/types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/src/ts/types/types.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -566,6 +566,7 @@ declare namespace MonkeyTypes {
 | 
			
		|||
    inboxUnreadSize: number;
 | 
			
		||||
    streak: number;
 | 
			
		||||
    maxStreak: number;
 | 
			
		||||
    streakHourOffset?: number;
 | 
			
		||||
    lbOptOut?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,13 @@
 | 
			
		|||
            <div class="badges"></div>
 | 
			
		||||
            <div class="allBadges"></div>
 | 
			
		||||
            <div class="joined" data-balloon-pos="up">-</div>
 | 
			
		||||
            <div class="streak" data-balloon-pos="up">-</div>
 | 
			
		||||
            <div
 | 
			
		||||
              class="streak"
 | 
			
		||||
              data-balloon-pos="up"
 | 
			
		||||
              data-balloon-length="large"
 | 
			
		||||
            >
 | 
			
		||||
              -
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="levelAndBar">
 | 
			
		||||
            <div class="level" data-balloon-pos="up">-</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2697,6 +2697,29 @@
 | 
			
		|||
        <div class="button" tabindex="0" onclick="this.blur();">open</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="section setStreakHourOffset needsAccount hidden">
 | 
			
		||||
      <div class="groupTitle">
 | 
			
		||||
        <i class="fas fa-clock"></i>
 | 
			
		||||
        <span>set streak hour offset</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="text">
 | 
			
		||||
        Streaks reset at midnight UTC by default. If this is not convenient for
 | 
			
		||||
        you (for example if it means that streaks reset in the middle of the
 | 
			
		||||
        day), you can change the hour offset here.
 | 
			
		||||
        <br />
 | 
			
		||||
        <span class="red">You can only do this once!</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="buttons">
 | 
			
		||||
        <div
 | 
			
		||||
          class="button danger"
 | 
			
		||||
          id="setStreakHourOffset"
 | 
			
		||||
          tabindex="0"
 | 
			
		||||
          onclick="this.blur();"
 | 
			
		||||
        >
 | 
			
		||||
          update hour offset
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="section updateAccountName needsAccount hidden">
 | 
			
		||||
      <div class="groupTitle">
 | 
			
		||||
        <i class="fas fa-user"></i>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -844,6 +844,25 @@
 | 
			
		|||
    <div class="button"><i class="fas fa-plus"></i></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div id="streakHourOffsetPopupWrapper" class="popupWrapper hidden">
 | 
			
		||||
  <div id="streakHourOffsetPopup">
 | 
			
		||||
    <div class="title">Set Streak Hour Offset</div>
 | 
			
		||||
    <div class="text">
 | 
			
		||||
      Streaks reset at midnight UTC by default. If this is not convenient for
 | 
			
		||||
      you (for example if it means that streaks reset in the middle of the day),
 | 
			
		||||
      you can change the hour offset here.
 | 
			
		||||
      <br />
 | 
			
		||||
      <br />
 | 
			
		||||
      This will not take daylight savings time into consideration!
 | 
			
		||||
      <br />
 | 
			
		||||
      <br />
 | 
			
		||||
      <span class="red">You can only do this once!</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <input type="number" min="-11" max="12" value="0" />
 | 
			
		||||
    <div class="preview"></div>
 | 
			
		||||
    <div class="button">set</div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div id="resultEditTagsPanelWrapper" class="popupWrapper hidden">
 | 
			
		||||
  <div id="resultEditTagsPanel" resultid="">
 | 
			
		||||
    <div class="buttons"></div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue