• 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/views/InAppNotificationBaseView.tsx
1
import { useCallback } from 'react';
2
import { Trans, useTranslation } from 'react-i18next';
3
import {
4
  Badge,
5
  Box,
6
  Divider,
7
  ListItemButton,
8
  ListItemButtonProps,
9
  ListItemButtonTypeMap,
10
  Typography,
11
} from '@mui/material';
12
import MarkEmailUnreadOutlinedIcon from '@mui/icons-material/MarkEmailUnreadOutlined';
13
import DraftsOutlinedIcon from '@mui/icons-material/DraftsOutlined';
14
import { DeleteOutline } from '@mui/icons-material';
15
import RouterLink, { RouterLinkProps } from '@/core/ui/link/RouterLink';
16
import BadgeCardView from '@/core/ui/list/BadgeCardView';
17
import Avatar from '@/core/ui/avatar/Avatar';
18
import Gutters from '@/core/ui/grid/Gutters';
19
import { Caption } from '@/core/ui/typography';
20
import { formatTimeElapsed } from '@/domain/shared/utils/formatTimeElapsed';
21
import { gutters } from '@/core/ui/grid/utils';
22
import ActionsMenu from '@/core/ui/card/ActionsMenu';
NEW
23
import MenuItemWithIcon from '@/core/ui/menu/MenuItemWithIcon';
×
24
import { NotificationEventInAppState, VisualType } from '@/core/apollo/generated/graphql-schema';
25
import { useInAppNotifications } from '../useInAppNotifications';
26
import { useInAppNotificationsContext } from '../InAppNotificationsContext';
27
import WrapperMarkdown from '@/core/ui/markdown/WrapperMarkdown';
28
import { getDefaultSpaceVisualUrl } from '@/domain/space/icons/defaultVisualUrls';
29
import { InAppNotificationModel } from '../model/InAppNotificationModel';
30
import { useScreenSize } from '@/core/ui/grid/constants';
31

32
const ACTIONS_WIDTH = 60;
33

34
interface InAppNotificationBaseViewProps {
35
  notification: InAppNotificationModel;
36
  values: Record<string, string | undefined>;
37
  url: string | undefined;
38
}
39

40
const Wrapper = <D extends React.ElementType = ListItemButtonTypeMap['defaultComponent'], P = {}>(
41
  props: ListItemButtonProps<D, P> & RouterLinkProps
42
) => <ListItemButton component={props.to ? RouterLink : Box} {...props} />;
NEW
43

×
44
const getSpaceAvatar = (space?: {
NEW
45
  id?: string;
×
46
  about?: { profile?: { cardBanner?: { uri?: string }; avatar?: { uri?: string } } };
NEW
47
}) => {
×
48
  return space?.about?.profile?.avatar?.uri || space?.about?.profile?.cardBanner?.uri || null;
49
};
50

