Skip to content

Commit f0117e3

Browse files
authored
Merge pull request #433 from WalletConnect/feat/add-skeleton-for-notifications-loading
feat: add skeleton for notifications loading
2 parents 4a2251d + c267b99 commit f0117e3

File tree

5 files changed

+193
-19
lines changed

5 files changed

+193
-19
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { forwardRef } from 'react'
2+
3+
import cn from 'classnames'
4+
import { LazyMotion, domMax } from 'framer-motion'
5+
6+
import './AppNotifications.scss'
7+
8+
const AppNotificationItemSkeleton = forwardRef<HTMLDivElement>(({}, ref) => {
9+
return (
10+
<LazyMotion features={domMax}>
11+
<div className="AppNotifications__item-skeleton">
12+
<div className="AppNotifications__item-skeleton__image" />
13+
<div className="AppNotifications__item-skeleton__content" ref={ref}>
14+
<div className="AppNotifications__item-skeleton__header">
15+
<div className="AppNotifications__item-skeleton__header__title" />
16+
<div className="AppNotifications__item-skeleton__header__date" />
17+
</div>
18+
<div className={cn('AppNotifications__item-skeleton__message')} />
19+
</div>
20+
</div>
21+
</LazyMotion>
22+
)
23+
})
24+
25+
export default AppNotificationItemSkeleton

src/components/notifications/AppNotifications/AppNotifications.scss

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,107 @@
265265
}
266266
}
267267
}
268+
269+
&__item-skeleton {
270+
position: relative;
271+
display: flex;
272+
gap: 0.75rem;
273+
width: 100%;
274+
border: 1px solid var(--border-color-2);
275+
align-items: center;
276+
padding: 1rem 1.25rem;
277+
border-radius: 0.75rem;
278+
text-decoration: none;
279+
280+
@media only screen and (max-width: 768px) {
281+
padding: 1rem 1.25rem;
282+
border-radius: 0px;
283+
border: none;
284+
border-bottom: 0.5px solid rgba(0, 0, 0, 0.05);
285+
}
286+
287+
&__status {
288+
position: relative;
289+
background-color: var(--shimmer-fg);
290+
}
291+
292+
&__image {
293+
width: 4em;
294+
height: 4em;
295+
aspect-ratio: 1/1;
296+
border-radius: 10px;
297+
object-fit: cover;
298+
align-self: flex-start;
299+
background-color: var(--shimmer-fg);
300+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
301+
302+
@media only screen and (max-width: 768px) {
303+
width: 48px;
304+
height: 48px;
305+
}
306+
}
307+
308+
&__content {
309+
display: flex;
310+
flex-direction: column;
311+
width: 100%;
312+
align-self: flex-start;
313+
margin-top: 5px;
314+
gap: 0.25rem;
315+
316+
@media only screen and (max-width: 768px) {
317+
margin-top: 2px;
318+
}
319+
}
320+
321+
&__header {
322+
display: flex;
323+
justify-content: space-between;
324+
align-items: center;
325+
326+
&__title {
327+
width: 50%;
328+
height: 1.25rem;
329+
background-color: var(--shimmer-fg);
330+
display: flex;
331+
align-items: center;
332+
gap: 2px;
333+
border-radius: 0.25rem;
334+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
335+
}
336+
337+
&__date {
338+
width: 10%;
339+
height: 1.25rem;
340+
background-color: var(--shimmer-fg);
341+
display: flex;
342+
align-items: center;
343+
border-radius: 0.25rem;
344+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
345+
}
346+
}
347+
348+
&__message {
349+
width: 70%;
350+
height: 1.25rem;
351+
background-color: var(--shimmer-fg);
352+
margin-top: 0.25rem;
353+
border-radius: 0.25rem;
354+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
355+
356+
@media only screen and (max-width: 768px) {
357+
margin-top: 0px;
358+
}
359+
}
360+
}
361+
362+
@keyframes pulse {
363+
0%,
364+
100% {
365+
opacity: 1;
366+
}
367+
50% {
368+
opacity: 0.5;
369+
}
370+
}
268371
}

