learning_ai_notes/mobile/src/app/(tabs)/inbox.tsx

216 lines
6.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store';
import { buttonA11y, textInputA11y, dynamicType } from '../../lib/accessibility';
import { colors } from '../../theme';
export default function InboxScreen() {
const approvals = useInboxStore((state: InboxState) => state.approvals);
const activity = useInboxStore((state: InboxState) => state.activity);
const isLoading = useInboxStore((state: InboxState) => state.isLoading);
const hydrate = useInboxStore((state: InboxState) => state.hydrate);
const approve = useInboxStore((state: InboxState) => state.approve);
const reject = useInboxStore((state: InboxState) => state.reject);
const [pendingApprovalId, setPendingApprovalId] = useState<string | null>(null);
const [reviewNote, setReviewNote] = useState('');
useEffect(() => {
void hydrate();
}, [hydrate]);
return (
<View style={styles.container}>
<Text style={styles.title}>Inbox</Text>
<Text style={styles.body}>Review simple agent actions and approve or reject them from mobile.</Text>
{isLoading ? <Text style={styles.empty}>Loading approvals</Text> : null}
{approvals.length > 0 ? (
<TextInput
{...textInputA11y('Review note')}
style={styles.reviewNoteInput}
placeholder="Optional review note…"
placeholderTextColor={colors.textSecondary}
value={reviewNote}
onChangeText={setReviewNote}
multiline
numberOfLines={2}
/>
) : null}
{approvals.map((item: ApprovalItem) => (
<View key={item.id} style={styles.card}>
<Text style={styles.cardTitle}>{item.title}</Text>
<Text style={styles.cardBody}>{item.summary}</Text>
<Text style={styles.status}>Status: {item.status}</Text>
<View style={styles.actionRow}>
<Pressable
{...buttonA11y(`Approve ${item.title}`, { disabled: pendingApprovalId === item.id || item.status !== 'pending' })}
style={[
styles.approveButton,
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
]}
disabled={pendingApprovalId === item.id || item.status !== 'pending'}
onPress={async () => {
setPendingApprovalId(item.id);
try {
const note = reviewNote.trim() || undefined;
await approve(item.id, note);
setReviewNote('');
} finally {
setPendingApprovalId(null);
}
}}
>
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.approveText}>
{pendingApprovalId === item.id ? 'Approving…' : 'Approve'}
</Text>
</Pressable>
<Pressable
{...buttonA11y(`Reject ${item.title}`, { disabled: pendingApprovalId === item.id || item.status !== 'pending' })}
style={[
styles.rejectButton,
pendingApprovalId === item.id || item.status !== 'pending' ? styles.buttonDisabled : null,
]}
disabled={pendingApprovalId === item.id || item.status !== 'pending'}
onPress={async () => {
setPendingApprovalId(item.id);
try {
const note = reviewNote.trim() || undefined;
await reject(item.id, note);
setReviewNote('');
} finally {
setPendingApprovalId(null);
}
}}
>
<Text maxFontSizeMultiplier={dynamicType.control} style={styles.rejectText}>
{pendingApprovalId === item.id ? 'Rejecting…' : 'Reject'}
</Text>
</Pressable>
</View>
</View>
))}
{!isLoading && approvals.length === 0 ? (
<Text style={styles.empty}>No approval items are waiting right now.</Text>
) : null}
<View style={styles.feedSection}>
<Text style={styles.feedTitle}>Recent activity</Text>
{activity.map((item: ActivityItem) => (
<View key={item.id} style={styles.feedCard}>
<Text style={styles.feedKind}>{item.kind}</Text>
<Text style={styles.cardTitle}>{item.title}</Text>
<Text style={styles.cardBody}>{item.summary}</Text>
</View>
))}
{!isLoading && activity.length === 0 ? (
<Text style={styles.empty}>No recent agent activity yet.</Text>
) : null}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: colors.bgCanvas,
gap: 14,
},
title: {
color: colors.textPrimary,
fontSize: 28,
fontWeight: '700',
},
body: {
color: colors.textSecondary,
fontSize: 15,
lineHeight: 21,
},
empty: {
color: colors.textSecondary,
fontSize: 14,
},
card: {
backgroundColor: colors.surfaceCard,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.borderDefault,
padding: 16,
gap: 8,
},
cardTitle: {
color: colors.textPrimary,
fontSize: 16,
fontWeight: '700',
},
cardBody: {
color: colors.textSecondary,
fontSize: 14,
},
status: {
color: colors.textSecondary,
fontSize: 13,
textTransform: 'capitalize',
},
actionRow: {
flexDirection: 'row',
gap: 10,
},
buttonDisabled: {
opacity: 0.6,
},
approveButton: {
backgroundColor: colors.accentPrimary,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
approveText: {
color: colors.textPrimary,
fontWeight: '700',
},
rejectButton: {
borderWidth: 1,
borderColor: colors.borderDefault,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
rejectText: {
color: colors.textSecondary,
fontWeight: '600',
},
feedSection: {
gap: 10,
},
feedTitle: {
color: colors.textPrimary,
fontSize: 18,
fontWeight: '700',
},
feedCard: {
backgroundColor: colors.bgElevated,
borderRadius: 14,
borderWidth: 1,
borderColor: colors.borderDefault,
padding: 14,
gap: 6,
},
feedKind: {
color: colors.accentSecondary,
fontSize: 12,
fontWeight: '700',
textTransform: 'uppercase',
},
reviewNoteInput: {
backgroundColor: colors.bgElevated,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.borderDefault,
color: colors.textPrimary,
padding: 12,
fontSize: 14,
minHeight: 48,
textAlignVertical: 'top' as const,
},
});