• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / client-web / #9533

05 Dec 2024 02:27PM UTC coverage: 5.947%. First build
#9533

Pull #7254

travis-ci

Pull Request #7254: In-app Notifications v1 for Beta Testers

194 of 10628 branches covered (1.83%)

Branch coverage included in aggregate %.

40 of 177 new or added lines in 15 files covered. (22.6%)

1532 of 18394 relevant lines covered (8.33%)

0.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/inAppNotifications/useInAppNotifications.ts
1
import { NotificationEventInAppState, NotificationEvent } from '@/core/apollo/generated/graphql-schema';
2
import {
3
  useInAppNotificationsQuery,
4
  useInAppNotificationsUnreadCountQuery,
5
  useUpdateNotificationStateMutation,
6
  useMarkNotificationsAsReadMutation,
7
} from '@/core/apollo/generated/apollo-hooks';
8
import { useInAppNotificationsContext } from './InAppNotificationsContext';
9
import { ApolloCache } from '@apollo/client';
10
import { InAppNotificationModel } from './model/InAppNotificationModel';
11
import { mapInAppNotificationToModel } from './util/mapInAppNotificationToModel';
12
import { useMemo, useCallback, useEffect, useRef } from 'react';
13
import { TagCategoryValues, error as logError } from '@/core/logging/sentry/log';
14
import { getNotificationTypesForFilter } from './notificationFilters';
15

16
export const IN_APP_NOTIFICATIONS_PAGE_SIZE = 10;
17

NEW
18
// Update the cache as refetching all could be expensive
×
19
const updateNotificationsCache = (
20
  cache: ApolloCache<unknown>,
21
  ids: string[],
22
  newState: NotificationEventInAppState
23
) => {
24
  // Modify individual notification objects in the cache
25
  ids.forEach(id => {
26
    cache.modify({
27
      id: cache.identify({ __typename: 'InAppNotification', id }),
28
      fields: {
29
        state: () => newState,
30
      },
31
    });
32
  });
33
};
34

35
// The set of notification event types currently being retrieved
36
export const NOTIFICATION_EVENT_TYPES: NotificationEvent[] = [];
37

