3d80ac2993
* mobile: add invitations, network creation, and settings (parity phase 1) Surface backend capabilities that already existed in the mobile API client but had no UI: - NetworkListScreen now lists pending invitations with an Accept action and a header "+" to create a network; empty state offers creation instead of pointing users to desktop. - New CreateNetworkSheet and use-invitations hooks (accept invite, create network) following the existing react-query patterns. - SettingsScreen replaces its placeholder with an email-notifications toggle (optimistic, mirrors desktop), app version, and sign out. - Wire the previously-unreachable Settings row into the Drawer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: network member management and avatars (parity phase 2) - New NetworkSettingsScreen (reachable from the stream-list header) lists members with admin remove, an invite-by-email sheet, and pending invitations with revoke — backed by new use-member-management hooks. - Avatars: add avatar_object_id to HumanSchema, uploadAvatar/deleteAvatar/ getAvatarDownloadUrl client methods (raw PUT via expo-file-system), a use-avatar-url hook, and image rendering in the shared Avatar component. AccountScreen gains a tap-to-change profile picture via expo-image-picker. - auth-store gains refreshUser to pick up avatar changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: task particles — view, edit, and compose (parity phase 3) Bring the task particle to parity with desktop's richer model: - Replace the thin `quest` schema with desktop's `task` model (ChecklistItem, TaskProperties: title/notes/checklist/assigned_to/done) in the discriminated union, the Firestore converter, and consumers (StreamCard, FallbackParticleView). - New TaskParticleView renders an editable card (round done checkbox, title, notes, checklist with add/toggle/edit/remove, assignee chips) persisting each edit to Firestore; an 8s dwell auto-advances and field focus suspends playback. Wired into StreamView's render switch. - Compose: a task button in the ComposeDock opens a TaskComposeSheet (createTaskParticle helper). Gated off in the new-stream flow, where a stream's first particle must be text or media. Note: particles are written client-side to Firestore, matching desktop; Orion's REST validator still only accepts `quest`, which is a pre-existing inconsistency to reconcile backend-side separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: paper and file particle views (parity phase 4) - Extract the shared markdown renderer/theme out of TextParticleView into a reusable MarkdownBody component (DRY). - PaperParticleView renders desktop-authored documents (title + markdown) with a length-based dwell. - FileParticleView shows name/size and a Download action that opens a signed URL via the OS. - Both wired into StreamView's render switch; FallbackParticleView is now a true catch-all for unknown/folder types only. Deferred (documented for a follow-up phase): composing papers/files from mobile, particle attachments + lightbox, and link previews in text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: billing & usage in network settings (parity phase 5) Surface the network plan, daily usage, and Stripe management — all backed by client methods that already existed. New use-billing hooks and a BillingSection (mirroring desktop): every member sees the plan + usage summary; admins get cadence selection + "Upgrade to Pro" (checkout) and "Manage subscription" (portal), opening Stripe in the system browser. Added to NetworkSettingsScreen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * mobile: let users dismiss the keyboard from a task card Focusing a task field opened the keyboard with no way out — it covered the card and the stream's tap-to-advance zones. Now: - A "Done" pill appears at the card's top-right while editing (reusing the existing `editing` flag) and calls Keyboard.dismiss(); the title row reserves space so the pill never overlaps a long title. - The card ScrollView gains keyboardDismissMode (interactive on iOS, on-drag on Android) so dragging the card also dismisses the keyboard. Dismissing blurs the active field, which flips `editing` off and resumes the dwell timer and tap navigation automatically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV * decrease clutter in stream-view * format * format * consolidate avatar --------- Co-authored-by: Claude <noreply@anthropic.com>
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import { useState } from 'react';
|
||
import { Pressable, Switch, Text, View } from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import Constants from 'expo-constants';
|
||
import { apiClient } from '@/api/client';
|
||
import { useAuthStore } from '@/stores/auth-store';
|
||
import { logError, toUserMessage } from '@/lib/errors';
|
||
import { toast } from 'sonner-native';
|
||
import type { RootStackScreenProps } from '@/navigation/types';
|
||
|
||
export function SettingsScreen({
|
||
navigation,
|
||
}: RootStackScreenProps<'Settings'>) {
|
||
const user = useAuthStore((s) => s.user);
|
||
const signOut = useAuthStore((s) => s.signOut);
|
||
const isSigningOut = useAuthStore((s) => s.isSigningOut);
|
||
const [emailNotifications, setEmailNotifications] = useState(
|
||
user?.email_notifications_enabled ?? true,
|
||
);
|
||
const version = Constants.expoConfig?.version ?? '—';
|
||
|
||
// Optimistic toggle — flip the local + auth-store state immediately, roll
|
||
// back on failure. Mirrors desktop's settings-page handler.
|
||
const handleToggleEmailNotifications = async (checked: boolean) => {
|
||
setEmailNotifications(checked);
|
||
useAuthStore.setState((state) => ({
|
||
user: state.user
|
||
? { ...state.user, email_notifications_enabled: checked }
|
||
: null,
|
||
}));
|
||
try {
|
||
await apiClient.updateSettings({ email_notifications_enabled: checked });
|
||
} catch (err) {
|
||
setEmailNotifications(!checked);
|
||
useAuthStore.setState((state) => ({
|
||
user: state.user
|
||
? { ...state.user, email_notifications_enabled: !checked }
|
||
: null,
|
||
}));
|
||
toast.error(toUserMessage(err));
|
||
logError(err, { scope: 'settings.emailNotifications' });
|
||
}
|
||
};
|
||
|
||
return (
|
||
<SafeAreaView className="flex-1 bg-background" edges={['top']}>
|
||
<View className="flex-row items-center px-3 py-3 border-b border-border">
|
||
<Pressable onPress={() => navigation.goBack()} className="px-2 py-1">
|
||
<Text className="text-foreground text-2xl">‹</Text>
|
||
</Pressable>
|
||
<Text className="flex-1 text-center text-foreground text-base font-semibold">
|
||
Settings
|
||
</Text>
|
||
<View className="w-8" />
|
||
</View>
|
||
|
||
<View className="px-4 py-4">
|
||
<SettingsGroup title="Notifications">
|
||
<View className="flex-row items-center justify-between px-4 py-3">
|
||
<Text className="text-foreground text-base">
|
||
Email notifications
|
||
</Text>
|
||
<Switch
|
||
value={emailNotifications}
|
||
onValueChange={handleToggleEmailNotifications}
|
||
/>
|
||
</View>
|
||
</SettingsGroup>
|
||
|
||
<SettingsGroup title="About">
|
||
<View className="flex-row items-center justify-between px-4 py-3">
|
||
<Text className="text-foreground text-base">Version</Text>
|
||
<Text className="text-muted-foreground text-base">{version}</Text>
|
||
</View>
|
||
</SettingsGroup>
|
||
|
||
<View className="mt-4">
|
||
<Pressable
|
||
onPress={() => void signOut()}
|
||
disabled={isSigningOut}
|
||
className="px-4 py-3 active:bg-accent rounded-xl"
|
||
>
|
||
<Text className="text-destructive text-base font-medium">
|
||
{isSigningOut ? 'Signing out…' : 'Sign out'}
|
||
</Text>
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
function SettingsGroup({
|
||
title,
|
||
children,
|
||
}: {
|
||
title: string;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<View className="mb-2">
|
||
<Text className="text-muted-foreground text-xs uppercase tracking-wide px-4 pt-4 pb-1">
|
||
{title}
|
||
</Text>
|
||
<View className="bg-muted/40 rounded-xl overflow-hidden">{children}</View>
|
||
</View>
|
||
);
|
||
}
|