51
export const InAppNotificationBaseView = ({ notification, values, url }: InAppNotificationBaseViewProps) => {
52
  const { id, state, triggeredAt, triggeredBy, payload, type } = notification;
53

54
  const { t } = useTranslation();
55
  const { updateNotificationState } = useInAppNotifications();
56
  const { setIsOpen } = useInAppNotificationsContext();
NEW
57
  const { isMediumSmallScreen: isMobile } = useScreenSize();
×
NEW
58

×
NEW
59
  const onNotificationClick = useCallback(() => {
×
60
    if (state === NotificationEventInAppState.Unread) {
NEW
61
      updateNotificationState(id, NotificationEventInAppState.Read);
×
NEW
62
    }
×
NEW
63
    if (url) {
×
64
      setIsOpen(false);
NEW
65
    }
×
66
  }, [id, url, state]);
67

NEW
68
  const getReadAction = useCallback(() => {
×
NEW
69
    switch (state) {
×
70
      case NotificationEventInAppState.Unread:
NEW
71
        return (
×
72
          <MenuItemWithIcon
73
            key={`${id}-mark-as-read`}
74
            iconComponent={DraftsOutlinedIcon}
NEW
75
            onClick={() => updateNotificationState(id, NotificationEventInAppState.Read)}
×
76
          >
77
            {t('components.inAppNotifications.action.read')}
78
          </MenuItemWithIcon>
79
        );
80
      case NotificationEventInAppState.Read:
NEW
81
        return (
×
82
          <MenuItemWithIcon
83
            key={`${id}-mark-as-unread`}
84
            iconComponent={MarkEmailUnreadOutlinedIcon}
NEW
85
            onClick={() => updateNotificationState(id, NotificationEventInAppState.Unread)}
×
86
          >
87
            {t('components.inAppNotifications.action.unread')}
88
          </MenuItemWithIcon>
89
        );
90
      default:
NEW
91
        return null;
×
92
    }
93
  }, [id, state, updateNotificationState]);
94

NEW
95
  const renderActions = useCallback(
×
NEW
96
    () => [
×
97
      getReadAction(),
98
      <MenuItemWithIcon
99
        key={`${id}-delete`}
100
        iconComponent={DeleteOutline}
NEW
101
        onClick={() => updateNotificationState(id, NotificationEventInAppState.Archived)}
×
102
      >
103
        {t('components.inAppNotifications.action.delete')}
104
      </MenuItemWithIcon>,
105
    ],
106
    [getReadAction, id, updateNotificationState, t]
107
  );
108

NEW
109
  const renderFormattedTranslation = useCallback(
×
110
    (key: string) => {
NEW
111
      return (
×
112
        <Trans
113
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
          i18nKey={key as any}
115
          values={values}
116
          components={{
117
            b: <strong />,
118
            br: <br />,
119
            pre: <pre />,
120
            i: <em />,
121
          }}
122
        />
123
      );
124
    },
125
    [values]
126
  );
127

NEW
128
  const renderComments = useCallback(() => {
×
NEW
129
    if (values.comment) {
×
NEW
130
      return (
×
131
        <WrapperMarkdown
132
          plain
NEW
133
          disableParagraphPadding
×
134
          caption
135
          sx={{
NEW
136
            display: '-webkit-box',
×
NEW
137
            WebkitLineClamp: 2,
×
NEW
138
            WebkitBoxOrient: 'vertical',
×
139
            overflow: 'hidden',
140
            textOverflow: 'ellipsis',
141
            wordBreak: 'break-word',
142
            ...(isMobile && {
143
              WebkitLineClamp: 1,
144
            }),
NEW
145
          }}
×
146
        >
147
          {values.comment}
NEW
148
        </WrapperMarkdown>
×
149
      );
NEW
150
    }
×
151

152
    return null;
153
  }, [values, isMobile]);
154

155
  const isUnread = state === NotificationEventInAppState.Unread;
156

157
  return (
158
    <>
159
      <BadgeCardView
160
        component={Wrapper}
161
        to={url}
162
        onClick={onNotificationClick}
163
        paddingLeft={isMobile ? gutters(0.5) : gutters(2)}
164
        paddingY={gutters(0.5)}
165
        sx={{
×
166
          borderRadius: 0,
167
        }}
×
168
        visual={
169
          <Badge
170
            overlap="circular"
171
            anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
172
            badgeContent={triggeredBy ? <Avatar size="small" src={triggeredBy?.profile?.visual?.uri} /> : null}
173
          >
174
            <Avatar
175
              size="regular"
176
              src={getSpaceAvatar(payload.space) || getDefaultSpaceVisualUrl(VisualType.Avatar, payload.space?.id)}
177
              alt={t('common.avatar')}
178
            />
179
          </Badge>
180
        }
×
181
      >
182
        <Gutters row disablePadding>
183
          <Gutters
184
            flexGrow={1}
×
185
            disablePadding
186
            disableGap
187
            justifyContent={'center'}
188
            sx={{ maxWidth: `calc(100% - ${ACTIONS_WIDTH}px)` }}
189
          >
190
            <Typography
191
              variant="h4"
192
              color="primary"
193
              sx={{
194
                display: 'flex',
195
                flexDirection: 'row',
196
                alignItems: 'flex-start',
197
                fontWeight: isUnread ? 'bold' : 'normal',
198
                marginBottom: gutters(0.5),
199
              }}
200
            >
201
              {isUnread && (
202
                <Box
203
                  sx={{
204
                    width: '10px',
205
                    minWidth: '10px',
206
                    height: '10px',
207
                    background: '#09BCD4',
208
                    borderRadius: '50%',
209
                    marginRight: gutters(0.5),
210
                    marginTop: '6px',
211
                    flexShrink: 0,
212
                  }}
213
                />
214
              )}
215
              <Box
216
                component="span"
217
                sx={{
218
                  flex: 1,
219
                  ...(isMobile && {
220
                    display: '-webkit-box',
221
                    WebkitLineClamp: 2,
222
                    WebkitBoxOrient: 'vertical',
223
                    overflow: 'hidden',
224
                    textOverflow: 'ellipsis',
225
                    wordBreak: 'break-word',
226
                  }),
227
                }}
228
              >
229
                {renderFormattedTranslation(`components.inAppNotifications.type.${type}.subject`)}
230
              </Box>
231
            </Typography>
232
            <Typography
233
              variant="body2"
234
              color="neutral.light"
235
              sx={{
236
                ...(isMobile && {
237
                  display: '-webkit-box',
238
                  WebkitLineClamp: 1,
239
                  WebkitBoxOrient: 'vertical',
240
                  overflow: 'hidden',
241
                  textOverflow: 'ellipsis',
242
                  wordBreak: 'break-word',
243
                }),
244
              }}
245
            >
246
              {renderFormattedTranslation(`components.inAppNotifications.type.${type}.description`)}
247
            </Typography>
248
            {renderComments()}
249
          </Gutters>
250
          <Gutters disablePadding alignItems={'center'}>
251
            <ActionsMenu>{renderActions()}</ActionsMenu>
252
            <Caption>{formatTimeElapsed(triggeredAt, t)}</Caption>
253
          </Gutters>
254
        </Gutters>
255
      </BadgeCardView>
256
      {!isMobile && (
257
        <Gutters row disablePadding disableGap display={'flex'} justifyContent={'center'}>
258
          <Divider sx={{ maxWidth: '300px', flex: 1 }} />
259
        </Gutters>
260
      )}
261
    </>
262
  );
263
};
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