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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion client/components/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ type Props = {
collaborativeOptions?: Maybe<CollaborativeOptions>;
discussionsOptions?: Maybe<DiscussionsOptions>;
debounceEditsMs?: number;
customMarks?: Record<string, MarkSpec>;
customMarks?: Record<string, MarkSpec | null>;
customNodes?: Record<string, NodeSpec>;
customPlugins?: Record<string, null | PluginLoader>;
disableLinkCreation?: boolean;
enableSuggestions?: boolean;
initialContent?: DocJson | null;
isReadOnly?: boolean;
Expand All @@ -58,6 +59,7 @@ const Editor = (props: Props) => {
customNodes = {},
customPlugins = {},
debounceEditsMs = 0,
disableLinkCreation = false,
discussionsOptions,
enableSuggestions = false,
initialContent: providedInitialContent,
Expand Down Expand Up @@ -91,6 +93,7 @@ const Editor = (props: Props) => {
noteManager,
discussionsOptions: discussionsOptions || null,
collaborativeOptions: collaborativeOptions || null,
disableLinkCreation,
initialDoc: initialDocNode,
isReadOnly,
nodeLabels,
Expand Down
2 changes: 1 addition & 1 deletion client/components/Editor/hooks/useInitialValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type InitialValues = {
type InitialValuesOptions = {
noteManager?: NoteManager;
customNodes: Record<string, NodeSpec>;
customMarks: Record<string, MarkSpec>;
customMarks: Record<string, MarkSpec | null>;
nodeLabels: NodeLabelMap;
initialContent: DocJson;
isReadOnly: boolean;
Expand Down
5 changes: 3 additions & 2 deletions client/components/Editor/plugins/inputRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,10 @@ function linkRule(markType: MarkType) {
// : (Schema) → Plugin
// A set of input rules for creating the basic block quotes, lists,
// code blocks, and heading.
export default (schema) => {
export default (schema, options?) => {
const rules = smartQuotes.concat(ellipsis, emDash);
if (schema.marks.link) rules.unshift(linkRule(schema.marks.link));
if (schema.marks.link && !options?.disableLinkCreation)
rules.unshift(linkRule(schema.marks.link));
if (schema.nodes.blockquote) rules.push(blockQuoteRule(schema.nodes.blockquote));
if (schema.nodes.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list));
if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list));
Expand Down
15 changes: 6 additions & 9 deletions client/components/Editor/plugins/keymap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) :
// * **Mod-BracketLeft** to `lift`
// * **Escape** to `selectParentNode`

export default (schema) => {
export default (schema, options?) => {
const keys = {};
const bind = (key, cmd) => {
keys[key] = cmd;
Expand Down Expand Up @@ -88,7 +88,7 @@ export default (schema) => {
if (schema.marks.code) {
bind('Mod-<', toggleMark(schema.marks.code));
}
if (schema.marks.link) {
if (schema.marks.link && !options?.disableLinkCreation) {
bind('Mod-k', toggleMark(schema.marks.link));
}

Expand Down Expand Up @@ -195,13 +195,10 @@ export default (schema) => {
// All but the custom block splitting command and the add link command in this chain are taken
// from the default chain in baseKeymap. We provide our own block splitter that preserves text
// align attributes between paragraphs.
const customEnterCommand = chainCommands(
addLinkCommand,
newlineInCode,
createParagraphNear,
liftEmptyBlock,
customBlockSplitter,
);
const enterCommands = options?.disableLinkCreation
? [newlineInCode, createParagraphNear, liftEmptyBlock, customBlockSplitter]
: [addLinkCommand, newlineInCode, createParagraphNear, liftEmptyBlock, customBlockSplitter];
const customEnterCommand = chainCommands(...enterCommands);

return [keymap(keys), keymap({ ...baseKeymap, Enter: customEnterCommand })];
};
1 change: 1 addition & 0 deletions client/components/Editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type PluginsOptions = {
noteManager?: NoteManager;
collaborativeOptions?: null | CollaborativeOptions;
discussionsOptions?: null | DiscussionsOptions;
disableLinkCreation?: boolean;
initialDoc: Node;
isReadOnly?: boolean;
nodeLabels: NodeLabelMap;
Expand Down
2 changes: 1 addition & 1 deletion client/components/FormattingBar/buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ export const reviewButtonSet = [
],
];

export const discussionButtonSet = [[strong, em, link], [rightToLeft], [simpleMedia]];
export const discussionButtonSet = [[strong, em], [rightToLeft], [simpleMedia]];

export const inlineMenuButtonSet = [[heading1, heading2, strong, em, link]];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useCallback, useState } from 'react';

import './defangedLink.scss';

import { Popover, Position } from '@blueprintjs/core';

type ClickedLink = {
href: string;
rect: DOMRect;
};

type Props = {
children: React.ReactNode;
};

const DefangedLinkPopover = ({ children }: Props) => {
const [clicked, setClicked] = useState<ClickedLink | null>(null);

const handleClick = useCallback((evt: React.MouseEvent) => {
const target = (evt.target as HTMLElement).closest('.defanged-link');
if (!target) return;
evt.preventDefault();
evt.stopPropagation();
const href = target.getAttribute('data-href') ?? '';
setClicked({ href, rect: target.getBoundingClientRect() });
}, []);

const handleClose = useCallback(() => setClicked(null), []);

return (
<div role="button" tabIndex={0} onClick={handleClick}>
{children}
{clicked && (
<Popover
isOpen
onClose={handleClose}
minimal
position={Position.TOP}
// placement="bottom-start"
content={
<div className="defanged-link-popover-content">
<p className="defanged-link-popover-notice">
We no longer allow links in comments. This comment was made before
the policy went into effect.
</p>
<p className="defanged-link-popover-notice">It pointed to:</p>
<code className="defanged-link-popover-url">{clicked.href}</code>
</div>
}
>
<span
style={{
position: 'fixed',
left: clicked.rect.left,
top: clicked.rect.bottom,
width: 1,
height: 1,
pointerEvents: 'none',
}}
/>
</Popover>
)}
</div>
);
};

export default DefangedLinkPopover;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { buttons, FormattingBar } from 'components/FormattingBar';
import { usePubContext } from 'containers/Pub/pubHooks';
import { usePageContext } from 'utils/hooks';

import { noLinksMarks } from './noLinks';

type OwnProps = {
discussionData: any;
isPubBottomInput?: boolean;
Expand Down Expand Up @@ -236,6 +238,8 @@ const DiscussionInput = (props: Props) => {
/>
<Editor
key={editorKey}
customMarks={noLinksMarks}
disableLinkCreation
placeholder={getPlaceholderText(isNewThread, isPubBottomInput)}
onChange={(editorChangeObject) => {
setChangeObject(editorChangeObject);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { getPartsOfFullName } from 'utils/names';

import './threadComment.scss';

import DefangedLinkPopover from './DefangedLinkPopover';
import { noLinksMarks } from './noLinks';

type Props = {
discussionData: any;
threadCommentData: any;
Expand Down Expand Up @@ -103,12 +106,16 @@ const ThreadComment = (props: Props) => {
onChange?: Callback<EditorChangeObject>,
) => {
return (
<Editor
key={key}
isReadOnly={isReadOnly}
initialContent={threadCommentData.content}
onChange={onChange}
/>
<DefangedLinkPopover>
<Editor
key={key}
isReadOnly={isReadOnly}
customMarks={noLinksMarks}
disableLinkCreation
initialContent={threadCommentData.content}
onChange={onChange}
/>
</DefangedLinkPopover>
);
};
const commenterName = discussionData.commenter?.name ?? threadCommentData.commenter?.name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.defanged-link-popover-content {
padding: 10px 14px;
max-width: 360px;
font-size: 13px;
line-height: 1.4;
}
.defanged-link-popover-notice {
margin: 0 0 6px;
color: #5c7080;
}
.defanged-link-popover-url {
display: block;
padding: 4px 8px;
background: #f5f5f5;
border-radius: 3px;
font-size: 12px;
word-break: break-all;
color: #394b59;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { DOMOutputSpec, MarkSpec } from 'prosemirror-model';

// strip protocol, bracket dots so crawlers won't follow or index the url
const defangUrl = (href: string): string => {
if (!href) return '';
return href.replace(/^https?:\/\//, '').replace(/\./g, '[.]');
};

export const noLinksMarks = {
link: {
inclusive: false,
attrs: {
href: { default: '' },
},
parseDOM: [
{
tag: 'span.defanged-link',
getAttrs: (dom) => ({
href: (dom.getAttribute('data-href') ?? '').replace(/\[.\]/g, '.'),
}),
},
{
tag: 'a[href]',
getAttrs: (dom) => ({
href: dom.getAttribute('href'),
}),
},
],
toDOM: (node) => {
return [
'span',
{
class: 'defanged-link',
'data-href': defangUrl(node.attrs.href),
},
0,
] as DOMOutputSpec;
},
},
} satisfies Record<string, MarkSpec>;
Loading