From 16a7bd3bdaebcd3da18dfcbea79918331906c31b Mon Sep 17 00:00:00 2001 From: Dakshesh Jain <65905942+dakshesh14@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:07:55 +0530 Subject: [PATCH] feat: user issue notifications (#1523) * feat: added new issue subscriber table * dev: notification model * feat: added CRUD operation for issue subscriber * Revert "feat: added CRUD operation for issue subscriber" This reverts commit b22e0625768f0b096b5898936ace76d6882b0736. * feat: added CRUD operation for issue subscriber * dev: notification models and operations * dev: remove delete endpoint response data * dev: notification endpoints and fix bg worker for saving notifications * feat: added list and unsubscribe function in issue subscriber * dev: filter by snoozed and response update for list and permissions * dev: update issue notifications * dev: notification segregation * dev: update notifications * dev: notification filtering * dev: add issue name in notifications * dev: notification new endpoints * fix: pushing local settings * feat: notification workflow setup and made basic UI * style: improved UX with toast alerts and other interactions refactor: changed classnames according to new theme structure, changed all icons to material icons * feat: showing un-read notification count * feat: not showing 'subscribe' button on issue created by user & assigned to user not showing 'Create by you' for view & guest of the workspace --------- Co-authored-by: NarayanBavisetti Co-authored-by: pablohashescobar --- apps/app/components/icons/archive-icon.tsx | 19 + apps/app/components/icons/bell-icon.tsx | 24 + apps/app/components/icons/clock-icon.tsx | 19 + apps/app/components/icons/index.ts | 6 + .../components/icons/single-comment-icon.tsx | 24 + apps/app/components/icons/sort-icon.tsx | 19 + apps/app/components/icons/x-mark-icon.tsx | 19 + apps/app/components/issues/sidebar.tsx | 21 +- apps/app/components/notifications/index.ts | 3 + .../notifications/notification-card.tsx | 200 ++ .../notifications/notification-popover.tsx | 293 ++ .../select-snooze-till-modal.tsx | 325 ++ apps/app/components/ui/icon-name-type.d.ts | 2991 +++++++++++++++++ .../app/components/workspace/sidebar-menu.tsx | 58 +- apps/app/constants/fetch-keys.ts | 23 +- apps/app/helpers/date-time.helper.ts | 88 +- apps/app/helpers/string.helper.ts | 33 + .../use-issue-notification-subscription.tsx | 76 + apps/app/hooks/use-user-notifications.tsx | 187 ++ .../public/empty-state/empty-notification.svg | 38 + apps/app/services/notifications.service.ts | 174 + apps/app/types/index.d.ts | 1 + apps/app/types/notifications.d.ts | 56 + 23 files changed, 4665 insertions(+), 32 deletions(-) create mode 100644 apps/app/components/icons/archive-icon.tsx create mode 100644 apps/app/components/icons/bell-icon.tsx create mode 100644 apps/app/components/icons/clock-icon.tsx create mode 100644 apps/app/components/icons/single-comment-icon.tsx create mode 100644 apps/app/components/icons/sort-icon.tsx create mode 100644 apps/app/components/icons/x-mark-icon.tsx create mode 100644 apps/app/components/notifications/index.ts create mode 100644 apps/app/components/notifications/notification-card.tsx create mode 100644 apps/app/components/notifications/notification-popover.tsx create mode 100644 apps/app/components/notifications/select-snooze-till-modal.tsx create mode 100644 apps/app/components/ui/icon-name-type.d.ts create mode 100644 apps/app/hooks/use-issue-notification-subscription.tsx create mode 100644 apps/app/hooks/use-user-notifications.tsx create mode 100644 apps/app/public/empty-state/empty-notification.svg create mode 100644 apps/app/services/notifications.service.ts create mode 100644 apps/app/types/notifications.d.ts diff --git a/apps/app/components/icons/archive-icon.tsx b/apps/app/components/icons/archive-icon.tsx new file mode 100644 index 000000000..6c9c791fb --- /dev/null +++ b/apps/app/components/icons/archive-icon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const ArchiveIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( + + + +); diff --git a/apps/app/components/icons/bell-icon.tsx b/apps/app/components/icons/bell-icon.tsx new file mode 100644 index 000000000..4aafb702e --- /dev/null +++ b/apps/app/components/icons/bell-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const BellNotificationIcon: React.FC = ({ + width = "24", + height = "24", + color = "rgb(var(--color-text-200))", + className, +}) => ( + + + +); diff --git a/apps/app/components/icons/clock-icon.tsx b/apps/app/components/icons/clock-icon.tsx new file mode 100644 index 000000000..3d2273364 --- /dev/null +++ b/apps/app/components/icons/clock-icon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const ClockIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( + + + +); diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index db7aad041..183b20c97 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -82,3 +82,9 @@ export * from "./command-icon"; export * from "./color-picker-icon"; export * from "./inbox-icon"; export * from "./stacked-layers-horizontal-icon"; +export * from "./sort-icon"; +export * from "./x-mark-icon"; +export * from "./archive-icon"; +export * from "./clock-icon"; +export * from "./bell-icon"; +export * from "./single-comment-icon"; diff --git a/apps/app/components/icons/single-comment-icon.tsx b/apps/app/components/icons/single-comment-icon.tsx new file mode 100644 index 000000000..b770124a1 --- /dev/null +++ b/apps/app/components/icons/single-comment-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const SingleCommentCard: React.FC = ({ + width = "24", + height = "24", + className, + color, +}) => ( + + + +); diff --git a/apps/app/components/icons/sort-icon.tsx b/apps/app/components/icons/sort-icon.tsx new file mode 100644 index 000000000..955cdadd5 --- /dev/null +++ b/apps/app/components/icons/sort-icon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const SortIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( + + + +); diff --git a/apps/app/components/icons/x-mark-icon.tsx b/apps/app/components/icons/x-mark-icon.tsx new file mode 100644 index 000000000..afebc8273 --- /dev/null +++ b/apps/app/components/icons/x-mark-icon.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const XMarkIcon: React.FC = ({ width = "24", height = "24", className, color }) => ( + + + +); diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index ad7747844..0c99b8e21 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -9,6 +9,7 @@ import { Controller, UseFormWatch } from "react-hook-form"; // hooks import useToast from "hooks/use-toast"; import useUserAuth from "hooks/use-user-auth"; +import useUserIssueNotificationSubscription from "hooks/use-issue-notification-subscription"; // services import issuesService from "services/issues.service"; import modulesService from "services/modules.service"; @@ -30,7 +31,7 @@ import { SidebarLabelSelect, } from "components/issues"; // ui -import { CustomDatePicker } from "components/ui"; +import { CustomDatePicker, Icon } from "components/ui"; // icons import { LinkIcon, @@ -86,6 +87,9 @@ export const IssueDetailsSidebar: React.FC = ({ const { user } = useUserAuth(); + const { loading, handleSubscribe, handleUnsubscribe, subscribed } = + useUserIssueNotificationSubscription(workspaceSlug, projectId, issueId); + const { memberRole } = useProjectMyMembership(); const { setToastAlert } = useToast(); @@ -232,6 +236,21 @@ export const IssueDetailsSidebar: React.FC = ({ {issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
+ {issueDetail?.created_by !== user?.id && + !issueDetail?.assignees.includes(user?.id ?? "") && + (fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( + + )} {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && ( + ))} +
+ + + ); +}; diff --git a/apps/app/components/notifications/notification-popover.tsx b/apps/app/components/notifications/notification-popover.tsx new file mode 100644 index 000000000..486a091e4 --- /dev/null +++ b/apps/app/components/notifications/notification-popover.tsx @@ -0,0 +1,293 @@ +import React, { Fragment } from "react"; + +import Image from "next/image"; +import { useRouter } from "next/router"; + +// hooks +import useTheme from "hooks/use-theme"; + +import { Popover, Transition } from "@headlessui/react"; + +// hooks +import useWorkspaceMembers from "hooks/use-workspace-members"; +import useUserNotification from "hooks/use-user-notifications"; + +// components +import { Spinner, Icon } from "components/ui"; +import { SnoozeNotificationModal, NotificationCard } from "components/notifications"; + +// helpers +import { getNumberCount } from "helpers/string.helper"; + +// type +import type { NotificationType } from "types"; + +export const NotificationPopover = () => { + const { + notifications, + archived, + readNotification, + selectedNotificationForSnooze, + selectedTab, + setArchived, + setReadNotification, + setSelectedNotificationForSnooze, + setSelectedTab, + setSnoozed, + snoozed, + notificationsMutate, + markNotificationArchivedStatus, + markNotificationReadStatus, + markSnoozeNotification, + notificationCount, + totalNotificationCount, + } = useUserNotification(); + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { isOwner, isMember } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + // theme context + const { collapsed: sidebarCollapse } = useTheme(); + + const notificationTabs: Array<{ + label: string; + value: NotificationType; + unreadCount?: number; + }> = [ + { + label: "My Issues", + value: "assigned", + unreadCount: notificationCount?.my_issues, + }, + { + label: "Created by me", + value: "created", + unreadCount: notificationCount?.created_issues, + }, + { + label: "Subscribed", + value: "watching", + unreadCount: notificationCount?.watching_notifications, + }, + ]; + + return ( + <> + setSelectedNotificationForSnooze(null)} + onSubmit={markSnoozeNotification} + notification={ + notifications?.find( + (notification) => notification.id === selectedNotificationForSnooze + ) || null + } + onSuccess={() => { + notificationsMutate(); + setSelectedNotificationForSnooze(null); + }} + /> + + {({ open: isActive, close: closePopover }) => ( + <> + + + {sidebarCollapse ? null : Notifications} + {totalNotificationCount && totalNotificationCount > 0 ? ( + + {getNumberCount(totalNotificationCount)} + + ) : null} + + + +
+

+ Notifications +

