Skip to content

Commit

Permalink
feat(base): notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
antonstjernquist committed Sep 20, 2024
1 parent 2f74478 commit deaaf72
Show file tree
Hide file tree
Showing 20 changed files with 648 additions and 166 deletions.
2 changes: 1 addition & 1 deletion src/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function App() {

return (
<Frame>
<motion.div className="flex flex-col flex-1 overflow-hidden">
<motion.div className="flex flex-col flex-1 overflow-hidden bg-primary text-primary">
<Header />

<motion.div
Expand Down
9 changes: 9 additions & 0 deletions src/ui/src/Apps/Calls/Call/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { useCurrentDevice } from '../../../api/hooks/useCurrentDevice';
import { TopNavigation } from '../../../components/Navigation/TopNavigation';
import { useEffectOnce } from '../../../hooks/useEffectOnce';
import { instance } from '../../../utils/fetch';
import { useSearchParams } from 'react-router-dom';

export const Call = () => {
const device = useCurrentDevice();
const [call, invalidate, refetch] = useActiveCall();
const [searchParams] = useSearchParams();
const params = useParams<{ phoneNumber: string }>();
const action = searchParams.get('action');
const { phoneNumber } = params;
const [error, setError] = useState<string | null>(null);

Expand Down Expand Up @@ -43,6 +46,12 @@ export const Call = () => {
handleCreateCall();
});

useEffectOnce(() => {
if (action === 'accept') {
handleAcceptCall();
}
});

if (!call) {
return (
<div className="p-4">
Expand Down
17 changes: 10 additions & 7 deletions src/ui/src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { InnerRouterProvider, RouterProvider } from './contexts/RouterContext';
import { routes } from './routes';
import { AppsProvider } from './contexts/AppsContext';
import { NavigationProvider } from './contexts/NavigationContext';
import { NotificationsProvider } from './contexts/NotificationContext';

export const queryClient = new QueryClient({
defaultOptions: {
Expand All @@ -23,13 +24,15 @@ export const Providers = () => {
return (
<QueryClientProvider client={queryClient}>
<NuiProvider>
<NavigationProvider>
<RouterProvider initialRoutes={routes}>
<AppsProvider>
<InnerRouterProvider />
</AppsProvider>
</RouterProvider>
</NavigationProvider>
<NotificationsProvider>
<NavigationProvider>
<RouterProvider initialRoutes={routes}>
<AppsProvider>
<InnerRouterProvider />
</AppsProvider>
</RouterProvider>
</NavigationProvider>
</NotificationsProvider>
</NuiProvider>
</QueryClientProvider>
);
Expand Down
15 changes: 15 additions & 0 deletions src/ui/src/api/hooks/useActiveCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { queryClient } from '../../Providers';
import { getActiveCall } from '../device';
import { useBroadcastEvent } from '../../hooks/useBroadcastEvent';
import { Call } from '../../../../shared/Types';
import { useNotifications } from '@/contexts/NotificationContext/useNotifications';
import { useCurrentDevice } from './useCurrentDevice';

type ActiveCallResult = Awaited<ReturnType<typeof getActiveCall>>['payload'] | undefined;

export const useActiveCall = (): [ActiveCallResult, (setEmpty?: boolean) => void, () => void] => {
const currentDevice = useCurrentDevice();
const { add } = useNotifications();
const { data, refetch } = useQuery({
initialData: null,
queryKey: ['active-call'],
Expand All @@ -26,6 +30,17 @@ export const useActiveCall = (): [ActiveCallResult, (setEmpty?: boolean) => void
useBroadcastEvent<Call>('active-call:updated', (data) => {
console.log('active-call:updated', data);
console.log('Setting active call to:', data);

if (!data?.ended_at && !data?.declined_at && currentDevice?.sim_card_id === data?.receiver_id) {
add({
type: 'call',
appId: 'calls',
title: 'Incoming Call',
path: '/apps/calls/call',
description: `Incoming call from ${data?.caller_id}`,
});
}

queryClient.setQueryData(['active-call'], { payload: data });
});

Expand Down
7 changes: 7 additions & 0 deletions src/ui/src/api/hooks/useContactPhoneNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useContacts } from './useContacts';

export const useContactPhoneNumber = (phoneNumber: string) => {
const [contacts] = useContacts();
const contact = contacts.find((contacts) => contacts.phone_number === phoneNumber);
return contact;
};
6 changes: 3 additions & 3 deletions src/ui/src/api/hooks/useContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { getContacts } from '../contacts';
import { queryClient } from '../../Providers';
import { Contact } from '../../../../shared/Types';

export const useContacts = (): [Contact[], () => void] => {
const { data } = useQuery({
export const useContacts = (): [Contact[], () => void, boolean] => {
const { data, isLoading } = useQuery({
queryKey: ['contacts'],
queryFn: getContacts,
});
Expand All @@ -15,5 +15,5 @@ export const useContacts = (): [Contact[], () => void] => {
});
};

return [data?.payload || [], invalidate];
return [data?.payload || [], invalidate, isLoading];
};
2 changes: 1 addition & 1 deletion src/ui/src/components/Main/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Notifications } from '../Notifications';

export const Header = () => {
return (
<header className="h-8 bg-secondary text-secondary px-6 flex gap-4">
<header className="h-8 bg-secondary text-secondary px-6 flex gap-4 z-10">
<span>22:03</span>
<Notifications />
</header>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Notification as NotificationType } from '@/contexts/NotificationContext';
import { Notification } from '.';
import { motion } from 'framer-motion';

interface ExtendedNotificationProps {
notification: NotificationType;
onClose: () => void;
onClick: () => void;
onDragChange: (isDragging: boolean) => void;
}

export const ExtendedNotification = ({
notification,
onDragChange,
onClick,
onClose,
}: ExtendedNotificationProps) => {
return (
<motion.div
layout
key={notification.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, x: -400 }}
>
<Notification
notification={notification}
onClick={onClick}
onClose={onClose}
onDragChange={onDragChange}
/>
</motion.div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useContactPhoneNumber } from '@/api/hooks/useContactPhoneNumber';
import { useApp } from '@/contexts/AppsContext/useApp';
import { Notification } from '@/contexts/NotificationContext';
import { clsx } from 'clsx';
import { DateTime } from 'luxon';
import { ReactNode } from 'react';

interface NotificationContentProps {
notification: Notification;
extended?: boolean;
className?: string;
onClick?: () => void;
Actions?: ReactNode;
}

export const NotificationContent = ({
Actions,
notification,
className,
onClick,
extended = false,
}: NotificationContentProps) => {
const contact = useContactPhoneNumber(notification.title);
const { title, description, overline, created_at, appId = 'settings' } = notification;
const app = useApp(appId);
const AppIcon = app ? app.Icon : '📢';

return (
<div
onClick={onClick}
className={clsx(
'flex p-3 items-center gap-3 rounded-lg relative outline outline-1 outline-secondary outline-offset-1',
!extended ? 'dark:bg-gray-950 bg-primary shadow-xl' : 'backdrop-blur-md bg-opacity-10',
className,
)}
>
<span className="p-1 bg-secondary rounded-lg text-2xl w-10 h-10 flex flex-col items-center justify-center">
{AppIcon}
</span>
<div className="flex flex-col overflow-hidden">
{overline && (
<span className="uppercase text-[10px] font-semibold line-clamp-3 tracking-wider">
{overline}
</span>
)}
<span
className={clsx(
'text-primary font-semibold text-sm',
!extended && 'whitespace-pre overflow-ellipsis overflow-hidden',
)}
>
{contact ? contact.name : title}
</span>
<span
className={clsx(
'text-secondary',
!extended && 'whitespace-pre overflow-ellipsis overflow-hidden',
)}
>
{description}
</span>
</div>

{Actions ? (
<div className="ml-auto">{Actions}</div>
) : (
<span className="text-xs opacity-40 absolute top-0 right-0 m-3">
{DateTime.fromISO(created_at).toFormat('t')}
</span>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
AnimatePresence,
motion,
PanInfo,
useAnimate,
useMotionValue,
useSpring,
useTransform,
} from 'framer-motion';
import { useState } from 'react';
import { clsx } from 'clsx';
import { FooterLine } from '@/components/FooterLine';
import { useNotifications } from '@/contexts/NotificationContext/useNotifications';
import { Button } from '@/components/ui/button';
import { ExtendedNotification } from './ExtendedNotification';

interface NotificationsExtendedListProps {
onChangeOpen: (isOpen: boolean) => void;
}
export const NotificationsExtendedList = ({
onChangeOpen: onToggle,
}: NotificationsExtendedListProps) => {
const rootHeight = document.getElementById('root')?.clientHeight;
const [isDraggingNotification, setIsDraggingNotification] = useState(false);
const height = rootHeight || 844;

const { notifications, remove, clear } = useNotifications();
const [scope, animate] = useAnimate();

const y = useMotionValue(-height + 32);
const transition = useSpring(y, { stiffness: 300, damping: 30 });
const opacity = useTransform(transition, [-height + 32, -height + 32 * 12, 0], [0, 1, 1]);

const handleDragEnd = async (_: unknown, panInfo: PanInfo) => {
if (isDraggingNotification) {
return;
}

const velocity = Math.abs(panInfo.velocity.y);

if (velocity < 70) {
const isOpen = panInfo.point.y < height / 2;
updateNotificationState(!isOpen);
return;
}

const isOpen = panInfo.velocity.y > -100;
updateNotificationState(isOpen);
};

const updateNotificationState = (isOpen: boolean) => {
if (isOpen) {
animate(scope.current, { y: 0 });
} else {
animate(scope.current, { y: -height + 32 });
}

onToggle(isOpen);
};

return (
<motion.div
ref={scope}
drag={!isDraggingNotification ? 'y' : false}
style={{ y: transition, opacity }}
dragConstraints={{ top: -height + 32, bottom: 0 }}
variants={{
open: { y: '0%', opacity: 1 },
closed: { y: 'calc(-100% + 32px)', opacity: 1 },
}}
transition={{ duration: 0.5 }}
className={clsx(
'absolute top-0 left-0 right-0 z-10 flex flex-col gap-2 h-full backdrop-blur-xl bg-gray-500 bg-opacity-5',
)}
onDragEnd={handleDragEnd}
>
<div className="flex justify-between p-6 pb-2">
<span className="tracking-wide font-semibold text-xl text-primary">Notifications</span>

<Button variant="ghost" onClick={clear}>
Clear all
</Button>
</div>

<div className="flex flex-col overflow-auto mr-2 scrollbar scroll-m-4">
<div className="flex flex-col gap-2 p-4 pr-2">
<AnimatePresence>
{notifications.map((notification) => (
<motion.div
layout
key={notification.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, x: -400 }}
>
<ExtendedNotification
notification={notification}
onClick={() => onToggle(false)}
onClose={() => remove(notification.id)}
onDragChange={setIsDraggingNotification}
/>
</motion.div>
))}
</AnimatePresence>
</div>
</div>

{notifications.length === 0 && (
<div className="flex flex-1 items-center justify-center">No notifications</div>
)}

<div className="h-8 px-6 flex gap-4 items-center mt-auto justify-center">
<FooterLine primary />
</div>
</motion.div>
);
};
Loading

0 comments on commit deaaf72

Please sign in to comment.