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

alkem-io / client-web / #9540

06 Dec 2024 09:13AM UTC coverage: 5.947%. First build
#9540

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
    errorPolicy: 'ignore',
55
    skip: !isEnabled || !isOpen,
56
  });
57

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

183
  return {
184
    notificationsInApp,
185
    unreadCount,
186
    isLoading: loading,
187
    updateNotificationState,
188
    markNotificationsAsRead,
189
    fetchMore: fetchMoreNotifications,
190
    hasMore,
191
    error,
192
  };
193
};
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

© 2026 Coveralls, Inc