diff --git a/src/app/_cache/forageStorage.ts b/src/app/_cache/forageStorage.ts index 6bcb43f44..e8baf6391 100644 --- a/src/app/_cache/forageStorage.ts +++ b/src/app/_cache/forageStorage.ts @@ -25,10 +25,35 @@ export async function migrateAssignees(lookupKey: string) { export async function getAssignees(lookupKey: string): Promise { if (typeof window === 'undefined') return [] - return (await localforage.getItem(`assignees.${lookupKey}`)) ?? [] + + try { + if (!(await document.hasStorageAccess())) { + console.info('Browswer has no storage access') + await document.requestStorageAccess() + } + + return (await localforage.getItem(`assignees.${lookupKey}`)) ?? [] + } catch (error: unknown) { + console.error( + "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", + ) + return [] + } } export async function setAssignees(lookupKey: string, value: any) { if (typeof window === 'undefined') return - return await localforage.setItem(`assignees.${lookupKey}`, value) + + try { + if (!(await document.hasStorageAccess())) { + console.info('Browswer has no storage access') + await document.requestStorageAccess() + } + + return await localforage.setItem(`assignees.${lookupKey}`, value) + } catch (error: unknown) { + console.error( + "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", + ) + } } diff --git a/src/app/api/tasks/task-notifications.service.ts b/src/app/api/tasks/task-notifications.service.ts index c6e38a63e..a235d22a7 100644 --- a/src/app/api/tasks/task-notifications.service.ts +++ b/src/app/api/tasks/task-notifications.service.ts @@ -26,8 +26,10 @@ export class TaskNotificationsService extends BaseService { const handleNotificationRead = { [AssigneeType.client]: this.notificationService.markClientNotificationAsRead, [AssigneeType.company]: this.notificationService.markAsReadForAllRecipients, + [AssigneeType.internalUser]: (task: Task) => + this.notificationService.deleteInternalUserNotificationsForTask(task.id), } - // @ts-expect-error This is completely safe + await handleNotificationRead[task?.assigneeType]?.(task) } } diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx index cd32fc896..62e5ae862 100644 --- a/src/app/detail/[task_id]/[user_type]/page.tsx +++ b/src/app/detail/[task_id]/[user_type]/page.tsx @@ -81,7 +81,7 @@ export default async function TaskDetailPage({ searchParams, }: { params: { task_id: string; task_name: string; user_type: UserType } - searchParams: { token: string; isRedirect?: string } + searchParams: { token: string; isRedirect?: string; fromNotificationCenter?: string } }) { const { token } = searchParams const { task_id, user_type } = params @@ -103,10 +103,17 @@ export default async function TaskDetailPage({ if (!tokenPayload) { throw new Error('Please provide a Valid Token') } + const fromNotificationCenter = !!searchParams.fromNotificationCenter console.info(`app/detail/${task_id}/${user_type}/page.tsx | Serving user ${token} with payload`, tokenPayload) if (!task) { - return + return ( + + ) } const isPreviewMode = !!getPreviewMode(tokenPayload) @@ -130,7 +137,7 @@ export default async function TaskDetailPage({ {token && } - + {isPreviewMode ? ( @@ -192,6 +199,7 @@ export default async function TaskDetailPage({ await deleteAttachment(token, id) }} userType={params.user_type} + token={token} /> {subTaskStatus.canCreateSubtask && ( @@ -207,7 +215,18 @@ export default async function TaskDetailPage({ - + { +export const ResponsiveStack = ({ + children, + fromNotificationCenter, +}: { + children: ReactNode + fromNotificationCenter: boolean +}) => { const { showSidebar } = useSelector(selectTaskDetails) + useEffect(() => { + store.dispatch(setFromNotificationCenter(fromNotificationCenter)) + }, [fromNotificationCenter]) + return ( - + {children} ) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index be92a7cd8..e351afed7 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -31,6 +31,7 @@ import { } from '@/utils/assignee' import { createDateFromFormattedDateString, formatDate } from '@/utils/dateHelper' import { NoAssignee } from '@/utils/noAssignee' +import { Box, Skeleton, Stack, styled, SxProps, Typography } from '@mui/material' import { getSelectedUserIds, getSelectedViewerIds, @@ -42,14 +43,20 @@ import { shouldConfirmBeforeReassignment, shouldConfirmViewershipBeforeReassignment, } from '@/utils/shouldConfirmBeforeReassign' -import { Box, Skeleton, Stack, styled, Typography } from '@mui/material' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { z } from 'zod' -const StyledText = styled(Typography)(({ theme }) => ({ +type StyledTypographyProps = { + display?: string +} + +const StyledText = styled(Typography, { + shouldForwardProp: (prop: string) => prop !== 'display', // don't pass to DOM +})(({ theme, display }) => ({ color: theme.color.gray[500], width: '80px', + display, })) export const Sidebar = ({ @@ -74,7 +81,7 @@ export const Sidebar = ({ portalUrl?: string }) => { const { activeTask, workflowStates, assignee, previewMode } = useSelector(selectTaskBoard) - const { showSidebar, showConfirmAssignModal } = useSelector(selectTaskDetails) + const { showSidebar, showConfirmAssignModal, fromNotificationCenter } = useSelector(selectTaskDetails) const { tokenPayload } = useSelector(selectAuthDetails) const [isHydrated, setIsHydrated] = useState(false) @@ -196,7 +203,8 @@ export const Sidebar = ({ }) } } - if (!showSidebar) { + + if (!showSidebar || fromNotificationCenter) { return ( } buttonContent={ @@ -301,11 +309,11 @@ export const Sidebar = ({ hideIusList name="Set client visibility" onChange={handleTaskViewerChange} - disabled={disabled && !previewMode} + disabled={(disabled && !previewMode) || fromNotificationCenter} initialValue={taskViewerValue || undefined} buttonContent={ } buttonContent={ @@ -474,11 +482,11 @@ export const Sidebar = ({ } outlined={true} @@ -529,11 +537,11 @@ export const Sidebar = ({ hideIusList name="Set client visibility" onChange={handleTaskViewerChange} - disabled={disabled && !previewMode} // allow visibility change in preview mode + disabled={(disabled && !previewMode) || fromNotificationCenter} // allow visibility change in preview mode initialValue={taskViewerValue || undefined} buttonContent={ } outlined={true} diff --git a/src/app/detail/ui/Subtasks.tsx b/src/app/detail/ui/Subtasks.tsx index 7bddbca44..f19af6eeb 100644 --- a/src/app/detail/ui/Subtasks.tsx +++ b/src/app/detail/ui/Subtasks.tsx @@ -10,6 +10,7 @@ import { useDebounce } from '@/hooks/useDebounce' import { GrayAddMediumIcon } from '@/icons' import { selectAuthDetails } from '@/redux/features/authDetailsSlice' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' +import { selectTaskDetails } from '@/redux/features/taskDetailsSlice' import { CreateTaskRequest, TaskResponse } from '@/types/dto/tasks.dto' import { fetcher } from '@/utils/fetcher' import { generateRandomString } from '@/utils/generateRandomString' @@ -40,6 +41,7 @@ export const Subtasks = ({ const [openTaskForm, setOpenTaskForm] = useState(false) const { workflowStates, assignee, activeTask } = useSelector(selectTaskBoard) const { tokenPayload } = useSelector(selectAuthDetails) + const { fromNotificationCenter } = useSelector(selectTaskDetails) const [optimisticUpdates, setOptimisticUpdates] = useState([]) //might need this server-temp id maps in the future. const [lastUpdated, setLastUpdated] = useState() const handleFormCancel = () => setOpenTaskForm(false) @@ -200,6 +202,7 @@ export const Subtasks = ({ variant="subtask" mode={mode} handleUpdate={handleSubTaskUpdate} + disableNavigation={fromNotificationCenter} /> ) })} diff --git a/src/app/detail/ui/TaskCardList.tsx b/src/app/detail/ui/TaskCardList.tsx index 4acdddd34..68882abd1 100644 --- a/src/app/detail/ui/TaskCardList.tsx +++ b/src/app/detail/ui/TaskCardList.tsx @@ -66,9 +66,19 @@ interface TaskCardListProps { handleUpdate?: (taskId: string, changes: Partial, updater: () => Promise) => Promise isTemp?: boolean sx?: SxProps | undefined + disableNavigation?: boolean } -export const TaskCardList = ({ task, variant, workflowState, mode, handleUpdate, isTemp, sx }: TaskCardListProps) => { +export const TaskCardList = ({ + task, + variant, + workflowState, + mode, + handleUpdate, + isTemp, + sx, + disableNavigation = false, +}: TaskCardListProps) => { const { assignee, workflowStates, previewMode, token, confirmAssignModalId, assigneeCache, confirmViewershipModalId } = useSelector(selectTaskBoard) const { tokenPayload } = useSelector(selectAuthDetails) @@ -173,8 +183,13 @@ export const TaskCardList = ({ task, variant, workflowState, mode, handleUpdate, justifyContent: 'flex-end', minWidth: 0, ':hover': { - cursor: 'pointer', - background: (theme) => (variant == 'subtask-board' ? theme.color.gray[150] : theme.color.gray[100]), + cursor: disableNavigation ? 'auto' : 'pointer', + background: (theme) => + disableNavigation + ? theme.color.base.white + : variant == 'subtask-board' + ? theme.color.gray[150] + : theme.color.gray[100], }, ':focus-visible': { outline: (theme) => `1px solid ${theme.color.borders.focusBorder2}`, @@ -220,7 +235,7 @@ export const TaskCardList = ({ task, variant, workflowState, mode, handleUpdate, disabled={checkIfTaskViewer(task.viewers, tokenPayload)} /> - {isTemp || variant === 'subtask-board' ? ( + {isTemp || variant === 'subtask-board' || disableNavigation ? (
- + {(task.subtaskCount > 0 || task.isArchived) && ( diff --git a/src/app/detail/ui/TaskEditor.tsx b/src/app/detail/ui/TaskEditor.tsx index 7cf39cb27..4682f12cd 100644 --- a/src/app/detail/ui/TaskEditor.tsx +++ b/src/app/detail/ui/TaskEditor.tsx @@ -31,6 +31,7 @@ interface Prop { postAttachment: (postAttachmentPayload: CreateAttachmentRequest) => void deleteAttachment: (id: string) => void userType: UserType + token: string } export const TaskEditor = ({ @@ -44,11 +45,12 @@ export const TaskEditor = ({ postAttachment, deleteAttachment, userType, + token, }: Prop) => { const [updateTitle, setUpdateTitle] = useState('') const [updateDetail, setUpdateDetail] = useState('') const { showConfirmDeleteModal, openImage } = useSelector(selectTaskDetails) - const { token, activeTask } = useSelector(selectTaskBoard) + const { activeTask } = useSelector(selectTaskBoard) const [isUserTyping, setIsUserTyping] = useState(false) const [activeUploads, setActiveUploads] = useState(0) diff --git a/src/app/detail/ui/ToggleController.tsx b/src/app/detail/ui/ToggleController.tsx index df989b692..32aad30d7 100644 --- a/src/app/detail/ui/ToggleController.tsx +++ b/src/app/detail/ui/ToggleController.tsx @@ -7,7 +7,7 @@ import { ReactNode, useEffect } from 'react' import { useSelector } from 'react-redux' export const ToggleController = ({ children }: { children: ReactNode }) => { - const { showSidebar } = useSelector(selectTaskDetails) + const { showSidebar, fromNotificationCenter } = useSelector(selectTaskDetails) const matches = useMediaQuery('(max-width:800px)') const nonMobile = useMediaQuery('(min-width:800px)') @@ -21,7 +21,7 @@ export const ToggleController = ({ children }: { children: ReactNode }) => { return ( + } + + const notificationDetail = await getNotificationDetail(token) + if (!notificationDetail) return + + const params = NotificationInProductCtaParamsSchema.parse(notificationDetail.deliveryTargets?.inProduct?.ctaParams) + + redirectIfTaskCta({ ...params, ...searchParams }, UserType.INTERNAL_USER, true) + + // Silent Error is shown if redirect fails. Only possible reason for redirect to not work can be of the taskId not found + return +} diff --git a/src/components/AttachmentLayout.tsx b/src/components/AttachmentLayout.tsx index 9ae706807..67144e188 100644 --- a/src/components/AttachmentLayout.tsx +++ b/src/components/AttachmentLayout.tsx @@ -178,7 +178,7 @@ const AttachmentLayout: React.FC = ({ maxWidth: '100%', overflow: 'hidden', '&:hover': { - border: isXsScreen ? 'none' : (theme) => `1px solid ${theme.color.gray[selected ? 600 : 300]}`, + border: (theme) => `1px solid ${theme.color.gray[selected ? 600 : 300]}`, '& .download-btn': { opacity: 1, }, diff --git a/src/components/cards/CommentCard.tsx b/src/components/cards/CommentCard.tsx index 4c2e57f0f..c056f9f54 100644 --- a/src/components/cards/CommentCard.tsx +++ b/src/components/cards/CommentCard.tsx @@ -50,6 +50,7 @@ export const CommentCard = ({ task_id, optimisticUpdates, commentInitiator, + 'data-comment-card': dataCommentCard, //for selection of the element while highlighting the container in notification }: { comment: LogResponse createComment: (postCommentPayload: CreateComment) => void @@ -57,6 +58,7 @@ export const CommentCard = ({ task_id: string optimisticUpdates: OptimisticUpdate[] commentInitiator: IAssigneeCombined | undefined + 'data-comment-card'?: string }) => { const [showReply, setShowReply] = useState(false) const [isHovered, setIsHovered] = useState(false) @@ -193,6 +195,7 @@ export const CommentCard = ({ }, [comment]) return ( (isReadOnly ? `${theme.color.gray[100]}` : `${theme.color.base.white}`), overflow: 'hidden', diff --git a/src/components/layouts/DeletedTaskRedirectPage.tsx b/src/components/layouts/DeletedTaskRedirectPage.tsx index 22529275d..613ad9109 100644 --- a/src/components/layouts/DeletedTaskRedirectPage.tsx +++ b/src/components/layouts/DeletedTaskRedirectPage.tsx @@ -1,12 +1,24 @@ +'use client' + import { UserRole } from '@/app/api/core/types/user' import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin' import { TasksListIcon } from '@/icons' + import { SxCenter } from '@/utils/mui' import { Box, Button, Stack, Typography } from '@mui/material' import Link from 'next/link' + import { z } from 'zod' -export const DeletedTaskRedirectPage = ({ userType, token }: { userType: UserRole; token: string }) => { +export const DeletedTaskRedirectPage = ({ + userType, + token, + fromNotificationCenter, +}: { + userType: UserRole + token: string + fromNotificationCenter: boolean +}) => { return ( <> @@ -39,24 +51,27 @@ export const DeletedTaskRedirectPage = ({ userType, token }: { userType: UserRol This task has been deleted. You can view your other tasks on the Tasks homepage. - - - + {/* disable board navigation on notification-center-view */} + {!fromNotificationCenter && ( + + + + )} diff --git a/src/hoc/DndWrapper.tsx b/src/hoc/DndWrapper.tsx index 95d283cf1..8feae32ba 100644 --- a/src/hoc/DndWrapper.tsx +++ b/src/hoc/DndWrapper.tsx @@ -5,9 +5,11 @@ import { DndProvider } from 'react-dnd' import { useMediaQuery } from '@mui/material' import { ModifiedHTML5Backend, ModifiedTouchBackend } from './ModifiedBackend' +import { useIsTouchDevice } from '@/hooks/useIsTouchDevice' export const DndWrapper = ({ children }: { children: ReactNode }) => { const [screenType, setScreenType] = useState<'mobile' | 'nonMobile' | undefined>(undefined) + const isTouch = useIsTouchDevice() const isMobileScreen = useMediaQuery('(max-width:600px)') useEffect(() => { @@ -22,7 +24,7 @@ export const DndWrapper = ({ children }: { children: ReactNode }) => { return ( { return false } -const createModifiedBackend = (Backend: BackendFactory, manager?: any, context?: any) => { - const instance = Backend(manager, context) +const createModifiedBackend = (Backend: BackendFactory, manager?: any, context?: any, options?: any) => { + const instance = Backend(manager, context, options) const listeners = [ 'handleTopDragStart', @@ -39,4 +39,5 @@ const createModifiedBackend = (Backend: BackendFactory, manager?: any, context?: export const ModifiedHTML5Backend = (manager: any, context: any) => createModifiedBackend(HTML5Backend, manager, context) -export const ModifiedTouchBackend = (manager: any, context: any) => createModifiedBackend(TouchBackend, manager, context) +export const ModifiedTouchBackend = (manager: any, context: any, options: any) => + createModifiedBackend(TouchBackend, manager, context, options) diff --git a/src/hoc/RealTime.tsx b/src/hoc/RealTime.tsx index d3478bae2..240101b23 100644 --- a/src/hoc/RealTime.tsx +++ b/src/hoc/RealTime.tsx @@ -3,6 +3,7 @@ import { RealtimeHandler } from '@/lib/realtime' import { supabase } from '@/lib/supabase' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' +import { selectTaskDetails } from '@/redux/features/taskDetailsSlice' import { Token } from '@/types/common' import { TaskResponse } from '@/types/dto/tasks.dto' import { AssigneeType } from '@prisma/client' @@ -29,7 +30,7 @@ export const RealTime = ({ tokenPayload: Token }) => { const { tasks, accessibleTasks, token, activeTask, assignee, accesibleTaskIds } = useSelector(selectTaskBoard) - const { showUnarchived, showArchived } = useSelector(selectTaskBoard) + const { fromNotificationCenter } = useSelector(selectTaskDetails) const pathname = usePathname() const router = useRouter() @@ -46,7 +47,8 @@ export const RealTime = ({ } const redirectToBoard = (updatedTask: RealTimeTaskResponse) => { - if (!pathname.includes('detail')) return + //disable board navigation if not in details page or from notification-center-view + if (!pathname.includes('detail') || fromNotificationCenter) return const isClientUser = pathname.includes('cu') const isAccessibleSubtask = updatedTask.parentId && accessibleTasks.some((task) => task.id === updatedTask.parentId) diff --git a/src/hooks/useIsTouchDevice.ts b/src/hooks/useIsTouchDevice.ts new file mode 100644 index 000000000..9800ebddd --- /dev/null +++ b/src/hooks/useIsTouchDevice.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +export const useIsTouchDevice = () => { + const [isTouchDevice, setIsTouchDevice] = useState(false) + + useEffect(() => { + if (typeof window !== 'undefined') { + const mediaQuery = window.matchMedia('(pointer: coarse) and (hover: none)') + setIsTouchDevice(mediaQuery.matches) + } + }, []) + + return isTouchDevice +} diff --git a/src/hooks/useScrollToElement.ts b/src/hooks/useScrollToElement.ts index 8a4494f4f..37747bfb2 100644 --- a/src/hooks/useScrollToElement.ts +++ b/src/hooks/useScrollToElement.ts @@ -18,15 +18,22 @@ const useScrollToElement = (paramName: string) => { const scrollToElement = () => { const targetElement = document.getElementById(elementId) - if (targetElement) { - setTimeout(() => { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }) - }, 100) - observer.disconnect() - } + if (!targetElement) return + const commentCard = targetElement.querySelector('[data-comment-card]') as HTMLElement | null + + setTimeout(() => { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }) + }, 100) + + setTimeout(() => { + if (commentCard) { + commentCard.classList.add('highlight-fade') + } + }, 600) + observer.disconnect() } scrollToElement() diff --git a/src/redux/features/taskDetailsSlice.ts b/src/redux/features/taskDetailsSlice.ts index 38ff2d502..f27c883c6 100644 --- a/src/redux/features/taskDetailsSlice.ts +++ b/src/redux/features/taskDetailsSlice.ts @@ -12,6 +12,7 @@ interface IInitialState { // URL of an image opened in preview modal for task description / comments openImage: string | null expandedComments: string[] + fromNotificationCenter: boolean } const initialState: IInitialState = { @@ -22,6 +23,7 @@ const initialState: IInitialState = { showConfirmAssignModal: false, openImage: null, expandedComments: [], + fromNotificationCenter: false, } const taskDetailsSlice = createSlice({ @@ -49,6 +51,9 @@ const taskDetailsSlice = createSlice({ setExpandedComments: (state, action: { payload: string[] }) => { state.expandedComments = action.payload }, + setFromNotificationCenter: (state, action: { payload: boolean }) => { + state.fromNotificationCenter = action.payload + }, }, }) @@ -62,6 +67,7 @@ export const { toggleShowConfirmAssignModal, setOpenImage, setExpandedComments, + setFromNotificationCenter, } = taskDetailsSlice.actions export default taskDetailsSlice.reducer diff --git a/src/types/common.ts b/src/types/common.ts index 0cea64f7d..eb18881fc 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,8 +1,8 @@ +import { type CommentInitiator, StateType } from '@prisma/client' +import { z } from 'zod' import { UserRole } from '@/app/api/core/types/user' import { validateNotificationRecipient } from '@/utils/notifications' -import { CommentInitiator, StateType } from '@prisma/client' -import { z } from 'zod' -import { UserIds } from './interfaces' +import type { UserIds } from './interfaces' export const Uuid = z.string().uuid() @@ -23,6 +23,7 @@ export const TokenSchema = z.object({ .optional(), internalUserId: z.string().optional(), workspaceId: z.string(), + notificationId: z.string().optional(), }) export type Token = z.infer @@ -263,7 +264,7 @@ export interface InitiatedEntity { initiatorType: CommentInitiator | null } -const rfc3339Regex = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[\+\-]\d{2}:\d{2}))$/ +const rfc3339Regex = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))$/ export const RFC3339DateSchema = z.string().refine((val) => rfc3339Regex.test(val), { message: 'Invalid RFC3339 datetime string', @@ -282,6 +283,27 @@ export type UrlActionParamsType = { oldPf?: string } +export const NotificationInProductCtaParamsSchema = z.object({ + taskId: z.string(), + commentId: z.string().optional(), +}) + +export const NotificationResponseSchema = z.object({ + deliveryTargets: z + .object({ + inProduct: z + .object({ + title: z.string().optional(), + isRead: z.boolean().optional(), + ctaParams: NotificationInProductCtaParamsSchema.optional(), + }) + .optional(), + }) + .optional(), + id: z.string(), +}) +export type NotificationResponseType = z.infer + export const ViewSettingUserIds = z .object({ internalUserId: z.string().nullable(), diff --git a/src/utils/CopilotAPI.ts b/src/utils/CopilotAPI.ts index 1299722e2..d734d02df 100644 --- a/src/utils/CopilotAPI.ts +++ b/src/utils/CopilotAPI.ts @@ -28,6 +28,8 @@ import { NotificationCreatedResponse, NotificationCreatedResponseSchema, NotificationRequestBody, + NotificationResponseSchema, + NotificationResponseType, Token, TokenSchema, WorkspaceResponse, @@ -264,6 +266,18 @@ export class CopilotAPI { await Promise.all(deletePromises) } + async _getIUNotification(id: string, workspaceId: string): Promise { + console.info('CopilotAPI#_deleteNotification', this.token) + const response = await this.manualFetch( + `notifications/${id}`, + { + includeRead: 'true', + }, + workspaceId, + ) + return NotificationResponseSchema.parse(response) + } + async _getClientNotifications( recipientClientId: string, recipientCompanyId: string, @@ -345,4 +359,5 @@ export class CopilotAPI { deleteNotification = this.wrapWithRetry(this._deleteNotification) bulkDeleteNotifications = this.wrapWithRetry(this._bulkDeleteNotifications) manualFetch = this.wrapWithRetry(this._manualFetch) + getIUNotification = this.wrapWithRetry(this._getIUNotification) } diff --git a/src/utils/redirect.ts b/src/utils/redirect.ts index f03e861eb..66509a169 100644 --- a/src/utils/redirect.ts +++ b/src/utils/redirect.ts @@ -5,17 +5,24 @@ import { redirect } from 'next/navigation' import { z } from 'zod' import { UserType } from '@/types/interfaces' -export const redirectIfTaskCta = (searchParams: Record, userType: UserType) => { +export const redirectIfTaskCta = ( + searchParams: Record, + userType: UserType, + fromNotificationCenter: boolean = false, +) => { const taskId = z.string().safeParse(searchParams.taskId) const commentId = z.string().safeParse(searchParams.commentId) if (taskId.data) { + const notificationCenterParam = fromNotificationCenter ? '&fromNotificationCenter=1' : '' if (commentId.data) { redirect( - `${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&commentId=${commentId.data}&isRedirect=1`, + `${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&commentId=${commentId.data}&isRedirect=1${notificationCenterParam}`, ) } - redirect(`${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&isRedirect=1`) + redirect( + `${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&isRedirect=1${notificationCenterParam}`, + ) } } diff --git a/vercel.json b/vercel.json index 976c04176..274e0133d 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,9 @@ { + "$schema": "https://openapi.vercel.sh/vercel.json", + "regions": [ + "iad1", + "pdx1" + ], "buildCommand": "./scripts/build.sh", "crons": [ {