src/components/notifications/AppNotifications/index.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useContext, useEffect, useRef, useState } from 'react'
1+
import { Fragment, createContext, useContext, useEffect, useRef, useState } from 'react'
22

33
import { AnimatePresence } from 'framer-motion'
44
import { motion } from 'framer-motion'
@@ -7,6 +7,7 @@ import { noop } from 'rxjs'
77

88
import Label from '@/components/general/Label'
99
import MobileHeader from '@/components/layout/MobileHeader'
10+
import AppNotificationItemSkeleton from '@/components/notifications/AppNotifications/AppNotificationItemSkeleton'
1011
import W3iContext from '@/contexts/W3iContext/context'
1112
import { useNotificationsInfiniteScroll } from '@/utils/hooks/useNotificationsInfiniteScroll'
1213

@@ -36,7 +37,7 @@ const AppNotifications = () => {
3637
const { topic } = useParams<{ topic: string }>()
3738
const { activeSubscriptions, notifyClientProxy } = useContext(W3iContext)
3839
const app = activeSubscriptions.find(mock => mock.topic === topic)
39-
const { notifications, intersectionObserverRef, nextPage, unshiftNewMessage } =
40+
const { isLoading, notifications, intersectionObserverRef, nextPage, unshiftNewMessage } =
4041
useNotificationsInfiniteScroll(topic)
4142

4243
const ref = useRef<HTMLDivElement>(null)
@@ -85,10 +86,10 @@ const AppNotifications = () => {
8586
title={app.metadata.name}
8687
/>
8788
<AppNotificationsCardMobile />
88-
{notifications.length > 0 ? (
89+
{isLoading || notifications.length > 0 ? (
8990
<div className="AppNotifications__list">
9091
<div className="AppNotifications__list__content">
91-
<Label color="main">Latest</Label>
92+
{notifications.length > 0 ? <Label color="main">Latest</Label> : null}
9293
{notifications.map((notification, index) => (
9394
<AppNotificationItem
9495
ref={index === notifications.length - 1 ? intersectionObserverRef : null}
@@ -109,6 +110,13 @@ const AppNotifications = () => {
109110
appLogo={app.metadata?.icons?.[0]}
110111
/>
111112
))}
113+
{isLoading ? (
114+
<Fragment>
115+
<AppNotificationItemSkeleton />
116+
<AppNotificationItemSkeleton />
117+
<AppNotificationItemSkeleton />
118+
</Fragment>
119+
) : null}
112120
</div>
113121
</div>
114122
) : (

src/reducers/notifications.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface TopicNotificationsState {
44
fullNotifications: NotifyClientTypes.NotifyNotification[]
55
existingIds: Set<string>
66
hasMore: boolean
7+
isLoading: boolean
78
}
89

910
export interface NotificationsState {
@@ -12,11 +13,15 @@ export interface NotificationsState {
1213

1314
export type NotificationsActions =
1415
| {
15-
type: 'FETCH_NOTIFICATIONS'
16+
type: 'FETCH_NOTIFICATIONS_DONE'
1617
notifications: NotifyClientTypes.NotifyNotification[]
1718
topic: string
1819
hasMore: boolean
1920
}
21+
| {
22+
type: 'FETCH_NOTIFICATIONS_LOADING'
23+
topic: string
24+
}
2025
| {
2126
type: 'UNSHIFT_NEW_NOTIFICATIONS'
2227
notifications: NotifyClientTypes.NotifyNotification[]
@@ -32,19 +37,42 @@ export const notificationsReducer = (
3237
): NotificationsState => {
3338
const topicState = state[action.topic] as TopicNotificationsState | undefined
3439

35-
const ids = topicState?.existingIds || new Set<string>()
36-
const filteredNotifications = action.notifications.filter(val => !ids.has(val.id))
37-
const notificationIds = action.notifications.map(notification => notification.id)
40+
function getTopicState(notifications: NotifyClientTypes.NotifyNotification[]) {
41+
const ids = topicState?.existingIds || new Set<string>()
42+
const filteredNotifications = notifications.filter(val => !ids.has(val.id))
43+
const notificationIds = notifications.map(notification => notification.id)
3844

39-
const fullNotifications = topicState?.fullNotifications || []
40-
const newFullIdsSet = new Set(topicState?.existingIds || [])
45+
const fullNotifications = topicState?.fullNotifications || []
46+
const newFullIdsSet = new Set(topicState?.existingIds || [])
4147

42-
for (const val of notificationIds) {
43-
newFullIdsSet.add(val)
48+
for (const val of notificationIds) {
49+
newFullIdsSet.add(val)
50+
}
51+
52+
return {
53+
filteredNotifications,
54+
fullNotifications,
55+
newFullIdsSet
56+
}
4457
}
4558

4659
switch (action.type) {
47-
case 'UNSHIFT_NEW_NOTIFICATIONS':
60+
case 'FETCH_NOTIFICATIONS_LOADING': {
61+
if (topicState) {
62+
return {
63+
...state,
64+
[action.topic]: {
65+
...topicState,
66+
isLoading: true
67+
}
68+
}
69+
}
70+
return state
71+
}
72+
case 'UNSHIFT_NEW_NOTIFICATIONS': {
73+
const { filteredNotifications, fullNotifications, newFullIdsSet } = getTopicState(
74+
action.notifications
75+
)
4876
const unshiftedNotifications = filteredNotifications.concat(fullNotifications)
4977

5078
return {
@@ -53,11 +81,15 @@ export const notificationsReducer = (
5381
...topicState,
5482
existingIds: newFullIdsSet,
5583
fullNotifications: unshiftedNotifications,
56-
hasMore: topicState?.hasMore || false
84+
hasMore: topicState?.hasMore || false,
85+
isLoading: false
5786
}
5887
}
59-
60-
case 'FETCH_NOTIFICATIONS':
88+
}
89+
case 'FETCH_NOTIFICATIONS_DONE': {
90+
const { filteredNotifications, fullNotifications, newFullIdsSet } = getTopicState(
91+
action.notifications
92+
)
6193
const concatenatedNotification = fullNotifications.concat(filteredNotifications)
6294

6395
return {
@@ -66,8 +98,10 @@ export const notificationsReducer = (
6698
...topicState,
6799
existingIds: newFullIdsSet,
68100
fullNotifications: concatenatedNotification,
69-
hasMore: action.hasMore
101+
hasMore: action.hasMore,
102+
isLoading: false
70103
}
71104
}
105+
}
72106
}
73107
}

src/utils/hooks/useNotificationsInfiniteScroll.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useContext, useEffect, useReducer, useRef } from 'react'
1+
import { useCallback, useContext, useEffect, useReducer, useRef, useState } from 'react'
22

33
import W3iContext from '@/contexts/W3iContext/context'
44
import { notificationsReducer } from '@/reducers/notifications'
@@ -22,14 +22,16 @@ export const useNotificationsInfiniteScroll = (topic?: string) => {
2222
return
2323
}
2424

25+
dispatch({ type: 'FETCH_NOTIFICATIONS_LOADING', topic })
26+
2527
const newNotifications = await notifyClientProxy.getNotificationHistory({
2628
topic,
2729
limit: NOTIFICATION_BATCH_SIZE,
2830
startingAfter: lastMessageId
2931
})
3032

3133
dispatch({
32-
type: 'FETCH_NOTIFICATIONS',
34+
type: 'FETCH_NOTIFICATIONS_DONE',
3335
notifications: newNotifications.notifications,
3436
hasMore: newNotifications.hasMore,
3537
topic
@@ -58,6 +60,7 @@ export const useNotificationsInfiniteScroll = (topic?: string) => {
5860

5961
const topicState = topic ? state?.[topic] : undefined
6062
const topicNotifications = topicState ? topicState.fullNotifications : []
63+
const isLoading = topicState ? topicState.isLoading : []
6164
const hasMore = topicState ? topicState.hasMore : false
6265

6366
const lastMessageId = topicNotifications.length
@@ -89,6 +92,7 @@ export const useNotificationsInfiniteScroll = (topic?: string) => {
8992

9093
return {
9194
hasMore,
95+
isLoading,
9296
notifications: topicNotifications,
9397
intersectionObserverRef,
9498
nextPage: () => nextPageInternal(lastMessageId),

0 commit comments

Comments
 (0)