mirror of
https://github.com/docmost/docmost.git
synced 2026-08-24 12:22:17 +10:00
feat(ee): space control to hide comments from viewers role
This commit is contained in:
@@ -508,6 +508,11 @@
|
|||||||
"Allow viewers to comment": "Allow viewers to comment",
|
"Allow viewers to comment": "Allow viewers to comment",
|
||||||
"Allow viewers to add comments on pages in this space.": "Allow viewers to add comments on pages in this space.",
|
"Allow viewers to add comments on pages in this space.": "Allow viewers to add comments on pages in this space.",
|
||||||
"Toggle viewer comments": "Toggle viewer comments",
|
"Toggle viewer comments": "Toggle viewer comments",
|
||||||
|
"Hide comments from viewers": "Hide comments from viewers",
|
||||||
|
"Viewers cannot see or add comments on pages in this space.": "Viewers cannot see or add comments on pages in this space.",
|
||||||
|
"Toggle hide comments from viewers": "Toggle hide comments from viewers",
|
||||||
|
"Turn off 'Allow viewers to comment' first": "Turn off 'Allow viewers to comment' first",
|
||||||
|
"Turn off 'Hide comments from viewers' first": "Turn off 'Hide comments from viewers' first",
|
||||||
"Public sharing is disabled at the workspace level": "Public sharing is disabled at the workspace level",
|
"Public sharing is disabled at the workspace level": "Public sharing is disabled at the workspace level",
|
||||||
"Prevent pages in this space from being shared publicly.": "Prevent pages in this space from being shared publicly.",
|
"Prevent pages in this space from being shared publicly.": "Prevent pages in this space from being shared publicly.",
|
||||||
"Page permissions": "Page permissions",
|
"Page permissions": "Page permissions",
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
|||||||
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
|
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
|
||||||
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
|
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
|
||||||
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
|
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
|
||||||
|
import { useCanViewComments } from "@/features/comment/hooks/use-can-view-comments.ts";
|
||||||
|
|
||||||
export default function Aside() {
|
export default function Aside() {
|
||||||
const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom);
|
const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const pageEditor = useAtomValue(pageEditorAtom);
|
const pageEditor = useAtomValue(pageEditorAtom);
|
||||||
|
const canViewComments = useCanViewComments();
|
||||||
const closeAside = () => setAsideState((s) => ({ ...s, isAsideOpen: false }));
|
const closeAside = () => setAsideState((s) => ({ ...s, isAsideOpen: false }));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -23,12 +25,18 @@ export default function Aside() {
|
|||||||
document.getElementById(ASIDE_PANEL_ID)?.focus();
|
document.getElementById(ASIDE_PANEL_ID)?.focus();
|
||||||
}, [isAsideOpen, tab]);
|
}, [isAsideOpen, tab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAsideOpen && tab === "comments" && !canViewComments) {
|
||||||
|
setAsideState({ tab: "", isAsideOpen: false });
|
||||||
|
}
|
||||||
|
}, [isAsideOpen, tab, canViewComments, setAsideState]);
|
||||||
|
|
||||||
let title: string;
|
let title: string;
|
||||||
let component: ReactNode;
|
let component: ReactNode;
|
||||||
|
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "comments":
|
case "comments":
|
||||||
component = <CommentListWithTabs />;
|
component = canViewComments ? <CommentListWithTabs /> : null;
|
||||||
title = "Comments";
|
title = "Comments";
|
||||||
break;
|
break;
|
||||||
case "toc":
|
case "toc":
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const Feature = {
|
|||||||
SHARING_CONTROLS: 'sharing:controls',
|
SHARING_CONTROLS: 'sharing:controls',
|
||||||
TEMPLATES: 'templates',
|
TEMPLATES: 'templates',
|
||||||
VIEWER_COMMENTS: 'comment:viewer',
|
VIEWER_COMMENTS: 'comment:viewer',
|
||||||
|
HIDE_COMMENTS: 'comment:hide',
|
||||||
PERSONAL_SPACES: 'spaces:personal',
|
PERSONAL_SPACES: 'spaces:personal',
|
||||||
DOCX_EXPORT: 'export:docx',
|
DOCX_EXPORT: 'export:docx',
|
||||||
BASES: 'bases',
|
BASES: 'bases',
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Group, Text, Switch, Tooltip } from "@mantine/core";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||||
|
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||||
|
import { useHasFeature } from "@/ee/hooks/use-feature.ts";
|
||||||
|
import { Feature } from "@/ee/features.ts";
|
||||||
|
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
|
||||||
|
|
||||||
|
type SpaceHideCommentsToggleProps = {
|
||||||
|
space: ISpace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SpaceHideCommentsToggle({
|
||||||
|
space,
|
||||||
|
}: SpaceHideCommentsToggleProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const hasHideComments = useHasFeature(Feature.HIDE_COMMENTS);
|
||||||
|
const upgradeLabel = useUpgradeLabel();
|
||||||
|
const allowViewerCommentsEnabled =
|
||||||
|
space.settings?.comments?.allowViewerComments === true;
|
||||||
|
const isDisabled = !hasHideComments || allowViewerCommentsEnabled;
|
||||||
|
const tooltipLabel = !hasHideComments
|
||||||
|
? upgradeLabel
|
||||||
|
: t("Turn off 'Allow viewers to comment' first");
|
||||||
|
const [checked, setChecked] = useState(
|
||||||
|
space.settings?.comments?.hideCommentsFromViewers === true,
|
||||||
|
);
|
||||||
|
const updateSpaceMutation = useUpdateSpaceMutation();
|
||||||
|
|
||||||
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.currentTarget.checked;
|
||||||
|
try {
|
||||||
|
await updateSpaceMutation.mutateAsync({
|
||||||
|
spaceId: space.id,
|
||||||
|
hideCommentsFromViewers: value,
|
||||||
|
});
|
||||||
|
setChecked(value);
|
||||||
|
} catch {
|
||||||
|
// error handled by mutation
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="md">{t("Hide comments from viewers")}</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t("Viewers cannot see or add comments on pages in this space.")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Tooltip label={tooltipLabel} disabled={!isDisabled} refProp="rootRef">
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={isDisabled}
|
||||||
|
aria-label={t("Toggle hide comments from viewers")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,7 +17,12 @@ export default function SpaceViewerCommentsToggle({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const hasViewerComments = useHasFeature(Feature.VIEWER_COMMENTS);
|
const hasViewerComments = useHasFeature(Feature.VIEWER_COMMENTS);
|
||||||
const upgradeLabel = useUpgradeLabel();
|
const upgradeLabel = useUpgradeLabel();
|
||||||
const isDisabled = !hasViewerComments;
|
const hideCommentsEnabled =
|
||||||
|
space.settings?.comments?.hideCommentsFromViewers === true;
|
||||||
|
const isDisabled = !hasViewerComments || hideCommentsEnabled;
|
||||||
|
const tooltipLabel = !hasViewerComments
|
||||||
|
? upgradeLabel
|
||||||
|
: t("Turn off 'Hide comments from viewers' first");
|
||||||
const [checked, setChecked] = useState(
|
const [checked, setChecked] = useState(
|
||||||
space.settings?.comments?.allowViewerComments === true,
|
space.settings?.comments?.allowViewerComments === true,
|
||||||
);
|
);
|
||||||
@@ -45,7 +50,7 @@ export default function SpaceViewerCommentsToggle({
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={upgradeLabel}
|
label={tooltipLabel}
|
||||||
disabled={!isDisabled}
|
disabled={!isDisabled}
|
||||||
refProp="rootRef"
|
refProp="rootRef"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||||
|
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||||
|
import { extractPageSlugId } from "@/lib";
|
||||||
|
|
||||||
|
export function useCanViewComments(): boolean {
|
||||||
|
const { pageSlug } = useParams();
|
||||||
|
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||||
|
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
|
||||||
|
|
||||||
|
const canEdit = !page?.deletedAt && (page?.permissions?.canEdit ?? false);
|
||||||
|
return (
|
||||||
|
canEdit || space?.settings?.comments?.hideCommentsFromViewers !== true
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -82,6 +82,8 @@ import {
|
|||||||
getCollabSocket,
|
getCollabSocket,
|
||||||
releaseCollabSocket,
|
releaseCollabSocket,
|
||||||
} from "@/features/editor/collab-socket";
|
} from "@/features/editor/collab-socket";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useCanViewComments } from "@/features/comment/hooks/use-can-view-comments.ts";
|
||||||
|
|
||||||
interface PageEditorProps {
|
interface PageEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -196,6 +198,7 @@ function CollabPageEditor({
|
|||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const slugId = extractPageSlugId(pageSlug);
|
const slugId = extractPageSlugId(pageSlug);
|
||||||
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
const currentPageEditMode = useAtomValue(currentPageEditModeAtom);
|
||||||
|
const canViewComments = useCanViewComments();
|
||||||
const canScroll = useCallback(
|
const canScroll = useCallback(
|
||||||
() => Boolean(isComponentMounted.current && editorRef.current),
|
() => Boolean(isComponentMounted.current && editorRef.current),
|
||||||
[isComponentMounted],
|
[isComponentMounted],
|
||||||
@@ -372,6 +375,7 @@ function CollabPageEditor({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!canViewComments) return;
|
||||||
document.addEventListener("ACTIVE_COMMENT_EVENT", handleActiveCommentEvent);
|
document.addEventListener("ACTIVE_COMMENT_EVENT", handleActiveCommentEvent);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
@@ -379,7 +383,7 @@ function CollabPageEditor({
|
|||||||
handleActiveCommentEvent,
|
handleActiveCommentEvent,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [canViewComments]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveCommentId(null);
|
setActiveCommentId(null);
|
||||||
@@ -430,7 +434,13 @@ function CollabPageEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-container" style={{ position: "relative" }}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"editor-container",
|
||||||
|
!canViewComments && "comments-hidden",
|
||||||
|
)}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
>
|
||||||
<div ref={menuContainerRef}>
|
<div ref={menuContainerRef}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
|
|
||||||
@@ -480,17 +490,21 @@ function StaticPageEditor({
|
|||||||
content: any;
|
content: any;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
}) {
|
}) {
|
||||||
|
const canViewComments = useCanViewComments();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorProvider
|
<div className={clsx(!canViewComments && "comments-hidden")}>
|
||||||
editable={false}
|
<EditorProvider
|
||||||
immediatelyRender={true}
|
editable={false}
|
||||||
extensions={mainExtensions}
|
immediatelyRender={true}
|
||||||
content={content}
|
extensions={mainExtensions}
|
||||||
editorProps={{
|
content={content}
|
||||||
attributes: {
|
editorProps={{
|
||||||
"aria-label": ariaLabel,
|
attributes: {
|
||||||
},
|
"aria-label": ariaLabel,
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,3 +315,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comments-hidden .ProseMirror .comment-mark {
|
||||||
|
background: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
useWatchPageMutation,
|
useWatchPageMutation,
|
||||||
useUnwatchPageMutation,
|
useUnwatchPageMutation,
|
||||||
} from "@/features/page/queries/watcher-query";
|
} from "@/features/page/queries/watcher-query";
|
||||||
|
import { useCanViewComments } from "@/features/comment/hooks/use-can-view-comments.ts";
|
||||||
|
|
||||||
interface PageHeaderMenuProps {
|
interface PageHeaderMenuProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -65,6 +66,7 @@ interface PageHeaderMenuProps {
|
|||||||
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const commentsTriggerProps = useAsideTriggerProps("comments");
|
const commentsTriggerProps = useAsideTriggerProps("comments");
|
||||||
|
const canViewComments = useCanViewComments();
|
||||||
const tocTriggerProps = useAsideTriggerProps("toc");
|
const tocTriggerProps = useAsideTriggerProps("toc");
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const { data: page } = usePageQuery({
|
const { data: page } = usePageQuery({
|
||||||
@@ -105,16 +107,18 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
|
|||||||
|
|
||||||
<PageShareModal readOnly={readOnly} />
|
<PageShareModal readOnly={readOnly} />
|
||||||
|
|
||||||
<Tooltip label={t("Comments")} openDelay={250} withArrow>
|
{canViewComments && (
|
||||||
<ActionIcon
|
<Tooltip label={t("Comments")} openDelay={250} withArrow>
|
||||||
variant="subtle"
|
<ActionIcon
|
||||||
color="dark"
|
variant="subtle"
|
||||||
aria-label={t("Comments")}
|
color="dark"
|
||||||
{...commentsTriggerProps}
|
aria-label={t("Comments")}
|
||||||
>
|
{...commentsTriggerProps}
|
||||||
<IconMessage size={20} stroke={2} />
|
>
|
||||||
</ActionIcon>
|
<IconMessage size={20} stroke={2} />
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
{!page?.isBase && (
|
{!page?.isBase && (
|
||||||
<Tooltip label={t("Table of contents")} openDelay={250} withArrow>
|
<Tooltip label={t("Table of contents")} openDelay={250} withArrow>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||||
import SpacePublicSharingToggle from "@/ee/security/components/space-public-sharing-toggle.tsx";
|
import SpacePublicSharingToggle from "@/ee/security/components/space-public-sharing-toggle.tsx";
|
||||||
import SpaceViewerCommentsToggle from "@/ee/security/components/space-viewer-comments-toggle.tsx";
|
import SpaceViewerCommentsToggle from "@/ee/security/components/space-viewer-comments-toggle.tsx";
|
||||||
|
import SpaceHideCommentsToggle from "@/ee/security/components/space-hide-comments-toggle.tsx";
|
||||||
|
|
||||||
type SpaceSecuritySettingsProps = {
|
type SpaceSecuritySettingsProps = {
|
||||||
space: ISpace;
|
space: ISpace;
|
||||||
@@ -29,6 +30,10 @@ export default function SpaceSecuritySettings({
|
|||||||
<Divider my="lg" />
|
<Divider my="lg" />
|
||||||
|
|
||||||
<SpaceViewerCommentsToggle space={space} />
|
<SpaceViewerCommentsToggle space={space} />
|
||||||
|
|
||||||
|
<Divider my="lg" />
|
||||||
|
|
||||||
|
<SpaceHideCommentsToggle space={space} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface ISpaceSharingSettings {
|
|||||||
|
|
||||||
export interface ISpaceCommentsSettings {
|
export interface ISpaceCommentsSettings {
|
||||||
allowViewerComments?: boolean;
|
allowViewerComments?: boolean;
|
||||||
|
hideCommentsFromViewers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISpaceSettings {
|
export interface ISpaceSettings {
|
||||||
@@ -36,6 +37,7 @@ export interface ISpace {
|
|||||||
// for updates
|
// for updates
|
||||||
disablePublicSharing?: boolean;
|
disablePublicSharing?: boolean;
|
||||||
allowViewerComments?: boolean;
|
allowViewerComments?: boolean;
|
||||||
|
hideCommentsFromViewers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMembership {
|
interface IMembership {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const Feature = {
|
|||||||
RETENTION: 'retention',
|
RETENTION: 'retention',
|
||||||
SHARING_CONTROLS: 'sharing:controls',
|
SHARING_CONTROLS: 'sharing:controls',
|
||||||
VIEWER_COMMENTS: 'comment:viewer',
|
VIEWER_COMMENTS: 'comment:viewer',
|
||||||
|
HIDE_COMMENTS: 'comment:hide',
|
||||||
TEMPLATES: 'templates',
|
TEMPLATES: 'templates',
|
||||||
PDF_EXPORT: 'export:pdf',
|
PDF_EXPORT: 'export:pdf',
|
||||||
PERSONAL_SPACES: 'spaces:personal',
|
PERSONAL_SPACES: 'spaces:personal',
|
||||||
|
|||||||
@@ -89,20 +89,29 @@ export class CommentController {
|
|||||||
@Body()
|
@Body()
|
||||||
pagination: PaginationOptions,
|
pagination: PaginationOptions,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
) {
|
) {
|
||||||
const page = await this.pageRepo.findById(input.pageId);
|
const page = await this.pageRepo.findById(input.pageId);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
throw new NotFoundException('Page not found');
|
throw new NotFoundException('Page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.pageAccessService.validateCanView(page, user);
|
await this.pageAccessService.validateCanViewComments(
|
||||||
|
page,
|
||||||
|
user,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
return this.commentService.findByPageId(page.id, pagination);
|
return this.commentService.findByPageId(page.id, pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post('info')
|
@Post('info')
|
||||||
async findOne(@Body() input: CommentIdDto, @AuthUser() user: User) {
|
async findOne(
|
||||||
|
@Body() input: CommentIdDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
const comment = await this.commentRepo.findById(input.commentId);
|
const comment = await this.commentRepo.findById(input.commentId);
|
||||||
if (!comment) {
|
if (!comment) {
|
||||||
throw new NotFoundException('Comment not found');
|
throw new NotFoundException('Comment not found');
|
||||||
@@ -113,7 +122,11 @@ export class CommentController {
|
|||||||
throw new NotFoundException('Page not found');
|
throw new NotFoundException('Page not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.pageAccessService.validateCanView(page, user);
|
await this.pageAccessService.validateCanViewComments(
|
||||||
|
page,
|
||||||
|
user,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
return comment;
|
return comment;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { CommentMentionEmail } from '@docmost/transactional/emails/comment-menti
|
|||||||
import { CommentCreateEmail } from '@docmost/transactional/emails/comment-created-email';
|
import { CommentCreateEmail } from '@docmost/transactional/emails/comment-created-email';
|
||||||
import { CommentResolvedEmail } from '@docmost/transactional/emails/comment-resolved-email';
|
import { CommentResolvedEmail } from '@docmost/transactional/emails/comment-resolved-email';
|
||||||
import { getPageTitle } from '../../../common/helpers';
|
import { getPageTitle } from '../../../common/helpers';
|
||||||
|
import { PageAccessService } from '../../page/page-access/page-access.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommentNotificationService {
|
export class CommentNotificationService {
|
||||||
@@ -25,6 +26,7 @@ export class CommentNotificationService {
|
|||||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||||
private readonly watcherRepo: WatcherRepo,
|
private readonly watcherRepo: WatcherRepo,
|
||||||
|
private readonly pageAccessService: PageAccessService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async processComment(data: ICommentNotificationJob, appUrl: string) {
|
async processComment(data: ICommentNotificationJob, appUrl: string) {
|
||||||
@@ -48,7 +50,7 @@ export class CommentNotificationService {
|
|||||||
);
|
);
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
const { actor, pageTitle, pageUrl } = context;
|
const { actor, pageTitle, pageUrl, spaceSettings } = context;
|
||||||
const notifiedUserIds = new Set<string>();
|
const notifiedUserIds = new Set<string>();
|
||||||
notifiedUserIds.add(actorId);
|
notifiedUserIds.add(actorId);
|
||||||
|
|
||||||
@@ -72,7 +74,16 @@ export class CommentNotificationService {
|
|||||||
pageId,
|
pageId,
|
||||||
[...usersWithSpaceAccess],
|
[...usersWithSpaceAccess],
|
||||||
);
|
);
|
||||||
const usersWithAccess = new Set(usersWithPageAccess);
|
let accessibleUserIds = usersWithPageAccess;
|
||||||
|
if (spaceSettings?.comments?.hideCommentsFromViewers === true) {
|
||||||
|
accessibleUserIds =
|
||||||
|
await this.pageAccessService.filterUserIdsWithPageEditAccess(
|
||||||
|
spaceId,
|
||||||
|
pageId,
|
||||||
|
accessibleUserIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const usersWithAccess = new Set(accessibleUserIds);
|
||||||
|
|
||||||
for (const userId of mentionedUserIds) {
|
for (const userId of mentionedUserIds) {
|
||||||
if (!usersWithAccess.has(userId)) continue;
|
if (!usersWithAccess.has(userId)) continue;
|
||||||
@@ -145,7 +156,7 @@ export class CommentNotificationService {
|
|||||||
);
|
);
|
||||||
if (!context) return;
|
if (!context) return;
|
||||||
|
|
||||||
const { actor, pageTitle, pageUrl } = context;
|
const { actor, pageTitle, pageUrl, spaceSettings } = context;
|
||||||
|
|
||||||
const roles = await this.spaceMemberRepo.getUserSpaceRoles(
|
const roles = await this.spaceMemberRepo.getUserSpaceRoles(
|
||||||
commentCreatorId,
|
commentCreatorId,
|
||||||
@@ -166,6 +177,16 @@ export class CommentNotificationService {
|
|||||||
);
|
);
|
||||||
if (hasPageAccess.length === 0) return;
|
if (hasPageAccess.length === 0) return;
|
||||||
|
|
||||||
|
if (spaceSettings?.comments?.hideCommentsFromViewers === true) {
|
||||||
|
const editCapable =
|
||||||
|
await this.pageAccessService.filterUserIdsWithPageEditAccess(
|
||||||
|
spaceId,
|
||||||
|
pageId,
|
||||||
|
[commentCreatorId],
|
||||||
|
);
|
||||||
|
if (editCapable.length === 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
const notification = await this.notificationService.create({
|
const notification = await this.notificationService.create({
|
||||||
userId: commentCreatorId,
|
userId: commentCreatorId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -225,7 +246,7 @@ export class CommentNotificationService {
|
|||||||
.executeTakeFirst(),
|
.executeTakeFirst(),
|
||||||
this.db
|
this.db
|
||||||
.selectFrom('spaces')
|
.selectFrom('spaces')
|
||||||
.select(['id', 'slug'])
|
.select(['id', 'slug', 'settings'])
|
||||||
.where('id', '=', spaceId)
|
.where('id', '=', spaceId)
|
||||||
.executeTakeFirst(),
|
.executeTakeFirst(),
|
||||||
]);
|
]);
|
||||||
@@ -236,6 +257,11 @@ export class CommentNotificationService {
|
|||||||
|
|
||||||
const pageUrl = `${appUrl}/s/${space.slug}/p/${page.slugId}`;
|
const pageUrl = `${appUrl}/s/${space.slug}/p/${page.slugId}`;
|
||||||
|
|
||||||
return { actor, pageTitle: getPageTitle(page.title), pageUrl };
|
return {
|
||||||
|
actor,
|
||||||
|
pageTitle: getPageTitle(page.title),
|
||||||
|
pageUrl,
|
||||||
|
spaceSettings: (space.settings ?? null) as Record<string, any> | null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
SpaceCaslSubject,
|
SpaceCaslSubject,
|
||||||
} from '../../casl/interfaces/space-ability.type';
|
} from '../../casl/interfaces/space-ability.type';
|
||||||
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||||
|
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PageAccessService {
|
export class PageAccessService {
|
||||||
@@ -14,6 +15,7 @@ export class PageAccessService {
|
|||||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||||
private readonly spaceAbility: SpaceAbilityFactory,
|
private readonly spaceAbility: SpaceAbilityFactory,
|
||||||
private readonly spaceRepo: SpaceRepo,
|
private readonly spaceRepo: SpaceRepo,
|
||||||
|
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,8 +120,68 @@ export class PageAccessService {
|
|||||||
|
|
||||||
const space = await this.spaceRepo.findById(page.spaceId, workspaceId);
|
const space = await this.spaceRepo.findById(page.spaceId, workspaceId);
|
||||||
const settings = space?.settings as Record<string, any> | null;
|
const settings = space?.settings as Record<string, any> | null;
|
||||||
if (!settings?.comments?.allowViewerComments) {
|
if (
|
||||||
|
!settings?.comments?.allowViewerComments ||
|
||||||
|
settings?.comments?.hideCommentsFromViewers
|
||||||
|
) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validateCanViewComments(
|
||||||
|
page: Page,
|
||||||
|
user: User,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { canEdit } = await this.validateCanViewWithPermissions(page, user);
|
||||||
|
if (canEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const space = await this.spaceRepo.findById(page.spaceId, workspaceId);
|
||||||
|
const settings = space?.settings as Record<string, any> | null;
|
||||||
|
if (settings?.comments?.hideCommentsFromViewers) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callers must pass userIds that already have space access (WS room members / pre-filtered notification recipients).
|
||||||
|
*/
|
||||||
|
async filterUserIdsWithPageEditAccess(
|
||||||
|
spaceId: string,
|
||||||
|
pageId: string,
|
||||||
|
userIds: string[],
|
||||||
|
): Promise<string[]> {
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const spaceHasRestrictedPages =
|
||||||
|
await this.pagePermissionRepo.hasRestrictedPagesInSpace(spaceId);
|
||||||
|
const hasRestriction =
|
||||||
|
spaceHasRestrictedPages &&
|
||||||
|
(await this.pagePermissionRepo.hasRestrictedAncestor(pageId));
|
||||||
|
|
||||||
|
if (!hasRestriction) {
|
||||||
|
const editCapableIds =
|
||||||
|
await this.spaceMemberRepo.getUserIdsWithSpaceEditAccess(
|
||||||
|
userIds,
|
||||||
|
spaceId,
|
||||||
|
);
|
||||||
|
return userIds.filter((id) => editCapableIds.has(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
userIds.map(async (userId) => {
|
||||||
|
const { canEdit } = await this.pagePermissionRepo.canUserEditPage(
|
||||||
|
userId,
|
||||||
|
pageId,
|
||||||
|
);
|
||||||
|
return canEdit ? userId : null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.filter((id): id is string => id !== null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,4 +15,8 @@ export class UpdateSpaceDto extends PartialType(CreateSpaceDto) {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
allowViewerComments: boolean;
|
allowViewerComments: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
hideCommentsFromViewers: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,35 @@ import {
|
|||||||
IAuditService,
|
IAuditService,
|
||||||
} from '../../../integrations/audit/audit.service';
|
} from '../../../integrations/audit/audit.service';
|
||||||
|
|
||||||
|
export function validateExclusiveCommentSettings(
|
||||||
|
dto: Partial<
|
||||||
|
Pick<UpdateSpaceDto, 'allowViewerComments' | 'hideCommentsFromViewers'>
|
||||||
|
>,
|
||||||
|
settingsBefore: Record<string, any>,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
dto.allowViewerComments === undefined &&
|
||||||
|
dto.hideCommentsFromViewers === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowViewerComments =
|
||||||
|
dto.allowViewerComments ??
|
||||||
|
settingsBefore.comments?.allowViewerComments ??
|
||||||
|
false;
|
||||||
|
const hideCommentsFromViewers =
|
||||||
|
dto.hideCommentsFromViewers ??
|
||||||
|
settingsBefore.comments?.hideCommentsFromViewers ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
if (allowViewerComments && hideCommentsFromViewers) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"'Allow viewers to comment' and 'Hide comments from viewers' cannot both be enabled",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceService {
|
export class SpaceService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -141,7 +170,8 @@ export class SpaceService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
typeof updateSpaceDto.disablePublicSharing !== 'undefined' ||
|
typeof updateSpaceDto.disablePublicSharing !== 'undefined' ||
|
||||||
typeof updateSpaceDto.allowViewerComments !== 'undefined'
|
typeof updateSpaceDto.allowViewerComments !== 'undefined' ||
|
||||||
|
typeof updateSpaceDto.hideCommentsFromViewers !== 'undefined'
|
||||||
) {
|
) {
|
||||||
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
const workspace = await this.workspaceRepo.findById(workspaceId, {
|
||||||
withLicenseKey: true,
|
withLicenseKey: true,
|
||||||
@@ -168,6 +198,17 @@ export class SpaceService {
|
|||||||
) {
|
) {
|
||||||
throw new ForbiddenException('This feature requires a valid license');
|
throw new ForbiddenException('This feature requires a valid license');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
updateSpaceDto.hideCommentsFromViewers === true &&
|
||||||
|
!this.licenseCheckService.hasFeature(
|
||||||
|
workspace.licenseKey,
|
||||||
|
Feature.HIDE_COMMENTS,
|
||||||
|
workspace.plan,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException('This feature requires a valid license');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const spaceBefore = await this.spaceRepo.findById(
|
const spaceBefore = await this.spaceRepo.findById(
|
||||||
@@ -176,6 +217,8 @@ export class SpaceService {
|
|||||||
);
|
);
|
||||||
const settingsBefore = (spaceBefore?.settings ?? {}) as Record<string, any>;
|
const settingsBefore = (spaceBefore?.settings ?? {}) as Record<string, any>;
|
||||||
|
|
||||||
|
validateExclusiveCommentSettings(updateSpaceDto, settingsBefore);
|
||||||
|
|
||||||
const before: Record<string, any> = {};
|
const before: Record<string, any> = {};
|
||||||
const after: Record<string, any> = {};
|
const after: Record<string, any> = {};
|
||||||
|
|
||||||
@@ -218,6 +261,23 @@ export class SpaceService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof updateSpaceDto.hideCommentsFromViewers !== 'undefined') {
|
||||||
|
const prev = settingsBefore?.comments?.hideCommentsFromViewers ?? false;
|
||||||
|
if (prev !== updateSpaceDto.hideCommentsFromViewers) {
|
||||||
|
before.hideCommentsFromViewers = prev;
|
||||||
|
after.hideCommentsFromViewers =
|
||||||
|
updateSpaceDto.hideCommentsFromViewers;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.spaceRepo.updateCommentSettings(
|
||||||
|
updateSpaceDto.spaceId,
|
||||||
|
workspaceId,
|
||||||
|
'hideCommentsFromViewers',
|
||||||
|
updateSpaceDto.hideCommentsFromViewers,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updatedSpace = await this.spaceRepo.updateSpace(
|
updatedSpace = await this.spaceRepo.updateSpace(
|
||||||
{
|
{
|
||||||
name: updateSpaceDto.name,
|
name: updateSpaceDto.name,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
CacheKey,
|
CacheKey,
|
||||||
PERMISSION_CACHE_TTL_MS,
|
PERMISSION_CACHE_TTL_MS,
|
||||||
} from '../../../common/helpers/cache-keys';
|
} from '../../../common/helpers/cache-keys';
|
||||||
|
import { SpaceRole } from '../../../common/helpers/types/permission';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpaceMemberRepo {
|
export class SpaceMemberRepo {
|
||||||
@@ -278,6 +279,32 @@ export class SpaceMemberRepo {
|
|||||||
return new Set(rows.map((r) => r.userId));
|
return new Set(rows.map((r) => r.userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserIdsWithSpaceEditAccess(
|
||||||
|
userIds: string[],
|
||||||
|
spaceId: string,
|
||||||
|
): Promise<Set<string>> {
|
||||||
|
if (userIds.length === 0) return new Set();
|
||||||
|
|
||||||
|
const rows = await this.db
|
||||||
|
.selectFrom('spaceMembers')
|
||||||
|
.select('userId')
|
||||||
|
.where('userId', 'in', userIds)
|
||||||
|
.where('spaceId', '=', spaceId)
|
||||||
|
.where('spaceMembers.role', 'in', [SpaceRole.ADMIN, SpaceRole.WRITER])
|
||||||
|
.unionAll(
|
||||||
|
this.db
|
||||||
|
.selectFrom('spaceMembers')
|
||||||
|
.innerJoin('groupUsers', 'groupUsers.groupId', 'spaceMembers.groupId')
|
||||||
|
.select('groupUsers.userId')
|
||||||
|
.where('groupUsers.userId', 'in', userIds)
|
||||||
|
.where('spaceMembers.spaceId', '=', spaceId)
|
||||||
|
.where('spaceMembers.role', 'in', [SpaceRole.ADMIN, SpaceRole.WRITER]),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return new Set(rows.map((r) => r.userId));
|
||||||
|
}
|
||||||
|
|
||||||
async getSpaceIdsByGroupId(groupId: string): Promise<string[]> {
|
async getSpaceIdsByGroupId(groupId: string): Promise<string[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.selectFrom('spaceMembers')
|
.selectFrom('spaceMembers')
|
||||||
|
|||||||
@@ -149,6 +149,17 @@ export class SpaceRepo {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSpaceSettings(
|
||||||
|
spaceId: string,
|
||||||
|
): Promise<Record<string, any> | null> {
|
||||||
|
const row = await this.db
|
||||||
|
.selectFrom('spaces')
|
||||||
|
.select('settings')
|
||||||
|
.where('id', '=', spaceId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
return (row?.settings as Record<string, any> | undefined) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
async insertSpace(
|
async insertSpace(
|
||||||
insertableSpace: InsertableSpace,
|
insertableSpace: InsertableSpace,
|
||||||
trx?: KyselyTransaction,
|
trx?: KyselyTransaction,
|
||||||
|
|||||||
+1
-1
Submodule apps/server/src/ee updated: 05529bcf97...f396df9bc5
@@ -3,6 +3,8 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
|||||||
import { Cache } from 'cache-manager';
|
import { Cache } from 'cache-manager';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
|
||||||
|
import { SpaceRepo } from '@docmost/db/repos/space/space.repo';
|
||||||
|
import { PageAccessService } from '../core/page/page-access/page-access.service';
|
||||||
import {
|
import {
|
||||||
TREE_EVENTS,
|
TREE_EVENTS,
|
||||||
WS_SPACE_RESTRICTION_CACHE_PREFIX,
|
WS_SPACE_RESTRICTION_CACHE_PREFIX,
|
||||||
@@ -17,6 +19,8 @@ export class WsService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly pagePermissionRepo: PagePermissionRepo,
|
private readonly pagePermissionRepo: PagePermissionRepo,
|
||||||
|
private readonly spaceRepo: SpaceRepo,
|
||||||
|
private readonly pageAccessService: PageAccessService,
|
||||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -67,9 +71,24 @@ export class WsService {
|
|||||||
spaceId: string,
|
spaceId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
data: any,
|
data: any,
|
||||||
|
opts?: { bypassVisibilityCheck?: boolean },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const room = getSpaceRoomName(spaceId);
|
const room = getSpaceRoomName(spaceId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!opts?.bypassVisibilityCheck &&
|
||||||
|
(await this.spaceHidesCommentsFromViewers(spaceId))
|
||||||
|
) {
|
||||||
|
await this.broadcastToUsersMatching(room, null, data, (candidateIds) =>
|
||||||
|
this.pageAccessService.filterUserIdsWithPageEditAccess(
|
||||||
|
spaceId,
|
||||||
|
pageId,
|
||||||
|
candidateIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hasRestrictions = await this.spaceHasRestrictions(spaceId);
|
const hasRestrictions = await this.spaceHasRestrictions(spaceId);
|
||||||
if (!hasRestrictions) {
|
if (!hasRestrictions) {
|
||||||
this.server.to(room).emit('message', data);
|
this.server.to(room).emit('message', data);
|
||||||
@@ -118,6 +137,17 @@ export class WsService {
|
|||||||
excludeSocketId: string | null,
|
excludeSocketId: string | null,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
data: any,
|
data: any,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.broadcastToUsersMatching(room, excludeSocketId, data, (ids) =>
|
||||||
|
this.pagePermissionRepo.getUserIdsWithPageAccess(pageId, ids),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async broadcastToUsersMatching(
|
||||||
|
room: string,
|
||||||
|
excludeSocketId: string | null,
|
||||||
|
data: any,
|
||||||
|
filterUserIds: (candidateUserIds: string[]) => Promise<string[]>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const sockets = await this.server.in(room).fetchSockets();
|
const sockets = await this.server.in(room).fetchSockets();
|
||||||
|
|
||||||
@@ -144,15 +174,9 @@ export class WsService {
|
|||||||
const candidateUserIds = Array.from(userSocketMap.keys());
|
const candidateUserIds = Array.from(userSocketMap.keys());
|
||||||
if (candidateUserIds.length === 0) return;
|
if (candidateUserIds.length === 0) return;
|
||||||
|
|
||||||
const authorizedUserIds =
|
const allowedSet = new Set(await filterUserIds(candidateUserIds));
|
||||||
await this.pagePermissionRepo.getUserIdsWithPageAccess(
|
|
||||||
pageId,
|
|
||||||
candidateUserIds,
|
|
||||||
);
|
|
||||||
|
|
||||||
const authorizedSet = new Set(authorizedUserIds);
|
|
||||||
for (const [userId, userSockets] of userSocketMap) {
|
for (const [userId, userSockets] of userSocketMap) {
|
||||||
if (authorizedSet.has(userId)) {
|
if (allowedSet.has(userId)) {
|
||||||
for (const socket of userSockets) {
|
for (const socket of userSockets) {
|
||||||
socket.emit('message', data);
|
socket.emit('message', data);
|
||||||
}
|
}
|
||||||
@@ -176,6 +200,13 @@ export class WsService {
|
|||||||
return hasRestrictions;
|
return hasRestrictions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async spaceHidesCommentsFromViewers(
|
||||||
|
spaceId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const settings = await this.spaceRepo.getSpaceSettings(spaceId);
|
||||||
|
return settings?.comments?.hideCommentsFromViewers === true;
|
||||||
|
}
|
||||||
|
|
||||||
private extractPageId(data: any): string | null {
|
private extractPageId(data: any): string | null {
|
||||||
switch (data.operation) {
|
switch (data.operation) {
|
||||||
case 'addTreeNode':
|
case 'addTreeNode':
|
||||||
|
|||||||
Reference in New Issue
Block a user