+
+ + + + + +
+
+ +
+ {snoozed || archived || readNotification ? ( +
+
+ +
+
+ ) : ( +
+ +
+ )} +
+ +
+ {notifications ? ( + notifications.filter( + (notification) => notification.data.issue_activity.field !== "None" + ).length > 0 ? ( + notifications.map((notification) => ( + + )) + ) : ( +
+ Empty +

+ You{"'"}re updated with all the notifications +

+

+ You have read all the notifications. +

+
+ ) + ) : ( +
+ +
+ )} +
+
+
+ + )} +
+ + ); +}; diff --git a/apps/app/components/notifications/select-snooze-till-modal.tsx b/apps/app/components/notifications/select-snooze-till-modal.tsx new file mode 100644 index 000000000..7bd78862b --- /dev/null +++ b/apps/app/components/notifications/select-snooze-till-modal.tsx @@ -0,0 +1,325 @@ +import React, { Fragment } from "react"; + +// next +import { useRouter } from "next/router"; + +// react hook form +import { useForm, Controller } from "react-hook-form"; + +import { Transition, Dialog, Listbox } from "@headlessui/react"; + +// date helper +import { getDatesAfterCurrentDate, getTimestampAfterCurrentTime } from "helpers/date-time.helper"; + +// hooks +import useToast from "hooks/use-toast"; + +// components +import { PrimaryButton, SecondaryButton, Icon } from "components/ui"; + +// types +import type { IUserNotification } from "types"; + +type SnoozeModalProps = { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + notification: IUserNotification | null; + onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise; +}; + +const dates = getDatesAfterCurrentDate(); +const timeStamps = getTimestampAfterCurrentTime(); + +const defaultValues = { + time: null, + date: null, +}; + +export const SnoozeNotificationModal: React.FC = (props) => { + const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { setToastAlert } = useToast(); + + const { + formState: { isSubmitting }, + reset, + handleSubmit, + control, + } = useForm({ + defaultValues, + }); + + const onSubmit = async (formData: any) => { + if (!workspaceSlug || !notification) return; + + const dateTime = new Date( + `${formData.date.toLocaleDateString()} ${formData.time.toLocaleTimeString()}` + ); + + await handleSubmitSnooze(notification.id, dateTime).then(() => { + onClose(); + onSuccess(); + setToastAlert({ + title: "Notification snoozed", + message: "Notification snoozed successfully", + type: "success", + }); + }); + }; + + const handleClose = () => { + onClose(); + const timeout = setTimeout(() => { + reset(); + clearTimeout(timeout); + }, 500); + }; + + return ( + + + +
+ + +
+
+ + +
+
+ + Customize Snooze Time + + +
+ +
+
+ +
+
+ ( + + {({ open }) => ( + <> +
+ + + + {value + ? new Date(value)?.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }) + : "Select Time"} + + + + + + + + + {timeStamps.map((time, index) => ( + + `relative cursor-default select-none py-2 pl-3 pr-9 ${ + active + ? "bg-custom-primary-100/80 text-custom-text-100" + : "text-custom-text-700" + }` + } + value={time.value} + > + {({ selected, active }) => ( + <> +
+ + {time.label} + +
+ + {selected ? ( + + + ) : null} + + )} +
+ ))} +
+
+
+ + )} +
+ )} + /> +
+
+ ( + + {({ open }) => ( + <> +
+ + + + {value + ? new Date(value)?.toLocaleDateString([], { + day: "numeric", + month: "long", + year: "numeric", + }) + : "Select Date"} + + + + + + + + + {dates.map((date, index) => ( + + `relative cursor-default select-none py-2 pl-3 pr-9 ${ + active + ? "bg-custom-primary-100/80 text-custom-text-100" + : "text-custom-text-700" + }` + } + value={date.value} + > + {({ selected, active }) => ( + <> +
+ + {date.label} + +
+ + {selected ? ( + + + ) : null} + + )} +
+ ))} +
+
+
+ + )} +
+ )} + /> +
+
+ +
+
+ Cancel + + {isSubmitting ? "Submitting..." : "Submit"} + +
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/apps/app/components/ui/icon-name-type.d.ts b/apps/app/components/ui/icon-name-type.d.ts new file mode 100644 index 000000000..0d886b08e --- /dev/null +++ b/apps/app/components/ui/icon-name-type.d.ts @@ -0,0 +1,2991 @@ +type IconName = + | "search" + | "home" + | "menu" + | "close" + | "settings" + | "done" + | "expand_more" + | "check_circle" + | "favorite" + | "add" + | "delete" + | "arrow_back" + | "star" + | "chevron_right" + | "logout" + | "arrow_forward_ios" + | "add_circle" + | "cancel" + | "arrow_back_ios" + | "arrow_forward" + | "arrow_drop_down" + | "more_vert" + | "check" + | "check_box" + | "toggle_on" + | "grade" + | "open_in_new" + | "check_box_outline_blank" + | "refresh" + | "login" + | "chevron_left" + | "expand_less" + | "radio_button_unchecked" + | "more_horiz" + | "apps" + | "arrow_right_alt" + | "radio_button_checked" + | "download" + | "remove" + | "bolt" + | "toggle_off" + | "arrow_upward" + | "filter_list" + | "delete_forever" + | "autorenew" + | "key" + | "arrow_downward" + | "sync" + | "sort" + | "block" + | "add_box" + | "arrow_back_ios_new" + | "restart_alt" + | "menu_open" + | "shopping_cart_checkout" + | "expand_circle_down" + | "backspace" + | "undo" + | "arrow_circle_right" + | "done_all" + | "arrow_right" + | "do_not_disturb_on" + | "open_in_full" + | "manage_search" + | "double_arrow" + | "sync_alt" + | "zoom_in" + | "done_outline" + | "drag_indicator" + | "fullscreen" + | "keyboard_double_arrow_right" + | "star_half" + | "settings_accessibility" + | "ios_share" + | "arrow_drop_up" + | "reply" + | "exit_to_app" + | "unfold_more" + | "library_add" + | "cached" + | "select_check_box" + | "terminal" + | "change_circle" + | "disabled_by_default" + | "swap_horiz" + | "swap_vert" + | "close_fullscreen" + | "app_registration" + | "download_for_offline" + | "arrow_circle_left" + | "arrow_circle_up" + | "file_open" + | "minimize" + | "open_with" + | "keyboard_double_arrow_left" + | "dataset" + | "add_task" + | "start" + | "keyboard_double_arrow_down" + | "keyboard_voice" + | "create_new_folder" + | "forward" + | "downloading" + | "settings_applications" + | "compare_arrows" + | "redo" + | "publish" + | "arrow_left" + | "zoom_out" + | "html" + | "token" + | "switch_access_shortcut" + | "arrow_circle_down" + | "fullscreen_exit" + | "sort_by_alpha" + | "delete_sweep" + | "indeterminate_check_box" + | "first_page" + | "keyboard_double_arrow_up" + | "view_timeline" + | "arrow_drop_down_circle" + | "assistant_navigation" + | "settings_backup_restore" + | "sync_problem" + | "clear_all" + | "density_medium" + | "heart_plus" + | "filter_alt_off" + | "expand" + | "last_page" + | "subdirectory_arrow_right" + | "unfold_less" + | "arrow_outward" + | "download_done" + | "123" + | "swipe_left" + | "saved_search" + | "system_update_alt" + | "place_item" + | "maximize" + | "javascript" + | "output" + | "search_off" + | "swipe_up" + | "fit_screen" + | "select_all" + | "dynamic_form" + | "hide_source" + | "swipe_right" + | "switch_access_shortcut_add" + | "browse_gallery" + | "check_small" + | "css" + | "density_small" + | "assistant_direction" + | "youtube_searched_for" + | "file_download_done" + | "move_up" + | "swap_horizontal_circle" + | "data_thresholding" + | "install_mobile" + | "move_down" + | "abc" + | "dataset_linked" + | "restore_from_trash" + | "enable" + | "install_desktop" + | "keyboard_command_key" + | "view_kanban" + | "browse_activity" + | "reply_all" + | "switch_left" + | "compress" + | "swipe_down" + | "swap_vertical_circle" + | "remove_done" + | "apps_outage" + | "filter_list_off" + | "star_rate" + | "hide" + | "switch_right" + | "swipe_vertical" + | "more_up" + | "sync_disabled" + | "pinch" + | "keyboard_control_key" + | "eject" + | "key_off" + | "php" + | "subdirectory_arrow_left" + | "view_cozy" + | "transcribe" + | "do_not_disturb_off" + | "send_time_extension" + | "width_normal" + | "heart_minus" + | "view_comfy_alt" + | "share_reviews" + | "width_full" + | "unfold_more_double" + | "view_compact_alt" + | "file_download_off" + | "extension_off" + | "open_in_new_off" + | "check_indeterminate_small" + | "more_down" + | "width_wide" + | "repartition" + | "density_large" + | "swipe_left_alt" + | "swipe_down_alt" + | "swipe_right_alt" + | "swipe_up_alt" + | "unfold_less_double" + | "keyboard_option_key" + | "cycle" + | "hls" + | "hls_off" + | "progress_activity" + | "file_upload_off" + | "rebase" + | "expand_content" + | "expand_all" + | "rebase_edit" + | "collapse_all" + | "empty_dashboard" + | "arrow_split" + | "quick_reference_all" + | "arrow_upward_alt" + | "switches" + | "arrow_downward_alt" + | "directory_sync" + | "quick_reference" + | "cards" + | "deployed_code" + | "side_navigation" + | "data_check" + | "acute" + | "bubble" + | "left_click" + | "sync_saved_locally" + | "data_alert" + | "move_item" + | "steppers" + | "stack" + | "stat_3" + | "arrow_left_alt" + | "clock_loader_60" + | "page_info" + | "data_info_alert" + | "search_check" + | "question_exchange" + | "point_scan" + | "preliminary" + | "toolbar" + | "clock_loader_10" + | "sweep" + | "new_window" + | "right_panel_close" + | "captive_portal" + | "star_rate_half" + | "page_control" + | "patient_list" + | "right_panel_open" + | "stat_2" + | "stat_minus_1" + | "unknown_med" + | "stat_minus_2" + | "clock_loader_40" + | "dialogs" + | "capture" + | "step_into" + | "arrow_insert" + | "heart_check" + | "magnification_large" + | "partner_reports" + | "stack_star" + | "drag_pan" + | "magnification_small" + | "stat_1" + | "clock_loader_90" + | "dropdown" + | "left_panel_close" + | "left_panel_open" + | "sliders" + | "bottom_panel_open" + | "clock_loader_80" + | "move_group" + | "bottom_navigation" + | "stack_off" + | "all_match" + | "clock_loader_20" + | "step" + | "stat_minus_3" + | "bottom_drawer" + | "buttons_alt" + | "chip_extraction" + | "bottom_sheets" + | "chronic" + | "pip" + | "resize" + | "tabs" + | "chips" + | "iframe" + | "input_circle" + | "reopen_window" + | "ripples" + | "rule_settings" + | "toast" + | "unknown_5" + | "share_windows" + | "shelf_position" + | "go_to_line" + | "right_click" + | "subheader" + | "bottom_right_click" + | "drag_click" + | "expand_circle_up" + | "step_out" + | "step_over" + | "switch_access" + | "arrow_and_edge" + | "error_med" + | "app_badging" + | "deployed_code_alert" + | "event_list" + | "move_selection_left" + | "move_selection_right" + | "output_circle" + | "amend" + | "pip_exit" + | "arrow_or_edge" + | "arrow_top_right" + | "bottom_app_bar" + | "deployed_code_update" + | "iframe_off" + | "shelf_auto_hide" + | "arrow_range" + | "bottom_panel_close" + | "bubbles" + | "position_bottom_right" + | "arrow_top_left" + | "arrows_outward" + | "back_to_tab" + | "jump_to_element" + | "move_selection_down" + | "move_selection_up" + | "open_in_new_down" + | "deployed_code_history" + | "position_bottom_left" + | "position_top_right" + | "expand_circle_right" + | "person" + | "group" + | "share" + | "thumb_up" + | "groups" + | "person_add" + | "public" + | "handshake" + | "support_agent" + | "face" + | "sentiment_satisfied" + | "rocket_launch" + | "group_add" + | "workspace_premium" + | "psychology" + | "diversity_3" + | "emoji_objects" + | "water_drop" + | "eco" + | "pets" + | "travel_explore" + | "mood" + | "sunny" + | "quiz" + | "health_and_safety" + | "sentiment_dissatisfied" + | "sentiment_very_satisfied" + | "military_tech" + | "thumb_down" + | "gavel" + | "recycling" + | "diamond" + | "monitor_heart" + | "emoji_people" + | "diversity_1" + | "workspaces" + | "vaccines" + | "compost" + | "forest" + | "recommend" + | "waving_hand" + | "person_remove" + | "wc" + | "medication" + | "sentiment_neutral" + | "group_work" + | "sentiment_very_dissatisfied" + | "diversity_2" + | "front_hand" + | "psychology_alt" + | "cruelty_free" + | "man" + | "medical_information" + | "coronavirus" + | "add_reaction" + | "rocket" + | "female" + | "potted_plant" + | "emoji_nature" + | "rainy" + | "person_off" + | "cookie" + | "woman" + | "connect_without_contact" + | "mood_bad" + | "male" + | "bedtime" + | "solar_power" + | "communication" + | "thunderstorm" + | "groups_2" + | "partly_cloudy_day" + | "thumbs_up_down" + | "emoji_flags" + | "masks" + | "hive" + | "heart_broken" + | "sentiment_extremely_dissatisfied" + | "clear_day" + | "boy" + | "whatshot" + | "cloudy_snowing" + | "emoji_food_beverage" + | "wind_power" + | "emoji_transportation" + | "elderly" + | "face_6" + | "reduce_capacity" + | "sick" + | "pregnant_woman" + | "face_3" + | "bloodtype" + | "group_remove" + | "egg" + | "medication_liquid" + | "groups_3" + | "co2" + | "clear_night" + | "weight" + | "skull" + | "follow_the_signs" + | "face_4" + | "emoji_events" + | "oil_barrel" + | "transgender" + | "elderly_woman" + | "clean_hands" + | "sanitizer" + | "person_2" + | "bring_your_own_ip" + | "public_off" + | "face_2" + | "social_distance" + | "routine" + | "sign_language" + | "south_america" + | "sunny_snowing" + | "emoji_symbols" + | "garden_cart" + | "flood" + | "face_5" + | "egg_alt" + | "cyclone" + | "girl" + | "person_4" + | "dentistry" + | "tsunami" + | "group_off" + | "outdoor_garden" + | "partly_cloudy_night" + | "severe_cold" + | "snowing" + | "person_3" + | "tornado" + | "landslide" + | "vaping_rooms" + | "safety_divider" + | "foggy" + | "woman_2" + | "no_adult_content" + | "volcano" + | "man_2" + | "blind" + | "18_up_rating" + | "6_ft_apart" + | "vape_free" + | "not_accessible" + | "man_4" + | "radiology" + | "rib_cage" + | "hand_bones" + | "bedtime_off" + | "rheumatology" + | "man_3" + | "orthopedics" + | "tibia" + | "skeleton" + | "partner_exchange" + | "humerus" + | "agender" + | "femur" + | "foot_bones" + | "tibia_alt" + | "femur_alt" + | "humerus_alt" + | "communities" + | "diversity_4" + | "ulna_radius" + | "ulna_radius_alt" + | "specific_gravity" + | "cognition" + | "breastfeeding" + | "eyeglasses" + | "psychiatry" + | "labs" + | "crowdsource" + | "footprint" + | "vital_signs" + | "nutrition" + | "neurology" + | "social_leaderboard" + | "demography" + | "globe_asia" + | "stethoscope" + | "conditions" + | "lab_research" + | "clinical_notes" + | "sentiment_excited" + | "sentiment_stressed" + | "taunt" + | "altitude" + | "glucose" + | "globe_uk" + | "mystery" + | "strategy" + | "home_health" + | "sentiment_calm" + | "crossword" + | "prayer_times" + | "recent_patient" + | "chess" + | "pill" + | "sentiment_sad" + | "share_off" + | "weather_hail" + | "cardiology" + | "falling" + | "helicopter" + | "mist" + | "prescriptions" + | "sentiment_content" + | "wrist" + | "deceased" + | "genetics" + | "weather_mix" + | "dew_point" + | "sentiment_frustrated" + | "cheer" + | "metabolism" + | "microbiology" + | "body_system" + | "earthquake" + | "ent" + | "explosion" + | "pulmonology" + | "stethoscope_check" + | "infrared" + | "oxygen_saturation" + | "person_raised_hand" + | "sentiment_worried" + | "sword_rose" + | "barefoot" + | "cookie_off" + | "domino_mask" + | "emoticon" + | "humidity_percentage" + | "playing_cards" + | "stethoscope_arrow" + | "water_lux" + | "comic_bubble" + | "lab_panel" + | "mountain_flag" + | "ophthalmology" + | "water_bottle" + | "water_do" + | "water_voc" + | "allergies" + | "allergy" + | "blood_pressure" + | "dermatology" + | "gynecology" + | "immunology" + | "manga" + | "oncology" + | "oral_disease" + | "person_apron" + | "short_stay" + | "water_orp" + | "water_ph" + | "body_fat" + | "endocrinology" + | "folded_hands" + | "hematology" + | "inpatient" + | "mixture_med" + | "moving_beds" + | "nephrology" + | "respiratory_rate" + | "symptoms" + | "ward" + | "congenital" + | "gastroenterology" + | "medical_mask" + | "outpatient" + | "outpatient_med" + | "pediatrics" + | "procedure" + | "rainy_snow" + | "salinity" + | "surgical" + | "syringe" + | "urology" + | "wounds_injuries" + | "admin_meds" + | "fluid" + | "fluid_balance" + | "fluid_med" + | "pill_off" + | "pregnancy" + | "total_dissolved_solids" + | "rainy_heavy" + | "rainy_light" + | "snowing_heavy" + | "water_bottle_large" + | "water_ec" + | "account_circle" + | "info" + | "visibility" + | "calendar_month" + | "schedule" + | "help" + | "language" + | "warning" + | "lock" + | "error" + | "visibility_off" + | "verified" + | "manage_accounts" + | "history" + | "task_alt" + | "event" + | "bookmark" + | "calendar_today" + | "question_mark" + | "lightbulb" + | "fingerprint" + | "category" + | "update" + | "lock_open" + | "priority_high" + | "code" + | "build" + | "date_range" + | "upload_file" + | "supervisor_account" + | "event_available" + | "ads_click" + | "today" + | "touch_app" + | "pending" + | "preview" + | "stars" + | "new_releases" + | "account_box" + | "celebration" + | "how_to_reg" + | "translate" + | "bug_report" + | "push_pin" + | "alarm" + | "edit_calendar" + | "edit_square" + | "label" + | "extension" + | "event_note" + | "record_voice_over" + | "rate_review" + | "web" + | "hourglass_empty" + | "published_with_changes" + | "support" + | "notification_important" + | "upload" + | "help_center" + | "accessibility_new" + | "bookmarks" + | "pan_tool_alt" + | "supervised_user_circle" + | "dangerous" + | "collections_bookmark" + | "interests" + | "all_inclusive" + | "rule" + | "change_history" + | "priority" + | "event_upcoming" + | "build_circle" + | "wysiwyg" + | "pan_tool" + | "api" + | "circle_notifications" + | "hotel_class" + | "manage_history" + | "web_asset" + | "accessible" + | "upgrade" + | "bookmark_add" + | "lock_reset" + | "input" + | "event_busy" + | "more_time" + | "flutter_dash" + | "model_training" + | "save_as" + | "backup" + | "accessibility" + | "dynamic_feed" + | "alarm_on" + | "pageview" + | "home_app_logo" + | "perm_contact_calendar" + | "label_important" + | "history_toggle_off" + | "approval" + | "square_foot" + | "more" + | "swipe" + | "component_exchange" + | "event_repeat" + | "bookmark_added" + | "app_shortcut" + | "unpublished" + | "open_in_browser" + | "offline_bolt" + | "notification_add" + | "no_accounts" + | "free_cancellation" + | "running_with_errors" + | "background_replace" + | "anchor" + | "webhook" + | "3d_rotation" + | "lock_person" + | "new_label" + | "lock_clock" + | "accessible_forward" + | "auto_delete" + | "add_alert" + | "domain_verification" + | "outbound" + | "hand_gesture" + | "tab" + | "settings_power" + | "chrome_reader_mode" + | "online_prediction" + | "gesture" + | "edit_notifications" + | "lightbulb_circle" + | "find_replace" + | "backup_table" + | "offline_pin" + | "wifi_protected_setup" + | "ad_units" + | "http" + | "bookmark_remove" + | "alarm_add" + | "pinch_zoom_out" + | "on_device_training" + | "snooze" + | "batch_prediction" + | "code_off" + | "pinch_zoom_in" + | "commit" + | "hourglass_disabled" + | "settings_overscan" + | "polymer" + | "target" + | "logo_dev" + | "youtube_activity" + | "time_auto" + | "voice_over_off" + | "person_add_disabled" + | "alarm_off" + | "update_disabled" + | "timer_10_alt_1" + | "rounded_corner" + | "label_off" + | "all_out" + | "timer_3_alt_1" + | "tab_unselected" + | "rsvp" + | "web_asset_off" + | "pin_invoke" + | "pin_end" + | "code_blocks" + | "approval_delegation" + | "arrow_selector_tool" + | "developer_guide" + | "feature_search" + | "reminder" + | "lists" + | "problem" + | "visibility_lock" + | "browse" + | "award_star" + | "data_loss_prevention" + | "ad_group" + | "select_window" + | "ad" + | "release_alert" + | "settings_account_box" + | "shadow" + | "pan_zoom" + | "draft_orders" + | "circles_ext" + | "ad_group_off" + | "add_ad" + | "account_circle_off" + | "gesture_select" + | "lock_open_right" + | "watch_screentime" + | "circles" + | "select_window_off" + | "shift" + | "help_clinic" + | "scrollable_header" + | "bookmark_manager" + | "ad_off" + | "alarm_smart_wake" + | "preview_off" + | "supervised_user_circle_off" + | "water_lock" + | "domain_verification_off" + | "measuring_tape" + | "warning_off" + | "info_i" + | "shift_lock" + | "mail" + | "call" + | "notifications" + | "send" + | "chat" + | "link" + | "forum" + | "inventory_2" + | "phone_in_talk" + | "contact_support" + | "chat_bubble" + | "notifications_active" + | "alternate_email" + | "sms" + | "comment" + | "power_settings_new" + | "hub" + | "person_search" + | "import_contacts" + | "contact_mail" + | "contacts" + | "live_help" + | "forward_to_inbox" + | "mark_email_unread" + | "reviews" + | "lan" + | "contact_phone" + | "mode_comment" + | "hourglass_top" + | "inbox" + | "drafts" + | "outgoing_mail" + | "hourglass_bottom" + | "mark_email_read" + | "link_off" + | "calendar_add_on" + | "add_comment" + | "phone_enabled" + | "speaker_notes" + | "perm_phone_msg" + | "g_translate" + | "co_present" + | "notifications_off" + | "topic" + | "call_end" + | "cell_tower" + | "mark_chat_unread" + | "schedule_send" + | "dialpad" + | "call_made" + | "satellite_alt" + | "mark_unread_chat_alt" + | "unarchive" + | "3p" + | "cancel_presentation" + | "mark_as_unread" + | "move_to_inbox" + | "attach_email" + | "phonelink_ring" + | "next_plan" + | "unsubscribe" + | "phone_callback" + | "call_received" + | "settings_phone" + | "call_split" + | "present_to_all" + | "add_call" + | "markunread_mailbox" + | "all_inbox" + | "phone_forwarded" + | "voice_chat" + | "mail_lock" + | "attribution" + | "voicemail" + | "duo" + | "contact_emergency" + | "mark_chat_read" + | "upcoming" + | "phone_disabled" + | "swap_calls" + | "outbox" + | "phonelink_lock" + | "spoke" + | "cancel_schedule_send" + | "ring_volume" + | "notifications_paused" + | "picture_in_picture_alt" + | "quickreply" + | "phone_missed" + | "comment_bank" + | "send_and_archive" + | "chat_add_on" + | "settings_bluetooth" + | "phonelink_erase" + | "picture_in_picture" + | "comments_disabled" + | "video_chat" + | "score" + | "pause_presentation" + | "cell_wifi" + | "speaker_phone" + | "speaker_notes_off" + | "auto_read_play" + | "mms" + | "call_merge" + | "play_for_work" + | "call_missed_outgoing" + | "call_missed" + | "wifi_channel" + | "calendar_apps_script" + | "phone_paused" + | "rtt" + | "auto_read_pause" + | "phone_locked" + | "chat_apps_script" + | "wifi_calling" + | "dialer_sip" + | "nat" + | "sip" + | "phone_bluetooth_speaker" + | "e911_avatar" + | "inbox_customize" + | "stacked_email" + | "tooltip" + | "business_messages" + | "notification_multiple" + | "chat_error" + | "ods" + | "chat_paste_go" + | "signal_cellular_add" + | "call_log" + | "outbox_alt" + | "call_quality" + | "odt" + | "stacked_inbox" + | "phonelink_ring_off" + | "network_manage" + | "wifi_proxy" + | "network_intelligence_history" + | "wifi_add" + | "network_intelligence_update" + | "edit" + | "photo_camera" + | "filter_alt" + | "image" + | "navigate_next" + | "tune" + | "timer" + | "picture_as_pdf" + | "circle" + | "palette" + | "add_a_photo" + | "photo_library" + | "navigate_before" + | "auto_stories" + | "add_photo_alternate" + | "brush" + | "imagesmode" + | "nature" + | "flash_on" + | "wb_sunny" + | "camera" + | "straighten" + | "looks_one" + | "landscape" + | "timelapse" + | "slideshow" + | "grid_on" + | "rotate_right" + | "crop_square" + | "adjust" + | "style" + | "crop_free" + | "aspect_ratio" + | "brightness_6" + | "photo" + | "nature_people" + | "filter_vintage" + | "image_search" + | "crop" + | "blur_on" + | "center_focus_strong" + | "contrast" + | "compare" + | "looks_two" + | "rotate_left" + | "colorize" + | "flare" + | "filter_none" + | "wb_incandescent" + | "filter_drama" + | "healing" + | "looks_3" + | "wb_twilight" + | "brightness_5" + | "invert_colors" + | "lens" + | "animation" + | "opacity" + | "incomplete_circle" + | "broken_image" + | "filter_center_focus" + | "add_to_photos" + | "brightness_4" + | "flip" + | "flash_off" + | "center_focus_weak" + | "auto_awesome_motion" + | "mic_external_on" + | "flip_camera_android" + | "lens_blur" + | "no_photography" + | "details" + | "grain" + | "image_not_supported" + | "web_stories" + | "panorama" + | "dehaze" + | "gif_box" + | "flaky" + | "loupe" + | "exposure_plus_1" + | "settings_brightness" + | "texture" + | "auto_awesome_mosaic" + | "looks_4" + | "filter_1" + | "timer_off" + | "flip_camera_ios" + | "panorama_fish_eye" + | "view_compact" + | "brightness_1" + | "filter" + | "control_point_duplicate" + | "photo_camera_front" + | "brightness_7" + | "photo_album" + | "transform" + | "linked_camera" + | "view_comfy" + | "crop_16_9" + | "looks" + | "hide_image" + | "looks_5" + | "exposure" + | "rotate_90_degrees_ccw" + | "filter_hdr" + | "gif" + | "brightness_3" + | "hdr_strong" + | "leak_add" + | "crop_7_5" + | "gradient" + | "hdr_auto" + | "crop_portrait" + | "vrpano" + | "camera_roll" + | "blur_circular" + | "motion_photos_auto" + | "rotate_90_degrees_cw" + | "photo_size_select_small" + | "brightness_2" + | "shutter_speed" + | "looks_6" + | "flash_auto" + | "camera_front" + | "crop_landscape" + | "filter_2" + | "filter_tilt_shift" + | "monochrome_photos" + | "deblur" + | "night_sight_auto" + | "crop_5_4" + | "hdr_weak" + | "filter_4" + | "motion_photos_paused" + | "filter_3" + | "crop_rotate" + | "crop_3_2" + | "tonality" + | "switch_camera" + | "photo_frame" + | "exposure_zero" + | "ev_shadow" + | "fluorescent" + | "macro_off" + | "photo_size_select_large" + | "filter_frames" + | "party_mode" + | "raw_on" + | "motion_blur" + | "exposure_plus_2" + | "photo_camera_back" + | "blur_linear" + | "exposure_neg_1" + | "wb_iridescent" + | "filter_b_and_w" + | "switch_video" + | "motion_photos_off" + | "panorama_horizontal" + | "filter_5" + | "blur_medium" + | "invert_colors_off" + | "face_retouching_off" + | "filter_7" + | "burst_mode" + | "panorama_photosphere" + | "hdr_on" + | "grid_off" + | "filter_9_plus" + | "filter_8" + | "blur_short" + | "timer_10" + | "filter_9" + | "dirty_lens" + | "wb_shade" + | "no_flash" + | "filter_6" + | "image_aspect_ratio" + | "trail_length" + | "exposure_neg_2" + | "vignette" + | "timer_3" + | "leak_remove" + | "60fps_select" + | "blur_off" + | "30fps_select" + | "perm_camera_mic" + | "mic_external_off" + | "trail_length_medium" + | "camera_rear" + | "panorama_vertical" + | "trail_length_short" + | "night_sight_auto_off" + | "autofps_select" + | "panorama_wide_angle" + | "mp" + | "hdr_off" + | "24mp" + | "hdr_on_select" + | "hdr_enhanced_select" + | "22mp" + | "10mp" + | "12mp" + | "18mp" + | "hdr_auto_select" + | "hdr_plus" + | "raw_off" + | "wb_auto" + | "9mp" + | "13mp" + | "20mp" + | "5mp" + | "7mp" + | "15mp" + | "hdr_off_select" + | "16mp" + | "hevc" + | "19mp" + | "14mp" + | "23mp" + | "2mp" + | "8mp" + | "3mp" + | "6mp" + | "11mp" + | "21mp" + | "17mp" + | "4mp" + | "gallery_thumbnail" + | "motion_photos_on" + | "masked_transitions" + | "photo_prints" + | "settings_photo_camera" + | "planner_banner_ad_pt" + | "settings_panorama" + | "unknown_2" + | "vr180_create2d" + | "settings_video_camera" + | "motion_mode" + | "settings_motion_mode" + | "settings_night_sight" + | "50mp" + | "background_dot_large" + | "background_grid_small" + | "settings_cinematic_blur" + | "settings_timelapse" + | "macro_auto" + | "settings_b_roll" + | "high_density" + | "contrast_rtl_off" + | "low_density" + | "settings_slow_motion" + | "shopping_cart" + | "payments" + | "shopping_bag" + | "monitoring" + | "credit_card" + | "receipt_long" + | "attach_money" + | "storefront" + | "sell" + | "trending_up" + | "database" + | "account_balance" + | "work" + | "paid" + | "account_balance_wallet" + | "analytics" + | "query_stats" + | "store" + | "savings" + | "monetization_on" + | "calculate" + | "qr_code_scanner" + | "bar_chart" + | "add_shopping_cart" + | "account_tree" + | "receipt" + | "redeem" + | "currency_exchange" + | "trending_flat" + | "shopping_basket" + | "qr_code_2" + | "domain" + | "precision_manufacturing" + | "qr_code" + | "leaderboard" + | "corporate_fare" + | "timeline" + | "insert_chart" + | "currency_rupee" + | "show_chart" + | "wallet" + | "euro" + | "work_history" + | "meeting_room" + | "credit_score" + | "barcode_scanner" + | "pie_chart" + | "loyalty" + | "copyright" + | "barcode" + | "conversion_path" + | "track_changes" + | "trending_down" + | "price_check" + | "euro_symbol" + | "schema" + | "add_business" + | "add_card" + | "card_membership" + | "currency_bitcoin" + | "price_change" + | "production_quantity_limits" + | "donut_large" + | "tenancy" + | "data_exploration" + | "bubble_chart" + | "donut_small" + | "contactless" + | "money" + | "stacked_line_chart" + | "stacked_bar_chart" + | "toll" + | "money_off" + | "cases" + | "currency_yen" + | "currency_pound" + | "area_chart" + | "atr" + | "remove_shopping_cart" + | "room_preferences" + | "add_chart" + | "shop" + | "domain_add" + | "card_travel" + | "grouped_bar_chart" + | "legend_toggle" + | "scatter_plot" + | "credit_card_off" + | "mediation" + | "ssid_chart" + | "candlestick_chart" + | "waterfall_chart" + | "currency_ruble" + | "full_stacked_bar_chart" + | "domain_disabled" + | "strikethrough_s" + | "shop_two" + | "next_week" + | "atm" + | "multiline_chart" + | "currency_lira" + | "currency_yuan" + | "no_meeting_room" + | "currency_franc" + | "troubleshoot" + | "finance" + | "data_table" + | "bid_landscape" + | "contactless_off" + | "bar_chart_4_bars" + | "universal_currency_alt" + | "chart_data" + | "podium" + | "order_approve" + | "family_history" + | "conveyor_belt" + | "send_money" + | "flowsheet" + | "forklift" + | "qr_code_2_add" + | "front_loader" + | "inactive_order" + | "pallet" + | "bid_landscape_disabled" + | "barcode_reader" + | "box" + | "trolley" + | "box_add" + | "conversion_path_off" + | "order_play" + | "work_alert" + | "box_edit" + | "work_update" + | "pin_drop" + | "location_on" + | "map" + | "home_pin" + | "explore" + | "restaurant" + | "flag" + | "my_location" + | "local_fire_department" + | "person_pin_circle" + | "local_mall" + | "near_me" + | "where_to_vote" + | "business_center" + | "east" + | "restaurant_menu" + | "handyman" + | "factory" + | "local_library" + | "medical_services" + | "home_work" + | "layers" + | "local_activity" + | "share_location" + | "emergency" + | "north_east" + | "add_location" + | "fastfood" + | "navigation" + | "warehouse" + | "person_pin" + | "local_parking" + | "home_repair_service" + | "local_hospital" + | "south" + | "local_police" + | "zoom_out_map" + | "location_searching" + | "local_florist" + | "location_away" + | "crisis_alert" + | "west" + | "local_gas_station" + | "park" + | "maps_ugc" + | "cleaning_services" + | "local_atm" + | "package" + | "360" + | "electrical_services" + | "north" + | "flag_circle" + | "add_location_alt" + | "directions" + | "fmd_bad" + | "theater_comedy" + | "local_drink" + | "location_home" + | "local_pizza" + | "local_post_office" + | "not_listed_location" + | "wine_bar" + | "beenhere" + | "local_convenience_store" + | "signpost" + | "alt_route" + | "tour" + | "trip_origin" + | "church" + | "traffic" + | "local_laundry_service" + | "safety_check" + | "ev_station" + | "takeout_dining" + | "moving" + | "zoom_in_map" + | "soup_kitchen" + | "pest_control" + | "stadium" + | "transfer_within_a_station" + | "location_off" + | "connecting_airports" + | "wrong_location" + | "multiple_stop" + | "edit_location" + | "plumbing" + | "mode_of_travel" + | "minor_crash" + | "south_east" + | "add_road" + | "local_pharmacy" + | "fire_truck" + | "castle" + | "dry_cleaning" + | "set_meal" + | "baby_changing_station" + | "edit_location_alt" + | "layers_clear" + | "mosque" + | "north_west" + | "local_car_wash" + | "edit_attributes" + | "run_circle" + | "transit_enterexit" + | "sos" + | "satellite" + | "edit_road" + | "south_west" + | "streetview" + | "add_home" + | "kebab_dining" + | "airline_stops" + | "local_see" + | "fire_hydrant" + | "assist_walker" + | "add_home_work" + | "flight_class" + | "remove_road" + | "no_meals" + | "synagogue" + | "fort" + | "temple_buddhist" + | "location_disabled" + | "compass_calibration" + | "temple_hindu" + | "explore_off" + | "pest_control_rodent" + | "near_me_disabled" + | "directions_alt" + | "pergola" + | "directions_off" + | "directions_alt_off" + | "pet_supplies" + | "moved_location" + | "move_location" + | "moving_ministry" + | "move" + | "safety_check_off" + | "description" + | "content_copy" + | "dashboard" + | "edit_note" + | "menu_book" + | "grid_view" + | "list" + | "folder" + | "list_alt" + | "inventory" + | "folder_open" + | "article" + | "fact_check" + | "attach_file" + | "format_list_bulleted" + | "assignment" + | "task" + | "checklist" + | "cloud_upload" + | "draft" + | "summarize" + | "feed" + | "draw" + | "cloud" + | "newspaper" + | "view_list" + | "file_copy" + | "note_add" + | "border_color" + | "book" + | "history_edu" + | "design_services" + | "pending_actions" + | "format_quote" + | "post_add" + | "request_quote" + | "cloud_download" + | "drag_handle" + | "contact_page" + | "table" + | "space_dashboard" + | "archive" + | "content_paste" + | "percent" + | "attachment" + | "assignment_ind" + | "format_list_numbered" + | "assignment_turned_in" + | "tag" + | "table_chart" + | "sticky_note_2" + | "text_fields" + | "dashboard_customize" + | "reorder" + | "integration_instructions" + | "format_bold" + | "find_in_page" + | "text_snippet" + | "note" + | "document_scanner" + | "checklist_rtl" + | "note_alt" + | "edit_document" + | "cloud_sync" + | "table_rows" + | "perm_media" + | "cloud_done" + | "title" + | "table_view" + | "content_cut" + | "notes" + | "cut" + | "data_object" + | "subject" + | "functions" + | "format_italic" + | "content_paste_search" + | "format_color_fill" + | "folder_shared" + | "plagiarism" + | "horizontal_rule" + | "file_present" + | "folder_copy" + | "format_align_left" + | "ballot" + | "team_dashboard" + | "format_paint" + | "cloud_off" + | "add_link" + | "view_column" + | "read_more" + | "difference" + | "view_agenda" + | "format_size" + | "format_underlined" + | "vertical_align_top" + | "toc" + | "height" + | "vertical_align_bottom" + | "copy_all" + | "view_week" + | "drive_folder_upload" + | "assignment_late" + | "format_color_text" + | "view_module" + | "drive_file_move" + | "low_priority" + | "assignment_return" + | "format_align_center" + | "segment" + | "folder_special" + | "calendar_view_month" + | "folder_zip" + | "polyline" + | "square" + | "breaking_news_alt_1" + | "format_align_right" + | "grading" + | "view_headline" + | "linear_scale" + | "edit_off" + | "view_quilt" + | "view_carousel" + | "text_increase" + | "request_page" + | "view_sidebar" + | "pages" + | "text_format" + | "format_align_justify" + | "calendar_view_week" + | "hexagon" + | "numbers" + | "docs_add_on" + | "folder_delete" + | "format_shapes" + | "forms_add_on" + | "imagesearch_roller" + | "calendar_view_day" + | "video_file" + | "font_download" + | "format_list_numbered_rtl" + | "join_inner" + | "add_to_drive" + | "content_paste_go" + | "restore_page" + | "rectangle" + | "vertical_split" + | "rule_folder" + | "format_color_reset" + | "cloud_circle" + | "view_stream" + | "format_indent_increase" + | "spellcheck" + | "assignment_returned" + | "data_array" + | "align_horizontal_left" + | "text_decrease" + | "pivot_table_chart" + | "deselect" + | "vertical_align_center" + | "pentagon" + | "merge_type" + | "space_bar" + | "format_strikethrough" + | "view_day" + | "flip_to_front" + | "join_left" + | "short_text" + | "border_all" + | "shape_line" + | "format_line_spacing" + | "line_weight" + | "horizontal_split" + | "format_indent_decrease" + | "align_horizontal_center" + | "join_right" + | "snippet_folder" + | "subtitles_off" + | "align_vertical_bottom" + | "folder_off" + | "align_horizontal_right" + | "glyphs" + | "format_clear" + | "function" + | "insert_page_break" + | "vertical_distribute" + | "content_paste_off" + | "superscript" + | "horizontal_distribute" + | "line_axis" + | "line_style" + | "flip_to_back" + | "align_vertical_center" + | "align_vertical_top" + | "margin" + | "clarify" + | "wrap_text" + | "view_array" + | "subscript" + | "border_clear" + | "border_style" + | "amp_stories" + | "border_outer" + | "type_specimen" + | "text_rotate_vertical" + | "padding" + | "forms_apps_script" + | "border_vertical" + | "ink_pen" + | "text_rotation_none" + | "counter_1" + | "format_textdirection_l_to_r" + | "ink_eraser" + | "format_overline" + | "docs_apps_script" + | "border_horizontal" + | "font_download_off" + | "format_textdirection_r_to_l" + | "newsmode" + | "text_rotation_angleup" + | "border_bottom" + | "text_rotation_down" + | "border_inner" + | "border_top" + | "border_left" + | "text_rotation_angledown" + | "assignment_add" + | "finance_chip" + | "text_rotate_up" + | "news" + | "border_right" + | "format_h1" + | "ink_highlighter" + | "view_column_2" + | "join" + | "full_coverage" + | "overview" + | "format_underlined_squiggle" + | "colors" + | "slide_library" + | "format_h2" + | "format_paragraph" + | "format_image_left" + | "format_list_bulleted_add" + | "breaking_news" + | "counter_2" + | "lab_profile" + | "frame_inspect" + | "equal" + | "variables" + | "counter_3" + | "format_image_right" + | "format_h3" + | "ink_marker" + | "format_h5" + | "format_h6" + | "csv" + | "voting_chip" + | "process_chart" + | "remove_selection" + | "format_h4" + | "signature" + | "format_ink_highlighter" + | "location_chip" + | "export_notes" + | "stylus_laser_pointer" + | "sticky_note" + | "shapes" + | "unknown_document" + | "frame_source" + | "add_notes" + | "counter_4" + | "format_text_overflow" + | "cell_merge" + | "format_letter_spacing_standard" + | "grid_guides" + | "select" + | "table_rows_narrow" + | "diagnosis" + | "regular_expression" + | "reset_image" + | "table_chart_view" + | "text_select_move_forward_character" + | "business_chip" + | "custom_typography" + | "draw_abstract" + | "source_notes" + | "decimal_increase" + | "folder_managed" + | "list_alt_add" + | "text_ad" + | "width" + | "insert_text" + | "lasso_select" + | "scan_delete" + | "smb_share" + | "ungroup" + | "counter_5" + | "format_letter_spacing_2" + | "line_end_arrow_notch" + | "line_start" + | "tab_close" + | "thumbnail_bar" + | "contract" + | "counter_6" + | "format_letter_spacing" + | "line_end" + | "match_case" + | "stroke_full" + | "draw_collage" + | "flex_wrap" + | "format_letter_spacing_wider" + | "language_chinese_quick" + | "line_end_square" + | "other_admission" + | "post" + | "scan" + | "text_select_start" + | "folder_supervised" + | "format_letter_spacing_wide" + | "language_spanish" + | "line_end_arrow" + | "line_start_diamond" + | "match_word" + | "align_justify_space_around" + | "align_justify_space_between" + | "align_space_between" + | "contract_edit" + | "format_text_clip" + | "line_end_circle" + | "line_start_circle" + | "special_character" + | "tab_duplicate" + | "tab_move" + | "tab_new_right" + | "text_select_jump_to_beginning" + | "top_panel_close" + | "top_panel_open" + | "counter_0" + | "counter_7" + | "counter_8" + | "flex_direction" + | "frame_reload" + | "heap_snapshot_multiple" + | "heap_snapshot_thumbnail" + | "ink_eraser_off" + | "language_french" + | "language_gb_english" + | "language_international" + | "language_korean_latin" + | "line_end_diamond" + | "sheets_rtl" + | "text_select_move_forward_word" + | "text_select_move_up" + | "tsv" + | "align_justify_space_even" + | "attach_file_add" + | "counter_9" + | "fit_width" + | "heap_snapshot_large" + | "language_chinese_dayi" + | "line_curve" + | "line_start_square" + | "person_book" + | "stroke_partial" + | "tab_group" + | "text_select_move_down" + | "align_justify_center" + | "align_justify_flex_end" + | "align_justify_flex_start" + | "align_space_around" + | "align_space_even" + | "contract_delete" + | "fit_page" + | "format_text_wrap" + | "highlighter_size_4" + | "language_chinese_array" + | "language_chinese_cangjie" + | "language_chinese_pinyin" + | "language_chinese_wubi" + | "language_pinyin" + | "language_us" + | "language_us_colemak" + | "language_us_dvorak" + | "letter_switch" + | "line_start_arrow" + | "line_start_arrow_notch" + | "pen_size_2" + | "pen_size_3" + | "pen_size_4" + | "pen_size_5" + | "tab_close_right" + | "tab_recent" + | "text_select_end" + | "text_select_jump_to_end" + | "align_center" + | "align_end" + | "align_flex_center" + | "align_flex_end" + | "align_flex_start" + | "align_items_stretch" + | "align_justify_stretch" + | "align_self_stretch" + | "align_start" + | "align_stretch" + | "decimal_decrease" + | "flex_no_wrap" + | "highlighter_size_1" + | "highlighter_size_2" + | "highlighter_size_3" + | "highlighter_size_5" + | "pen_size_1" + | "text_select_move_back_character" + | "text_select_move_back_word" + | "play_arrow" + | "play_circle" + | "mic" + | "videocam" + | "volume_up" + | "pause" + | "music_note" + | "library_books" + | "movie" + | "skip_next" + | "speed" + | "replay" + | "volume_off" + | "pause_circle" + | "view_in_ar" + | "fiber_manual_record" + | "skip_previous" + | "stop_circle" + | "stop" + | "equalizer" + | "subscriptions" + | "video_library" + | "fast_forward" + | "playlist_add" + | "video_call" + | "repeat" + | "volume_mute" + | "shuffle" + | "mic_off" + | "hearing" + | "library_music" + | "playlist_add_check" + | "podcasts" + | "fast_rewind" + | "sound_detection_dog_barking" + | "queue_music" + | "video_camera_front" + | "subtitles" + | "volume_down" + | "play_pause" + | "album" + | "discover_tune" + | "radio" + | "av_timer" + | "library_add_check" + | "videocam_off" + | "closed_caption" + | "stream" + | "forward_10" + | "not_started" + | "playlist_play" + | "replay_10" + | "fiber_new" + | "branding_watermark" + | "text_to_speech" + | "recent_actors" + | "playlist_remove" + | "interpreter_mode" + | "slow_motion_video" + | "frame_person" + | "playlist_add_check_circle" + | "settings_voice" + | "video_settings" + | "featured_play_list" + | "sound_detection_loud_sound" + | "audio_file" + | "lyrics" + | "play_lesson" + | "hd" + | "repeat_one" + | "call_to_action" + | "high_quality" + | "add_to_queue" + | "music_off" + | "video_camera_back" + | "spatial_audio_off" + | "shuffle_on" + | "playlist_add_circle" + | "volume_down_alt" + | "hearing_disabled" + | "featured_video" + | "replay_5" + | "repeat_on" + | "queue_play_next" + | "speech_to_text" + | "spatial_audio" + | "art_track" + | "explicit" + | "airplay" + | "forward_5" + | "forward_30" + | "4k" + | "music_video" + | "replay_30" + | "control_camera" + | "spatial_tracking" + | "closed_caption_disabled" + | "digital_out_of_home" + | "video_label" + | "fiber_smart_record" + | "play_disabled" + | "repeat_one_on" + | "broadcast_on_personal" + | "sd" + | "missed_video_call" + | "surround_sound" + | "fiber_pin" + | "10k" + | "sound_detection_glass_break" + | "60fps" + | "remove_from_queue" + | "brand_awareness" + | "broadcast_on_home" + | "fiber_dvr" + | "30fps" + | "4k_plus" + | "video_stable" + | "8k" + | "1k" + | "privacy" + | "8k_plus" + | "2k" + | "instant_mix" + | "7k" + | "1k_plus" + | "9k" + | "9k_plus" + | "5k" + | "6k" + | "2k_plus" + | "5k_plus" + | "6k_plus" + | "3k" + | "7k_plus" + | "3k_plus" + | "ar_on_you" + | "no_sound" + | "cinematic_blur" + | "video_search" + | "hangout_video" + | "genres" + | "media_link" + | "mic_double" + | "autoplay" + | "video_camera_front_off" + | "movie_edit" + | "autopause" + | "forward_media" + | "movie_info" + | "resume" + | "hangout_video_off" + | "select_to_speak" + | "autostop" + | "2d" + | "forward_circle" + | "view_in_ar_off" + | "frame_person_off" + | "sound_sampler" + | "local_shipping" + | "directions_car" + | "flight" + | "directions_run" + | "directions_walk" + | "flight_takeoff" + | "directions_bus" + | "directions_bike" + | "train" + | "airport_shuttle" + | "pedal_bike" + | "directions_boat" + | "two_wheeler" + | "agriculture" + | "local_taxi" + | "sailing" + | "electric_car" + | "flight_land" + | "hail" + | "no_crash" + | "commute" + | "motorcycle" + | "car_crash" + | "tram" + | "departure_board" + | "subway" + | "electric_moped" + | "turn_right" + | "electric_scooter" + | "fork_right" + | "directions_subway" + | "tire_repair" + | "electric_bike" + | "rv_hookup" + | "bus_alert" + | "turn_left" + | "transportation" + | "airlines" + | "taxi_alert" + | "u_turn_left" + | "directions_railway" + | "electric_rickshaw" + | "turn_slight_right" + | "u_turn_right" + | "fork_left" + | "railway_alert" + | "bike_scooter" + | "turn_sharp_right" + | "turn_slight_left" + | "no_transfer" + | "snowmobile" + | "turn_sharp_left" + | "flightsmode" + | "swap_driving_apps_wheel" + | "ambulance" + | "school" + | "campaign" + | "construction" + | "engineering" + | "volunteer_activism" + | "science" + | "sports_esports" + | "confirmation_number" + | "real_estate_agent" + | "cake" + | "self_improvement" + | "sports_soccer" + | "air" + | "biotech" + | "water" + | "hiking" + | "architecture" + | "sports_score" + | "personal_injury" + | "sports_basketball" + | "waves" + | "theaters" + | "sports_tennis" + | "switch_account" + | "nights_stay" + | "sports_gymnastics" + | "how_to_vote" + | "backpack" + | "sports_motorsports" + | "sports_kabaddi" + | "surfing" + | "piano" + | "sports" + | "toys" + | "sports_volleyball" + | "sports_baseball" + | "sports_martial_arts" + | "camping" + | "downhill_skiing" + | "swords" + | "kayaking" + | "scoreboard" + | "phishing" + | "sports_handball" + | "sports_football" + | "skateboarding" + | "sports_golf" + | "sports_cricket" + | "toys_fan" + | "nordic_walking" + | "kitesurfing" + | "roller_skating" + | "rowing" + | "scuba_diving" + | "trophy" + | "storm" + | "sports_mma" + | "paragliding" + | "snowboarding" + | "sports_hockey" + | "ice_skating" + | "snowshoeing" + | "sports_rugby" + | "sledding" + | "piano_off" + | "no_backpack" + | "family_link" + | "rewarded_ads" + | "ifl" + | "cake_add" + | "mindfulness" + | "health_metrics" + | "steps" + | "sprint" + | "exercise" + | "stress_management" + | "menstrual_health" + | "readiness_score" + | "relax" + | "ecg_heart" + | "laps" + | "azm" + | "pace" + | "distance" + | "person_play" + | "floor" + | "avg_time" + | "person_celebrate" + | "avg_pace" + | "fertile" + | "onsen" + | "podiatry" + | "spo2" + | "water_full" + | "bath_outdoor" + | "play_shapes" + | "eda" + | "bia" + | "water_medium" + | "interactive_space" + | "elevation" + | "hr_resting" + | "glass_cup" + | "water_loss" + | "monitor_weight_loss" + | "sauna" + | "sleep_score" + | "bath_private" + | "monitor_weight_gain" + | "bath_public_large" + | "check_in_out" + | "physical_therapy" + | "phone_iphone" + | "save" + | "smartphone" + | "print" + | "keyboard_arrow_down" + | "computer" + | "devices" + | "desktop_windows" + | "smart_display" + | "dns" + | "keyboard_backspace" + | "headphones" + | "smart_toy" + | "phone_android" + | "keyboard_arrow_right" + | "memory" + | "keyboard" + | "live_tv" + | "laptop_mac" + | "headset_mic" + | "keyboard_arrow_up" + | "tv" + | "device_thermostat" + | "mouse" + | "balance" + | "route" + | "point_of_sale" + | "keyboard_arrow_left" + | "laptop_chromebook" + | "keyboard_return" + | "watch" + | "power" + | "laptop_windows" + | "router" + | "developer_board" + | "display_settings" + | "scale" + | "book_online" + | "developer_mode" + | "fax" + | "cast" + | "cast_for_education" + | "videogame_asset" + | "device_hub" + | "straight" + | "screen_search_desktop" + | "desktop_mac" + | "mobile_friendly" + | "settings_ethernet" + | "settings_input_antenna" + | "monitor" + | "important_devices" + | "tablet_mac" + | "devices_other" + | "send_to_mobile" + | "system_update" + | "settings_remote" + | "monitor_weight" + | "screen_rotation" + | "screen_share" + | "keyboard_alt" + | "settings_input_component" + | "speaker" + | "merge" + | "sim_card" + | "keyboard_tab" + | "vibration" + | "power_off" + | "tablet" + | "connected_tv" + | "screenshot_monitor" + | "remember_me" + | "browser_updated" + | "security_update_good" + | "sd_card" + | "cast_connected" + | "device_unknown" + | "tablet_android" + | "charging_station" + | "phonelink_setup" + | "punch_clock" + | "scanner" + | "screenshot" + | "settings_input_hdmi" + | "stay_current_portrait" + | "tap_and_play" + | "keyboard_hide" + | "print_disabled" + | "security_update_warning" + | "disc_full" + | "app_blocking" + | "keyboard_capslock" + | "speaker_group" + | "mobile_screen_share" + | "aod" + | "sd_card_alert" + | "tty" + | "add_to_home_screen" + | "lift_to_talk" + | "earbuds" + | "perm_device_information" + | "stop_screen_share" + | "mobile_off" + | "headset_off" + | "desktop_access_disabled" + | "reset_tv" + | "offline_share" + | "adf_scanner" + | "headphones_battery" + | "screen_lock_portrait" + | "roundabout_right" + | "dock" + | "settop_component" + | "settings_input_svideo" + | "watch_off" + | "smart_screen" + | "stay_current_landscape" + | "chromecast_device" + | "settings_cell" + | "earbuds_battery" + | "home_max" + | "power_input" + | "no_sim" + | "screen_lock_landscape" + | "ramp_right" + | "roundabout_left" + | "stay_primary_landscape" + | "stay_primary_portrait" + | "developer_board_off" + | "tv_off" + | "home_mini" + | "phonelink_off" + | "ramp_left" + | "screen_lock_rotation" + | "videogame_asset_off" + | "aod_tablet" + | "open_in_phone" + | "gamepad" + | "robot" + | "rear_camera" + | "jamboard_kiosk" + | "mimo" + | "app_promo" + | "devices_wearables" + | "tv_options_edit_channels" + | "developer_mode_tv" + | "mimo_disconnect" + | "ambient_screen" + | "touchpad_mouse" + | "camera_video" + | "tv_signin" + | "aod_watch" + | "joystick" + | "ecg" + | "memory_alt" + | "robot_2" + | "tv_guide" + | "devices_off" + | "night_sight_max" + | "hard_drive" + | "open_jam" + | "screenshot_tablet" + | "stream_apps" + | "cast_pause" + | "cast_warning" + | "keyboard_tab_rtl" + | "pacemaker" + | "deskphone" + | "watch_wake" + | "hard_drive_2" + | "lda" + | "print_lock" + | "tv_remote" + | "watch_button_press" + | "audio_video_receiver" + | "print_add" + | "print_connect" + | "print_error" + | "ventilator" + | "dark_mode" + | "light_mode" + | "wifi" + | "signal_cellular_alt" + | "password" + | "widgets" + | "pin" + | "storage" + | "rss_feed" + | "android" + | "wifi_off" + | "bluetooth" + | "battery_charging_full" + | "dvr" + | "thermostat" + | "graphic_eq" + | "nightlight" + | "battery_5_bar" + | "signal_wifi_4_bar" + | "gpp_maybe" + | "cable" + | "gpp_bad" + | "data_usage" + | "battery_4_bar" + | "signal_cellular_4_bar" + | "radar" + | "airplanemode_active" + | "battery_0_bar" + | "cameraswitch" + | "wallpaper" + | "signal_disconnected" + | "flashlight_on" + | "network_check" + | "battery_6_bar" + | "charger" + | "wifi_tethering" + | "sim_card_download" + | "usb" + | "quick_phrases" + | "splitscreen" + | "battery_3_bar" + | "battery_1_bar" + | "adb" + | "network_wifi_3_bar" + | "battery_low" + | "battery_alert" + | "bluetooth_searching" + | "network_wifi" + | "bluetooth_connected" + | "5g" + | "wifi_find" + | "battery_2_bar" + | "brightness_high" + | "network_cell" + | "pattern" + | "nfc" + | "data_saver_on" + | "bluetooth_disabled" + | "signal_wifi_statusbar_not_connected" + | "signal_wifi_bad" + | "signal_cellular_3_bar" + | "network_wifi_2_bar" + | "noise_control_off" + | "network_wifi_1_bar" + | "brightness_medium" + | "signal_wifi_off" + | "battery_very_low" + | "mode_standby" + | "brightness_low" + | "mobiledata_off" + | "signal_wifi_0_bar" + | "battery_charging_20" + | "battery_charging_80" + | "grid_4x4" + | "battery_saver" + | "battery_charging_90" + | "flashlight_off" + | "signal_wifi_statusbar_null" + | "battery_charging_50" + | "settings_system_daydream" + | "battery_unknown" + | "signal_cellular_2_bar" + | "screen_rotation_alt" + | "wifi_calling_3" + | "signal_cellular_1_bar" + | "badge_critical_battery" + | "4g_mobiledata" + | "wifi_lock" + | "noise_aware" + | "do_not_disturb_on_total_silence" + | "battery_charging_60" + | "signal_cellular_connected_no_internet_0_bar" + | "nearby_error" + | "signal_cellular_0_bar" + | "battery_charging_30" + | "network_ping" + | "signal_cellular_connected_no_internet_4_bar" + | "wifi_tethering_error" + | "brightness_auto" + | "wifi_calling_1" + | "edgesensor_high" + | "wifi_2_bar" + | "airplanemode_inactive" + | "signal_cellular_nodata" + | "1x_mobiledata" + | "grid_3x3" + | "lte_mobiledata" + | "perm_data_setting" + | "signal_cellular_alt_2_bar" + | "bluetooth_drive" + | "devices_fold" + | "perm_scan_wifi" + | "network_locked" + | "media_bluetooth_on" + | "wifi_calling_2" + | "4g_plus_mobiledata" + | "signal_cellular_off" + | "timer_10_select" + | "wifi_tethering_off" + | "signal_cellular_alt_1_bar" + | "edgesensor_low" + | "3g_mobiledata" + | "usb_off" + | "wifi_1_bar" + | "apk_install" + | "signal_cellular_null" + | "lte_plus_mobiledata" + | "grid_goldenratio" + | "g_mobiledata" + | "portable_wifi_off" + | "noise_control_on" + | "media_bluetooth_off" + | "timer_3_select" + | "e_mobiledata" + | "apk_document" + | "nearby_off" + | "h_mobiledata" + | "r_mobiledata" + | "h_plus_mobiledata" + | "dual_screen" + | "nearby" + | "dock_to_left" + | "stylus" + | "stylus_note" + | "screenshot_region" + | "dock_to_right" + | "overview_key" + | "keyboard_keys" + | "battery_status_good" + | "brightness_alert" + | "brightness_empty" + | "splitscreen_left" + | "splitscreen_right" + | "keyboard_off" + | "screen_record" + | "screenshot_keyboard" + | "dock_to_bottom" + | "keyboard_external_input" + | "magnify_fullscreen" + | "wallpaper_slideshow" + | "1x_mobiledata_badge" + | "battery_change" + | "display_external_input" + | "magnify_docked" + | "screenshot_frame" + | "backlight_low" + | "battery_plus" + | "keyboard_full" + | "keyboard_onscreen" + | "wifi_notification" + | "4g_mobiledata_badge" + | "5g_mobiledata_badge" + | "keyboard_capslock_badge" + | "keyboard_previous_language" + | "lte_mobiledata_badge" + | "lte_plus_mobiledata_badge" + | "screen_rotation_up" + | "wifi_home" + | "3g_mobiledata_badge" + | "backlight_high" + | "battery_error" + | "battery_share" + | "e_mobiledata_badge" + | "ev_mobiledata_badge" + | "g_mobiledata_badge" + | "grid_3x3_off" + | "h_mobiledata_badge" + | "h_plus_mobiledata_badge" + | "signal_cellular_pause" + | "splitscreen_bottom" + | "splitscreen_top" + | "badge" + | "verified_user" + | "admin_panel_settings" + | "report" + | "security" + | "vpn_key" + | "shield" + | "policy" + | "exclamation" + | "privacy_tip" + | "assured_workload" + | "vpn_lock" + | "disabled_visible" + | "e911_emergency" + | "enhanced_encryption" + | "private_connectivity" + | "vpn_key_off" + | "add_moderator" + | "no_encryption" + | "sync_lock" + | "wifi_password" + | "key_visualizer" + | "remove_moderator" + | "encrypted" + | "report_off" + | "shield_lock" + | "shield_person" + | "vpn_key_alert" + | "shield_locked" + | "apartment" + | "location_city" + | "fitness_center" + | "lunch_dining" + | "spa" + | "cottage" + | "local_cafe" + | "hotel" + | "family_restroom" + | "beach_access" + | "local_bar" + | "pool" + | "other_houses" + | "luggage" + | "liquor" + | "airplane_ticket" + | "casino" + | "sports_bar" + | "bakery_dining" + | "ramen_dining" + | "nightlife" + | "local_dining" + | "holiday_village" + | "icecream" + | "escalator_warning" + | "dinner_dining" + | "museum" + | "food_bank" + | "night_shelter" + | "festival" + | "attractions" + | "golf_course" + | "stairs" + | "villa" + | "smoke_free" + | "smoking_rooms" + | "car_rental" + | "airline_seat_recline_normal" + | "elevator" + | "gite" + | "child_friendly" + | "airline_seat_recline_extra" + | "breakfast_dining" + | "carpenter" + | "car_repair" + | "cabin" + | "brunch_dining" + | "no_food" + | "do_not_touch" + | "houseboat" + | "rice_bowl" + | "tapas" + | "wheelchair_pickup" + | "bento" + | "no_drinks" + | "do_not_step" + | "bungalow" + | "airline_seat_flat" + | "airline_seat_individual_suite" + | "escalator" + | "chalet" + | "no_luggage" + | "airline_seat_legroom_extra" + | "airline_seat_flat_angled" + | "airline_seat_legroom_normal" + | "airline_seat_legroom_reduced" + | "no_stroller" + | "travel" + | "your_trips" + | "house" + | "bed" + | "ac_unit" + | "chair" + | "coffee" + | "electric_bolt" + | "child_care" + | "sensors" + | "back_hand" + | "checkroom" + | "emergency_home" + | "grass" + | "shower" + | "mode_fan" + | "mop" + | "kitchen" + | "room_service" + | "thermometer" + | "styler" + | "yard" + | "bathtub" + | "king_bed" + | "roofing" + | "energy_savings_leaf" + | "window" + | "valve" + | "cooking" + | "garage_home" + | "door_front" + | "mode_heat" + | "light" + | "foundation" + | "outdoor_grill" + | "garage" + | "dining" + | "table_restaurant" + | "sensor_occupied" + | "deck" + | "weekend" + | "coffee_maker" + | "humidity_high" + | "flatware" + | "highlight" + | "fireplace" + | "humidity_low" + | "mode_night" + | "electric_meter" + | "tv_gen" + | "humidity_mid" + | "bedroom_parent" + | "chair_alt" + | "blender" + | "scene" + | "microwave" + | "oven_gen" + | "single_bed" + | "bedroom_baby" + | "heat_pump" + | "bathroom" + | "in_home_mode" + | "hot_tub" + | "hardware" + | "mode_off_on" + | "sprinkler" + | "table_bar" + | "gas_meter" + | "crib" + | "soap" + | "countertops" + | "living" + | "mode_cool" + | "home_iot_device" + | "propane_tank" + | "fire_extinguisher" + | "outlet" + | "remote_gen" + | "matter" + | "gate" + | "sensor_door" + | "event_seat" + | "airware" + | "faucet" + | "dishwasher_gen" + | "energy_program_saving" + | "air_freshener" + | "balcony" + | "wash" + | "camera_indoor" + | "water_damage" + | "bedroom_child" + | "house_siding" + | "switch" + | "microwave_gen" + | "detector_smoke" + | "door_sliding" + | "iron" + | "desk" + | "energy_program_time_used" + | "umbrella" + | "water_heater" + | "dresser" + | "fence" + | "door_back" + | "doorbell" + | "mode_fan_off" + | "hvac" + | "kettle" + | "camera_outdoor" + | "emergency_heat" + | "air_purifier_gen" + | "emergency_share" + | "stroller" + | "curtains" + | "multicooker" + | "shield_moon" + | "sensors_off" + | "mode_heat_cool" + | "thermostat_auto" + | "emergency_recording" + | "smart_outlet" + | "blinds" + | "controller_gen" + | "roller_shades" + | "dry" + | "blinds_closed" + | "roller_shades_closed" + | "propane" + | "sensor_window" + | "thermostat_carbon" + | "range_hood" + | "doorbell_3p" + | "blanket" + | "tv_with_assistant" + | "vertical_shades_closed" + | "vertical_shades" + | "curtains_closed" + | "mode_heat_off" + | "mode_cool_off" + | "tamper_detection_off" + | "shelves" + | "thermometer_gain" + | "wall_art" + | "thermometer_loss" + | "hallway" + | "emergency_share_off" + | "stadia_controller" + | "door_open" + | "nest_eco_leaf" + | "device_reset" + | "nest_clock_farsight_analog" + | "nest_remote_comfort_sensor" + | "laundry" + | "battery_horiz_075" + | "shield_with_heart" + | "temp_preferences_eco" + | "familiar_face_and_zone" + | "tools_power_drill" + | "airwave" + | "productivity" + | "battery_horiz_050" + | "nest_heat_link_gen_3" + | "nest_display" + | "weather_snowy" + | "activity_zone" + | "ev_charger" + | "nest_remote" + | "cleaning_bucket" + | "settings_alert" + | "nest_cam_indoor" + | "arrows_more_up" + | "nest_heat_link_e" + | "home_storage" + | "nest_multi_room" + | "nest_secure_alarm" + | "battery_horiz_000" + | "light_group" + | "google_wifi" + | "nest_cam_outdoor" + | "detection_and_zone" + | "nest_thermostat_gen_3" + | "mfg_nest_yale_lock" + | "tools_pliers_wire_stripper" + | "detector_alarm" + | "nest_cam_iq_outdoor" + | "tools_ladder" + | "early_on" + | "floor_lamp" + | "nest_clock_farsight_digital" + | "nest_cam_iq" + | "home_speaker" + | "nest_mini" + | "nest_hello_doorbell" + | "home_max_dots" + | "nest_audio" + | "nest_wifi_router" + | "house_with_shield" + | "zone_person_urgent" + | "nest_display_max" + | "motion_sensor_active" + | "cool_to_dry" + | "shield_with_house" + | "nest_farsight_weather" + | "chromecast_2" + | "battery_profile" + | "window_closed" + | "heat_pump_balance" + | "arming_countdown" + | "nest_found_savings" + | "detector_status" + | "self_care" + | "tools_level" + | "window_open" + | "nest_thermostat_zirconium_eu" + | "arrows_more_down" + | "nest_true_radiant" + | "nest_cam_wired_stand" + | "zone_person_alert" + | "detector" + | "climate_mini_split" + | "nest_detect" + | "nest_wifi_point" + | "door_sensor" + | "nest_doorbell_visitor" + | "quiet_time" + | "nest_cam_floodlight" + | "nest_tag" + | "tools_installation_kit" + | "nest_connect" + | "nest_thermostat_sensor_eu" + | "nest_sunblock" + | "tools_phillips" + | "nest_thermostat_sensor" + | "nest_wifi_gale" + | "nest_thermostat_e_eu" + | "doorbell_chime" + | "detector_co" + | "detector_battery" + | "tools_flat_head" + | "nest_wake_on_approach" + | "nest_wake_on_press" + | "motion_sensor_urgent" + | "table_lamp" + | "motion_sensor_alert" + | "window_sensor" + | "tamper_detection_on" + | "nest_cam_magnet_mount" + | "zone_person_idle" + | "quiet_time_active" + | "nest_cam_stand" + | "detector_offline" + | "wall_lamp" + | "nest_cam_wall_mount" + | "motion_sensor_idle" + | "nest_thermostat" + | "water_pump" + | "assistant_on_hub" + | "nest_protect" + | "google_tv_remote" + | "feedback" + | "flutter"; diff --git a/apps/app/components/workspace/sidebar-menu.tsx b/apps/app/components/workspace/sidebar-menu.tsx index 6a0683926..4f4d841b6 100644 --- a/apps/app/components/workspace/sidebar-menu.tsx +++ b/apps/app/components/workspace/sidebar-menu.tsx @@ -1,10 +1,36 @@ import React from "react"; -import { useRouter } from "next/router"; import Link from "next/link"; +import { useRouter } from "next/router"; // hooks import useTheme from "hooks/use-theme"; + +import { NotificationPopover } from "components/notifications"; + +const workspaceLinks = (workspaceSlug: string) => [ + { + icon: "grid_view", + name: "Dashboard", + href: `/${workspaceSlug}`, + }, + { + icon: "bar_chart", + name: "Analytics", + href: `/${workspaceSlug}/analytics`, + }, + { + icon: "work", + name: "Projects", + href: `/${workspaceSlug}/projects`, + }, + { + icon: "task_alt", + name: "My Issues", + href: `/${workspaceSlug}/me/my-issues`, + }, +]; + // components import { Icon, Tooltip } from "components/ui"; @@ -15,34 +41,6 @@ export const WorkspaceSidebarMenu = () => { // theme context const { collapsed: sidebarCollapse } = useTheme(); - const workspaceLinks = (workspaceSlug: string) => [ - { - icon: "grid_view", - name: "Dashboard", - href: `/${workspaceSlug}`, - }, - { - icon: "bar_chart", - name: "Analytics", - href: `/${workspaceSlug}/analytics`, - }, - { - icon: "work", - name: "Projects", - href: `/${workspaceSlug}/projects`, - }, - { - icon: "task_alt", - name: "My Issues", - href: `/${workspaceSlug}/me/my-issues`, - }, - { - icon: "settings", - name: "Settings", - href: `/${workspaceSlug}/settings`, - }, - ]; - return (
{workspaceLinks(workspaceSlug as string).map((link, index) => { @@ -75,6 +73,8 @@ export const WorkspaceSidebarMenu = () => { ); })} + +
); }; diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index b483c9e41..d0e9a0e19 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -1,4 +1,4 @@ -import { IAnalyticsParams, IJiraMetadata } from "types"; +import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types"; const paramsToKey = (params: any) => { const { state, priority, assignees, created_by, labels, target_date, sub_issue } = params; @@ -223,3 +223,24 @@ export const DEFAULT_ANALYTICS = (workspaceSlug: string, params?: Partial { + const { type, snoozed, archived, read } = params; + + return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug.toUpperCase()}_TYPE_${( + type ?? "assigned" + ).toUpperCase()}_SNOOZED_${snoozed}_ARCHIVED_${archived}_READ_${read}`; +}; + +export const USER_WORKSPACE_NOTIFICATIONS_DETAILS = ( + workspaceSlug: string, + notificationId: string +) => + `USER_WORKSPACE_NOTIFICATIONS_DETAILS_${workspaceSlug.toUpperCase()}_${notificationId.toUpperCase()}`; + +export const UNREAD_NOTIFICATIONS_COUNT = (workspaceSlug: string) => + `UNREAD_NOTIFICATIONS_COUNT_${workspaceSlug.toUpperCase()}`; diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts index dcc33541e..894bf317d 100644 --- a/apps/app/helpers/date-time.helper.ts +++ b/apps/app/helpers/date-time.helper.ts @@ -92,7 +92,7 @@ export const timeAgo = (time: any) => { list_choice = 2; } var i = 0, - format; + format: any[]; while ((format = time_formats[i++])) if (seconds < format[0]) { if (typeof format[2] == "string") return format[list_choice]; @@ -101,6 +101,35 @@ export const timeAgo = (time: any) => { return time; }; +export const formatDateDistance = (date: string | Date) => { + const today = new Date(); + const eventDate = new Date(date); + const timeDiff = Math.abs(eventDate.getTime() - today.getTime()); + const days = Math.ceil(timeDiff / (1000 * 3600 * 24)); + + if (days < 1) { + const hours = Math.ceil(timeDiff / (1000 * 3600)); + if (hours < 1) { + const minutes = Math.ceil(timeDiff / (1000 * 60)); + if (minutes < 1) { + return "Just now"; + } else { + return `${minutes}m`; + } + } else { + return `${hours}h`; + } + } else if (days < 7) { + return `${days}d`; + } else if (days < 30) { + return `${Math.floor(days / 7)}w`; + } else if (days < 365) { + return `${Math.floor(days / 30)}m`; + } else { + return `${Math.floor(days / 365)}y`; + } +}; + export const getDateRangeStatus = ( startDate: string | null | undefined, endDate: string | null | undefined @@ -247,3 +276,60 @@ export const renderLongDateFormat = (dateString: string) => { } return `${day}${suffix} ${monthName} ${year}`; }; + +/** + * + * @returns {Array} Array of time objects with label and value as keys + */ + +export const getTimestampAfterCurrentTime = (): Array<{ + label: string; + value: Date; +}> => { + const current = new Date(); + const time = []; + for (let i = 0; i < 24; i++) { + const newTime = new Date(current.getTime() + i * 60 * 60 * 1000); + time.push({ + label: newTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + value: newTime, + }); + } + return time; +}; + +/** + * @returns {Array} Array of date objects with label and value as keys + * @description Returns an array of date objects starting from current date to 7 days after + */ + +export const getDatesAfterCurrentDate = (): Array<{ + label: string; + value: Date; +}> => { + const current = new Date(); + const date = []; + for (let i = 0; i < 7; i++) { + const newDate = new Date(Math.round(current.getTime() / (30 * 60 * 1000)) * 30 * 60 * 1000); + date.push({ + label: newDate.toLocaleDateString([], { + day: "numeric", + month: "long", + year: "numeric", + }), + value: newDate, + }); + } + return date; +}; + +/** + * @returns {boolean} true if date is valid + * @description Returns true if date is valid + * @param {string} date + * @example checkIfStringIsDate("2021-01-01") // true + * @example checkIfStringIsDate("2021-01-32") // false + */ + +export const checkIfStringIsDate = (date: string): boolean => + new Date(date).toString() !== "Invalid Date"; diff --git a/apps/app/helpers/string.helper.ts b/apps/app/helpers/string.helper.ts index a5fc05e78..ae78d6f97 100644 --- a/apps/app/helpers/string.helper.ts +++ b/apps/app/helpers/string.helper.ts @@ -118,3 +118,36 @@ export const getFirstCharacters = (str: string) => { return words[0].charAt(0) + words[1].charAt(0); } }; + +/** + * @description: This function will remove all the HTML tags from the string + * @param {string} html + * @return {string} + * @example: + * const html = "

