feat(mobile): Privatsphaere-Einstellungen im Profil bearbeiten

Wer darf Nachrichten schreiben (dmPolicy) und wer darf die Follower sehen
(followerVisibility) waren nur im Web einstellbar. Neue PrivacySettings-
Komponente mit denselben Optionen/Texten wie im Web, speichert jede Auswahl
sofort ueber pets.update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-04 16:08:17 +02:00
1 parent 0a0ff90745
commit 66230a8695
4 files changed
+182 -1

No files matched your search

+10 -1
View File
@@ -11,6 +11,7 @@ import { TextField } from "../../../../src/components/TextField";
import { DateField } from "../../../../src/components/DateField";
import { Button } from "../../../../src/components/Button";
import { BreedPicker } from "../../../../src/components/pet/BreedPicker";
import { PrivacySettings } from "../../../../src/components/pet/PrivacySettings";
import { radii, getTypography, type ColorTokens, type Typography } from "../../../../src/theme/tokens";
import { displayDateToIso, isoToDisplayDate } from "../../../../src/lib/date-format";
import { useBottomSafePadding } from "../../../../src/hooks/useBottomSafePadding";
@@ -18,7 +19,8 @@ import { useBottomSafePadding } from "../../../../src/hooks/useBottomSafePadding
/**
* Edit form (name, nickname, breed, bio, adoption story, birthday, adoption
* date) — a subset of web's src/app/(app)/pets/[petId]/edit/page.tsx.
* DM-policy/follower-visibility/avatar upload aren't editable here yet.
* Privacy (DM policy, follower visibility) lives in PrivacySettings and saves
* on its own; avatar upload isn't editable here yet.
* Species itself stays fixed (only its breed is re-selectable) — found.md
* asked specifically for breed correction, not a full species change.
*/
@@ -168,6 +170,13 @@ export default function EditPetScreen() {
<Button title={t("common.save")} disabled={updateMutation.isPending} onPress={handleSave} />
<PrivacySettings
petId={pet.id}
petName={pet.name}
dmPolicy={pet.dmPolicy}
followerVisibility={pet.followerVisibility}
/>
<Pressable style={styles.deleteRow} onPress={confirmDelete} disabled={deleteMutation.isPending}>
<Trash2 color={colors.alertRed} size={18} />
<Text style={styles.deleteText}>{t("editPet.deletePet")}</Text>
@@ -0,0 +1,142 @@
import { useMemo } from "react";
import { View, Text, Pressable, Alert, StyleSheet } from "react-native";
import { useTranslation } from "react-i18next";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Check } from "lucide-react-native";
import { useTRPC } from "../../api/trpc";
import { useTheme } from "../../context/ThemeContext";
import { radii, getTypography, type ColorTokens, type Typography } from "../../theme/tokens";
type DmPolicy = "EVERYONE" | "FOLLOWERS_ONLY";
type FollowerVisibility = "EVERYONE" | "FOLLOWERS_ONLY" | "NOBODY";
type Props = {
petId: string;
petName: string;
dmPolicy: DmPolicy;
followerVisibility: FollowerVisibility;
};
/**
* Port of the "Privatsphäre" block from web's src/app/(app)/pets/[petId]/edit/page.tsx
* (found.md: only editable on web so far). Each choice is saved immediately via
* pets.update — no separate save button, so it can't get lost in the long form.
*/
export function PrivacySettings({ petId, petName, dmPolicy, followerVisibility }: Props) {
const { t } = useTranslation();
const trpc = useTRPC();
const queryClient = useQueryClient();
const { colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]);
const update = useMutation(
trpc.pets.update.mutationOptions({
onSuccess: () => queryClient.invalidateQueries({ queryKey: trpc.pets.byId.queryOptions({ petId }).queryKey }),
onError: () => Alert.alert(t("privacy.saveFailed")),
})
);
const dmOptions: { value: DmPolicy; label: string }[] = [
{ value: "EVERYONE", label: t("privacy.everyone") },
{ value: "FOLLOWERS_ONLY", label: t("privacy.onlyFollowedPets") },
];
const followerOptions: { value: FollowerVisibility; label: string }[] = [
{ value: "EVERYONE", label: t("privacy.everyone") },
{ value: "FOLLOWERS_ONLY", label: t("privacy.onlyMyFollowers") },
{ value: "NOBODY", label: t("privacy.onlyMe") },
];
const followerHint = {
EVERYONE: t("privacy.followerEveryoneHint", { name: petName }),
FOLLOWERS_ONLY: t("privacy.followerFollowersOnlyHint", { name: petName }),
NOBODY: t("privacy.followerNobodyHint", { name: petName }),
}[followerVisibility];
return (
<View style={styles.section}>
<Text style={styles.heading}>{t("privacy.heading")}</Text>
<Text style={styles.question}>{t("privacy.whoCanMessage", { name: petName })}</Text>
<OptionGroup
options={dmOptions}
value={dmPolicy}
disabled={update.isPending}
onChange={(value) => update.mutate({ petId, dmPolicy: value })}
styles={styles}
colors={colors}
/>
<Text style={styles.hint}>
{dmPolicy === "FOLLOWERS_ONLY" ? t("privacy.dmFollowersOnlyHint") : t("privacy.dmEveryoneHint")}
</Text>
<Text style={styles.question}>{t("privacy.whoCanSeeFollowers", { name: petName })}</Text>
<OptionGroup
options={followerOptions}
value={followerVisibility}
disabled={update.isPending}
onChange={(value) => update.mutate({ petId, followerVisibility: value })}
styles={styles}
colors={colors}
/>
<Text style={styles.hint}>{followerHint}</Text>
</View>
);
}
function OptionGroup<T extends string>({
options,
value,
disabled,
onChange,
styles,
colors,
}: {
options: { value: T; label: string }[];
value: T;
disabled: boolean;
onChange: (value: T) => void;
styles: ReturnType<typeof makeStyles>;
colors: ColorTokens;
}) {
return (
<View style={styles.group} accessibilityRole="radiogroup">
{options.map((option, index) => {
const isSelected = option.value === value;
return (
<Pressable
key={option.value}
style={[styles.option, index > 0 && styles.optionDivider]}
onPress={() => !isSelected && onChange(option.value)}
disabled={disabled}
accessibilityRole="radio"
accessibilityState={{ checked: isSelected, disabled }}
>
<Text style={[styles.optionText, isSelected && styles.optionTextSelected]}>{option.label}</Text>
{isSelected && <Check color={colors.marigoldOrange} size={18} />}
</Pressable>
);
})}
</View>
);
}
function makeStyles(colors: ColorTokens) {
const typography: Typography = getTypography(colors);
return StyleSheet.create({
section: { marginTop: 24, gap: 8 },
heading: { ...typography.title, fontSize: 16 },
question: { ...typography.body, fontFamily: typography.title.fontFamily, marginTop: 8 },
group: { borderWidth: 1, borderColor: colors.hairlineGray, borderRadius: radii.lg, overflow: "hidden" },
option: {
minHeight: 48,
paddingHorizontal: 14,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: colors.pureWhite,
},
optionDivider: { borderTopWidth: 1, borderTopColor: colors.hairlineGray },
optionText: { ...typography.body },
optionTextSelected: { fontFamily: typography.title.fontFamily, color: colors.marigoldOrange },
hint: { ...typography.label, textTransform: "none", letterSpacing: 0 },
});
}
+15
View File
@@ -438,5 +438,20 @@
"foundingMember": {
"label": "Gründungsmitglied",
"tooltip": "Gründungsmitglied – eines der ersten 250 Tiere auf PawFeed"
},
"privacy": {
"heading": "Privatsphäre",
"whoCanMessage": "Wer kann {{name}} Nachrichten schreiben?",
"everyone": "Alle",
"onlyFollowedPets": "Nur Tiere, denen ich folge",
"dmFollowersOnlyHint": "Nur Tiere, denen du folgst, können dir Direktnachrichten schreiben.",
"dmEveryoneHint": "Jedes Tier auf PawFeed kann dir Direktnachrichten schreiben.",
"whoCanSeeFollowers": "Wer darf die Follower von {{name}} sehen?",
"onlyMyFollowers": "Nur meine Follower",
"onlyMe": "Nur ich",
"followerEveryoneHint": "Jeder kann die Follower- und Folgt-Listen von {{name}} sehen.",
"followerFollowersOnlyHint": "Nur Tiere, die {{name}} folgen, können die Follower- und Folgt-Listen sehen.",
"followerNobodyHint": "Nur du kannst die Follower- und Folgt-Listen von {{name}} sehen.",
"saveFailed": "Privatsphäre-Einstellung konnte nicht gespeichert werden"
}
}
+15
View File
@@ -438,5 +438,20 @@
"foundingMember": {
"label": "Founding member",
"tooltip": "Founding member – one of the first 250 pets on PawFeed"
},
"privacy": {
"heading": "Privacy",
"whoCanMessage": "Who can message {{name}}?",
"everyone": "Everyone",
"onlyFollowedPets": "Only pets I follow",
"dmFollowersOnlyHint": "Only pets that you follow can send you direct messages.",
"dmEveryoneHint": "Any pet on PawFeed can send you direct messages.",
"whoCanSeeFollowers": "Who can see {{name}}'s followers?",
"onlyMyFollowers": "Only my followers",
"onlyMe": "Only me",
"followerEveryoneHint": "Anyone can see {{name}}'s followers and following lists.",
"followerFollowersOnlyHint": "Only pets that follow {{name}} can see the followers and following lists.",
"followerNobodyHint": "Only you can see {{name}}'s followers and following lists.",
"saveFailed": "Failed to save privacy setting"
}
}