feat: indicate premium users (fehmer) (#5092)

* feat: indicate premium users

* frontend

* Test multiple userFlags, remove later

* cleanup

* fix flag alignment on profile and leaderboards

* fix name auto scaling

* update screenshot watermark

* update header text

* use userFlags for lbOptOut

* use flex end

* removeo unused code, increase margin

---------

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
Christian Fehmer 2024-03-05 16:09:23 +01:00 committed by GitHub
parent 7e957fb449
commit c95e3b2fa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 373 additions and 91 deletions

View file

@ -51,6 +51,9 @@ describe("user controller test", () => {
enabled: false,
maxMail: 0,
},
premium: {
enabled: true,
},
},
} as any);

View file

@ -3,6 +3,8 @@ import { ObjectId } from "mongodb";
import * as UserDal from "../../src/dal/user";
import * as LeaderboardsDal from "../../src/dal/leaderboards";
import * as PublicDal from "../../src/dal/public";
import * as Configuration from "../../src/init/configuration";
const configuration = Configuration.getCachedConfiguration();
import * as DB from "../../src/init/db";
@ -50,10 +52,10 @@ describe("LeaderboardsDal", () => {
const lb = result.map((it) => _.omit(it, ["_id"]));
expect(lb).toEqual([
expectedLbEntry(1, rank1, "15"),
expectedLbEntry(2, rank2, "15"),
expectedLbEntry(3, rank3, "15"),
expectedLbEntry(4, rank4, "15"),
expectedLbEntry("15", { rank: 1, user: rank1 }),
expectedLbEntry("15", { rank: 2, user: rank2 }),
expectedLbEntry("15", { rank: 3, user: rank3 }),
expectedLbEntry("15", { rank: 4, user: rank4 }),
]);
});
it("should create leaderboard time english 60", async () => {
@ -76,10 +78,10 @@ describe("LeaderboardsDal", () => {
const lb = result.map((it) => _.omit(it, ["_id"]));
expect(lb).toEqual([
expectedLbEntry(1, rank1, "60"),
expectedLbEntry(2, rank2, "60"),
expectedLbEntry(3, rank3, "60"),
expectedLbEntry(4, rank4, "60"),
expectedLbEntry("60", { rank: 1, user: rank1 }),
expectedLbEntry("60", { rank: 2, user: rank2 }),
expectedLbEntry("60", { rank: 3, user: rank3 }),
expectedLbEntry("60", { rank: 4, user: rank4 }),
]);
});
it("should not include discord properties for users without discord connection", async () => {
@ -154,10 +156,113 @@ describe("LeaderboardsDal", () => {
//THEN
expect(result).toEqual({ "20": 2, "110": 2 });
});
it("should create leaderboard with badges", async () => {
//GIVEN
const noBadge = await createUser(lbBests(pb(4)));
const oneBadgeSelected = await createUser(lbBests(pb(3)), {
inventory: { badges: [{ id: 1, selected: true }] },
});
const oneBadgeNotSelected = await createUser(lbBests(pb(2)), {
inventory: { badges: [{ id: 1, selected: false }] },
});
const multipleBadges = await createUser(lbBests(pb(1)), {
inventory: {
badges: [
{ id: 1, selected: false },
{ id: 2, selected: true },
{ id: 3, selected: true },
],
},
});
//WHEN
await LeaderboardsDal.update("time", "15", "english");
const result = (await LeaderboardsDal.get(
"time",
"15",
"english",
0
)) as SharedTypes.LeaderboardEntry[];
//THEN
const lb = result.map((it) => _.omit(it, ["_id"]));
expect(lb).toEqual([
expectedLbEntry("15", { rank: 1, user: noBadge }),
expectedLbEntry("15", {
rank: 2,
user: oneBadgeSelected,
badgeId: 1,
}),
expectedLbEntry("15", { rank: 3, user: oneBadgeNotSelected }),
expectedLbEntry("15", {
rank: 4,
user: multipleBadges,
badgeId: 2,
}),
]);
});
it("should create leaderboard with premium", async () => {
await enablePremiumFeatures(true);
//GIVEN
const noPremium = await createUser(lbBests(pb(4)));
const lifetime = await createUser(lbBests(pb(3)), premium(-1));
const validPremium = await createUser(lbBests(pb(2)), premium(10));
const expiredPremium = await createUser(lbBests(pb(1)), premium(-10));
//WHEN
await LeaderboardsDal.update("time", "15", "english");
const result = (await LeaderboardsDal.get(
"time",
"15",
"english",
0
)) as SharedTypes.LeaderboardEntry[];
//THEN
const lb = result.map((it) => _.omit(it, ["_id"]));
expect(lb).toEqual([
expectedLbEntry("15", { rank: 1, user: noPremium }),
expectedLbEntry("15", {
rank: 2,
user: lifetime,
isPremium: true,
}),
expectedLbEntry("15", {
rank: 3,
user: validPremium,
isPremium: true,
}),
expectedLbEntry("15", { rank: 4, user: expiredPremium }),
]);
});
it("should create leaderboard without premium if feature disabled", async () => {
await enablePremiumFeatures(false);
//GIVEN
const lifetime = await createUser(lbBests(pb(3)), premium(-1));
//WHEN
await LeaderboardsDal.update("time", "15", "english");
const result = (await LeaderboardsDal.get(
"time",
"15",
"english",
0
)) as SharedTypes.LeaderboardEntry[];
//THEN
expect(result[0]?.isPremium).toBeUndefined();
});
});
});
function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
function expectedLbEntry(
time: string,
{ rank, user, badgeId, isPremium }: ExpectedLbEntry
) {
const lbBest: SharedTypes.PersonalBest =
user.lbPersonalBests?.time[time].english;
@ -172,7 +277,8 @@ function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
consistency: lbBest.consistency,
discordId: user.discordId,
discordAvatar: user.discordAvatar,
badgeId: 2,
badgeId,
isPremium,
};
}
@ -192,12 +298,6 @@ async function createUser(
timeTyping: 7200,
discordId: "discord " + uid,
discordAvatar: "avatar " + uid,
inventory: {
badges: [
{ id: 1, selected: false },
{ id: 2, selected: true },
],
},
...userProperties,
lbPersonalBests,
},
@ -234,3 +334,32 @@ function pb(
timestamp,
};
}
function premium(expirationDeltaSeconds) {
return {
premium: {
startTimestamp: 0,
expirationTimestamp:
expirationDeltaSeconds === -1
? -1
: Date.now() + expirationDeltaSeconds * 1000,
},
};
}
interface ExpectedLbEntry {
rank: number;
user: MonkeyTypes.DBUser;
badgeId?: number;
isPremium?: boolean;
}
async function enablePremiumFeatures(premium: boolean): Promise<void> {
const mockConfig = _.merge(await configuration, {
users: { premium: { enabled: premium } },
});
jest
.spyOn(Configuration, "getCachedConfiguration")
.mockResolvedValue(mockConfig);
}

View file

@ -105,7 +105,8 @@ export async function getDailyLeaderboard(
const topResults = await dailyLeaderboard.getResults(
minRank,
maxRank,
req.ctx.configuration.dailyLeaderboards
req.ctx.configuration.dailyLeaderboards,
req.ctx.configuration.users.premium.enabled
);
return new MonkeyResponse("Daily leaderboard retrieved", topResults);

View file

@ -489,6 +489,8 @@ export async function addResult(
(isDevEnvironment() || (user.timeTyping ?? 0) > 7200);
const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
const isPremium =
(await UserDAL.checkIfUserIsPremium(user.uid, user)) || undefined;
if (dailyLeaderboard && validResultCriteria) {
incrementDailyLeaderboard(
@ -508,6 +510,7 @@ export async function addResult(
discordAvatar: user.discordAvatar,
discordId: user.discordId,
badgeId: selectedBadgeId,
isPremium,
},
dailyLeaderboardsConfig
);

View file

@ -753,6 +753,7 @@ export async function getProfile(
streak: streak?.length ?? 0,
maxStreak: streak?.maxLength ?? 0,
lbOptOut,
isPremium: await UserDAL.checkIfUserIsPremium(user.uid, user),
};
if (banned) {

View file

@ -3,6 +3,7 @@ import Logger from "../utils/logger";
import { performance } from "perf_hooks";
import { setLeaderboard } from "../utils/prometheus";
import { isDevEnvironment } from "../utils/misc";
import { getCachedConfiguration } from "../init/configuration";
const leaderboardUpdating: Record<string, boolean> = {};
@ -27,6 +28,13 @@ export async function get(
.skip(skip)
.limit(limit)
.toArray();
const premiumFeaturesEnabled = (await getCachedConfiguration(true)).users
.premium.enabled;
if (!premiumFeaturesEnabled) {
preset.forEach((it) => (it.isPremium = undefined));
}
return preset;
} catch (e) {
if (e.error === 175) {
@ -77,7 +85,6 @@ export async function update(
const key = `lbPersonalBests.${mode}.${mode2}.${language}`;
const lbCollectionName = `leaderboards.${language}.${mode}.${mode2}`;
leaderboardUpdating[`${language}_${mode}_${mode2}`] = true;
const start1 = performance.now();
const lb = db
.collection<MonkeyTypes.DBUser>("users")
.aggregate<SharedTypes.LeaderboardEntry>(
@ -127,41 +134,43 @@ export async function update(
discordId: 1,
discordAvatar: 1,
inventory: 1,
premium: 1,
},
},
{
$addFields: {
[`${key}.uid`]: "$uid",
[`${key}.name`]: "$name",
[`${key}.discordId`]: {
$ifNull: ["$discordId", "$$REMOVE"],
},
[`${key}.discordAvatar`]: {
$ifNull: ["$discordAvatar", "$$REMOVE"],
},
"user.uid": "$uid",
"user.name": "$name",
"user.discordId": { $ifNull: ["$discordId", "$$REMOVE"] },
"user.discordAvatar": { $ifNull: ["$discordAvatar", "$$REMOVE"] },
[`${key}.consistency`]: {
$ifNull: [`$${key}.consistency`, "$$REMOVE"],
},
[`${key}.rank`]: {
calculated: {
$function: {
body: "function() {try {row_number+= 1;} catch (e) {row_number= 1;}return row_number;}",
args: [],
lang: "js",
},
},
[`${key}.badgeId`]: {
$function: {
body: "function(badges) {if (!badges) return null; for(let i=0;i<badges.length;i++){ if(badges[i].selected) return badges[i].id;}return null;}",
args: ["$inventory.badges"],
lang: "js",
args: [
"$premium.expirationTimestamp",
"$$NOW",
"$inventory.badges",
],
body: `function(expiration, currentTime, badges) {
try {row_number+= 1;} catch (e) {row_number= 1;}
var badgeId = undefined;
if(badges)for(let i=0; i<badges.length; i++){
if(badges[i].selected){ badgeId = badges[i].id; break}
}
var isPremium = expiration !== undefined && (expiration === -1 || new Date(expiration)>currentTime) || undefined;
return {rank:row_number,badgeId, isPremium};
}`,
},
},
},
},
{
$replaceRoot: {
newRoot: `$${key}`,
$replaceWith: {
$mergeObjects: [`$${key}`, "$user", "$calculated"],
},
},
{ $out: lbCollectionName },
@ -169,6 +178,7 @@ export async function update(
{ allowDiskUse: true }
);
const start1 = performance.now();
await lb.toArray();
const end1 = performance.now();
@ -179,7 +189,6 @@ export async function update(
const end2 = performance.now();
//update speedStats
const start3 = performance.now();
const boundaries = [...Array(32).keys()].map((it) => it * 10);
const statsKey = `${language}_${mode}_${mode2}`;
const src = await db.collection(lbCollectionName);
@ -218,6 +227,7 @@ export async function update(
],
{ allowDiskUse: true }
);
const start3 = performance.now();
await histogram.toArray();
const end3 = performance.now();
@ -258,6 +268,7 @@ async function createIndex(key: string): Promise<void> {
discordId: 1,
discordAvatar: 1,
inventory: 1,
premium: 1,
};
const partial = {
partialFilterExpression: {
@ -275,4 +286,10 @@ async function createIndex(key: string): Promise<void> {
export async function createIndicies(): Promise<void> {
await createIndex("lbPersonalBests.time.15.english");
await createIndex("lbPersonalBests.time.60.english");
if (isDevEnvironment()) {
Logger.info("Updating leaderboards in dev mode...");
await update("time", "15", "english");
await update("time", "60", "english");
}
}

View file

@ -7,6 +7,7 @@ import MonkeyError from "../utils/error";
import { Collection, ObjectId, Long, UpdateFilter } from "mongodb";
import Logger from "../utils/logger";
import { flattenObjectDeep, isToday, isYesterday } from "../utils/misc";
import { getCachedConfiguration } from "../init/configuration";
const SECONDS_PER_HOUR = 3600;
@ -1049,6 +1050,11 @@ export async function checkIfUserIsPremium(
uid: string,
userInfoOverride?: MonkeyTypes.DBUser
): Promise<boolean> {
const premiumFeaturesEnabled = (await getCachedConfiguration(true)).users
.premium.enabled;
if (!premiumFeaturesEnabled) {
return false;
}
const user = userInfoOverride ?? (await getUser(uid, "checkIfUserIsPremium"));
const expirationDate = user.premium?.expirationTimestamp;

View file

@ -20,7 +20,7 @@ declare namespace MonkeyTypes {
type DBUser = Omit<
SharedTypes.User,
"resultFilterPresets" | "tags" | "customThemes"
"resultFilterPresets" | "tags" | "customThemes" | "isPremium"
> & {
_id: ObjectId;
resultFilterPresets?: WithObjectIdArray<SharedTypes.ResultFilters[]>;

View file

@ -14,6 +14,7 @@ type DailyLeaderboardEntry = {
discordAvatar?: string;
discordId?: string;
badgeId?: number;
isPremium?: boolean;
};
type GetRankResponse = {
@ -123,7 +124,8 @@ export class DailyLeaderboard {
public async getResults(
minRank: number,
maxRank: number,
dailyLeaderboardsConfig: SharedTypes.Configuration["dailyLeaderboards"]
dailyLeaderboardsConfig: SharedTypes.Configuration["dailyLeaderboards"],
premiumFeaturesEnabled: boolean
): Promise<LbEntryWithRank[]> {
const connection = RedisClient.getConnection();
if (!connection || !dailyLeaderboardsConfig.enabled) {
@ -156,6 +158,10 @@ export class DailyLeaderboard {
})
);
if (!premiumFeaturesEnabled) {
resultsWithRanks.forEach((it) => (it.isPremium = undefined));
}
return resultsWithRanks;
}

View file

@ -31,7 +31,8 @@ async function handleDailyLeaderboardResults(
const allResults = await dailyLeaderboard.getResults(
0,
-1,
dailyLeaderboardsConfig
dailyLeaderboardsConfig,
false
);
if (allResults.length === 0) {

View file

@ -29,7 +29,10 @@
<div class="avatar"></div>
</div>
<div>
<div class="name">-</div>
<div class="user">
<div class="name">-</div>
<div class="userFlags"></div>
</div>
<div class="badges"></div>
<div class="allBadges"></div>
<div class="joined" data-balloon-pos="up">-</div>

View file

@ -27,7 +27,10 @@
<div class="avatar"></div>
</div>
<div>
<div class="name">-</div>
<div class="user">
<div class="name">-</div>
<div class="userFlags"></div>
</div>
<div class="badges"></div>
<div class="allBadges"></div>
<div class="joined" data-balloon-pos="up">-</div>

View file

@ -159,6 +159,12 @@
.badge {
font-size: 0.6rem;
}
.flagsAndBadge {
display: flex;
gap: 0.5rem;
color: var(--sub-color);
place-items: center;
}
}
tr td:first-child {

View file

@ -17,8 +17,11 @@ nav {
.textButton {
.text {
font-size: 0.65em;
font-size: 0.75em;
align-self: center;
.fas {
margin-left: 0.33em;
}
}
.icon,
@ -34,7 +37,7 @@ nav {
&.account {
position: relative;
align-items: center;
gap: 0.25em;
gap: 0.33em;
display: grid;
grid-auto-flow: column;
.loading,
@ -107,8 +110,13 @@ nav {
}
}
}
&:hover .level {
background-color: var(--text-color);
&:hover {
.level {
background-color: var(--text-color);
}
.userFlags {
color: var(--text-color);
}
}
}
}

View file

@ -355,17 +355,20 @@
grid-column: 1/2;
}
}
.name {
// font-size: 3rem;
// line-height: 2.5rem;
.user {
display: flex;
flex-wrap: wrap;
font-size: 1rem;
line-height: 100%;
// line-height: 100%;
width: max-content;
.bannedIcon {
margin-left: 0.5em;
display: inline-grid;
font-size: 75%;
color: var(--error-color);
gap: 0.35rem;
.userFlags {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
// font-size: 0.75em;
color: var(--sub-color);
place-items: center left;
}
}
.badge {

View file

@ -107,8 +107,14 @@
.ssWatermark {
font-size: 1.25rem;
color: var(--sub-color);
line-height: 1rem;
text-align: right;
display: flex;
justify-content: flex-end;
gap: 0 1em;
flex-wrap: wrap;
.fas {
margin-left: 0.33em;
}
}
#timerNumber {

View file

@ -40,6 +40,7 @@ import {
} from "../test/test-config";
import * as ConnectionState from "../states/connection";
import { navigate } from "./route-controller";
import { getHtmlByUserFlags } from "./user-flag-controller";
let signedOutThisSession = false;
@ -114,6 +115,9 @@ async function getDataAndInit(): Promise<boolean> {
LoadingPage.updateText("Applying settings...");
const snapshot = DB.getSnapshot() as MonkeyTypes.Snapshot;
$("nav .textButton.account > .text").text(snapshot.name);
$("nav .textButton.account > .text").append(
getHtmlByUserFlags(snapshot, { iconsOnly: true })
);
showFavoriteQuoteLength();
ResultFilters.loadTags(snapshot.tags);

View file

@ -0,0 +1,83 @@
const flags: UserFlag[] = [
{
name: "Prime Ape",
description: "Paying for a monthly subscription",
icon: "fa-dollar-sign",
test: (it) => it.isPremium === true,
},
{
name: "Banned",
description: "This account is banned",
icon: "fa-gavel",
color: "var(--error-color)",
test: (it) => it.banned === true,
},
{
name: "LbOptOut",
description: "This account has opted out of leaderboards",
icon: "fa-crown",
color: "var(--error-color)",
test: (it) => it.lbOptOut === true,
},
];
type SupportsFlags = {
isPremium?: boolean;
banned?: boolean;
lbOptOut?: boolean;
};
type UserFlag = {
readonly name: string;
readonly description: string;
readonly icon: string;
readonly color?: string;
readonly background?: string;
readonly customStyle?: string;
test(source: SupportsFlags): boolean;
};
type UserFlagOptions = {
iconsOnly?: boolean;
};
const USER_FLAG_OPTIONS_DEFAULT: UserFlagOptions = {
iconsOnly: false,
};
function getMatchingFlags(source: SupportsFlags): UserFlag[] {
const result = flags.filter((it) => it.test(source));
return result;
}
function toHtml(flag: UserFlag, formatOptions: UserFlagOptions): string {
const icon = `<i class="fas ${flag.icon}"></i>`;
if (formatOptions.iconsOnly) {
return icon;
}
const style = [];
if (flag.background !== undefined) {
style.push(`background: ${flag.background};`);
}
if (flag?.color !== undefined) {
style.push(`color: ${flag.color};`);
}
if (flag?.customStyle !== undefined) {
style.push(flag.customStyle);
}
const balloon = `aria-label="${flag.description}" data-balloon-pos="right"`;
return `<div class="flag" ${balloon} style="${style.join("")}">${icon}</div>`;
}
export function getHtmlByUserFlags(
source: SupportsFlags,
options?: UserFlagOptions
): string {
const formatOptions = { ...USER_FLAG_OPTIONS_DEFAULT, ...options };
return getMatchingFlags(source)
.map((it) => toHtml(it, formatOptions))
.join("");
}

View file

@ -12,6 +12,7 @@ import * as Skeleton from "../utils/skeleton";
import { debounce } from "throttle-debounce";
import Format from "../utils/format";
import SlimSelect from "slim-select";
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
const wrapperId = "leaderboardsWrapper";
@ -336,7 +337,10 @@ async function fillTable(lb: LbKey): Promise<void> {
<a href="${location.origin}/profile/${
entry.uid
}?isUid" class="entryName" uid=${entry.uid} router-link>${entry.name}</a>
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
<div class="flagsAndBadge">
${getHtmlByUserFlags(entry)}
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
</div>
</div>
</td>
<td class="alignRight">${Format.typingSpeed(entry.wpm, {

View file

@ -7,6 +7,7 @@ import { throttle } from "throttle-debounce";
import * as EditProfilePopup from "../popups/edit-profile-popup";
import * as ActivePage from "../states/active-page";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
type ProfileViewPaths = "profile" | "account";
type UserProfileOrSnapshot = SharedTypes.UserProfile | MonkeyTypes.Snapshot;
@ -23,7 +24,6 @@ export async function update(
profileElement.attr("uid", profile.uid ?? "");
profileElement.attr("name", profile.name ?? "");
profileElement.attr("lbOptOut", `${profile.lbOptOut ?? false}`);
// ============================================================================
// DO FREAKING NOT USE .HTML OR .APPEND HERE - USER INPUT!!!!!!
@ -31,8 +31,6 @@ export async function update(
const banned = profile.banned === true;
const lbOptOut = profile.lbOptOut === true;
if (
details === undefined ||
profile === undefined ||
@ -78,22 +76,9 @@ export async function update(
}
details.find(".name").text(profile.name);
details.find(".userFlags").html(getHtmlByUserFlags(profile));
if (banned) {
details
.find(".name")
.append(
`<div class="bannedIcon" aria-label="This account is banned" data-balloon-pos="up"><i class="fas fa-gavel"></i></div>`
);
}
if (lbOptOut) {
details
.find(".name")
.append(
`<div class="bannedIcon" aria-label="This account has opted out of leaderboards" data-balloon-pos="up"><i class="fas fa-crown"></i></div>`
);
if (profile.lbOptOut === true) {
if (where === "profile") {
profileElement
.find(".lbOptOutReminder")
@ -413,7 +398,7 @@ export function updateNameFontSize(where: ProfileViewPaths): void {
details = $(".pageProfile .profile .details");
}
if (!details) return;
const nameFieldjQ = details.find(".name");
const nameFieldjQ = details.find(".user");
const nameFieldParent = nameFieldjQ.parent()[0];
const nameField = nameFieldjQ[0];
const upperLimit = Misc.convertRemToPixels(2);

View file

@ -19,7 +19,10 @@ function reset(): void {
<div class="avatar"></div>
</div>
<div>
<div class="name">-</div>
<div class="user">
<div class="name">-</div>
<div class="userFlags"></div>
</div>
<div class="badges"></div>
<div class="allBadges"></div>
<div class="joined" data-balloon-pos="up">-</div>

View file

@ -23,6 +23,7 @@ import * as ResultWordHighlight from "../elements/result-word-highlight";
import * as ActivePage from "../states/active-page";
import Format from "../utils/format";
import * as Loader from "../elements/loader";
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
return (await import("html2canvas")).default;
@ -449,17 +450,20 @@ export async function screenshot(): Promise<void> {
const dateNow = new Date(Date.now());
$("#resultReplay").addClass("hidden");
$(".pageTest .ssWatermark").removeClass("hidden");
$(".pageTest .ssWatermark").text(
format(dateNow, "dd MMM yyyy HH:mm") + " | monkeytype.com "
);
if (isAuthenticated()) {
$(".pageTest .ssWatermark").text(
DB.getSnapshot()?.name +
" | " +
format(dateNow, "dd MMM yyyy HH:mm") +
" | monkeytype.com "
);
const snapshot = DB.getSnapshot();
const ssWatermark = [format(dateNow, "dd MMM yyyy HH:mm"), "monkeytype.com"];
if (snapshot?.name !== undefined) {
const userText = `${snapshot?.name}${getHtmlByUserFlags(snapshot, {
iconsOnly: true,
})}`;
ssWatermark.unshift(userText);
}
$(".pageTest .ssWatermark").html(
ssWatermark
.map((el) => `<span>${el}</span>`)
.join("<span class='pipe'>|</span>")
);
$(".pageTest .buttons").addClass("hidden");
$("#notificationCenter").addClass("hidden");
$("#commandLineMobileButton").addClass("hidden");

View file

@ -446,7 +446,8 @@ declare namespace SharedTypes {
discordId?: string;
discordAvatar?: string;
rank: number;
badgeId: number | null;
badgeId?: number;
isPremium?: boolean;
}
type PostResultResponse = {
@ -526,6 +527,7 @@ declare namespace SharedTypes {
profileDetails?: UserProfileDetails;
customThemes?: CustomTheme[];
premium?: PremiumInfo;
isPremium?: boolean;
quoteRatings?: UserQuoteRatings;
favoriteQuotes?: Record<string, string[]>;
lbMemory?: UserLbMemory;
@ -575,6 +577,7 @@ declare namespace SharedTypes {
| "lbOptOut"
| "inventory"
| "uid"
| "isPremium"
> & {
typingStats: {
completedTests: User["completedTests"];