Some text

"; + * const text = stripHTML(html); + * console.log(text); // Some text + */ + +export const stripHTML = (html: string) => { + const tmp = document.createElement("DIV"); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ""; +}; + +/** + * @description: This function return number count in string if number is more than 100 then it will return 99+ + * @param {number} number + * @return {string} + * @example: + * const number = 100; + * const text = getNumberCount(number); + * console.log(text); // 99+ + */ + +export const getNumberCount = (number: number): string => { + if (number > 99) { + return "99+"; + } + return number.toString(); +}; diff --git a/apps/app/hooks/use-issue-notification-subscription.tsx b/apps/app/hooks/use-issue-notification-subscription.tsx new file mode 100644 index 000000000..38bf8bf22 --- /dev/null +++ b/apps/app/hooks/use-issue-notification-subscription.tsx @@ -0,0 +1,76 @@ +import { useCallback } from "react"; + +import useSWR from "swr"; + +// hooks +import useUserAuth from "hooks/use-user-auth"; +// services +import userNotificationServices from "services/notifications.service"; + +const useUserIssueNotificationSubscription = ( + workspaceSlug?: string | string[] | null, + projectId?: string | string[] | null, + issueId?: string | string[] | null +) => { + const { user } = useUserAuth(); + + const { data, error, mutate } = useSWR( + workspaceSlug && projectId && issueId + ? `SUBSCRIPTION_STATUE_${workspaceSlug}_${projectId}_${issueId}` + : null, + workspaceSlug && projectId && issueId + ? () => + userNotificationServices.getIssueNotificationSubscriptionStatus( + workspaceSlug.toString(), + projectId.toString(), + issueId.toString() + ) + : null + ); + + const handleUnsubscribe = useCallback(() => { + if (!workspaceSlug || !projectId || !issueId) return; + + userNotificationServices + .unsubscribeFromIssueNotifications( + workspaceSlug as string, + projectId as string, + issueId as string + ) + .then(() => { + mutate({ + subscribed: false, + }); + }); + }, [workspaceSlug, projectId, issueId, mutate]); + + const handleSubscribe = useCallback(() => { + console.log(workspaceSlug, projectId, issueId, user); + + if (!workspaceSlug || !projectId || !issueId || !user) return; + + userNotificationServices + .subscribeToIssueNotifications( + workspaceSlug as string, + projectId as string, + issueId as string, + { + subscriber: user.id, + } + ) + .then(() => { + mutate({ + subscribed: true, + }); + }); + }, [workspaceSlug, projectId, issueId, mutate, user]); + + return { + loading: !data && !error, + subscribed: data?.subscribed, + handleSubscribe, + handleUnsubscribe, + } as const; +}; + +export default useUserIssueNotificationSubscription; diff --git a/apps/app/hooks/use-user-notifications.tsx b/apps/app/hooks/use-user-notifications.tsx new file mode 100644 index 000000000..4de9d296c --- /dev/null +++ b/apps/app/hooks/use-user-notifications.tsx @@ -0,0 +1,187 @@ +import { useState } from "react"; + +import { useRouter } from "next/router"; + +// swr +import useSWR from "swr"; + +// services +import userNotificationServices from "services/notifications.service"; + +// fetch-keys +import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; + +// type +import type { NotificationType } from "types"; + +const useUserNotification = () => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const [snoozed, setSnoozed] = useState(false); + const [archived, setArchived] = useState(false); + const [readNotification, setReadNotification] = useState(false); + const [selectedNotificationForSnooze, setSelectedNotificationForSnooze] = useState( + null + ); + const [selectedTab, setSelectedTab] = useState("assigned"); + + const { data: notifications, mutate: notificationsMutate } = useSWR( + workspaceSlug + ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), { + type: selectedTab, + snoozed, + archived, + read: selectedTab === null ? !readNotification : undefined, + }) + : null, + workspaceSlug + ? () => + userNotificationServices.getUserNotifications(workspaceSlug.toString(), { + type: selectedTab, + snoozed, + archived, + read: selectedTab === null ? !readNotification : undefined, + }) + : null + ); + + const { data: notificationCount, mutate: mutateNotificationCount } = useSWR( + workspaceSlug ? UNREAD_NOTIFICATIONS_COUNT(workspaceSlug.toString()) : null, + () => + workspaceSlug + ? userNotificationServices.getUnreadNotificationsCount(workspaceSlug.toString()) + : null + ); + + const markNotificationReadStatus = async (notificationId: string) => { + if (!workspaceSlug) return; + const isRead = + notifications?.find((notification) => notification.id === notificationId)?.read_at !== null; + + if (isRead) { + await userNotificationServices + .markUserNotificationAsUnread(workspaceSlug.toString(), notificationId) + .then(() => { + notificationsMutate((prev) => + prev?.map((prevNotification) => { + if (prevNotification.id === notificationId) { + return { + ...prevNotification, + read_at: null, + }; + } + return prevNotification; + }) + ); + mutateNotificationCount(); + }) + .catch(() => { + throw new Error("Something went wrong"); + }); + } else { + await userNotificationServices + .markUserNotificationAsRead(workspaceSlug.toString(), notificationId) + .then(() => { + notificationsMutate((prev) => + prev?.map((prevNotification) => { + if (prevNotification.id === notificationId) { + return { + ...prevNotification, + read_at: new Date(), + }; + } + return prevNotification; + }) + ); + mutateNotificationCount(); + }) + .catch(() => { + throw new Error("Something went wrong"); + }); + } + }; + + const markNotificationArchivedStatus = async (notificationId: string) => { + if (!workspaceSlug) return; + const isArchived = + notifications?.find((notification) => notification.id === notificationId)?.archived_at !== + null; + + if (isArchived) { + await userNotificationServices + .markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId) + .then(() => { + notificationsMutate(); + }) + .catch(() => { + throw new Error("Something went wrong"); + }); + } else { + await userNotificationServices + .markUserNotificationAsArchived(workspaceSlug.toString(), notificationId) + .then(() => { + notificationsMutate((prev) => + prev?.filter((prevNotification) => prevNotification.id !== notificationId) + ); + }) + .catch(() => { + throw new Error("Something went wrong"); + }); + } + }; + + const markSnoozeNotification = async (notificationId: string, dateTime?: Date) => { + if (!workspaceSlug) return; + + const isSnoozed = + notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !== + null; + + if (isSnoozed) { + await userNotificationServices + .patchUserNotification(workspaceSlug.toString(), notificationId, { + snoozed_till: null, + }) + .then(() => { + notificationsMutate(); + }); + } else + await userNotificationServices + .patchUserNotification(workspaceSlug.toString(), notificationId, { + snoozed_till: dateTime, + }) + .then(() => { + notificationsMutate( + (prevData) => prevData?.filter((prev) => prev.id !== notificationId) || [] + ); + }); + }; + + return { + notifications, + notificationsMutate, + markNotificationReadStatus, + markNotificationArchivedStatus, + markSnoozeNotification, + snoozed, + setSnoozed, + archived, + setArchived, + readNotification, + setReadNotification, + selectedNotificationForSnooze, + setSelectedNotificationForSnooze, + selectedTab, + setSelectedTab, + totalNotificationCount: notificationCount + ? notificationCount.created_issues + + notificationCount.watching_notifications + + notificationCount.my_issues + : null, + notificationCount, + mutateNotificationCount, + }; +}; + +export default useUserNotification; diff --git a/apps/app/public/empty-state/empty-notification.svg b/apps/app/public/empty-state/empty-notification.svg new file mode 100644 index 000000000..700a1552f --- /dev/null +++ b/apps/app/public/empty-state/empty-notification.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/app/services/notifications.service.ts b/apps/app/services/notifications.service.ts new file mode 100644 index 000000000..f2b49b954 --- /dev/null +++ b/apps/app/services/notifications.service.ts @@ -0,0 +1,174 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +// types +import { IUserNotification, INotificationParams } from "types"; + +class UserNotificationsServices extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async getUserNotifications( + workspaceSlug: string, + params: INotificationParams + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/users/notifications`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUserNotificationDetailById( + workspaceSlug: string, + notificationId: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsRead( + workspaceSlug: string, + notificationId: string + ): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsUnread( + workspaceSlug: string, + notificationId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsArchived( + workspaceSlug: string, + notificationId: string + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async markUserNotificationAsUnarchived( + workspaceSlug: string, + notificationId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async patchUserNotification( + workspaceSlug: string, + notificationId: string, + data: Partial + ): Promise { + return this.patch( + `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async deleteUserNotification(workspaceSlug: string, notificationId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async subscribeToIssueNotifications( + workspaceSlug: string, + projectId: string, + issueId: string, + data: { + subscriber: string; + } + ): Promise { + return this.post( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/issue-subscribers/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssueNotificationSubscriptionStatus( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise<{ + subscribed: boolean; + }> { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async unsubscribeFromIssueNotifications( + workspaceSlug: string, + projectId: string, + issueId: string + ): Promise { + return this.delete( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getUnreadNotificationsCount(workspaceSlug: string): Promise<{ + created_issues: number; + my_issues: number; + watching_notifications: number; + }> { + return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} + +const userNotificationServices = new UserNotificationsServices(); + +export default userNotificationServices; diff --git a/apps/app/types/index.d.ts b/apps/app/types/index.d.ts index c66734c93..c2a8efbf1 100644 --- a/apps/app/types/index.d.ts +++ b/apps/app/types/index.d.ts @@ -15,6 +15,7 @@ export * from "./importer"; export * from "./inbox"; export * from "./analytics"; export * from "./calendar"; +export * from "./notifications"; export * from "./waitlist"; export type NestedKeyOf = { diff --git a/apps/app/types/notifications.d.ts b/apps/app/types/notifications.d.ts new file mode 100644 index 000000000..b49c1c9be --- /dev/null +++ b/apps/app/types/notifications.d.ts @@ -0,0 +1,56 @@ +import type { IUserLite } from "./users"; + +export interface IUserNotification { + id: string; + created_at: Date; + updated_at: Date; + data: Data; + entity_identifier: string; + entity_name: string; + title: string; + message: null; + message_html: string; + message_stripped: null; + sender: string; + read_at: Date | null; + archived_at: Date | null; + snoozed_till: Date | null; + created_by: null; + updated_by: null; + workspace: string; + project: string; + triggered_by: string; + triggered_by_details: IUserLite; + receiver: string; +} + +export interface Data { + issue: IIssueLite; + issue_activity: { + actor: string; + field: string; + id: string; + issue_comment: string | null; + new_value: string; + old_value: string; + verb: "created" | "updated"; + }; +} + +export interface IIssueLite { + id: string; + name: string; + identifier: string; + state_name: string; + sequence_id: number; + state_group: string; +} + +export type NotificationType = "created" | "assigned" | "watching" | null; + +export interface INotificationParams { + snoozed?: boolean; + type?: NotificationType; + archived?: boolean; + read?: boolean; +}