Skip to content

Commit

Permalink
community-notfications
Browse files Browse the repository at this point in the history
  • Loading branch information
salman-neslit committed Dec 20, 2024
1 parent 099bd31 commit 8e24534
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 240 deletions.
34 changes: 18 additions & 16 deletions packages/commonwealth/client/scripts/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { openFeatureProvider } from './helpers/feature-flags';
import useAppStatus from './hooks/useAppStatus';
import { trpc, trpcClient } from './utils/trpcClient';
import { AddToHomeScreenPrompt } from './views/components/AddToHomeScreenPrompt';
import { KnockFeedWrapper } from './views/components/KnockNotifications/KnockFeedWrapper';
import { Mava } from './views/components/Mava';

OpenFeature.setProvider(openFeatureProvider);
Expand All @@ -30,22 +31,23 @@ const App = () => {
<trpc.Provider client={trpcClient} queryClient={queryClient}>
{/*@ts-expect-error StrictNullChecks*/}
<OpenFeatureProvider client={undefined}>
{isLoading ? (
<Splash />
) : (
<>
<Mava />
<RouterProvider router={router()} />
{isAddedToHomeScreen || isMarketingPage ? null : (
<AddToHomeScreenPrompt
isIOS={isIOS}
isAndroid={isAndroid}
displayDelayMilliseconds={1000}
/>
)}
</>
)}

<KnockFeedWrapper>
{isLoading ? (
<Splash />
) : (
<>
<Mava />
<RouterProvider router={router()} />
{isAddedToHomeScreen || isMarketingPage ? null : (
<AddToHomeScreenPrompt
isIOS={isIOS}
isAndroid={isAndroid}
displayDelayMilliseconds={1000}
/>
)}
</>
)}
</KnockFeedWrapper>
<ToastContainer />
{import.meta.env.DEV && <ReactQueryDevtools />}
</OpenFeatureProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
useKnockClient,
useNotifications,
useNotificationStore,
} from '@knocklabs/react';
import { useEffect } from 'react';

const KNOCK_IN_APP_FEED_ID =
process.env.KNOCK_IN_APP_FEED_ID || 'fc6e68e5-b7b9-49c1-8fab-6dd7e3510ffb';

const useFetchNotifications = () => {
const knockClient = useKnockClient();
const feedClient = useNotifications(knockClient, KNOCK_IN_APP_FEED_ID);

const { items } = useNotificationStore(feedClient);

useEffect(() => {
feedClient.fetch();
}, [feedClient]);

return { items };
};

export default useFetchNotifications;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Knock from '@knocklabs/client';
import { KnockFeedProvider, KnockProvider } from '@knocklabs/react';
import React, { ReactNode, useEffect } from 'react';
import useUserStore from 'state/ui/user';

const KNOCK_PUBLIC_API_KEY =
process.env.KNOCK_PUBLIC_API_KEY ||
'pk_test_Hd4ZpzlVcz9bqepJQoo9BvZHokgEqvj4T79fPdKqpYM';

const KNOCK_IN_APP_FEED_ID =
process.env.KNOCK_IN_APP_FEED_ID || 'fc6e68e5-b7b9-49c1-8fab-6dd7e3510ffb';

const knock = new Knock(KNOCK_PUBLIC_API_KEY);

const getBrowserTimezone = (): string => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
};

interface KnockFeedWrapperProps {
children: ReactNode;
}

