Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ setup:
just api/setup
just web/setup

# restore notification files under api/dev
restore-notifications:
git checkout ./api/dev/notifications

# starts ignoring a file already tracked by git. (gitignore will not apply to these files)
[group('git')]
Expand Down
1 change: 0 additions & 1 deletion web/components/Loading/Error.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { cn } from '~/components/shadcn/utils';
const props = withDefaults(
defineProps<{
class?: string;
/** hasdfsa */
loading: boolean;
error: Error | null | undefined;
}>(),
Expand Down
48 changes: 20 additions & 28 deletions web/components/Notifications/Indicator.vue
Original file line number Diff line number Diff line change
@@ -1,32 +1,21 @@
<script setup lang="ts">
import { BellIcon, ExclamationTriangleIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
import { useQuery } from '@vue/apollo-composable';
import { cn } from '~/components/shadcn/utils';
import { Importance } from '~/composables/gql/graphql';
import { Importance, type OverviewQuery } from '~/composables/gql/graphql';
import { onWatcherCleanup } from 'vue';
import { unreadOverview } from './graphql/notification.query';

const { result } = useQuery(unreadOverview, null, {
pollInterval: 2_000, // 2 seconds
});

const overview = computed(() => {
if (!result.value) {
return;
}
return result.value.notifications.overview.unread;
});
const props = defineProps<{ overview?: OverviewQuery['notifications']['overview'] }>();

const indicatorLevel = computed(() => {
if (!overview.value) {
if (!props.overview?.unread) {
return undefined;
}
switch (true) {
case overview.value.alert > 0:
case props.overview.unread.alert > 0:
return Importance.Alert;
case overview.value.warning > 0:
case props.overview.unread.warning > 0:
return Importance.Warning;
case overview.value.total > 0:
case props.overview.unread.total > 0:
return 'UNREAD';
default:
return undefined;
Expand All @@ -52,18 +41,21 @@ const icon = computed<{ component: Component; color: string } | null>(() => {
/** whether new notifications ocurred */
const hasNewNotifications = ref(false);
// watch for new notifications, set a temporary indicator when they're reveived
watch(overview, (newVal, oldVal) => {
if (!newVal || !oldVal) {
return;
watch(
() => props.overview?.unread,
(newVal, oldVal) => {
if (!newVal || !oldVal) {
return;
}
hasNewNotifications.value = newVal.total > oldVal.total;
// lifetime of 'new notification' state
const msToLive = 30_000;
const timeout = setTimeout(() => {
hasNewNotifications.value = false;
}, msToLive);
onWatcherCleanup(() => clearTimeout(timeout));
}
hasNewNotifications.value = newVal.total > oldVal.total;
// lifetime of 'new notification' state
const msToLive = 30_000;
const timeout = setTimeout(() => {
hasNewNotifications.value = false;
}, msToLive);
onWatcherCleanup(() => clearTimeout(timeout));
});
);
</script>

<template>
Expand Down
29 changes: 24 additions & 5 deletions web/components/Notifications/Sidebar.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<script setup lang="ts">
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/shadcn/sheet';
import { useMutation } from '@vue/apollo-composable';
import { useMutation, useQuery } from '@vue/apollo-composable';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- false positive :(
import { Importance, NotificationType } from '~/composables/gql/graphql';
import { archiveAllNotifications, deleteAllNotifications } from './graphql/notification.query';
import {
archiveAllNotifications,
deleteAllNotifications,
notificationsOverview,
} from './graphql/notification.query';

const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
const { mutate: deleteAll, loading: loadingDeleteAll } = useMutation(deleteAllNotifications);
Expand All @@ -23,13 +27,24 @@ const confirmAndDeleteAll = async () => {
await deleteAll();
}
};

const { result } = useQuery(notificationsOverview, null, {
pollInterval: 2_000, // 2 seconds
});

const overview = computed(() => {
if (!result.value) {
return;
}
return result.value.notifications.overview;
});
</script>

<template>
<Sheet>
<SheetTrigger @click="determineTeleportTarget">
<span class="sr-only">Notifications</span>
<NotificationsIndicator />
<NotificationsIndicator :overview="overview" />
</SheetTrigger>

<!-- We remove the horizontal padding from the container to keep the NotificationList's scrollbar in the right place -->
Expand All @@ -51,8 +66,12 @@ const confirmAndDeleteAll = async () => {
<Tabs default-value="unread" class="flex-1 flex flex-col min-h-0" activation-mode="manual">
<div class="flex flex-row justify-between items-center flex-wrap gap-5 px-6">
<TabsList class="ml-[1px]">
<TabsTrigger value="unread"> Unread </TabsTrigger>
<TabsTrigger value="archived"> Archived </TabsTrigger>
<TabsTrigger value="unread">
Unread <span v-if="overview">({{ overview.unread.total }})</span>
</TabsTrigger>
<TabsTrigger value="archived">
Archived <span v-if="overview">({{ overview.archive.total }})</span>
</TabsTrigger>
</TabsList>
<TabsContent value="unread">
<Button
Expand Down
6 changes: 5 additions & 1 deletion web/components/Notifications/graphql/notification.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,20 @@ export const deleteAllNotifications = graphql(/* GraphQL */ `
}
`);

export const unreadOverview = graphql(/* GraphQL */ `
export const notificationsOverview = graphql(/* GraphQL */ `
query Overview {
notifications {
id
overview {
unread {
info
warning
alert
total
}
archive {
total
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions web/composables/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const documents = {
"\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n": types.ArchiveAllNotificationsDocument,
"\n mutation DeleteNotification($id: String!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n": types.DeleteNotificationDocument,
"\n mutation DeleteAllNotifications {\n deleteAllNotifications {\n archive {\n total\n }\n unread {\n total\n }\n }\n }\n": types.DeleteAllNotificationsDocument,
"\n query Overview {\n notifications {\n overview {\n unread {\n info\n warning\n alert\n total\n }\n }\n }\n }\n": types.OverviewDocument,
"\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n": types.OverviewDocument,
"\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument,
"\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc,
Expand Down Expand Up @@ -77,7 +77,7 @@ export function graphql(source: "\n mutation DeleteAllNotifications {\n dele
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query Overview {\n notifications {\n overview {\n unread {\n info\n warning\n alert\n total\n }\n }\n }\n }\n"): (typeof documents)["\n query Overview {\n notifications {\n overview {\n unread {\n info\n warning\n alert\n total\n }\n }\n }\n }\n"];
export function graphql(source: "\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n"): (typeof documents)["\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
4 changes: 2 additions & 2 deletions web/composables/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@ export type DeleteAllNotificationsMutation = { __typename?: 'Mutation', deleteAl
export type OverviewQueryVariables = Exact<{ [key: string]: never; }>;


export type OverviewQuery = { __typename?: 'Query', notifications: { __typename?: 'Notifications', overview: { __typename?: 'NotificationOverview', unread: { __typename?: 'NotificationCounts', info: number, warning: number, alert: number, total: number } } } };
export type OverviewQuery = { __typename?: 'Query', notifications: { __typename?: 'Notifications', id: string, overview: { __typename?: 'NotificationOverview', unread: { __typename?: 'NotificationCounts', info: number, warning: number, alert: number, total: number }, archive: { __typename?: 'NotificationCounts', total: number } } } };

export type ConnectSignInMutationVariables = Exact<{
input: ConnectSignInInput;
Expand Down Expand Up @@ -1758,7 +1758,7 @@ export const ArchiveNotificationDocument = {"kind":"Document","definitions":[{"k
export const ArchiveAllNotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveAllNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archiveAll"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode<ArchiveAllNotificationsMutation, ArchiveAllNotificationsMutationVariables>;
export const DeleteNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationType"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode<DeleteNotificationMutation, DeleteNotificationMutationVariables>;
export const DeleteAllNotificationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteAllNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteAllNotifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]} as unknown as DocumentNode<DeleteAllNotificationsMutation, DeleteAllNotificationsMutationVariables>;
export const OverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Overview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"overview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]}}]} as unknown as DocumentNode<OverviewQuery, OverviewQueryVariables>;
export const OverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Overview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"overview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]}}]}}]}}]} as unknown as DocumentNode<OverviewQuery, OverviewQueryVariables>;
export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<ConnectSignInMutation, ConnectSignInMutationVariables>;
export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode<SignOutMutation, SignOutMutationVariables>;
export const serverStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<serverStateQuery, serverStateQueryVariables>;
Expand Down
Loading
Loading