feat: Gravatar opt-in (off by default), configurable in Settings
Previously every account without a custom avatar automatically got its email MD5-hashed and sent to gravatar.com at signup, with no way to turn it off. Adds users.useGravatar (default false): removed the automatic signup-time lookup entirely, and "remove photo" now falls back to the initials placeholder instead of silently re-deriving a Gravatar URL. New toggle in Settings -> Profile, off by default, description explains the MD5-hash-to-third-party tradeoff. Existing accounts' current avatarUrl is left untouched either way — no retroactive avatar changes for anyone already using one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ type UserProps = {
|
||||
isPrivate: boolean;
|
||||
hasCustomAvatar: boolean;
|
||||
username: string | null;
|
||||
useGravatar: boolean;
|
||||
};
|
||||
|
||||
export function SettingsForm({ user }: { user: UserProps }) {
|
||||
@@ -43,6 +44,8 @@ export function SettingsForm({ user }: { user: UserProps }) {
|
||||
const [username, setUsername] = useState(user.username ?? "");
|
||||
const [savingUsername, setSavingUsername] = useState(false);
|
||||
const [usernameError, setUsernameError] = useState<string | null>(null);
|
||||
const [useGravatar, setUseGravatar] = useState(user.useGravatar);
|
||||
const [savingGravatar, setSavingGravatar] = useState(false);
|
||||
|
||||
async function saveProfile() {
|
||||
setSaving(true);
|
||||
@@ -102,6 +105,36 @@ export function SettingsForm({ user }: { user: UserProps }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUseGravatar(checked: boolean) {
|
||||
setSavingGravatar(true);
|
||||
const previous = useGravatar;
|
||||
setUseGravatar(checked);
|
||||
try {
|
||||
const res = await fetch("/api/v1/users/me", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ useGravatar: checked }),
|
||||
});
|
||||
if (res.ok) {
|
||||
// Only affects the displayed avatar for accounts without a custom
|
||||
// upload — matches the server's own condition in api/v1/users/me.
|
||||
if (!hasCustomAvatar) {
|
||||
const data = await res.json() as { avatarUrl?: string | null };
|
||||
setAvatarImage(data.avatarUrl ?? null);
|
||||
}
|
||||
toast.success(t_common("saved"));
|
||||
} else {
|
||||
setUseGravatar(previous);
|
||||
toast.error(t_common("saveFailed"));
|
||||
}
|
||||
} catch {
|
||||
setUseGravatar(previous);
|
||||
toast.error(t_common("saveFailed"));
|
||||
} finally {
|
||||
setSavingGravatar(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function savePrivacy(checked: boolean) {
|
||||
setSavingPrivacy(true);
|
||||
const previous = isPrivate;
|
||||
@@ -138,6 +171,18 @@ export function SettingsForm({ user }: { user: UserProps }) {
|
||||
setHasCustomAvatar(custom);
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{t("useGravatar")}</p>
|
||||
<p className="text-xs text-muted-foreground max-w-prose">{t("useGravatarDescription")}</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="use-gravatar"
|
||||
checked={useGravatar}
|
||||
disabled={savingGravatar}
|
||||
onCheckedChange={(checked) => { void saveUseGravatar(checked); }}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("displayName")}</Label>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
|
||||
Reference in New Issue
Block a user