Skip to content

Commit

Permalink
feat: respond to notification functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
owengretzinger committed Jan 27, 2025
1 parent 3e713eb commit adb931f
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 23 deletions.
16 changes: 16 additions & 0 deletions src/api/habits/mock-habits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ export const mockHabitCompletions: Record<HabitIdT, AllCompletionsT> = {
},
},
},
['4' as HabitIdT]: {
['2' as UserIdT]: {
entries: {
'2024-12-13': { numberOfCompletions: 1 },
'2024-12-14': { numberOfCompletions: 1 },
'2024-12-15': { numberOfCompletions: 1, note: 'Very peaceful session' },
},
},
['4' as UserIdT]: {
entries: {
'2024-12-12': { numberOfCompletions: 1 },
'2024-12-13': { numberOfCompletions: 1 },
'2024-12-14': { numberOfCompletions: 1, note: 'Feeling zen' },
},
},
},
};
export const setMockHabitCompletions = (
newCompletions: Record<HabitIdT, AllCompletionsT>,
Expand Down
165 changes: 165 additions & 0 deletions src/api/notifications/use-respond-to-notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/* eslint-disable max-lines-per-function */
import { showMessage } from 'react-native-flash-message';
import { createMutation } from 'react-query-kit';

import { addTestDelay, queryClient } from '../common';
import {
mockHabitCompletions,
mockHabits,
setMockHabits,
} from '../habits/mock-habits';
import { mockRelationships } from '../users/mock-users';
import { mockNotifications, setMockNotifications } from './mock-notifications';
import { type NotificationT } from './types';

type Response = void;
type Variables = {
notification: NotificationT;
response: 'confirm' | 'delete';
};
type Context = {
previousNotifications: NotificationT[] | undefined;
};

export const useRespondToNotification = createMutation<
Response,
Variables,
Error,
Context
>({
mutationFn: async ({ notification, response }) => {
if (response === 'confirm') {
if (notification.type === 'friendRequest') {
// Add to relationships
const senderId = notification.senderId;
const receiverId = notification.receiverId;

if (!mockRelationships[senderId]) {
mockRelationships[senderId] = {};
}
if (!mockRelationships[receiverId]) {
mockRelationships[receiverId] = {};
}

mockRelationships[senderId][receiverId] = {
status: 'friends',
friendsSince: new Date(),
};
mockRelationships[receiverId][senderId] = {
status: 'friends',
friendsSince: new Date(),
};
} else if (notification.type === 'habitInvite') {
// Add user to habit participants
const habitToUpdate = mockHabits.find(
(h) => h.id === notification.habitId,
);
if (habitToUpdate) {
const updatedHabit = {
...habitToUpdate,
data: {
...habitToUpdate.data,
participants: {
...habitToUpdate.data.participants,
[notification.receiverId]: {
displayName: 'John Doe', // In a real app, get from user profile
username: 'john_doe',
lastActivity: new Date(),
isOwner: false,
},
},
},
};

setMockHabits(
mockHabits.map((h) =>
h.id === notification.habitId ? updatedHabit : h,
),
);

// Initialize empty completions for the new participant
if (!mockHabitCompletions[notification.habitId]) {
mockHabitCompletions[notification.habitId] = {};
}
mockHabitCompletions[notification.habitId][notification.receiverId] =
{
entries: {},
};
}
}
}

// Remove notification
const updatedNotifications = mockNotifications.filter((n) => {
if (n.type !== notification.type) return true;
if (n.senderId !== notification.senderId) return true;
if (n.receiverId !== notification.receiverId) return true;

// Additional check for habit-related notifications
if (
(n.type === 'habitInvite' || n.type === 'nudge') &&
(notification.type === 'habitInvite' || notification.type === 'nudge')
) {
return n.habitId !== notification.habitId;
}

return false;
});

setMockNotifications(updatedNotifications);

await addTestDelay(undefined);
},
onMutate: async ({ notification }) => {
// Cancel any outgoing refetches so they don't overwrite our optimistic update
await queryClient.cancelQueries({ queryKey: ['notifications'] });

// Snapshot the previous value
const previousNotifications = queryClient.getQueryData<NotificationT[]>([
'notifications',
]);

// Optimistically update the cache
queryClient.setQueryData<NotificationT[]>(['notifications'], (old) => {
if (!old) return [];
return old.filter((n) => {
if (n.type !== notification.type) return true;
if (n.senderId !== notification.senderId) return true;
if (n.receiverId !== notification.receiverId) return true;
if (
(n.type === 'habitInvite' || n.type === 'nudge') &&
(notification.type === 'habitInvite' || notification.type === 'nudge')
) {
return n.habitId !== notification.habitId;
}
return false;
});
});

return { previousNotifications };
},
onSuccess: (_, { notification, response }) => {
queryClient.invalidateQueries({ queryKey: ['notifications'] });
if (response === 'confirm') {
if (notification.type === 'friendRequest') {
queryClient.invalidateQueries({ queryKey: ['friends'] });
} else if (notification.type === 'habitInvite') {
queryClient.invalidateQueries({ queryKey: ['habits'] });
}
}
},
onError: (err, variables, context) => {
// Rollback optimistic update
if (context?.previousNotifications) {
queryClient.setQueryData(
['notifications'],
context.previousNotifications,
);
}
showMessage({
message: 'Failed to respond to notification',
type: 'danger',
duration: 2000,
});
},
});
44 changes: 25 additions & 19 deletions src/app/(tabs)/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,27 +73,33 @@ export default function Notifications() {
<Header title="Notifications" />
<ScrollView className="flex-1">
<View className="flex flex-col gap-6">
{sortedNotifications.map((notification) => {
const userName = getUserName(notification.senderId);
const habit =
notification.type !== 'friendRequest'
? getHabitData(notification.habitId)
: undefined;
{sortedNotifications.length === 0 ? (
<Text className="text-center text-stone-500 dark:text-stone-400">
You have no more notifications 🎉
</Text>
) : (
sortedNotifications.map((notification) => {
const userName = getUserName(notification.senderId);
const habit =
notification.type !== 'friendRequest'
? getHabitData(notification.habitId)
: undefined;

const isLoading =
userName === null ||
(notification.type !== 'friendRequest' && habit === null);
const isLoading =
userName === null ||
(notification.type !== 'friendRequest' && habit === null);

return (
<NotificationCard
key={`${notification.type}-${notification.sentAt}`}
notification={notification}
userName={userName}
habit={habit}
isLoading={isLoading}
/>
);
})}
return (
<NotificationCard
key={`${notification.type}-${notification.sentAt}`}
notification={notification}
userName={userName}
habit={habit}
isLoading={isLoading}
/>
);
})
)}
</View>
</ScrollView>
</ScreenContainer>
Expand Down
14 changes: 10 additions & 4 deletions src/components/notification-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useColorScheme } from 'nativewind';
import React from 'react';

import { type HabitT, type NotificationT, useUser } from '@/api';
import { useRespondToNotification } from '@/api/notifications/use-respond-to-notification';
import { HabitIcon } from '@/components/habit-icon';
import HabitInfoCard from '@/components/habit-info-card';
import UserPicture from '@/components/picture';
Expand Down Expand Up @@ -107,15 +108,20 @@ export function NotificationCard({
isLoading,
}: NotificationCardProps) {
const modal = useModal();
const { mutate: respondToNotification } = useRespondToNotification();

const handleConfirm = () => {
// TODO: Implement confirmation logic
console.log('Confirmed');
respondToNotification({
notification,
response: 'confirm',
});
};

const handleDelete = () => {
// TODO: Implement deletion logic
console.log('Deleted');
respondToNotification({
notification,
response: 'delete',
});
};

return (
Expand Down

0 comments on commit adb931f

Please sign in to comment.