<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. Create a page navigation provider

To be able to navigate from Notifications to the component where their content origin from, whe need to save metadata about the pages where the are placed.

We use a provider that communicates with the Weavy API on the server side. This is done by saving page information in the app metadata.

  • Create a provider in /src/providers/weavy/navigation.ts.
/src/providers/weavy/navigation.ts
"use server"
import { WeavyTypes } from "@weavy/uikit-react"

/**
 * Sets page metadata for an app.
 * The metadata is used when clicking notifications to be able to navigate back to the component that generated the content.
 */
export const setPageNavigation = async (uid: string, path: string) => {
  let app: WeavyTypes.AppType | undefined

  {
    // Check for any existing metadata
    const response = await fetch(new URL(`/api/apps/${uid}`, process.env.NEXT_PUBLIC_WEAVY_URL), {
      method: "GET",
      headers: {
        "content-type": "application/json",
        Authorization: `Bearer ${process.env.WEAVY_APIKEY}`,
      },
    })

    if (!response.ok) {
      throw new Error(`Could fetch app: ${uid}`)
    }
    app = await response.json()
  }

  // Only set the metadata if it's not set already.
  if (app && !app.metadata?.page) {
    console.log("Setting page navigation", uid)

    const body = JSON.stringify({
      metadata: Object.assign({}, app.metadata, {
        page: path,
      }),
    })

    const response = await fetch(new URL(`/api/apps/${uid}`, process.env.NEXT_PUBLIC_WEAVY_URL), {
      method: "PATCH",
      headers: {
        "content-type": "application/json",
        Authorization: `Bearer ${process.env.WEAVY_APIKEY}`,
      },
      body,
    })

    if (!response.ok) {
      throw new Error(`Could not update app metadata: ${uid}`)
    }
  }
}

6. Add a page navigation hook

To simplify the use of the navigation provider, we'll create a hook that handles the page navigation provider when needed. The hook can then easily be used in components.

  • Create a hook in /src/hooks/weavy/usePageNavigation.ts.
/src/hooks/weavy/usePageNavigation.ts
"use client"
import { setPageNavigation } from "@providers/weavy/navigation"
import { WeavyTypes } from "@weavy/uikit-react"
import { useCallback, useEffect, useState } from "react"

export type AppWithPageType = WeavyTypes.AppType & {
  metadata?: WeavyTypes.AppType["metadata"] & {
    page?: string
  }
}

export type WyAppRef = (HTMLElement & { uid?: string; whenApp: () => Promise<AppWithPageType> }) | null

/**
 * Calls the page navigation provider to update page metadata for a component.
 * It is only called when the ref is updated or deps change.
 * 
 * @param path - Provides the path to the component. Preferably provide it as a function to make it re-evaluate when deps change.
 * @param deps - Any deps that should be monitored to re-trigger the provider call.
 * @returns Ref callback function to be used with the ref attribute of a component.
 */
export const usePageNavigation = (path: string | (() => string), deps: React.DependencyList) => {
  const [component, setComponent] = useState<WyAppRef>()

  useEffect(() => {
    if (component && component.uid) {
      // The (current) path to save
      const componentPath: string = typeof path === "function" ? path() : path

      requestAnimationFrame(() => {
        component.whenApp().then((app) => {
          //console.log("Current refine page", component.uid, app.metadata?.page)
          // Check if metadata.page already is set
          if (component.uid && !app.metadata?.page) {
            console.log("Setting refine page", component.uid, componentPath)
            // Update using server function
            setPageNavigation(component.uid, componentPath)
          }
        })
      })
    }
  }, [component, component?.uid, ...deps])

  // Ref callback function to use with components
  return useCallback((ref: WyAppRef | null) => {
    const currentComponent = ref?.whenApp ? ref : undefined
    setComponent(currentComponent)
  }, [])
}

7. Prepare other Weavy components

Each contextual Weavy component (components with an uid) 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 save the pathname of the page into the metadata of the Weavy component. The pathname is provided by the useParsed() hook in Refine. You may have to add any #hash that you want to use to the path string.

Simply connect the usePageNavigation(path, deps) hook to the ref attribute of the component.

Make sure all your contextual Weavy components are using the usePageNavigation hook.

Add usePageNavigation to all Weavy components
"use client";
import { WyComments } from "@weavy/uikit-react";
import { useParsed } from "@refinedev/core";
import { usePageNavigation } from "@hooks/weavy/usePageNavigation";

export default function SomeComponent({ id }) {
 
  // Construct an uid
  const myUid = id ? `refine:${id}:comments`: undefined
  
  // Save page metadata for navigation
  const { pathname } = useParsed();
  const componentRefCallback = usePageNavigation(() => `${pathname}`, [myUid]);

  return (
    <>
      <WyComments uid={myUid} ref={componentRefCallback} notifications="none" />
    </>
  );
}
It's a good idea to provide any constructed uid as a dependency if it's not available right away or changes over time.

8. 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 stored in the metadata. We can fetch 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, fetch the metadata page path 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 = async (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 (weavy && appUid) {
      // Show a contextual block by navigation to another page

      if (appUid.startsWith("refine:")) {
        // First we much fetch the app metadata from the server
        const response = await weavy.fetch(`/api/apps/${appUid}`)
        if (!response.ok) {
          console.error("Error fetching app")
          return
        }

        const { metadata } = (await response.json()) as AppWithPageType
        const route = metadata?.page

        // We can navigate if there is page metadata
        if (route) {
          console.log("Trying to 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>
    </>
  )
}

9. 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 = async (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 (weavy && appUid) {
      // Show a contextual block by navigation to another page

      if (appUid.startsWith("refine:")) {
        // First we much fetch the app metadata from the server
        const response = await weavy.fetch(`/api/apps/${appUid}`)
        if (!response.ok) {
          console.error("Error fetching app")
          return
        }

        const { metadata } = (await response.json()) as AppWithPageType
        const route = metadata?.page

        // We can navigate if there is page metadata
        if (route) {
          console.log("Trying to 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>
    </>
  )
}

10. 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 = async (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 (weavy && appUid) {
      // Show a contextual block by navigation to another page

      if (appUid.startsWith("refine:")) {
        // First we much fetch the app metadata from the server
        const response = await weavy.fetch(`/api/apps/${appUid}`)
        if (!response.ok) {
          console.error("Error fetching app")
          return
        }

        const { metadata } = (await response.json()) as AppWithPageType
        const route = metadata?.page

        // We can navigate if there is page metadata
        if (route) {
          console.log("Trying to 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>
    </>
  )
}

11. 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