38
export const useInAppNotifications = () => {
39
  const { isEnabled, isOpen, selectedFilter } = useInAppNotificationsContext();
40
  const prevIsOpenRef = useRef(isOpen);
41
  const prevFilterRef = useRef(selectedFilter);
42

43
  const [updateState] = useUpdateNotificationStateMutation();
44
  const [markAsRead] = useMarkNotificationsAsReadMutation();
45

46
  // Get the notification types based on the selected filter
47
  const notificationTypes = useMemo(() => getNotificationTypesForFilter(selectedFilter), [selectedFilter]);
48

49
  const { data, loading, error, fetchMore, refetch } = useInAppNotificationsQuery({
50
    variables: {
51
      types: notificationTypes,
52
      first: IN_APP_NOTIFICATIONS_PAGE_SIZE,
53
    },
54
    skip: !isEnabled || !isOpen,
55
  });
56

57
  // Refetch notifications when the dialog is opened OR when the filter changes
58
  useEffect(() => {
59
    const filterChanged = prevFilterRef.current !== selectedFilter;
60
    const dialogOpened = isEnabled && isOpen && !prevIsOpenRef.current;
61

62
    if ((dialogOpened || filterChanged) && refetch) {
63
      refetch();
64
    }
65

66
    prevIsOpenRef.current = isOpen;
67
    prevFilterRef.current = selectedFilter;
68
  }, [isOpen, isEnabled, selectedFilter, refetch]);
69

70
  const { data: unreadCountData } = useInAppNotificationsUnreadCountQuery({
71
    skip: !isEnabled,
72
  });
73

74
  // Memoize the filtered and mapped notifications to avoid unnecessary re-processing
75
  const notificationsInApp = useMemo(() => {
76
    const notifications: InAppNotificationModel[] = [];
77

78
    for (const notificationData of data?.me?.notifications?.inAppNotifications ?? []) {
79
      if (notificationData.state === NotificationEventInAppState.Archived) {
80
        continue; // Skip archived notifications
81
      }
82

83
      const notification = mapInAppNotificationToModel(notificationData);
84
      if (notification) {
85
        notifications.push(notification);
86
      } else {
87
        logError('Broken InAppNotification', {
88
          category: TagCategoryValues.NOTIFICATIONS,
89
          label: `id=${notificationData?.id}, type=${notificationData?.type}`,
NEW
90
        });
×
NEW
91
      }
×
NEW
92
    }
×
93

NEW
94
    return notifications;
×
95
  }, [data?.me?.notifications?.inAppNotifications]);
NEW
96

×
97
  // Calculate total unread count from the dedicated query
98
  const unreadCount = useMemo(() => {
99
    return unreadCountData?.me?.notificationsUnreadCount ?? 0;
100
  }, [unreadCountData?.me?.notificationsUnreadCount]);
101

102
  // Check if there are more notifications to load
NEW
103
  const hasMore = useMemo(() => {
×
NEW
104
    return data?.me?.notifications?.pageInfo?.hasNextPage ?? false;
×
NEW
105
  }, [data?.me?.notifications?.pageInfo?.hasNextPage]);
×
106

107
  // Function to fetch more notifications
NEW
108
  const fetchMoreNotifications = useCallback(async () => {
×
109
    if (!hasMore || loading) {
110
      return;
NEW
111
    }
×
NEW
112

×
113
    try {
×
NEW
114
      await fetchMore({
×
NEW
115
        variables: {
×
116
          types: notificationTypes,
117
          first: IN_APP_NOTIFICATIONS_PAGE_SIZE,
118
          after: data?.me?.notifications?.pageInfo?.endCursor,
NEW
119
        },
×
NEW
120
        updateQuery: (prev, { fetchMoreResult }) => {
×
121
          if (!fetchMoreResult) return prev;
122

123
          return {
124
            ...prev,
125
            me: {
126
              ...prev.me,
127
              notifications: {
128
                ...fetchMoreResult.me.notifications,
NEW
129
                inAppNotifications: [
×
130
                  ...(prev.me?.notifications?.inAppNotifications ?? []),
131
                  ...(fetchMoreResult.me?.notifications?.inAppNotifications ?? []),
132
                ],
133
              },
134
            },
135
          };
136
        },
137
      });
138
    } catch (error) {
139
      console.error('Failed to fetch more notifications:', error);
140
    }
141
  }, [fetchMore, hasMore, loading, data?.me?.notifications?.pageInfo?.endCursor, notificationTypes]);
142

143
  const updateNotificationState = useCallback(
144
    async (id: string, status: NotificationEventInAppState) => {
145
      try {
146
        await updateState({
147
          variables: {
148
            ID: id,
149
            state: status,
150
          },
151
          update: (cache, result) => {
152
            if (result?.data?.updateNotificationState === status) {
153
              updateNotificationsCache(cache, [id], status);
154
            }
155
          },
156
        });
157
      } catch (error) {
158
        console.error('Failed to update notification state:', error);
159
      }
160
    },
161
    [updateState]
162
  );
163

164
  const markNotificationsAsRead = useCallback(async () => {
165
    try {
166
      await markAsRead({
167
        variables: {
168
          types: notificationTypes as NotificationEvent[],
169
        },
170
        update: (_, result) => {
171
          if (result?.data?.markNotificationsAsRead) {
172
            // far simpler than updating the cache manually
173
            refetch?.();
174
          }
175
        },
176
      });
177
    } catch (error) {
178
      console.error('Failed to mark notifications as read:', error);
179
    }
180
  }, [markAsRead, notificationTypes, refetch]);
181

182
  return {
183
    notificationsInApp,
184
    unreadCount,
185
    isLoading: loading,
186
    updateNotificationState,
187
    markNotificationsAsRead,
188
    fetchMore: fetchMoreNotifications,
189
    hasMore,
190
    error,
191
  };
192
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc