diff --git a/server/src/database.ts b/server/src/database.ts index 76d3c7ec..d50a82b7 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -75,12 +75,12 @@ interface Database { setModStatus(userId: string, isMod: boolean) - setSpeakerStatus(userId: string, isSpeaker: boolean) + setSpeakerStatus(userId: string, year: string, isSpeaker: boolean) banUser(user: User, isBanned: boolean) modList(): Promise - speakerList(): Promise + speakerListForYear(year: string): Promise // ----------------------------------------------------------------- // SETTINGS DATA diff --git a/server/src/endpoints/toggleSpeakerStatus.ts b/server/src/endpoints/toggleSpeakerStatus.ts index 90eae207..689304b4 100644 --- a/server/src/endpoints/toggleSpeakerStatus.ts +++ b/server/src/endpoints/toggleSpeakerStatus.ts @@ -1,12 +1,13 @@ import { AuthenticatedEndpointFunction, LogFn } from '../endpoint' -import { awardUserBadge, isSpeaker, minimizeUser, User } from '../user' +import { awardUserBadge, isSpeaker, isSpeakerForYear, minimizeUser, User } from '../user' import { DB } from '../database' import { UnlockableBadgeMap } from '../badges' const toggleSpeakerStatus: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => { const userIdToToggle: string = inputs.userId + const year: string = inputs.year const thisYear: string = `${(new Date()).getFullYear()}` - const isForPastYear = inputs.year !== thisYear + const isForPastYear = year !== thisYear if (!userIdToToggle) { return { @@ -19,27 +20,26 @@ const toggleSpeakerStatus: AuthenticatedEndpointFunction = async (user: User, in let toggledUser: User - /* toggle past speaker */ - if (isForPastYear) { - const pastSpeakerBadge = UnlockableBadgeMap['🎙️'] + // Set their database status (which will give them the special badge if current year) + if (await isSpeakerForYear(userIdToToggle, year)) { + log(`[MOD] Setting user ${userIdToToggle} to speaker=false for ${year}`) + toggledUser = await DB.setSpeakerStatus(userIdToToggle, year, false) + } else { + log(`[MOD] Setting user ${userIdToToggle} to speaker=true for ${year}`) + toggledUser = await DB.setSpeakerStatus(userIdToToggle, year, true) + } - const profile = await DB.getUser(userIdToToggle) + // Assign them the 'past speaker' badge + // (We don't currently have per-year speakers, that would be a reasonable improvement) + const pastSpeakerBadge = UnlockableBadgeMap['🎙️'] - if (profile.unlockedBadges.includes(pastSpeakerBadge)) { - const remainingUnlockedBadges = profile.unlockedBadges.filter(badge => badge !== pastSpeakerBadge) - toggledUser = await DB.setPartialUserProfile(userIdToToggle, { unlockedBadges: remainingUnlockedBadges }) - } else { - toggledUser = await awardUserBadge(userIdToToggle, pastSpeakerBadge) - } - /* toggle current speaker */ + const profile = await DB.getUser(userIdToToggle) + + if (profile.unlockedBadges.includes(pastSpeakerBadge)) { + const remainingUnlockedBadges = profile.unlockedBadges.filter(badge => badge !== pastSpeakerBadge) + toggledUser = await DB.setPartialUserProfile(userIdToToggle, { unlockedBadges: remainingUnlockedBadges }) } else { - if (await isSpeaker(userIdToToggle)) { - log(`[MOD] Setting user ${userIdToToggle} to speaker=false`) - toggledUser = await DB.setSpeakerStatus(userIdToToggle, false) - } else { - log(`[MOD] Setting user ${userIdToToggle} to speaker=true`) - toggledUser = await DB.setSpeakerStatus(userIdToToggle, true) - } + toggledUser = await awardUserBadge(userIdToToggle, pastSpeakerBadge) } return { diff --git a/server/src/redis.ts b/server/src/redis.ts index a0c303d5..96bd18a3 100644 --- a/server/src/redis.ts +++ b/server/src/redis.ts @@ -36,8 +36,8 @@ interface RedisInternal extends Database { addMod (userId: string) removeMod (userId: string) - addSpeaker (userId: string) - removeSpeaker (userId: string) + addSpeakerForYear (userId: string, year: string) + removeSpeakerForYear (userId: string, year: string) } const Redis: RedisInternal = { @@ -213,8 +213,8 @@ const Redis: RedisInternal = { return await getSet(modListKey) || [] }, - async speakerList (): Promise { - return await getSet(speakerListKey) || [] + async speakerListForYear (year: string): Promise { + return await getSet(speakerListKey(year)) || [] }, async setModStatus (userId: string, isMod: boolean) { @@ -236,24 +236,30 @@ const Redis: RedisInternal = { await removeFromSet(modListKey, userId) }, - async setSpeakerStatus (userId: string, isSpeaker: boolean) { + async setSpeakerStatus (userId: string, year: string, isSpeaker: boolean) { + const profile = await Redis.getUser(userId) + if (!profile.speakerYears) profile.speakerYears = [] + if (isSpeaker) { - await Redis.addSpeaker(userId) + await Redis.addSpeakerForYear(userId, year) + if (!profile.speakerYears.includes(year)) { + profile.speakerYears.push(year); + } } else { - await Redis.removeSpeaker(userId) + await Redis.removeSpeakerForYear(userId, year) + profile.speakerYears = profile.speakerYears.filter(y => y !== year) } - const profile = await Redis.getUser(userId) - profile.isSpeaker = isSpeaker + await Redis.setUserProfile(userId, profile) return profile }, - async addSpeaker (userId: string) { - await addToSet(speakerListKey, userId) + async addSpeakerForYear (userId: string, year: string) { + await addToSet(speakerListKey(year), userId) }, - async removeSpeaker (userId: string) { - await removeFromSet(speakerListKey, userId) + async removeSpeakerForYear (userId: string, year: string) { + await removeFromSet(speakerListKey(year), userId) }, // Server settings @@ -403,7 +409,6 @@ const Redis: RedisInternal = { const activeUsersKey = 'activeUsersList' const modListKey = 'mods' -const speakerListKey = 'speakers' const serverSettingsKey = 'serverSettings' @@ -411,6 +416,10 @@ const allUserIdsKey = 'allUserIds' const roomIdsKey = 'roomIds' +function speakerListKey (year: string): string { + return 'speakers_${year}' +} + function roomDataKey (roomId: string): string { return `room_${roomId}` } diff --git a/server/src/user.ts b/server/src/user.ts index 724899e4..c8131815 100644 --- a/server/src/user.ts +++ b/server/src/user.ts @@ -18,7 +18,7 @@ export interface MinimalUser { username: string; pronouns?: string; isMod?: boolean; - isSpeaker?: boolean // TODO: Currently never set + speakerYears?: string[] isBanned?: boolean; // From https://www.w3schools.com/colors/colors_names.asp nameColor?: string; @@ -57,11 +57,16 @@ export async function isMod (userId: string) { return modList.includes(userId) } -export async function isSpeaker (userId: string) { - const speakerList = await DB.speakerList() +export async function isSpeakerForYear (userId: string, year: string) { + const speakerList = await DB.speakerListForYear(year) return speakerList.includes(userId) } +export async function isSpeaker (userId: string) { + const thisYear: string = `${(new Date()).getFullYear()}` + return await isSpeakerForYear(userId, thisYear) +} + export async function updateModStatus (userId: string) { const userIsMod = await isMod(userId) @@ -214,7 +219,7 @@ export function minimizeUser (user: User | PublicUser): MinimalUser { item: user.item, polymorph: user.polymorph, isMod: user.isMod, - isSpeaker: user.isSpeaker, + speakerYears: user.speakerYears, fontReward: user.fontReward, equippedBadges: user.equippedBadges, pronouns: user.pronouns diff --git a/src/components/NameView.tsx b/src/components/NameView.tsx index 4a31ccca..ba0bf83b 100644 --- a/src/components/NameView.tsx +++ b/src/components/NameView.tsx @@ -33,19 +33,21 @@ export default function NameView (props: Props) { const isSelf = props.userId === myId + // This will fail if the user's client has the wrong year set, + // that shouldn't be a concern? + const thisYear: string = `${(new Date()).getFullYear()}` + const lastYear: string = `${(new Date()).getFullYear() - 1}` + const user: User = userMap[props.userId] const username = user && user.username const isMod = user && user.isMod - const isSpeaker = user && user.isSpeaker + const isSpeaker = user && user.speakerYears?.includes(thisYear) const isBanned = user && user.isBanned // isMod = the user whose name being rendered is a mod // userIsMod = the user who is logged in is a mod const userIsMod = userMap[myId].isMod - const thisYear: string = `${(new Date()).getFullYear()}` - const lastYear: string = `${(new Date()).getFullYear() - 1}` - // This sometimes gets called before `connect` returns any users // That itself is a bug to fix, but this can at least guard against it. if (!user || (Object.keys(userMap).length === 1 && !isSelf)) { @@ -157,8 +159,7 @@ export default function NameView (props: Props) { const badges = (user.equippedBadges || []) .map((b, i) => ) - // TODO: This is not yet being set anywhere - if (user.isSpeaker) { + if (isSpeaker) { badges.unshift( )