Skip to content
This repository was archived by the owner on Dec 8, 2025. It is now read-only.

Commit 5adf1a9

Browse files
committed
feat: add skeleton for notifications loading
1 parent 4a2251d commit 5adf1a9

File tree

4 files changed

+145
-5
lines changed

4 files changed

+145
-5
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/utils/hooks/useNotificationsInfiniteScroll.tsx

Lines changed: 5 additions & 1 deletion
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'
@@ -15,18 +15,21 @@ export const useNotificationsInfiniteScroll = (topic?: string) => {
1515
const intersectionObserverRef = useRef<HTMLDivElement>(null)
1616
const { notifyClientProxy } = useContext(W3iContext)
1717
const [state, dispatch] = useReducer(notificationsReducer, {})
18+
const [isLoading, setIsLoading] = useState(false)
1819

1920
const nextPageInternal = useCallback(
2021
async (lastMessageId?: string) => {
2122
if (!(notifyClientProxy && topic)) {
2223
return
2324
}
2425

26+
setIsLoading(true)
2527
const newNotifications = await notifyClientProxy.getNotificationHistory({
2628
topic,
2729
limit: NOTIFICATION_BATCH_SIZE,
2830
startingAfter: lastMessageId
2931
})
32+
setIsLoading(false)
3033

3134
dispatch({
3235
type: 'FETCH_NOTIFICATIONS',
@@ -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)