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:
1 parent
0a0ff90745
commit
66230a8695
4 files changed
+182
-1
No files matched your search
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user