export const KnockFeedWrapper = ({ children }: KnockFeedWrapperProps) => {
const user = useUserStore();

useEffect(() => {
if (!user.id || !user.isLoggedIn) return;
if (!user.knockJWT) {
console.warn('user knockJWT not set! Will not attempt to identify.');
return;
}

const timezone = getBrowserTimezone();
async function doAsync() {
knock.authenticate(`${user.id}`, user.knockJWT);
await knock.user.identify({
id: user.id,
email: user.email,
timezone,
});
}

doAsync().catch(console.error);
}, [user.email, user.id, user.isLoggedIn, user.knockJWT]);

if (!user.id || !user.isLoggedIn || !user.knockJWT) return null;

return (
<KnockProvider
apiKey={KNOCK_PUBLIC_API_KEY}
userId={`${user.id}`}
userToken={user.knockJWT}
>
<KnockFeedProvider feedId={KNOCK_IN_APP_FEED_ID} colorMode="light">
{children}
</KnockFeedProvider>
</KnockProvider>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import Knock from '@knocklabs/client';
import {
KnockFeedProvider,
KnockProvider,
NotificationFeedPopover,
NotificationIconButton,
} from '@knocklabs/react';
import '@knocklabs/react-notification-feed/dist/index.css';
import React, { memo, useEffect, useRef, useState } from 'react';
import React, { memo, useRef, useState } from 'react';
import useUserStore from 'state/ui/user';
import CustomNotificationCell from './CustomNotificationCell';
import './KnockNotifications.scss';
Expand All @@ -17,58 +16,19 @@ const KNOCK_PUBLIC_API_KEY =
const KNOCK_IN_APP_FEED_ID =
process.env.KNOCK_IN_APP_FEED_ID || 'fc6e68e5-b7b9-49c1-8fab-6dd7e3510ffb';

const knock = new Knock(KNOCK_PUBLIC_API_KEY);

const getBrowserTimezone = (): string => {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
};

export const KnockNotifications = memo(function KnockNotifications() {
const user = useUserStore();
const [isVisible, setIsVisible] = useState(false);

const notifButtonRef = useRef(null);

useEffect(() => {
if (!user.id || !user.isLoggedIn) {
return;
}

if (!user.knockJWT) {
console.warn('user knockJWT not set! Will not attempt to identify.');
return;
}

const timezone = getBrowserTimezone();
async function doAsync() {
knock.authenticate(`${user.id}`, user.knockJWT);

await knock.user.identify({
id: user.id,
email: user.email,
timezone,
});
}

doAsync().catch(console.error);
}, [user.email, user.id, user.isLoggedIn, user.knockJWT]);

if (!user.id || !user.isLoggedIn) {
return null;
}

if (!user.knockJWT) {
return null;
}

return (
<div className="KnockNotifications">
<KnockProvider
apiKey={KNOCK_PUBLIC_API_KEY}
userId={`${user.id}`}
userToken={user.knockJWT}
>
{/* Optionally, use the KnockFeedProvider to connect an in-app feed */}
<KnockFeedProvider feedId={KNOCK_IN_APP_FEED_ID} colorMode="light">
<div>
<NotificationIconButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';

import { FeedItem } from '@knocklabs/client';
import app from 'state';
import { Contest } from 'views/pages/CommunityManagement/Contests/ContestsList';
import { isContestActive } from 'views/pages/CommunityManagement/Contests/utils';
Expand Down Expand Up @@ -41,3 +42,11 @@ export const getUniqueTopicIdsIncludedInActiveContest = (

return [...new Set(topicIds)];
};

export const calculateUnreadCount = (
communityName: string,
items: FeedItem[],
) =>
items.filter(
(item) => !item.read_at && item?.data?.community_name === communityName,
).length;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.notification-icon-container {
position: absolute;
top: 0;
right: 0;
display: inline-block;
}

.notification-icon {
font-size: 24px;
color: #333;
position: relative;
cursor: pointer;
}

.notification-badge {
position: absolute;
top: 22px;
right: -5px;
background-color: red;
color: white;
font-size: 12px;
font-weight: bold;
padding: 2px 6px;
border-radius: 12px;
line-height: 1;
border: 2px solid white;
}

.hidden-feed {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import '@knocklabs/react-notification-feed/dist/index.css';
import React from 'react';
import './sidebar_notification_icon.scss';

type SideBarNotificationIconProps = {
unreadCount: number;
};

export const SideBarNotificationIcon = ({
unreadCount,
}: SideBarNotificationIconProps) => (
<div className="notification-icon-container">
<div className="notification-icon" style={{ cursor: 'pointer' }}>
<i className="bell-icon" />
{unreadCount > 0 && (
<span className="notification-badge">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</div>
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,9 @@
&::-webkit-scrollbar {
width: 4px;
}

.community-avatar-container {
position: relative;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import useFetchNotifications from 'client/scripts/hooks/useFetchNotifications';
import clsx from 'clsx';
import { navigateToCommunity, useCommonNavigate } from 'navigation/helpers';
import React from 'react';
Expand All @@ -7,6 +8,8 @@ import useUserStore from 'state/ui/user';
import { CWCommunityAvatar } from '../component_kit/cw_community_avatar';
import { CWDivider } from '../component_kit/cw_divider';
import { CWIconButton } from '../component_kit/cw_icon_button';
import { calculateUnreadCount } from './helpers';
import { SideBarNotificationIcon } from './sidebar_notification_icon';
import './sidebar_quick_switcher.scss';

export const SidebarQuickSwitcher = ({
Expand All @@ -20,6 +23,8 @@ export const SidebarQuickSwitcher = ({
const { setMenu } = useSidebarStore();
const user = useUserStore();

const { items } = useFetchNotifications();

const location = useLocation();
const pathname = location.pathname;
const communityId = pathname.split('/')[1];
Expand Down Expand Up @@ -52,23 +57,28 @@ export const SidebarQuickSwitcher = ({
{user.communities
.filter((x) => x.isStarred)
.map((community) => (
<CWCommunityAvatar
key={community.id}
size="large"
selectedCommunity={communityId}
community={{
id: community.id,
iconUrl: community.iconUrl,
name: community.name,
}}
onClick={() =>
navigateToCommunity({
navigate,
path: '',
chain: community.id,
})
}
/>
<div className="community-avatar-container" key={community.id}>
<CWCommunityAvatar
key={community.id}
size="large"
selectedCommunity={communityId}
community={{
id: community.id,
iconUrl: community.iconUrl,
name: community.name,
}}
onClick={() =>
navigateToCommunity({
navigate,
path: '',
chain: community.id,
})
}
/>
<SideBarNotificationIcon
unreadCount={calculateUnreadCount(community.name, items)}
/>
</div>
))}
</div>
)}
Expand All @@ -77,19 +87,24 @@ export const SidebarQuickSwitcher = ({
)}
<div className="scrollable-community-bar">
{user.communities.map((community) => (
<CWCommunityAvatar
key={community.id}
size="large"
selectedCommunity={communityId}
community={{
id: community.id,
iconUrl: community.iconUrl,
name: community.name,
}}
onClick={() =>
navigateToCommunity({ navigate, path: '', chain: community.id })
}
/>
<div className="community-avatar-container" key={community.id}>
<CWCommunityAvatar
key={community.id}
size="large"
selectedCommunity={communityId}
community={{
id: community.id,
iconUrl: community.iconUrl,
name: community.name,
}}
onClick={() =>
navigateToCommunity({ navigate, path: '', chain: community.id })
}
/>
<SideBarNotificationIcon
unreadCount={calculateUnreadCount(community.name, items)}
/>
</div>
))}
</div>
</div>
Expand Down
Loading

0 comments on commit 8e24534

Please sign in to comment.