feat: comments deep link (#2550)
* feat: comments deeplinks * cleanup * feat: add support to in-app notification
This commit is contained in:
parent
d3a0c9bfdf
commit
42e07d8019
16 changed files with 259 additions and 39 deletions
|
|
@ -3,6 +3,7 @@ import { atom } from 'jotai';
|
|||
export const showCommentPopupAtom = atom<boolean>(false);
|
||||
export const activeCommentIdAtom = atom<string>('');
|
||||
export const draftCommentIdAtom = atom<string>('');
|
||||
export const commentPanelTabAtom = atom<'open' | 'resolved'>('open');
|
||||
|
||||
// Read-only comment state
|
||||
export const showReadOnlyCommentPopupAtom = atom<boolean>(false);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useState } from "react";
|
||||
import { Dialog, Group, Stack, Text } from "@mantine/core";
|
||||
import { useClickOutside } from "@mantine/hooks";
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
activeCommentIdAtom,
|
||||
commentPanelTabAtom,
|
||||
draftCommentIdAtom,
|
||||
showCommentPopupAtom,
|
||||
showReadOnlyCommentPopupAtom,
|
||||
|
|
@ -32,6 +33,7 @@ function CommentDialog({ editor, pageId, readOnly }: CommentDialogProps) {
|
|||
const [, setShowReadOnlyCommentPopup] = useAtom(showReadOnlyCommentPopupAtom);
|
||||
const [readOnlyCommentData, setReadOnlyCommentData] = useAtom(readOnlyCommentDataAtom);
|
||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||
const setCommentPanelTab = useSetAtom(commentPanelTabAtom);
|
||||
const [draftCommentId, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setAsideState] = useAtom(asideStateAtom);
|
||||
|
|
@ -90,6 +92,7 @@ function CommentDialog({ editor, pageId, readOnly }: CommentDialogProps) {
|
|||
});
|
||||
}
|
||||
setActiveCommentId(createdComment.id);
|
||||
setCommentPanelTab("open");
|
||||
|
||||
setAsideState({ tab: "comments", isAsideOpen: true });
|
||||
setTimeout(() => {
|
||||
|
|
@ -121,6 +124,7 @@ function CommentDialog({ editor, pageId, readOnly }: CommentDialogProps) {
|
|||
});
|
||||
|
||||
setActiveCommentId(createdComment.id);
|
||||
setCommentPanelTab("open");
|
||||
setAsideState({ tab: "comments", isAsideOpen: true });
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -150,16 +150,19 @@ function CommentListItem({
|
|||
/>
|
||||
)}
|
||||
|
||||
{(currentUser?.user?.id === comment.creatorId || userSpaceRole === 'admin') && (
|
||||
<CommentMenu
|
||||
onEditComment={handleEditToggle}
|
||||
onDeleteComment={handleDeleteComment}
|
||||
onResolveComment={handleResolveComment}
|
||||
canEdit={currentUser?.user?.id === comment.creatorId}
|
||||
isResolved={comment.resolvedAt != null}
|
||||
isParentComment={!comment.parentCommentId}
|
||||
/>
|
||||
)}
|
||||
<CommentMenu
|
||||
commentId={comment.id}
|
||||
onEditComment={handleEditToggle}
|
||||
onDeleteComment={handleDeleteComment}
|
||||
onResolveComment={handleResolveComment}
|
||||
canEdit={currentUser?.user?.id === comment.creatorId}
|
||||
canManage={
|
||||
currentUser?.user?.id === comment.creatorId ||
|
||||
userSpaceRole === "admin"
|
||||
}
|
||||
isResolved={comment.resolvedAt != null}
|
||||
isParentComment={!comment.parentCommentId}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@ import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts"
|
|||
import { IconArrowUp, IconMessageOff } from "@tabler/icons-react";
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import { commentPanelTabAtom } from "@/features/comment/atoms/comment-atom";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
|
||||
function CommentListWithTabs() {
|
||||
const { t } = useTranslation();
|
||||
const { pageSlug } = useParams();
|
||||
const [activeTab, setActiveTab] = useAtom(commentPanelTabAtom);
|
||||
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||
const {
|
||||
data: comments,
|
||||
|
|
@ -185,7 +187,10 @@ function CommentListWithTabs() {
|
|||
}}
|
||||
>
|
||||
<Tabs
|
||||
defaultValue="open"
|
||||
value={activeTab}
|
||||
onChange={(value) =>
|
||||
setActiveTab(value === "resolved" ? "resolved" : "open")
|
||||
}
|
||||
variant="default"
|
||||
style={{
|
||||
flex: "1 1 auto",
|
||||
|
|
|
|||
|
|
@ -2,36 +2,51 @@ import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
|||
import {
|
||||
IconDots,
|
||||
IconEdit,
|
||||
IconLink,
|
||||
IconTrash,
|
||||
IconCircleCheck,
|
||||
IconCircleCheckFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { Feature } from "@/ee/features";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
import { useClipboard } from "@/hooks/use-clipboard";
|
||||
import { getAppUrl } from "@/lib/config";
|
||||
|
||||
type CommentMenuProps = {
|
||||
commentId: string;
|
||||
onEditComment: () => void;
|
||||
onDeleteComment: () => void;
|
||||
onResolveComment?: () => void;
|
||||
canEdit?: boolean;
|
||||
canManage?: boolean;
|
||||
isResolved?: boolean;
|
||||
isParentComment?: boolean;
|
||||
};
|
||||
|
||||
function CommentMenu({
|
||||
commentId,
|
||||
onEditComment,
|
||||
onDeleteComment,
|
||||
onResolveComment,
|
||||
canEdit = true,
|
||||
canManage = false,
|
||||
isResolved = false,
|
||||
isParentComment = false,
|
||||
}: CommentMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const canResolve = useHasFeature(Feature.COMMENT_RESOLUTION);
|
||||
const upgradeLabel = useUpgradeLabel();
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const link = `${getAppUrl()}${window.location.pathname}?commentId=${commentId}`;
|
||||
clipboard.copy(link);
|
||||
notifications.show({ message: t("Link copied") });
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
const openDeleteModal = () =>
|
||||
|
|
@ -65,6 +80,7 @@ function CommentMenu({
|
|||
</Menu.Item>
|
||||
)}
|
||||
{isParentComment &&
|
||||
canManage &&
|
||||
(canResolve ? (
|
||||
<Menu.Item
|
||||
onClick={onResolveComment}
|
||||
|
|
@ -86,11 +102,19 @@ function CommentMenu({
|
|||
</Tooltip>
|
||||
))}
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
leftSection={<IconLink size={14} />}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
{t("Delete comment")}
|
||||
{t("Copy link")}
|
||||
</Menu.Item>
|
||||
{canManage && (
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
{t("Delete comment")}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,19 @@
|
|||
border: 2px solid #8d7249;
|
||||
}
|
||||
|
||||
.deepLinkHighlight {
|
||||
animation: comment-deep-link-flash 3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes comment-deep-link-flash {
|
||||
0% {
|
||||
background-color: var(--mantine-color-yellow-light);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.textSelection {
|
||||
margin-top: 4px;
|
||||
border-left: 2px solid var(--mantine-color-gray-6);
|
||||
|
|
|
|||
153
apps/client/src/features/comment/hooks/use-comment-deep-link.ts
Normal file
153
apps/client/src/features/comment/hooks/use-comment-deep-link.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useCommentsQuery } from "@/features/comment/queries/comment-query";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import {
|
||||
activeCommentIdAtom,
|
||||
commentPanelTabAtom,
|
||||
} from "@/features/comment/atoms/comment-atom";
|
||||
import classes from "@/features/comment/components/comment.module.css";
|
||||
|
||||
const COMMENT_ID_PARAM = "commentId";
|
||||
const HIGHLIGHT_DURATION = 3000;
|
||||
const RETRY_INTERVAL = 100;
|
||||
const MAX_RETRIES = 40; // ~4s for the panel/editor to mount and render
|
||||
|
||||
/**
|
||||
* Reveals a comment referenced by a `?commentId=` query param (e.g. from a
|
||||
* notification email): opens the comments panel on the right tab, then scrolls
|
||||
* to and briefly highlights the comment in the panel and, for inline comments,
|
||||
* its mark in the document.
|
||||
*/
|
||||
export function useCommentDeepLink(pageId: string | undefined) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const commentId = searchParams.get(COMMENT_ID_PARAM);
|
||||
|
||||
const setAsideState = useSetAtom(asideStateAtom);
|
||||
const setActiveCommentId = useSetAtom(activeCommentIdAtom);
|
||||
const setCommentPanelTab = useSetAtom(commentPanelTabAtom);
|
||||
|
||||
// Only fetch comments when a deep link is present so normal page loads
|
||||
// don't pay for an eager comments request.
|
||||
const { data: comments, isLoading } = useCommentsQuery({
|
||||
pageId: (commentId ? pageId : undefined) as string,
|
||||
});
|
||||
|
||||
const handledRef = useRef<string | null>(null);
|
||||
const timersRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
||||
|
||||
// Cancel pending retries on unmount only, so clearing the URL param (which
|
||||
// re-runs the activation effect) cannot abort them.
|
||||
useEffect(
|
||||
() => () => {
|
||||
timersRef.current.forEach(clearTimeout);
|
||||
timersRef.current = [];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Reset per page so a deep-link tab override never outlives its page.
|
||||
useEffect(() => {
|
||||
setCommentPanelTab("open");
|
||||
}, [pageId, setCommentPanelTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!commentId) {
|
||||
handledRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (!pageId) return;
|
||||
if (isLoading || !comments?.items) return;
|
||||
if (handledRef.current === commentId) return;
|
||||
handledRef.current = commentId;
|
||||
|
||||
// Normalize the URL once handled, whatever the outcome.
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
nextParams.delete(COMMENT_ID_PARAM);
|
||||
setSearchParams(nextParams, { replace: true });
|
||||
|
||||
const target = comments.items.find((c) => c.id === commentId);
|
||||
if (!target) return; // unknown, deleted, or inaccessible comment
|
||||
|
||||
// Replies have no mark or panel container of their own; surface the thread.
|
||||
const threadId = target.parentCommentId ?? target.id;
|
||||
const thread = comments.items.find((c) => c.id === threadId);
|
||||
|
||||
setCommentPanelTab(thread?.resolvedAt ? "resolved" : "open");
|
||||
setActiveCommentId(threadId);
|
||||
setAsideState({ tab: "comments", isAsideOpen: true });
|
||||
|
||||
// Act once the element exists AND its position has settled, so the scroll
|
||||
// lands correctly even while the page is still laying out (static -> live
|
||||
// editor swap, images, fonts). Falls back to the last attempt if layout
|
||||
// never fully settles within the window.
|
||||
const onElementReady = (
|
||||
selector: string,
|
||||
onFound: (el: HTMLElement) => void,
|
||||
) => {
|
||||
let attempts = 0;
|
||||
let lastTop: number | null = null;
|
||||
let stableTicks = 0;
|
||||
const tick = () => {
|
||||
let el: HTMLElement | null = null;
|
||||
try {
|
||||
el = document.querySelector<HTMLElement>(selector);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
attempts += 1;
|
||||
const lastAttempt = attempts >= MAX_RETRIES;
|
||||
if (el) {
|
||||
const top = Math.round(el.getBoundingClientRect().top + window.scrollY);
|
||||
stableTicks =
|
||||
lastTop !== null && Math.abs(top - lastTop) <= 1 ? stableTicks + 1 : 0;
|
||||
lastTop = top;
|
||||
if (stableTicks >= 2 || lastAttempt) {
|
||||
onFound(el);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!lastAttempt) {
|
||||
timersRef.current.push(setTimeout(tick, RETRY_INTERVAL));
|
||||
}
|
||||
};
|
||||
tick();
|
||||
};
|
||||
|
||||
onElementReady(`div[data-comment-id="${threadId}"]`, (el) => {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add(classes.deepLinkHighlight);
|
||||
timersRef.current.push(
|
||||
setTimeout(
|
||||
() => el.classList.remove(classes.deepLinkHighlight),
|
||||
HIGHLIGHT_DURATION,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Inline marks render only for unresolved selection comments.
|
||||
if (thread?.selection && !thread.resolvedAt) {
|
||||
onElementReady(`.comment-mark[data-comment-id="${threadId}"]`, (el) => {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("comment-highlight");
|
||||
timersRef.current.push(
|
||||
setTimeout(
|
||||
() => el.classList.remove("comment-highlight"),
|
||||
HIGHLIGHT_DURATION,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
commentId,
|
||||
pageId,
|
||||
isLoading,
|
||||
comments,
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
setAsideState,
|
||||
setActiveCommentId,
|
||||
setCommentPanelTab,
|
||||
]);
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import {
|
|||
collabExtensions,
|
||||
mainExtensions,
|
||||
} from "@/features/editor/extensions/extensions";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import {
|
||||
currentPageEditModeAtom,
|
||||
|
|
@ -41,6 +41,7 @@ import {
|
|||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import {
|
||||
activeCommentIdAtom,
|
||||
commentPanelTabAtom,
|
||||
showCommentPopupAtom,
|
||||
showReadOnlyCommentPopupAtom,
|
||||
} from "@/features/comment/atoms/comment-atom";
|
||||
|
|
@ -188,6 +189,7 @@ function CollabPageEditor({
|
|||
const [, setEditor] = useAtom(pageEditorAtom);
|
||||
const [, setAsideState] = useAtom(asideStateAtom);
|
||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||
const setCommentPanelTab = useSetAtom(commentPanelTabAtom);
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [showReadOnlyCommentPopup] = useAtom(showReadOnlyCommentPopupAtom);
|
||||
const [lightboxRequest, setLightboxRequest] = useAtom(lightboxRequestAtom);
|
||||
|
|
@ -386,6 +388,7 @@ function CollabPageEditor({
|
|||
}
|
||||
|
||||
setActiveCommentId(commentId);
|
||||
setCommentPanelTab("open");
|
||||
setAsideState({ tab: "comments", isAsideOpen: true });
|
||||
|
||||
//wait if aside is closed
|
||||
|
|
@ -409,7 +412,6 @@ function CollabPageEditor({
|
|||
useEffect(() => {
|
||||
setActiveCommentId(null);
|
||||
setShowCommentPopup(false);
|
||||
setAsideState({ tab: "", isAsideOpen: false });
|
||||
setLightboxRequest(null);
|
||||
}, [pageId]);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,13 +74,16 @@ export function NotificationItem({
|
|||
}
|
||||
};
|
||||
|
||||
const commentParam = notification.commentId
|
||||
? `?commentId=${notification.commentId}`
|
||||
: "";
|
||||
const pageUrl =
|
||||
notification.page && notification.space
|
||||
? buildPageUrl(
|
||||
notification.space.slug,
|
||||
notification.page.slugId,
|
||||
notification.page.title,
|
||||
)
|
||||
) + commentParam
|
||||
: undefined;
|
||||
|
||||
const isSiemDestination = notification.type.startsWith("siem_destination.");
|
||||
|
|
|
|||
|
|
@ -86,12 +86,12 @@ function redirectToLogin() {
|
|||
// the oauth consent page redirects to login itself, preserving its query string
|
||||
"/oauth/consent",
|
||||
];
|
||||
if (!exemptPaths.some((path) => window.location.pathname.startsWith(path))) {
|
||||
const redirectTo = window.location.pathname;
|
||||
if (redirectTo === APP_ROUTE.HOME) {
|
||||
const { pathname, search, hash } = window.location;
|
||||
if (!exemptPaths.some((path) => pathname.startsWith(path))) {
|
||||
if (pathname === APP_ROUTE.HOME) {
|
||||
window.location.href = APP_ROUTE.AUTH.LOGIN;
|
||||
} else {
|
||||
const params = new URLSearchParams({ redirect: redirectTo });
|
||||
const params = new URLSearchParams({ redirect: pathname + search + hash });
|
||||
window.location.href = `${APP_ROUTE.AUTH.LOGIN}?${params.toString()}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ import HistoryModal from "@/features/page-history/components/history-modal";
|
|||
import PageHeader from "@/features/page/components/header/page-header.tsx";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useCommentDeepLink } from "@/features/comment/hooks/use-comment-deep-link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import { EmptyState } from "@/components/ui/empty-state.tsx";
|
||||
import { IconAlertTriangle, IconFileOff } from "@tabler/icons-react";
|
||||
import { Button } from "@mantine/core";
|
||||
|
|
@ -57,6 +60,14 @@ function PageContent({ pageSlug }: { pageSlug: string | undefined }) {
|
|||
error,
|
||||
} = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
|
||||
const setAsideState = useSetAtom(asideStateAtom);
|
||||
|
||||
// Keep above useCommentDeepLink so a deep link can reopen the aside.
|
||||
useEffect(() => {
|
||||
setAsideState({ tab: "", isAsideOpen: false });
|
||||
}, [page?.id, setAsideState]);
|
||||
|
||||
useCommentDeepLink(page?.id);
|
||||
|
||||
const hasBases = useHasFeature(Feature.BASES);
|
||||
const canEdit = !page?.deletedAt && (page?.permissions?.canEdit ?? false);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export class CommentNotificationService {
|
|||
);
|
||||
if (!context) return;
|
||||
|
||||
const { actor, pageTitle, pageUrl } = context;
|
||||
const { actor, pageTitle, commentUrl } = context;
|
||||
const notifiedUserIds = new Set<string>();
|
||||
notifiedUserIds.add(actorId);
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ export class CommentNotificationService {
|
|||
userId,
|
||||
notification.id,
|
||||
`${actor.name} mentioned you in a comment`,
|
||||
CommentMentionEmail({ actorName: actor.name, pageTitle, pageUrl }),
|
||||
CommentMentionEmail({ actorName: actor.name, pageTitle, commentUrl }),
|
||||
NotificationType.COMMENT_USER_MENTION,
|
||||
);
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ export class CommentNotificationService {
|
|||
recipientId,
|
||||
notification.id,
|
||||
`${actor.name} commented on ${pageTitle}`,
|
||||
CommentCreateEmail({ actorName: actor.name, pageTitle, pageUrl }),
|
||||
CommentCreateEmail({ actorName: actor.name, pageTitle, commentUrl }),
|
||||
NotificationType.COMMENT_CREATED,
|
||||
);
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ export class CommentNotificationService {
|
|||
);
|
||||
if (!context) return;
|
||||
|
||||
const { actor, pageTitle, pageUrl } = context;
|
||||
const { actor, pageTitle, commentUrl } = context;
|
||||
|
||||
const roles = await this.spaceMemberRepo.getUserSpaceRoles(
|
||||
commentCreatorId,
|
||||
|
|
@ -183,7 +183,7 @@ export class CommentNotificationService {
|
|||
commentCreatorId,
|
||||
notification.id,
|
||||
subject,
|
||||
CommentResolvedEmail({ actorName: actor.name, pageTitle, pageUrl }),
|
||||
CommentResolvedEmail({ actorName: actor.name, pageTitle, commentUrl }),
|
||||
NotificationType.COMMENT_RESOLVED,
|
||||
);
|
||||
}
|
||||
|
|
@ -235,7 +235,8 @@ export class CommentNotificationService {
|
|||
}
|
||||
|
||||
const pageUrl = `${appUrl}/s/${space.slug}/p/${page.slugId}`;
|
||||
const commentUrl = `${pageUrl}?commentId=${commentId}`;
|
||||
|
||||
return { actor, pageTitle: getPageTitle(page.title), pageUrl };
|
||||
return { actor, pageTitle: getPageTitle(page.title), commentUrl };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit d6260785de583ddb85109802c92499eb9c21b8f0
|
||||
Subproject commit cc28defeec44c8f71e686cbec6e4298bb447e0e7
|
||||
|
|
@ -6,13 +6,13 @@ import { EmailButton, MailBody } from '../partials/partials';
|
|||
interface Props {
|
||||
actorName: string;
|
||||
pageTitle: string;
|
||||
pageUrl: string;
|
||||
commentUrl: string;
|
||||
}
|
||||
|
||||
export const CommentCreateEmail = ({
|
||||
actorName,
|
||||
pageTitle,
|
||||
pageUrl,
|
||||
commentUrl,
|
||||
}: Props) => {
|
||||
return (
|
||||
<MailBody>
|
||||
|
|
@ -23,7 +23,7 @@ export const CommentCreateEmail = ({
|
|||
<strong>{pageTitle}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
<EmailButton href={pageUrl}>View</EmailButton>
|
||||
<EmailButton href={commentUrl}>View</EmailButton>
|
||||
</MailBody>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { EmailButton, MailBody } from '../partials/partials';
|
|||
interface Props {
|
||||
actorName: string;
|
||||
pageTitle: string;
|
||||
pageUrl: string;
|
||||
commentUrl: string;
|
||||
}
|
||||
|
||||
export const CommentMentionEmail = ({
|
||||
actorName,
|
||||
pageTitle,
|
||||
pageUrl,
|
||||
commentUrl,
|
||||
}: Props) => {
|
||||
return (
|
||||
<MailBody>
|
||||
|
|
@ -23,7 +23,7 @@ export const CommentMentionEmail = ({
|
|||
<strong>{pageTitle}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
<EmailButton href={pageUrl}>View</EmailButton>
|
||||
<EmailButton href={commentUrl}>View</EmailButton>
|
||||
</MailBody>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { EmailButton, MailBody } from '../partials/partials';
|
|||
interface Props {
|
||||
actorName: string;
|
||||
pageTitle: string;
|
||||
pageUrl: string;
|
||||
commentUrl: string;
|
||||
}
|
||||
|
||||
export const CommentResolvedEmail = ({
|
||||
actorName,
|
||||
pageTitle,
|
||||
pageUrl,
|
||||
commentUrl,
|
||||
}: Props) => {
|
||||
return (
|
||||
<MailBody>
|
||||
|
|
@ -23,7 +23,7 @@ export const CommentResolvedEmail = ({
|
|||
<strong>{pageTitle}</strong>.
|
||||
</Text>
|
||||
</Section>
|
||||
<EmailButton href={pageUrl}>View</EmailButton>
|
||||
<EmailButton href={commentUrl}>View</EmailButton>
|
||||
</MailBody>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue