tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
29
pulls
pipelines
add did mention facet
awarm.space
4 months ago
65ff2d66
046e4251
+140
-11
8 changed files
expand all
collapse all
unified
split
actions
publishToPublication.ts
components
Blocks
TextBlock
RenderYJSFragment.tsx
index.tsx
schema.ts
lexicons
api
lexicons.ts
types
pub
leaflet
richtext
facet.ts
pub
leaflet
richtext
facet.json
src
facet.ts
+5
actions/publishToPublication.ts
···
544
544
$type: "pub.leaflet.richtext.facet#strikethrough",
545
545
});
546
546
547
547
+
if (d.attributes?.didMention)
548
548
+
facet.features.push({
549
549
+
$type: "pub.leaflet.richtext.facet#didMention",
550
550
+
did: d.attributes.didMention.did,
551
551
+
});
547
552
if (d.attributes?.code)
548
553
facet.features.push({ $type: "pub.leaflet.richtext.facet#code" });
549
554
if (d.attributes?.highlight)
+2
-1
components/Blocks/TextBlock/RenderYJSFragment.tsx
···
27
27
return (
28
28
<BlockWrapper wrapper={wrapper} attrs={attrs}>
29
29
{children.length === 0 ? (
30
30
-
<div />
30
30
+
<br />
31
31
) : (
32
32
node.toArray().map((node, index) => {
33
33
if (node.constructor === XmlText) {
···
103
103
strong?: {};
104
104
code?: {};
105
105
em?: {};
106
106
+
didMention?: { did: string };
106
107
underline?: {};
107
108
strikethrough?: {};
108
109
highlight?: { color: string };
+57
-10
components/Blocks/TextBlock/index.tsx
···
1
1
-
import { useRef, useEffect, useState } from "react";
1
1
+
import { useRef, useEffect, useState, useCallback } from "react";
2
2
import { elementId } from "src/utils/elementId";
3
3
import { useReplicache, useEntity } from "src/replicache";
4
4
import { isVisible } from "src/utils/isVisible";
5
5
import { EditorState, TextSelection } from "prosemirror-state";
6
6
+
import { EditorView } from "prosemirror-view";
6
7
import { RenderYJSFragment } from "./RenderYJSFragment";
7
8
import { useInitialPageLoad } from "components/InitialPageLoadProvider";
8
9
import { BlockProps } from "../Block";
···
23
24
import { useLeafletPublicationData } from "components/PageSWRDataProvider";
24
25
import { DotLoader } from "components/utils/DotLoader";
25
26
import { useMountProsemirror } from "./mountProsemirror";
27
27
+
import { schema } from "./schema";
28
28
+
import { MentionAutocomplete } from "app/[leaflet_id]/publish/BskyPostEditorProsemirror";
26
29
27
30
const HeadingStyle = {
28
31
1: "text-xl font-bold",
···
183
186
let editorState = useEditorStates(
184
187
(s) => s.editorStates[props.entityID],
185
188
)?.editor;
189
189
+
let { viewRef, handleMentionSelect, setMentionState } = useMentionState(
190
190
+
props.entityID,
191
191
+
);
186
192
187
187
-
let { mountRef, actionTimeout } = useMountProsemirror({
188
188
-
props,
189
189
-
});
193
193
+
let { mountRef, actionTimeout } = useMountProsemirror({ props });
190
194
191
195
return (
192
196
<>
···
199
203
? "blockquote pt-3"
200
204
: "blockquote"
201
205
: ""
202
202
-
}
203
203
-
204
204
-
`}
206
206
+
}`}
205
207
>
206
208
<pre
207
209
data-entityid={props.entityID}
···
249
251
${props.className}`}
250
252
ref={mountRef}
251
253
/>
254
254
+
{editorState && focused && (
255
255
+
<MentionAutocomplete
256
256
+
editorState={editorState}
257
257
+
view={viewRef}
258
258
+
onSelect={handleMentionSelect}
259
259
+
onMentionStateChange={(active, range, selectedMention) => {
260
260
+
setMentionState({ active, range, selectedMention });
261
261
+
}}
262
262
+
/>
263
263
+
)}
252
264
{editorState?.doc.textContent.length === 0 &&
253
265
props.previousBlock === null &&
254
266
props.nextBlock === null ? (
···
439
451
);
440
452
};
441
453
442
442
-
const useMentionState = () => {
443
443
-
const [editorState, setEditorState] = useState<EditorState | null>(null);
454
454
+
const useMentionState = (entityID: string) => {
455
455
+
let view = useEditorStates((s) => s.editorStates[entityID])?.view;
456
456
+
let viewRef = useRef(view || null);
457
457
+
viewRef.current = view || null;
444
458
const [mentionState, setMentionState] = useState<{
445
459
active: boolean;
446
460
range: { from: number; to: number } | null;
···
448
462
}>({ active: false, range: null, selectedMention: null });
449
463
const mentionStateRef = useRef(mentionState);
450
464
mentionStateRef.current = mentionState;
451
451
-
return { mentionStateRef };
465
465
+
466
466
+
const handleMentionSelect = useCallback(
467
467
+
(
468
468
+
mention: { handle: string; did: string },
469
469
+
range: { from: number; to: number },
470
470
+
) => {
471
471
+
let view = useEditorStates.getState().editorStates[entityID]?.view;
472
472
+
if (!view) return;
473
473
+
const { from, to } = range;
474
474
+
const tr = view.state.tr;
475
475
+
476
476
+
// Delete the query text (keep the @)
477
477
+
tr.delete(from + 1, to);
478
478
+
479
479
+
// Insert the mention text after the @
480
480
+
const mentionText = mention.handle;
481
481
+
tr.insertText(mentionText, from + 1);
482
482
+
483
483
+
// Apply mention mark to @ and handle
484
484
+
tr.addMark(
485
485
+
from,
486
486
+
from + 1 + mentionText.length,
487
487
+
schema.marks.didMention.create({ did: mention.did }),
488
488
+
);
489
489
+
490
490
+
// Add a space after the mention
491
491
+
tr.insertText(" ", from + 1 + mentionText.length);
492
492
+
493
493
+
view.dispatch(tr);
494
494
+
view.focus();
495
495
+
},
496
496
+
[],
497
497
+
);
498
498
+
return { mentionStateRef, handleMentionSelect, viewRef, setMentionState };
452
499
};
+27
components/Blocks/TextBlock/schema.ts
···
103
103
return ["a", { href, target: "_blank" }, 0];
104
104
},
105
105
} as MarkSpec,
106
106
+
107
107
+
didMention: {
108
108
+
attrs: {
109
109
+
did: {},
110
110
+
},
111
111
+
inclusive: false,
112
112
+
parseDOM: [
113
113
+
{
114
114
+
tag: "span.didMention",
115
115
+
getAttrs(dom: HTMLElement) {
116
116
+
return {
117
117
+
did: dom.getAttribute("data-did"),
118
118
+
};
119
119
+
},
120
120
+
},
121
121
+
],
122
122
+
toDOM(node) {
123
123
+
return [
124
124
+
"span",
125
125
+
{
126
126
+
class: "didMention text-accent-contrast",
127
127
+
"data-did": node.attrs.did,
128
128
+
},
129
129
+
0,
130
130
+
];
131
131
+
},
132
132
+
} as MarkSpec,
106
133
},
107
134
nodes: {
108
135
doc: { content: "block" },
+12
lexicons/api/lexicons.ts
···
1861
1861
type: 'union',
1862
1862
refs: [
1863
1863
'lex:pub.leaflet.richtext.facet#link',
1864
1864
+
'lex:pub.leaflet.richtext.facet#didMention',
1864
1865
'lex:pub.leaflet.richtext.facet#code',
1865
1866
'lex:pub.leaflet.richtext.facet#highlight',
1866
1867
'lex:pub.leaflet.richtext.facet#underline',
···
1897
1898
properties: {
1898
1899
uri: {
1899
1900
type: 'string',
1901
1901
+
},
1902
1902
+
},
1903
1903
+
},
1904
1904
+
didMention: {
1905
1905
+
type: 'object',
1906
1906
+
description: 'Facet feature for mentioning a did.',
1907
1907
+
required: ['did'],
1908
1908
+
properties: {
1909
1909
+
did: {
1910
1910
+
type: 'string',
1911
1911
+
format: 'did',
1900
1912
},
1901
1913
},
1902
1914
},
+17
lexicons/api/types/pub/leaflet/richtext/facet.ts
···
20
20
index: ByteSlice
21
21
features: (
22
22
| $Typed<Link>
23
23
+
| $Typed<DidMention>
23
24
| $Typed<Code>
24
25
| $Typed<Highlight>
25
26
| $Typed<Underline>
···
72
73
73
74
export function validateLink<V>(v: V) {
74
75
return validate<Link & V>(v, id, hashLink)
76
76
+
}
77
77
+
78
78
+
/** Facet feature for mentioning a did. */
79
79
+
export interface DidMention {
80
80
+
$type?: 'pub.leaflet.richtext.facet#didMention'
81
81
+
did: string
82
82
+
}
83
83
+
84
84
+
const hashDidMention = 'didMention'
85
85
+
86
86
+
export function isDidMention<V>(v: V) {
87
87
+
return is$typed(v, id, hashDidMention)
88
88
+
}
89
89
+
90
90
+
export function validateDidMention<V>(v: V) {
91
91
+
return validate<DidMention & V>(v, id, hashDidMention)
75
92
}
76
93
77
94
/** Facet feature for inline code. */
+14
lexicons/pub/leaflet/richtext/facet.json
···
20
20
"type": "union",
21
21
"refs": [
22
22
"#link",
23
23
+
"#didMention",
23
24
"#code",
24
25
"#highlight",
25
26
"#underline",
···
59
60
"properties": {
60
61
"uri": {
61
62
"type": "string"
63
63
+
}
64
64
+
}
65
65
+
},
66
66
+
"didMention": {
67
67
+
"type": "object",
68
68
+
"description": "Facet feature for mentioning a did.",
69
69
+
"required": [
70
70
+
"did"
71
71
+
],
72
72
+
"properties": {
73
73
+
"did": {
74
74
+
"type": "string",
75
75
+
"format": "did"
62
76
}
63
77
}
64
78
},
+6
lexicons/src/facet.ts
···
9
9
uri: { type: "string" },
10
10
},
11
11
},
12
12
+
didMention: {
13
13
+
type: "object",
14
14
+
description: "Facet feature for mentioning a did.",
15
15
+
required: ["did"],
16
16
+
properties: { did: { type: "string", format: "did" } },
17
17
+
},
12
18
code: {
13
19
type: "object",
14
20
description: "Facet feature for inline code.",