<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PP7S83N" height="0" width="0" style="display:none;visibility:hidden">
Tutorial

Add realtime notifications to your Refine app

This tutorial will show how to create a notification list, add an unread badge, handle navigation and provide notification toasts to your Refine powered React app using Weavy.

Have your favorite code editor ready, and let's get started!

1. Prepare your Refine app

Make sure you have a Refine powered app. For this tutorial we make use of NextJS with Ant Design.

Refine has a ready-to-go Authentication with NextAuth.js example for this.

npm create refine-app@latest -- --example with-nextjs-next-auth

2. Configuring Weavy

We need Weavy to be configured with an environment url and authentication to get the components running.

Make sure you have followed the tutorial for What to do first with Refine to configure authentication.

3. Create a Notifications component

We're adding the notifications list component to a drawer triggered from the navigation header. We need to add a button and a drawer. We will add a badge component to show how many unread notifications we have. We will also make use of the built-in notification system in Refine to show notification toasts.

  • Create a new component in /src/app/components/weavy/notifications.tsx.
  • Import a <Drawer> component from antd and make it use an open boolean state. Set the initial state to false, so the drawer will be hidden initially.
  • Import a <Button> component from antd and make it toggle the open state on clicks. Set the icon attribute to the <BellOutlined /> icon from  @ant-design/icons.
  • Import the <WyNotifications> component from @weavy/uikit-react and place it in the drawer.
/src/components/weavy/notifications.tsx
"use client"
import React, { useState } from "react"
import { Button, Drawer } from "antd"
import { WyNotifications } from "@weavy/uikit-react"

export const WeavyNotifications: React.FC = () => {
  const [open, setOpen] = useState(false)

  const showDrawer = () => {
    setOpen(true)
  }

  const closeDrawer = () => {
    setOpen(false)
  }

  return (
    <>
      <Button
        type="default"
        onClick={showDrawer}
        title="Notifications"
        // @ts-expect-error Ant Design Icon's v5.0.1 has an issue with @types/react@^18.2.66
        icon={<BellOutlined />}
      ></Button>
      <Drawer onClose={closeDrawer} open={open} styles={{ body: { padding: 0 } }}>
        <WyNotifications />
      </Drawer>
    </>
  )
}

4. Add the component in the layout

Now that the component is ready, we just need to add it in our navigation bar. To make everything align nicely, we'll change the <Space> layouts to <Flex> layouts.

  • Open /src/components/header/index.tsx.
  • Change the outer <Space> layout to a <Flex align="center" gap="small"> layout.
  • Change the avatar <Space> layout to a <Flex style={{ marginLeft: "8px" }} align="center" gap="middle"> layout.
  • Import and add the <WeavyNotifications /> component you created last in the outer <Space> layout.
/src/components/header/index.tsx
"use client"
import { WeavyMessenger } from "@components/weavy/messenger"
import { ColorModeContext } from "@contexts/color-mode"
import type { RefineThemedLayoutV2HeaderProps } from "@refinedev/antd"
import { useGetIdentity } from "@refinedev/core"
import { Layout as AntdLayout, Avatar, Flex, Switch, Typography, theme } from "antd"
import React, { useContext } from "react"

const { Text } = Typography
const { useToken } = theme

type IUser = {
  id: number
  name: string
  avatar: string
}

export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky }) => {
  const { token } = useToken()
  const { data: user } = useGetIdentity<IUser>()
  const { mode, setMode } = useContext(ColorModeContext)

  const headerStyles: React.CSSProperties = {
    backgroundColor: token.colorBgElevated,
    display: "flex",
    justifyContent: "flex-end",
    alignItems: "center",
    padding: "0px 24px",
    height: "64px",
  }

  if (sticky) {
    headerStyles.position = "sticky"
    headerStyles.top = 0
    headerStyles.zIndex = 1
  }

  return (
    <AntdLayout.Header style={headerStyles}>
      <Flex align="center" gap="small">
        <Switch checkedChildren="🌛" unCheckedChildren="🔆" onChange={() => setMode(mode === "light" ? "dark" : "light")} defaultChecked={mode === "dark"} />
        {(user?.name || user?.avatar) && (
          <Flex style={{ marginLeft: "8px" }} align="center" gap="middle">
            {user?.name && <Text strong>{user.name}</Text>}
            {user?.avatar && <Avatar src={user?.avatar} alt={user?.name} />}
          </Flex>
        )}
        <WeavyNotifications />
      </Flex>
    </AntdLayout.Header>
  )
}

5. Prepare other Weavy components

Each Weavy component will generate notifications that will show up in the notification list.

To be able to handle notification clicks and navigate back to the place where the component is located, we need to embed the pathname of the page into the uid of the Weavy component. 

We will encode the pathname using Base64 encoding. This can be done using btoa(pathname). We can also embed any #hash we need in the path to show the correct tab for instance.

We'll also add a refine: prefix to the uid to be sure that the uid comes from our refine app.

Make sure all your Weavy components are using an uid with an embedded pathname.

<WyChat uid={`refine:${btoa(`${pathname}#chat`)}`} />

6. Add notification link handling

To make the notifications component navigate to the right place when clicking a notification we need to make use of the wy:link event. It contains metadata that helps us navigate to the correct place in our app.

We'll take advantage of the pathname that we Base64 encoded into the uid of the components. We can extract the path and use it together with the go() function in Refine.

To handle any links that comes from the Messenger we need to look at the appType and see if it is a Conversation type (since the messenger isn't using a uid).

  • Add an event handler that listens to the wy:link event of the <WyNotification> component.
  • Check if the appType is present in the ConversationTypes set. If so, navigate to the Messenger.
  • If it wasn't the Messenger, extract the path from the uid and use it to navigate. 
  • Make sure to close the Navigation drawer after you navigate.
/src/components/weavy/notifications.tsx
"use client"
import React, { useState } from "react"
import { Button, Drawer } from "antd"
import { ConversationTypes, WyLinkEventType, WyNotifications } from "@weavy/uikit-react"
import { BellOutlined } from "@ant-design/icons"
import { useGo, useParsed } from "@refinedev/core"

export const WeavyNotifications: React.FC = () => {
  const [open, setOpen] = useState(false)
  const { pathname } = useParsed()
  const go = useGo()

  const showDrawer = () => {
    setOpen(true)
  }

  const closeDrawer = () => {
    setOpen(false)
  }

  const handleLink = (e: WyLinkEventType) => {
    const appType = e.detail.app?.type
    let appUid = e.detail.app?.uid

    // Check if the appType guid exists in the ConversationTypes map
    if (ConversationTypes.has(appType as string)) {
      // Show the messenger
      go({ hash: "messenger" })
      closeDrawer()
    } else if (appUid) {
      // Show a contextual block by navigation to another page

      // The uid should look something like "refine:adb567a"
      // We have embedded base-64 encoded path information in the uid and to use it we need to decode it.
      if (appUid.startsWith("refine:")) {
        let [_prefix, route] = appUid.split(":")

        if (route) {
          // decode base-64 encoded pathname
          route = atob(route)
        }

        console.log("trying navigate", route)

        // Only navigate if necessary
        if (!pathname?.startsWith(route)) {
          go({ to: route })
          closeDrawer()
        }
      }
    }
  }

  return (
    <>
      <Button
        type="default"
        onClick={showDrawer}
        title="Notifications"
        // @ts-expect-error Ant Design Icon's v5.0.1 has an issue with @types/react@^18.2.66
        icon={<BellOutlined />}
      ></Button>
      <Drawer onClose={closeDrawer} open={open} styles={{ body: { padding: 0 } }}>
        <WyNotifications onWyLink={handleLink} />
      </Drawer>
    </>
  )
}

7. Add a notifications badge

We'll add a badge with the number of unread notifications to the button. We can get the number of unread notifications from the Weavy Web API. We can make use of the weavy.get() function to get API data as the currently authenticated user.

  • Wrap the <Button> in a <Badge> component from antd.
  • Add a state for notificationCount with the default value of 0 and set it to the count attribute of the Badge.
  • Make an async updateNotificationCount() function that retrieves the notification count using the Weavy Web API and updates the notificationCount state. 
  • Call the updateNotificationCount() function in a useEffect() hook once we have the weavy instance from the WeavyContext.
/src/components/weavy/notifications.tsx
"use client"
import React, { useContext, useEffect, useState } from "react"
import { Badge, Button, Drawer } from "antd"
import { ConversationTypes, WeavyContext, WyLinkEventType, WyNotifications } from "@weavy/uikit-react"
import { BellOutlined } from "@ant-design/icons"
import { useGo, useParsed } from "@refinedev/core"

export const WeavyNotifications: React.FC = () => {
  const [open, setOpen] = useState(false)
  const [notificationCount, setNotificationCount] = useState(0)
  const { pathname } = useParsed()
  const go = useGo()

  const weavy = useContext(WeavyContext)

  const showDrawer = () => {
    setOpen(true)
  }

  const closeDrawer = () => {
    setOpen(false)
  }

  const handleLink = (e: WyLinkEventType) => {
    const appType = e.detail.app?.type
    let appUid = e.detail.app?.uid

    // Check if the appType guid exists in the ConversationTypes map
    if (ConversationTypes.has(appType as string)) {
      // Show the messenger
      go({ hash: "messenger" })
      closeDrawer()
    } else if (appUid) {
      // Show a contextual block by navigation to another page

      // The uid should look something like "refine:adb567a"
      // We have embedded base-64 encoded path information in the uid and to use it we need to decode it.
      if (appUid.startsWith("refine:")) {
        let [_prefix, route] = appUid.split(":")

        if (route) {
          // decode base-64 encoded pathname
          route = atob(route)
        }

        console.log("trying navigate", route)

        // Only navigate if necessary
        if (!pathname?.startsWith(route)) {
          go({ to: route })
          closeDrawer()
        }
      }
    }
  }

  const updateNotificationCount = async () => {
    if (weavy) {
      // Fetch notification count from the Weavy Web API.
      // See https://www.weavy.com/docs/reference/web-api/notifications#list-notifications

      const queryParams = new URLSearchParams({
        type: "",
        countOnly: "true",
        unread: "true",
      })

      // Use weavy.get() for fetching from the Weavy Web API to fetch on behalf of the currently authenticated user.
      const response = await weavy.get(`/api/notifications?${queryParams.toString()}`)
      if (response.ok) {
        const result = await response.json()

        // Update the count
        setNotificationCount(result.count)
      }
    }
  }

  useEffect(() => {
    if (weavy) {
      // Get initial notification count
      updateNotificationCount()
    }
  }, [weavy])

  return (
    <>
      <Badge count={notificationCount}>
        <Button
          type="default"
          onClick={showDrawer}
          title="Notifications"
          // @ts-expect-error Ant Design Icon's v5.0.1 has an issue with @types/react@^18.2.66
          icon={<BellOutlined />}
        ></Button>
      </Badge>
      <Drawer onClose={closeDrawer} open={open} styles={{ body: { padding: 0 } }}>
        <WyNotifications onWyLink={handleLink} />
      </Drawer>
    </>
  )
}

8. Connect realtime notifications

To display notification toasts, we could use the <WyNotificationToasts> component that has all notification handling built-in. But instead we'll integrate the notifications into Refines existing notification system.

To update the badge in realtime and display notification toasts in realtime, we need to subscribe to the wy:notifications event. The event is emitted on the weavy.host.

We will update the badge whenever any notification event is received, but we will only display a notification toast when a new notification is created.

  • Make a handleNotifications event listener function.
  • Let the function open a notification using the useNotification() context hook whenever a notification has the action "notification_created".
  • Always call the updateNotificationCount() to update the badge whenever a notification is received.
  • Make sure to turn on the realtime notifications using weavy.notificationEvents = true. Then connect the handleNotifications listener to the wy:notifications event of weavy.host. This is done using the useEffect() hook when weavy is available.
/src/components/weavy/notifications.tssx
"use client"
import React, { useContext, useEffect, useState } from "react"
import { Badge, Button, Drawer } from "antd"
import { ConversationTypes, WeavyContext, WyLinkEventType, WyNotifications, WyNotificationsEventType } from "@weavy/uikit-react"
import { BellOutlined } from "@ant-design/icons"
import { useGo, useNotification, useParsed } from "@refinedev/core"

export const WeavyNotifications: React.FC = () => {
  const [open, setOpen] = useState(false)
  const [notificationCount, setNotificationCount] = useState(0)
  const { open: openNotification } = useNotification()
  const { pathname } = useParsed()
  const go = useGo()

  const weavy = useContext(WeavyContext)

  const showDrawer = () => {
    setOpen(true)
  }

  const closeDrawer = () => {
    setOpen(false)
  }

  const handleLink = (e: WyLinkEventType) => {
    const appType = e.detail.app?.type
    let appUid = e.detail.app?.uid

    // Check if the appType guid exists in the ConversationTypes map
    if (ConversationTypes.has(appType as string)) {
      // Show the messenger
      go({ hash: "messenger" })
      closeDrawer()
    } else if (appUid) {
      // Show a contextual block by navigation to another page

      // The uid should look something like "refine:adb567a"
      // We have embedded base-64 encoded path information in the uid and to use it we need to decode it.
      if (appUid.startsWith("refine:")) {
        let [_prefix, route] = appUid.split(":")

        if (route) {
          // decode base-64 encoded pathname
          route = atob(route)
        }

        console.log("trying navigate", route)

        // Only navigate if necessary
        if (!pathname?.startsWith(route)) {
          go({ to: route })
          closeDrawer()
        }
      }
    }
  }

  const updateNotificationCount = async () => {
    if (weavy) {
      // Fetch notification count from the Weavy Web API.
      // See https://www.weavy.com/docs/reference/web-api/notifications#list-notifications

      const queryParams = new URLSearchParams({
        type: "",
        countOnly: "true",
        unread: "true",
      })

      // Use weavy.get() for fetching from the Weavy Web API to fetch on behalf of the currently authenticated user.
      const response = await weavy.get(`/api/notifications?${queryParams.toString()}`)
      if (response.ok) {
        const result = await response.json()

        // Update the count
        setNotificationCount(result.count)
      }
    }
  }

  const handleNotifications = (e: WyNotificationsEventType) => {
    if (e.detail.notification && e.detail.action === "notification_created") {
      // Only show notifications when a new notification is received

      // Show notifications using the Refine API
      openNotification?.({
        message: e.detail.notification.plain,
        // @ts-expect-error empty type for plain notification
        type: "",
      })
    }

    // Always update the notification count when notifications updates are received
    updateNotificationCount()
  }

  useEffect(() => {
    if (weavy) {
      // Get initial notification count
      updateNotificationCount()
      
      // Configure realtime notifications listener
      weavy.notificationEvents = true

      // Add a realtime notification event listener
      weavy.host?.addEventListener("wy:notifications", handleNotifications)

      return () => {
        // Unregister the event listener when the component is unmounted
        weavy.host?.removeEventListener("wy:notifications", handleNotifications)
      }
    }
  }, [weavy])

  return (
    <>
      <Badge count={notificationCount}>
        <Button
          type="default"
          onClick={showDrawer}
          title="Notifications"
          // @ts-expect-error Ant Design Icon's v5.0.1 has an issue with @types/react@^18.2.66
          icon={<BellOutlined />}
        ></Button>
      </Badge>
      <Drawer onClose={closeDrawer} open={open} styles={{ body: { padding: 0 } }}>
        <WyNotifications onWyLink={handleLink} />
      </Drawer>
    </>
  )
}

9. Done!

The realtime notifications are now ready for use. Ask your friend to post something or @mention you to get a notification.

Try clicking a notification in the notifications list to navigate to the place where the component is!

refine-notifications
Ask AI
Support

To access live chat with our developer success team you need a Weavy account.

Sign in or create a Weavy account