Skip to content

Commit 3581326

Browse files
feat: implement notification response handling (#156)
### TL;DR Added notification response functionality allowing users to accept/reject friend requests and habit invites. https://github.com/user-attachments/assets/20296030-4207-4d49-a63d-8d4b9666d7f2 ### What changed? - Created `useRespondToNotification` mutation hook to handle notification responses - Added empty state message when no notifications exist - Implemented notification confirmation and deletion in the NotificationCard component - Added mock data for habit completions to support testing ### How to test? 1. Navigate to the Notifications tab 2. Try accepting a friend request - verify the friendship is established 3. Try accepting a habit invite - verify you're added as a participant 4. Try rejecting any notification - verify it's removed from the list 5. Clear all notifications - verify the empty state message appears
1 parent 9097a7a commit 3581326

File tree

4 files changed

+216
-23
lines changed

4 files changed

+216
-23
lines changed

src/api/habits/mock-habits.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ export const mockHabitCompletions: Record<HabitIdT, AllCompletionsT> = {
201201
},
202202
},
203203
},
204+
['4' as HabitIdT]: {
205+
['2' as UserIdT]: {
206+
entries: {
207+
'2024-12-13': { numberOfCompletions: 1 },
208+
'2024-12-14': { numberOfCompletions: 1 },
209+
'2024-12-15': { numberOfCompletions: 1, note: 'Very peaceful session' },
210+
},
211+
},
212+
['4' as UserIdT]: {
213+
entries: {
214+
'2024-12-12': { numberOfCompletions: 1 },
215+
'2024-12-13': { numberOfCompletions: 1 },
216+
'2024-12-14': { numberOfCompletions: 1, note: 'Feeling zen' },
217+
},
218+
},
219+
},
204220
};
205221
export const setMockHabitCompletions = (
206222
newCompletions: Record<HabitIdT, AllCompletionsT>,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/* eslint-disable max-lines-per-function */
2+
import { showMessage } from 'react-native-flash-message';
3+
import { createMutation } from 'react-query-kit';
4+
5+
import { addTestDelay, queryClient } from '../common';
6+
import {
7+
mockHabitCompletions,
8+
mockHabits,
9+
setMockHabits,
10+
} from '../habits/mock-habits';
11+
import { mockRelationships } from '../users/mock-users';
12+
import { mockNotifications, setMockNotifications } from './mock-notifications';
13+
import { type NotificationT } from './types';
14+
15+
type Response = void;
16+
type Variables = {
17+
notification: NotificationT;
18+
response: 'confirm' | 'delete';
19+
};
20+
type Context = {
21+
previousNotifications: NotificationT[] | undefined;
22+
};
23+
24+
export const useRespondToNotification = createMutation<
25+
Response,
26+
Variables,
27+
Error,
28+
Context
29+
>({
30+
mutationFn: async ({ notification, response }) => {
31+
if (response === 'confirm') {
32+
if (notification.type === 'friendRequest') {
33+
// Add to relationships
34+
const senderId = notification.senderId;
35+
const receiverId = notification.receiverId;
36+
37+
if (!mockRelationships[senderId]) {
38+
mockRelationships[senderId] = {};
39+
}
40+
if (!mockRelationships[receiverId]) {
41+
mockRelationships[receiverId] = {};
42+
}
43+
44+
mockRelationships[senderId][receiverId] = {
45+
status: 'friends',
46+
friendsSince: new Date(),
47+
};
48+
mockRelationships[receiverId][senderId] = {
49+
status: 'friends',
50+
friendsSince: new Date(),
51+
};
52+
} else if (notification.type === 'habitInvite') {
53+
// Add user to habit participants
54+
const habitToUpdate = mockHabits.find(
55+
(h) => h.id === notification.habitId,
56+
);
57+
if (habitToUpdate) {
58+
const updatedHabit = {
59+
...habitToUpdate,
60+
data: {
61+
...habitToUpdate.data,
62+
participants: {
63+
...habitToUpdate.data.participants,
64+
[notification.receiverId]: {
65+
displayName: 'John Doe', // In a real app, get from user profile
66+
username: 'john_doe',
67+
lastActivity: new Date(),
68+
isOwner: false,
69+
},
70+
},
71+
},
72+
};
73+
74+
setMockHabits(
75+
mockHabits.map((h) =>
76+
h.id === notification.habitId ? updatedHabit : h,
77+
),
78+
);
79+
80+
// Initialize empty completions for the new participant
81+
if (!mockHabitCompletions[notification.habitId]) {
82+
mockHabitCompletions[notification.habitId] = {};
83+
}
84+
mockHabitCompletions[notification.habitId][notification.receiverId] =
85+
{
86+
entries: {},
87+
};
88+
}
89+
}
90+
}
91+
92+
// Remove notification
93+
const updatedNotifications = mockNotifications.filter((n) => {
94+
if (n.type !== notification.type) return true;
95+
if (n.senderId !== notification.senderId) return true;
96+
if (n.receiverId !== notification.receiverId) return true;
97+
98+
// Additional check for habit-related notifications
99+
if (
100+
(n.type === 'habitInvite' || n.type === 'nudge') &&
101+
(notification.type === 'habitInvite' || notification.type === 'nudge')
102+
) {
103+
return n.habitId !== notification.habitId;
104+
}
105+
106+
return false;
107+
});
108+
109+
setMockNotifications(updatedNotifications);
110+
111+
await addTestDelay(undefined);
112+
},
113+
onMutate: async ({ notification }) => {
114+
// Cancel any outgoing refetches so they don't overwrite our optimistic update
115+
await queryClient.cancelQueries({ queryKey: ['notifications'] });
116+
117+
// Snapshot the previous value
118+
const previousNotifications = queryClient.getQueryData<NotificationT[]>([
119+
'notifications',
120+
]);
121+
122+
// Optimistically update the cache
123+
queryClient.setQueryData<NotificationT[]>(['notifications'], (old) => {
124+
if (!old) return [];
125+
return old.filter((n) => {
126+
if (n.type !== notification.type) return true;
127+
if (n.senderId !== notification.senderId) return true;
128+
if (n.receiverId !== notification.receiverId) return true;
129+
if (
130+
(n.type === 'habitInvite' || n.type === 'nudge') &&
131+
(notification.type === 'habitInvite' || notification.type === 'nudge')
132+
) {
133+
return n.habitId !== notification.habitId;
134+
}
135+
return false;
136+
});
137+
});
138+
139+
return { previousNotifications };
140+
},
141+
onSuccess: (_, { notification, response }) => {
142+
queryClient.invalidateQueries({ queryKey: ['notifications'] });
143+
if (response === 'confirm') {
144+
if (notification.type === 'friendRequest') {
145+
queryClient.invalidateQueries({ queryKey: ['friends'] });
146+
} else if (notification.type === 'habitInvite') {
147+
queryClient.invalidateQueries({ queryKey: ['habits'] });
148+
}
149+
}
150+
},
151+
onError: (err, variables, context) => {
152+
// Rollback optimistic update
153+
if (context?.previousNotifications) {
154+
queryClient.setQueryData(
155+
['notifications'],
156+
context.previousNotifications,
157+
);
158+
}
159+
showMessage({
160+
message: 'Failed to respond to notification',
161+
type: 'danger',
162+
duration: 2000,
163+
});
164+
},
165+
});

src/app/(tabs)/notifications.tsx

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,27 +73,33 @@ export default function Notifications() {
7373
<Header title="Notifications" />
7474
<ScrollView className="flex-1">
7575
<View className="flex flex-col gap-6">
76-
{sortedNotifications.map((notification) => {
77-
const userName = getUserName(notification.senderId);
78-
const habit =
79-
notification.type !== 'friendRequest'
80-
? getHabitData(notification.habitId)
81-
: undefined;
76+
{sortedNotifications.length === 0 ? (
77+
<Text className="text-center text-stone-500 dark:text-stone-400">
78+
You have no more notifications 🎉
79+
</Text>
80+
) : (
81+
sortedNotifications.map((notification) => {
82+
const userName = getUserName(notification.senderId);
83+
const habit =
84+
notification.type !== 'friendRequest'
85+
? getHabitData(notification.habitId)
86+
: undefined;
8287

83-
const isLoading =
84-
userName === null ||
85-
(notification.type !== 'friendRequest' && habit === null);
88+
const isLoading =
89+
userName === null ||
90+
(notification.type !== 'friendRequest' && habit === null);
8691

87-
return (
88-
<NotificationCard
89-
key={`${notification.type}-${notification.sentAt}`}
90-
notification={notification}
91-
userName={userName}
92-
habit={habit}
93-
isLoading={isLoading}
94-
/>
95-
);
96-
})}
92+
return (
93+
<NotificationCard
94+
key={`${notification.type}-${notification.sentAt}`}
95+
notification={notification}
96+
userName={userName}
97+
habit={habit}
98+
isLoading={isLoading}
99+
/>
100+
);
101+
})
102+
)}
97103
</View>
98104
</ScrollView>
99105
</ScreenContainer>

src/components/notification-card.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useColorScheme } from 'nativewind';
55
import React from 'react';
66

77
import { type HabitT, type NotificationT, useUser } from '@/api';
8+
import { useRespondToNotification } from '@/api/notifications/use-respond-to-notification';
89
import { HabitIcon } from '@/components/habit-icon';
910
import HabitInfoCard from '@/components/habit-info-card';
1011
import UserPicture from '@/components/picture';
@@ -107,15 +108,20 @@ export function NotificationCard({
107108
isLoading,
108109
}: NotificationCardProps) {
109110
const modal = useModal();
111+
const { mutate: respondToNotification } = useRespondToNotification();
110112

111113
const handleConfirm = () => {
112-
// TODO: Implement confirmation logic
113-
console.log('Confirmed');
114+
respondToNotification({
115+
notification,
116+
response: 'confirm',
117+
});
114118
};
115119

116120
const handleDelete = () => {
117-
// TODO: Implement deletion logic
118-
console.log('Deleted');
121+
respondToNotification({
122+
notification,
123+
response: 'delete',
124+
});
119125
};
120126

121127
return (

0 commit comments

Comments
 (0)