@hadoku/task-ui-components 4.23.6 → 4.23.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AvatarBuilder.d.ts.map +1 -1
- package/dist/components/AvatarBuilder.js +59 -2
- package/dist/components/AvatarBuilder.js.map +1 -1
- package/dist/components/AvatarPicker.d.ts +2 -8
- package/dist/components/AvatarPicker.d.ts.map +1 -1
- package/dist/components/AvatarPicker.js +2 -78
- package/dist/components/AvatarPicker.js.map +1 -1
- package/dist/components/ConnectedSettings.d.ts.map +1 -1
- package/dist/components/ConnectedSettings.js +18 -0
- package/dist/components/ConnectedSettings.js.map +1 -1
- package/dist/components/LinkedAccounts.d.ts +21 -0
- package/dist/components/LinkedAccounts.d.ts.map +1 -0
- package/dist/components/LinkedAccounts.js +119 -0
- package/dist/components/LinkedAccounts.js.map +1 -0
- package/dist/vendor/wolffm/prefs-client/avatar.js.map +1 -1
- package/dist/vendor/wolffm/prefs-client/cosmetics.js +2 -2
- package/dist/vendor/wolffm/prefs-client/cosmetics.js.map +1 -1
- package/package.json +2 -2
- package/src/components/AvatarBuilder.tsx +68 -2
- package/src/components/AvatarPicker.tsx +11 -112
- package/src/components/ConnectedSettings.tsx +25 -0
- package/src/components/LinkedAccounts.tsx +176 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkedAccounts — Discord and Steam, linked and unlinked from the settings
|
|
3
|
+
* popout's Account tab (operator, 2026-11-02: linking is an account matter, not
|
|
4
|
+
* part of changing the avatar). A linked account's picture then shows up as a
|
|
5
|
+
* choice in the avatar dialog's Picture tab.
|
|
6
|
+
*
|
|
7
|
+
* Loaded only when the person opens the row, never at mount: the read can
|
|
8
|
+
* refresh a linked picture from Discord or Steam, which opening the gear must
|
|
9
|
+
* not cost (settings-prefetch e2e).
|
|
10
|
+
*
|
|
11
|
+
* Linking signs in through a popup (`linkInPopup`), so the page under the
|
|
12
|
+
* popout never navigates and a game keeps running.
|
|
13
|
+
*
|
|
14
|
+
* COMPLIANCE (Steam Web API terms; Discord developer terms), carried over from
|
|
15
|
+
* hadoku.me/account: nothing is fetched from a provider until the person reads
|
|
16
|
+
* what is read and kept and presses Continue; no provider logos, and no implied
|
|
17
|
+
* endorsement.
|
|
18
|
+
*/
|
|
19
|
+
import React, { useEffect, useState } from 'react'
|
|
20
|
+
import type { AvatarSettings, LinkProvider } from '@wolffm/prefs-client/avatar'
|
|
21
|
+
import { AVATAR_EVENT } from '../lib/avatarEvent'
|
|
22
|
+
import { avatarClient, avatarErrorText } from '../lib/avatarData'
|
|
23
|
+
|
|
24
|
+
const LABEL: Record<LinkProvider, string> = { discord: 'Discord', steam: 'Steam' }
|
|
25
|
+
|
|
26
|
+
const CONSENT: Record<LinkProvider, { points: string[]; go: string }> = {
|
|
27
|
+
discord: {
|
|
28
|
+
points: [
|
|
29
|
+
'Discord asks you to share your basic profile ("identify").',
|
|
30
|
+
'Kept: your Discord user ID and the address of your profile picture, refreshed while linked.',
|
|
31
|
+
'Not kept: your username, email, servers or any token.',
|
|
32
|
+
'Unlink deletes both.'
|
|
33
|
+
],
|
|
34
|
+
go: 'Continue to Discord'
|
|
35
|
+
},
|
|
36
|
+
steam: {
|
|
37
|
+
points: [
|
|
38
|
+
'You sign in on steamcommunity.com; your password never reaches hadoku.me.',
|
|
39
|
+
'Your public Steam profile picture is then read through the Steam Web API, now and periodically while linked.',
|
|
40
|
+
'Kept: your Steam ID and the address of that picture.',
|
|
41
|
+
'Unlink deletes both and stops the reads.'
|
|
42
|
+
],
|
|
43
|
+
go: 'Continue to Steam'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const S = {
|
|
48
|
+
stack: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' },
|
|
49
|
+
line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
|
50
|
+
grow: { flex: 1, minWidth: 0 },
|
|
51
|
+
list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }
|
|
52
|
+
} satisfies Record<string, React.CSSProperties>
|
|
53
|
+
|
|
54
|
+
export function LinkedAccounts() {
|
|
55
|
+
const [settings, setSettings] = useState<AvatarSettings | null>(null)
|
|
56
|
+
const [busy, setBusy] = useState(false)
|
|
57
|
+
const [error, setError] = useState<string | null>(null)
|
|
58
|
+
const [consent, setConsent] = useState<LinkProvider | null>(null)
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let live = true
|
|
62
|
+
avatarClient
|
|
63
|
+
.get()
|
|
64
|
+
.then(s => live && setSettings(s))
|
|
65
|
+
.catch((err: unknown) => live && setError(avatarErrorText(err)))
|
|
66
|
+
return () => {
|
|
67
|
+
live = false
|
|
68
|
+
}
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
const run = async (work: () => Promise<AvatarSettings>) => {
|
|
72
|
+
setBusy(true)
|
|
73
|
+
setError(null)
|
|
74
|
+
try {
|
|
75
|
+
const next = await work()
|
|
76
|
+
setSettings(next)
|
|
77
|
+
// The page under the popout (a watchparty room, a game) re-reads the
|
|
78
|
+
// avatar from this, as it does after a change in the avatar dialog.
|
|
79
|
+
window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setError(avatarErrorText(err))
|
|
82
|
+
} finally {
|
|
83
|
+
setBusy(false)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!settings) {
|
|
88
|
+
return (
|
|
89
|
+
<span className="settings-popout__hint" role={error ? 'alert' : undefined}>
|
|
90
|
+
{error ?? '…'}
|
|
91
|
+
</span>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Must stay synchronous up to linkInPopup: the popup opens before its first
|
|
96
|
+
// await, inside this click, which is what popup blockers allow.
|
|
97
|
+
const link = (provider: LinkProvider) => {
|
|
98
|
+
setConsent(null)
|
|
99
|
+
void run(async () => {
|
|
100
|
+
const next = await avatarClient.linkInPopup(provider)
|
|
101
|
+
// Linking is choosing, when the account came back with a picture.
|
|
102
|
+
return next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div style={S.stack}>
|
|
108
|
+
{(['discord', 'steam'] as const).map(p => {
|
|
109
|
+
const linked = settings.linked[p]
|
|
110
|
+
return (
|
|
111
|
+
<div key={p} style={S.line}>
|
|
112
|
+
<span className="settings-popout__label" style={S.grow}>
|
|
113
|
+
{LABEL[p]}
|
|
114
|
+
{!settings.providers[p]
|
|
115
|
+
? ' · not available yet'
|
|
116
|
+
: linked
|
|
117
|
+
? linked.avatarUrl
|
|
118
|
+
? ' · linked'
|
|
119
|
+
: ' · linked, no picture'
|
|
120
|
+
: ''}
|
|
121
|
+
</span>
|
|
122
|
+
{settings.providers[p] && !linked && (
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
className="settings-popout__btn"
|
|
126
|
+
disabled={busy}
|
|
127
|
+
onClick={() => setConsent(consent === p ? null : p)}
|
|
128
|
+
>
|
|
129
|
+
Link
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
{linked && (
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className="settings-popout__btn"
|
|
136
|
+
disabled={busy}
|
|
137
|
+
onClick={() => void run(() => avatarClient.unlink(p))}
|
|
138
|
+
>
|
|
139
|
+
Unlink
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
})}
|
|
145
|
+
|
|
146
|
+
{consent && (
|
|
147
|
+
<div style={S.stack}>
|
|
148
|
+
<ul style={S.list}>
|
|
149
|
+
{CONSENT[consent].points.map(t => (
|
|
150
|
+
<li key={t}>{t}</li>
|
|
151
|
+
))}
|
|
152
|
+
</ul>
|
|
153
|
+
<span style={S.line}>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className="settings-popout__btn settings-popout__btn--primary"
|
|
157
|
+
disabled={busy}
|
|
158
|
+
onClick={() => link(consent)}
|
|
159
|
+
>
|
|
160
|
+
{CONSENT[consent].go}
|
|
161
|
+
</button>
|
|
162
|
+
<button type="button" className="settings-popout__btn" onClick={() => setConsent(null)}>
|
|
163
|
+
Cancel
|
|
164
|
+
</button>
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{error && (
|
|
170
|
+
<span className="settings-popout__error" role="alert">
|
|
171
|
+
{error}
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|