Skip to content

Commit 0ff630e

Browse files
committed
Add nickname change functionality
1 parent 4360bc9 commit 0ff630e

File tree

12 files changed

+546
-81
lines changed

12 files changed

+546
-81
lines changed

apps/desktop/packages/mainWindow/src/managers/ModalsManager/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ const getDefaultModals = (t: TypedTFunction) => ({
123123
component: lazy(() => import("./modals/ChangeGDLAccountRecoveryEmail")),
124124
title: t("modals:_trn_change_recovery_email")
125125
},
126+
changeGDLAccountNickname: {
127+
component: lazy(() => import("./modals/ChangeGDLAccountNickname")),
128+
title: t("modals:_trn_change_nickname")
129+
},
126130
modDetails: {
127131
component: lazy(() => import("./modals/ModDetails")),
128132
title: t("modals:_trn_mod_details")
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { useModal } from ".."
2+
import ModalLayout from "../ModalLayout"
3+
import { Button, Input } from "@gd/ui"
4+
import { Trans, useTransContext } from "@gd/i18n"
5+
import { createEffect, createSignal, onCleanup, Show } from "solid-js"
6+
import { queryClient, rspc } from "@/utils/rspcClient"
7+
import { useGlobalStore } from "@/components/GlobalStoreContext"
8+
import { convertSecondsToHumanTime } from "@/utils/helpers"
9+
10+
const ChangeGDLAccountNickname = () => {
11+
const [t] = useTransContext()
12+
const modalsContext = useModal()
13+
const [newNickname, setNewNickname] = createSignal("")
14+
const [isLoading, setIsLoading] = createSignal(false)
15+
const [error, setError] = createSignal<string | null>(null)
16+
const [cooldown, setCooldown] = createSignal(0)
17+
18+
let cooldownInterval: ReturnType<typeof setInterval> | undefined
19+
20+
const globalStore = useGlobalStore()
21+
22+
const validGDLUser = () =>
23+
globalStore.gdlAccount.data?.status === "valid"
24+
? globalStore.gdlAccount.data?.value
25+
: undefined
26+
27+
// Initialize cooldown from GDL user data
28+
createEffect(() => {
29+
const timeout = validGDLUser()?.nicknameChangeTimeout
30+
if (timeout && timeout > 0) {
31+
setCooldown(timeout)
32+
startCooldownTimer()
33+
}
34+
})
35+
36+
const startCooldownTimer = () => {
37+
if (cooldownInterval) {
38+
clearInterval(cooldownInterval)
39+
}
40+
41+
cooldownInterval = setInterval(() => {
42+
setCooldown((prev) => {
43+
if (prev <= 1) {
44+
clearInterval(cooldownInterval)
45+
cooldownInterval = undefined
46+
return 0
47+
}
48+
return prev - 1
49+
})
50+
}, 1000)
51+
}
52+
53+
onCleanup(() => {
54+
if (cooldownInterval) {
55+
clearInterval(cooldownInterval)
56+
}
57+
})
58+
59+
const changeNicknameMutation = rspc.createMutation(() => ({
60+
mutationKey: ["account.changeGdlAccountNickname"]
61+
}))
62+
63+
const isValid = () => {
64+
const nickname = newNickname().trim()
65+
return nickname.length >= 5 && nickname.length <= 20
66+
}
67+
68+
return (
69+
<ModalLayout
70+
title={t("accounts:_trn_change_nickname_title")}
71+
height="h-70"
72+
width="w-140"
73+
>
74+
<div class="flex h-full flex-col justify-between">
75+
<div class="flex flex-col gap-4">
76+
<div>
77+
<Trans key="accounts:_trn_change_nickname_description" />
78+
</div>
79+
<Input
80+
placeholder={t("auth:_trn_login.nickname")}
81+
value={newNickname()}
82+
onInput={(e) => {
83+
setNewNickname(e.currentTarget.value)
84+
setError(null)
85+
}}
86+
disabled={!!cooldown()}
87+
/>
88+
<Show when={error()}>
89+
<div class="text-red-500 text-sm">{error()}</div>
90+
</Show>
91+
<Show when={cooldown()}>
92+
<div class="text-lightSlate-500 text-sm">
93+
<Trans
94+
key="accounts:_trn_nickname_change_cooldown"
95+
options={{
96+
time: convertSecondsToHumanTime(cooldown())
97+
}}
98+
/>
99+
</div>
100+
</Show>
101+
</div>
102+
103+
<div class="flex w-full justify-between">
104+
<Button
105+
onClick={() => {
106+
modalsContext?.closeModal()
107+
}}
108+
type="secondary"
109+
>
110+
<Trans key="accounts:_trn_cancel" />
111+
</Button>
112+
<Button
113+
type="primary"
114+
disabled={isLoading() || !isValid() || !!cooldown()}
115+
onClick={async () => {
116+
const uuid = globalStore?.currentlySelectedAccountUuid?.data
117+
118+
if (!uuid) {
119+
throw new Error("No active uuid")
120+
}
121+
122+
const nickname = newNickname().trim()
123+
124+
if (!nickname) {
125+
setError(t("auth:_trn_login.nickname_required"))
126+
return
127+
}
128+
129+
if (nickname.length < 5) {
130+
setError(t("auth:_trn_login.nickname_too_short"))
131+
return
132+
}
133+
134+
setIsLoading(true)
135+
try {
136+
const result = await changeNicknameMutation.mutateAsync({
137+
uuid,
138+
nickname
139+
})
140+
141+
if (!result) {
142+
// Mutation returned null - likely a backend error
143+
setError(t("accounts:_trn_nickname_change_failed"))
144+
setIsLoading(false)
145+
return
146+
}
147+
148+
if (result.status === "success") {
149+
queryClient.invalidateQueries({
150+
queryKey: ["account.getNicknameHistory"]
151+
})
152+
modalsContext?.closeModal()
153+
} else if (result.status === "failed" && result.value) {
154+
setIsLoading(false)
155+
setCooldown(result.value)
156+
startCooldownTimer()
157+
} else if (result.status === "failed") {
158+
setError(t("accounts:_trn_nickname_change_failed"))
159+
setIsLoading(false)
160+
}
161+
} catch (err) {
162+
console.error(err)
163+
setError(String(err))
164+
setIsLoading(false)
165+
}
166+
}}
167+
>
168+
<Trans key="accounts:_trn_confirm" />
169+
</Button>
170+
</div>
171+
</div>
172+
</ModalLayout>
173+
)
174+
}
175+
176+
export default ChangeGDLAccountNickname

apps/desktop/packages/mainWindow/src/managers/ModalsManager/modals/Changelogs/changelogs.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ const changelogs: Changelog = {
1919
media:
2020
"https://cdn.gdl.gg/launcher/changelog/2.0.26/addons-browser-overhaul.mp4"
2121
},
22+
{
23+
title: "Custom Profile Avatar",
24+
description:
25+
"Upload and customize your GDL account profile picture. Your avatar is displayed throughout the launcher and to other users."
26+
},
27+
{
28+
title: "Nickname Change",
29+
description:
30+
"Change your GDL account nickname from the account settings. A 7-day cooldown applies between changes."
31+
},
32+
{
33+
title: "Nickname History",
34+
description:
35+
"View your past nicknames Steam-style. Your nickname history is public, but you can clear it anytime from the account settings."
36+
},
2237
{
2338
title: "Japanese Language Support",
2439
description:

apps/desktop/packages/mainWindow/src/pages/Settings/Accounts.tsx

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import {
1515
TooltipContent,
1616
TooltipTrigger
1717
} from "@gd/ui"
18-
import { port, rspc } from "@/utils/rspcClient"
18+
import { port, queryClient, rspc } from "@/utils/rspcClient"
1919
import PageTitle from "./components/PageTitle"
2020
import Row from "./components/Row"
2121
import Title from "./components/Title"
2222
import RowsContainer from "./components/RowsContainer"
2323
import { useGlobalStore } from "@/components/GlobalStoreContext"
2424
import {
2525
createEffect,
26+
createMemo,
2627
createSignal,
2728
For,
2829
JSX,
@@ -42,6 +43,7 @@ const GDLAccountRowItem = (props: {
4243
value?: string | null | undefined
4344
children?: JSX.Element
4445
onEdit?: () => void
46+
extraAction?: JSX.Element
4547
}) => {
4648
return (
4749
<div class="flex items-center justify-between">
@@ -69,6 +71,7 @@ const GDLAccountRowItem = (props: {
6971
EDIT
7072
</div>
7173
</Show>
74+
{props.extraAction}
7275
<div class="hidden group-hover:block">
7376
<div class="i-hugeicons:clipboard text-lightSlate-50 text-lg" />
7477
</div>
@@ -214,12 +217,22 @@ const Accounts = () => {
214217
const deleteAvatarMutation = rspc.createMutation(() => ({
215218
mutationKey: ["account.deleteProfileIcon"]
216219
}))
220+
const clearNicknameHistoryMutation = rspc.createMutation(() => ({
221+
mutationKey: ["account.clearNicknameHistory"]
222+
}))
217223

218224
const validGDLUser = () =>
219225
globalStore.gdlAccount.data?.status === "valid"
220226
? globalStore.gdlAccount.data?.value
221227
: undefined
222228

229+
const userId = createMemo(() => validGDLUser()?.id)
230+
231+
const nicknameHistoryQuery = rspc.createQuery(() => ({
232+
queryKey: ["account.getNicknameHistory", userId() ?? 0],
233+
enabled: !!userId()
234+
}))
235+
223236
const invalidGDLUser = () => globalStore.gdlAccount.data?.status === "invalid"
224237

225238
// Initialize avatar preview from GDL account
@@ -414,7 +427,88 @@ const Accounts = () => {
414427
class="rounded-md"
415428
dialogTitle={t("accounts:_trn_select_avatar_image")}
416429
/>
417-
{validGDLUser()?.nickname}
430+
<GDLAccountRowItem
431+
title={t("accounts:_trn_nickname")}
432+
value={validGDLUser()?.nickname}
433+
onEdit={() => {
434+
modalsContext?.openModal({
435+
name: "changeGDLAccountNickname"
436+
})
437+
}}
438+
extraAction={
439+
<Show
440+
when={
441+
nicknameHistoryQuery.data &&
442+
nicknameHistoryQuery.data.length > 0
443+
}
444+
>
445+
<div onClick={(e) => e.stopPropagation()}>
446+
<Popover>
447+
<PopoverTrigger>
448+
<div class="text-md text-lightSlate-700 hover:text-lightSlate-50 underline transition-all duration-100 ease-spring">
449+
<Trans key="accounts:_trn_nickname_history" />
450+
</div>
451+
</PopoverTrigger>
452+
<PopoverContent class="max-h-60 w-64 overflow-y-auto">
453+
<div class="flex flex-col gap-2">
454+
<div class="text-lightSlate-50 font-medium">
455+
<Trans key="accounts:_trn_nickname_history" />
456+
</div>
457+
<For each={nicknameHistoryQuery.data}>
458+
{(entry) => (
459+
<div class="text-lightSlate-300 flex justify-between text-sm">
460+
<span>{entry.nickname}</span>
461+
<span class="text-lightSlate-500">
462+
{new Date(
463+
entry.changedAt
464+
).toLocaleDateString()}
465+
</span>
466+
</div>
467+
)}
468+
</For>
469+
<Button
470+
type="secondary"
471+
size="small"
472+
class="mt-2"
473+
onClick={async () => {
474+
const uuid =
475+
globalStore
476+
?.currentlySelectedAccountUuid?.data
477+
if (!uuid) return
478+
479+
try {
480+
await clearNicknameHistoryMutation.mutateAsync(
481+
uuid
482+
)
483+
queryClient.invalidateQueries({
484+
queryKey: [
485+
"account.getNicknameHistory"
486+
]
487+
})
488+
toast.success(
489+
t(
490+
"accounts:_trn_nickname_history_cleared"
491+
)
492+
)
493+
} catch (err) {
494+
console.error(err)
495+
toast.error(
496+
t(
497+
"accounts:_trn_nickname_history_clear_failed"
498+
)
499+
)
500+
}
501+
}}
502+
>
503+
<Trans key="accounts:_trn_clear_nickname_history" />
504+
</Button>
505+
</div>
506+
</PopoverContent>
507+
</Popover>
508+
</div>
509+
</Show>
510+
}
511+
/>
418512
</div>
419513
<GDLAccountRowItem
420514
title={t("accounts:_trn_friend_code")}

0 commit comments

Comments
 (0)