Skip to content

Commit

Permalink
[Notifi] notification popover integration V2 + V3 (osmosis-labs#2035)
Browse files Browse the repository at this point in the history
* Notifi integration: feat: target verify/ discard modal/ major refactor/ other fixes

* Notification integration: feat: unread count badge

---------

Co-authored-by: Eric Lee <[email protected]>
  • Loading branch information
eric-notifi and Eric Lee committed Sep 11, 2023
1 parent b39bd3d commit 51f7e58
Show file tree
Hide file tree
Showing 32 changed files with 1,304 additions and 562 deletions.
25 changes: 6 additions & 19 deletions packages/web/components/layouts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { MainMenu } from "~/components/main-menu";
import { NavBar } from "~/components/navbar";
import NavbarOsmoPrice from "~/components/navbar-osmo-price";
import { MainLayoutMenu } from "~/components/types";
import { useCurrentLanguage, useFeatureFlags, useWindowSize } from "~/hooks";
import { NotifiContextProvider } from "~/integrations/notifi";
import { useCurrentLanguage, useWindowSize } from "~/hooks";

export const MainLayout: FunctionComponent<{
menus: MainLayoutMenu[];
Expand All @@ -28,8 +27,6 @@ export const MainLayout: FunctionComponent<{
({ selectionTest }) => selectionTest?.test(router.pathname) ?? false
);

const featureFlags = useFeatureFlags();

return (
<React.Fragment>
{showFixedLogo && (
Expand All @@ -51,21 +48,11 @@ export const MainLayout: FunctionComponent<{
<NavbarOsmoPrice />
</div>
</article>
{featureFlags.notifications ? (
<NotifiContextProvider>
<NavBar
className="ml-sidebar md:ml-0"
title={selectedMenuItem?.label ?? ""}
menus={menus}
/>
</NotifiContextProvider>
) : (
<NavBar
className="ml-sidebar md:ml-0"
title={selectedMenuItem?.label ?? ""}
menus={menus}
/>
)}
<NavBar
className="ml-sidebar md:ml-0"
title={selectedMenuItem?.label ?? ""}
menus={menus}
/>
<div className="ml-sidebar h-content bg-osmoverse-900 md:ml-0 md:h-content-mobile">
{children}
</div>
Expand Down
19 changes: 9 additions & 10 deletions packages/web/components/navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ import {
} from "~/hooks";
import { useFeatureFlags } from "~/hooks/use-feature-flags";
import { useWalletSelect } from "~/hooks/wallet-select";
import { NotifiModal, NotifiPopover } from "~/integrations/notifi";
import { useNotifiBreadcrumb } from "~/integrations/notifi/hooks";
import {
NotifiContextProvider,
NotifiModal,
NotifiPopover,
} from "~/integrations/notifi";
import { ModalBase, ModalBaseProps, SettingsModal } from "~/modals";
import { ProfileModal } from "~/modals/profile";
import { UserUpgradesModal } from "~/modals/user-upgrades";
Expand Down Expand Up @@ -116,8 +119,6 @@ export const NavBar: FunctionComponent<
const router = useRouter();
const { isLoading: isWalletLoading } = useWalletSelect();

const { hasUnreadNotification } = useNotifiBreadcrumb();

useEffect(() => {
const handler = () => {
closeMobileMenuRef.current();
Expand Down Expand Up @@ -309,16 +310,14 @@ export const NavBar: FunctionComponent<
</div>
)}
{featureFlags.notifications && walletSupportsNotifications && (
<>
<NotifiPopover
hasUnreadNotification={hasUnreadNotification}
className="z-40 px-3 outline-none"
/>
<NotifiContextProvider>
<NotifiPopover className="z-40 px-3 outline-none" />
<NotifiModal
isOpen={isNotifiOpen}
onRequestClose={onCloseNotifi}
onOpenNotifi={onOpenNotifi}
/>
</>
</NotifiContextProvider>
)}
<IconButton
aria-label="Open settings dropdown"
Expand Down
57 changes: 27 additions & 30 deletions packages/web/integrations/notifi/hooks/use-notifi-breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useNotifiClientContext } from "@notifi-network/notifi-react-card";
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";

import { useStore } from "~/stores";

Expand All @@ -12,40 +11,38 @@ export const useNotifiBreadcrumb = () => {
accountStore,
} = useStore();
const { client } = useNotifiClientContext();
const [hasUnreadNotification, setHasUnreadNotification] = useState(false);
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
const hasUnreadNotification = useMemo(
() => (unreadNotificationCount > 0 ? true : false),
[unreadNotificationCount]
);

useEffect(() => {
const wallet = accountStore.getWallet(chainId);
if (!wallet?.address || !client?.isAuthenticated) return;

client
.getUnreadNotificationHistoryCount()
.then((res) => {
const unreadNotificationCount = res.count;
setUnreadNotificationCount(unreadNotificationCount);
})
.catch((_e) => {
/* Intentionally empty (Concurrent can only possibly happens here instead of inside interval) */
});

const interval = setInterval(() => {
const wallet = accountStore.getWallet(chainId);
if (!wallet?.address || !client?.isAuthenticated)
return setHasUnreadNotification(true);
const localStorageKey = `lastStoredTimestamp:${wallet.address}`;

client
.getNotificationHistory({ first: 1 })
.then((res) => {
const newestHistoryItem = res.nodes?.[0];
const newestNotificationCreatedDate = newestHistoryItem?.createdDate
? dayjs(newestHistoryItem?.createdDate)
: dayjs("2022-01-05T12:30:00.792Z");

const lastStoredTimestamp = dayjs(
window.localStorage.getItem(localStorageKey)
).isValid()
? dayjs(window.localStorage.getItem(localStorageKey))
: dayjs("2022-01-05T10:30:00.792Z");

if (newestNotificationCreatedDate.isAfter(lastStoredTimestamp)) {
setHasUnreadNotification(true);
} else {
setHasUnreadNotification(false);
}
})
.catch(() => setHasUnreadNotification(true));
}, 5000);
if (!wallet?.address || !client?.isAuthenticated) return;

client.getUnreadNotificationHistoryCount().then((res) => {
const unreadNotificationCount = res.count;
setUnreadNotificationCount(unreadNotificationCount);
});
}, Math.floor(Math.random() * 5000) + 5000); // a random interval between 5 and 10 seconds to avoid spamming the server

return () => clearInterval(interval);
}, [client?.isAuthenticated]);

return { hasUnreadNotification };
return { hasUnreadNotification, unreadNotificationCount };
};
154 changes: 154 additions & 0 deletions packages/web/integrations/notifi/hooks/use-notifi-setting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { NotifiFrontendClient } from "@notifi-network/notifi-frontend-client";
import {
useNotifiClientContext,
useNotifiForm,
} from "@notifi-network/notifi-react-card";
import { useCallback, useEffect, useMemo, useState } from "react";

import { useNotifiConfig } from "~/integrations/notifi/notifi-config-context";

export type TargetGroupFragment = Awaited<
ReturnType<NotifiFrontendClient["getTargetGroups"]>
>[number];

type TargetStates = Readonly<{
targetGroup: TargetGroupFragment | undefined;
emailSelected: boolean;
telegramSelected: boolean;
smsSelected: boolean;
}>;

export const useNotifiSetting = () => {
const config = useNotifiConfig();
const { client } = useNotifiClientContext();
const {
setEmail: setFormEmail,
setPhoneNumber: setFormPhoneNumber,
setTelegram: setFormTelegram,
} = useNotifiForm();
const [alertStates, setAlertStates] = useState<Record<string, boolean>>({});
const [targetStates, setTargetStates] = useState<TargetStates>({
targetGroup: undefined,
emailSelected: false,
telegramSelected: false,
smsSelected: false,
});

const initialAlertStates = useMemo<Record<string, boolean>>(() => {
if (config.state !== "fetched") {
return {};
}

const alerts = client.data?.alerts ?? [];
const newStates: Record<string, boolean> = {};
config.data.eventTypes.forEach((row) => {
const isActive = alerts.find((it) => it?.name === row.name) !== undefined;
newStates[row.name] = isActive;
});
return newStates;
}, [client, config]);

useEffect(() => {
if (
Object.keys(alertStates).length === 0 &&
Object.keys(initialAlertStates).length !== 0
) {
setAlertStates(initialAlertStates);
}
}, [initialAlertStates, alertStates]);

const needsSave = useMemo<"alerts" | "targets" | null>(() => {
// Changed alerts need save
if (config.state === "fetched") {
for (let i = 0; i < config.data.eventTypes.length; ++i) {
const row = config.data.eventTypes[i];
if (initialAlertStates[row.name] !== alertStates[row.name]) {
return "alerts";
}
}
}

const isOriginalEmailExist = !!targetStates.targetGroup?.emailTargets?.[0];
const isOriginalPhoneNumberExist =
!!targetStates.targetGroup?.smsTargets?.[0];
const isOriginalTelegramExist =
!!targetStates.targetGroup?.telegramTargets?.[0];
if (
(isOriginalEmailExist && !targetStates.emailSelected) ||
(isOriginalPhoneNumberExist && !targetStates.smsSelected) ||
(isOriginalTelegramExist && !targetStates.telegramSelected)
) {
return "targets";
} else {
return null;
}
}, [config, targetStates, initialAlertStates, alertStates]);

const revertChanges = useCallback(() => {
setAlertStates(initialAlertStates);
const targetGroup = client.data?.targetGroups?.find(
(it) => it.name === "Default"
);
if (targetGroup === undefined) {
return;
}
const emailTarget = targetGroup.emailTargets?.[0];
const emailSelected = emailTarget !== undefined;
const telegramTarget = targetGroup.telegramTargets?.[0];
const telegramSelected = telegramTarget !== undefined;
const smsTarget = targetGroup.smsTargets?.[0];
const smsSelected = smsTarget !== undefined;
setFormEmail(emailTarget?.emailAddress ?? "");
setFormTelegram(telegramTarget?.telegramId ?? "");
setFormPhoneNumber(smsTarget?.phoneNumber ?? "");
setTargetStates({
targetGroup,
emailSelected,
telegramSelected,
smsSelected,
});
}, []);

useEffect(() => {
const targetGroup = client.data?.targetGroups?.find(
(it) => it.name === "Default"
);
if (targetGroup === targetStates.targetGroup) {
return;
}

if (targetGroup !== undefined) {
const emailTarget = targetGroup.emailTargets?.[0];
const emailSelected = emailTarget !== undefined;
const telegramTarget = targetGroup.telegramTargets?.[0];
const telegramSelected = telegramTarget !== undefined;
const smsTarget = targetGroup.smsTargets?.[0];
const smsSelected = smsTarget !== undefined;
setFormEmail(emailTarget?.emailAddress ?? "");
setFormTelegram(telegramTarget?.telegramId ?? "");
setFormPhoneNumber(smsTarget?.phoneNumber ?? "");
setTargetStates({
targetGroup,
emailSelected,
telegramSelected,
smsSelected,
});
} else {
setTargetStates({
targetGroup: undefined,
emailSelected: false,
telegramSelected: false,
smsSelected: false,
});
}
}, [client, targetStates, setFormEmail, setFormPhoneNumber, setFormTelegram]);

return {
alertStates,
setAlertStates,
targetStates,
setTargetStates,
needsSave,
revertChanges,
};
};
22 changes: 20 additions & 2 deletions packages/web/integrations/notifi/notifi-modal-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ interface NotifiModalFunctions {
account: string;
location: Location;
innerState: Partial<ModalBaseProps>;
isOverLayEnabled: boolean;
setIsOverLayEnabled: (isOverLayEnabled: boolean) => void;
selectedHistoryEntry?: HistoryRowData;
setSelectedHistoryEntry: React.Dispatch<
React.SetStateAction<HistoryRowData | undefined>
>;
renderView: (location: Location) => void;
setInnerState: React.Dispatch<React.SetStateAction<Partial<ModalBaseProps>>>;
/** The following 8 states for modalBase/Popover pop-up status*/
isOverLayEnabled: boolean; // The background overlay (outside of the card)
setIsOverLayEnabled: React.Dispatch<React.SetStateAction<boolean>>;
isInCardOverlayEnabled: boolean; // The background overlay (inside of the card)
setIsInCardOverlayEnabled: React.Dispatch<React.SetStateAction<boolean>>;
isCardOpen: boolean;
setIsCardOpen: React.Dispatch<React.SetStateAction<boolean>>;
isPreventingCardClosed: boolean; // Preventing card from closing while isCardOpen is true
setIsPreventingCardClosed: React.Dispatch<React.SetStateAction<boolean>>;
}

const NotifiModalContext = createContext<NotifiModalFunctions>({
Expand All @@ -39,6 +47,9 @@ export const NotifiModalContextProvider: FunctionComponent<
const [innerState, setInnerState] = useState<Partial<ModalBaseProps>>({});
const [location, setLocation] = useState<Location>("signup");
const [isOverLayEnabled, setIsOverLayEnabled] = useState(false);
const [isInCardOverlayEnabled, setIsInCardOverlayEnabled] = useState(false);
const [isPreventingCardClosed, setIsPreventingCardClosed] = useState(false);
const [isCardOpen, setIsCardOpen] = useState(true);
const [selectedHistoryEntry, setSelectedHistoryEntry] = useState<
HistoryRowData | undefined
>(undefined);
Expand Down Expand Up @@ -116,7 +127,14 @@ export const NotifiModalContextProvider: FunctionComponent<
setSelectedHistoryEntry,
location,
isOverLayEnabled,
isInCardOverlayEnabled,
setIsOverLayEnabled,
setIsInCardOverlayEnabled,
isPreventingCardClosed,
setIsPreventingCardClosed,
isCardOpen,
setIsCardOpen,
setInnerState,
}}
>
{children}
Expand Down
Loading

0 comments on commit 51f7e58

Please sign in to comment.