collabmd 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -17
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{preview-render-compiler-WD3A76I6.js → chunk-5QRWYPYT.js} +18 -18
- package/public/assets/js/chunks/chunk-HEWVH67U.js +9 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +1 -0
- package/public/assets/js/chunks/editor-session-AH6Z3MXW.js +22 -0
- package/public/assets/js/chunks/preview-render-compiler-UZ4SZDQQ.js +1 -0
- package/public/assets/js/chunks/quick-switcher-controller-J7I3CUET.js +5 -0
- package/public/assets/js/{excalidraw-editor-LKW2AZBU.js → excalidraw-editor-ZDYKMZOL.js} +33 -33
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +59 -59
- package/public/assets/js/preview-render-worker.js +19 -19
- package/src/client/application/app-shell/presence-feature.js +2 -1
- package/src/client/application/app-shell/ui-feature.js +113 -1
- package/src/client/application/app-shell/workspace-feature.js +9 -0
- package/src/client/application/preview-render-compiler.js +55 -5
- package/src/client/application/preview-render-executor.js +8 -0
- package/src/client/application/preview-render-worker.js +13 -2
- package/src/client/application/preview-renderer.js +5 -0
- package/src/client/application/workspace-chrome-controller.js +12 -1
- package/src/client/application/workspace-coordinator.js +16 -7
- package/src/client/application/workspace-preview-controller.js +45 -5
- package/src/client/application/workspace-route-controller.js +4 -0
- package/src/client/bootstrap/collabmd-app-shell.js +15 -3
- package/src/client/domain/room.js +37 -0
- package/src/client/domain/vault-utils.js +46 -0
- package/src/client/infrastructure/comment-thread-store.js +98 -0
- package/src/client/infrastructure/editor-paste-utils.js +45 -0
- package/src/client/infrastructure/editor-session.js +10 -0
- package/src/client/infrastructure/editor-view-adapter.js +34 -1
- package/src/client/infrastructure/vault-api-client.js +17 -0
- package/src/client/presentation/comment-markdown-renderer.js +68 -0
- package/src/client/presentation/comment-ui-controller.js +311 -15
- package/src/client/presentation/file-explorer-controller.js +5 -0
- package/src/client/presentation/file-explorer-view.js +10 -2
- package/src/client/presentation/file-tree-state.js +7 -1
- package/src/client/presentation/image-lightbox-controller.js +394 -0
- package/src/client/styles/style.css +539 -14
- package/src/domain/comment-threads.js +95 -13
- package/src/domain/file-kind.js +16 -1
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +48 -2
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +67 -1
- package/src/server/infrastructure/http/request-body.js +11 -2
- package/src/server/infrastructure/persistence/path-utils.js +1 -1
- package/src/server/infrastructure/persistence/vault-file-store.js +187 -2
- package/public/assets/js/chunks/chunk-BBHPYU2R.js +0 -1
- package/public/assets/js/chunks/chunk-OG2TNZEU.js +0 -9
- package/public/assets/js/chunks/chunk-QSTBGTWJ.js +0 -1
- package/public/assets/js/chunks/chunk-SR3U53EQ.js +0 -1
- package/public/assets/js/chunks/editor-session-IHFTYG5D.js +0 -22
- package/public/assets/js/chunks/quick-switcher-controller-JYDIJVAJ.js +0 -5
|
@@ -2,12 +2,15 @@ import {
|
|
|
2
2
|
COMMENT_BODY_MAX_LENGTH,
|
|
3
3
|
normalizeCommentQuoteForComparison,
|
|
4
4
|
} from '../../domain/comment-threads.js';
|
|
5
|
+
import { renderCommentMarkdownToHtml } from './comment-markdown-renderer.js';
|
|
5
6
|
|
|
6
7
|
const COMMENT_CARD_OFFSET = 14;
|
|
7
|
-
const COMMENT_CARD_WIDTH =
|
|
8
|
+
const COMMENT_CARD_WIDTH = 520;
|
|
8
9
|
const COMMENT_SELECTION_REVEAL_DELAY_MS = 150;
|
|
9
10
|
const COMMENT_SELECTION_CHIP_GAP = 12;
|
|
10
11
|
const COMMENT_CONTROL_SLOT_HEIGHT = 36;
|
|
12
|
+
const COMMENT_REACTION_PRESET_EMOJIS = Object.freeze(['👍', '❤️', '🎉', '👀', '🚀']);
|
|
13
|
+
const COMMENT_REACTION_MORE_EMOJIS = Object.freeze(['😂', '🔥', '✅', '🙏', '💡', '🤔', '👏', '😄', '🎯', '🙌']);
|
|
11
14
|
|
|
12
15
|
function clamp(value, min, max) {
|
|
13
16
|
return Math.min(Math.max(value, min), max);
|
|
@@ -81,6 +84,61 @@ function parseLineNumber(value) {
|
|
|
81
84
|
return Number.isFinite(parsed) ? parsed : null;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
function getLatestMessage(messages = []) {
|
|
88
|
+
return messages.reduce((latest, message) => {
|
|
89
|
+
if (!latest) {
|
|
90
|
+
return message;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (message?.createdAt ?? 0) >= (latest?.createdAt ?? 0)
|
|
94
|
+
? message
|
|
95
|
+
: latest;
|
|
96
|
+
}, null);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getLatestGroupMessage(group) {
|
|
100
|
+
return group?.threads?.reduce((latest, thread) => {
|
|
101
|
+
const next = getLatestMessage(thread?.messages ?? []);
|
|
102
|
+
if (!next) {
|
|
103
|
+
return latest;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (next.createdAt ?? 0) >= (latest?.createdAt ?? 0)
|
|
107
|
+
? next
|
|
108
|
+
: latest;
|
|
109
|
+
}, null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function createRenderedCommentBody(body, className = 'comment-markdown') {
|
|
113
|
+
const container = document.createElement('div');
|
|
114
|
+
container.className = className;
|
|
115
|
+
container.innerHTML = renderCommentMarkdownToHtml(body);
|
|
116
|
+
return container;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasLocalReaction(reaction, localUserId) {
|
|
120
|
+
return Boolean(localUserId && reaction?.users?.some((user) => user?.userId === localUserId));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatReactionCount(reaction) {
|
|
124
|
+
return String(Array.isArray(reaction?.users) ? reaction.users.length : 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isReactionPickerOpen(reactionPicker, threadId, messageId) {
|
|
128
|
+
return reactionPicker?.threadId === threadId && reactionPicker?.messageId === messageId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getReactionPickerBounds(card) {
|
|
132
|
+
const picker = card?.querySelector?.('.comment-reaction-picker');
|
|
133
|
+
const wrap = picker?.closest?.('.comment-reaction-picker-wrap');
|
|
134
|
+
const scroll = card?.querySelector?.('.comment-card-scroll');
|
|
135
|
+
if (!(picker instanceof HTMLElement) || !(wrap instanceof HTMLElement) || !(scroll instanceof HTMLElement)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { picker, scroll, wrap };
|
|
140
|
+
}
|
|
141
|
+
|
|
84
142
|
function overlapsAnchorRange(element, anchor) {
|
|
85
143
|
const startLine = parseLineNumber(element?.getAttribute?.('data-source-line'));
|
|
86
144
|
const endLine = parseLineNumber(element?.getAttribute?.('data-source-line-end')) ?? startLine;
|
|
@@ -224,6 +282,7 @@ export class CommentUiController {
|
|
|
224
282
|
onCreateThread,
|
|
225
283
|
onNavigateToLine,
|
|
226
284
|
onReplyToThread,
|
|
285
|
+
onToggleReaction,
|
|
227
286
|
onResolveThread,
|
|
228
287
|
previewContainer,
|
|
229
288
|
previewElement,
|
|
@@ -237,6 +296,7 @@ export class CommentUiController {
|
|
|
237
296
|
this.onCreateThread = onCreateThread;
|
|
238
297
|
this.onNavigateToLine = onNavigateToLine;
|
|
239
298
|
this.onReplyToThread = onReplyToThread;
|
|
299
|
+
this.onToggleReaction = onToggleReaction;
|
|
240
300
|
this.onResolveThread = onResolveThread;
|
|
241
301
|
this.previewContainer = previewContainer;
|
|
242
302
|
this.previewElement = previewElement;
|
|
@@ -258,6 +318,7 @@ export class CommentUiController {
|
|
|
258
318
|
this.previewHighlightLayer = null;
|
|
259
319
|
this.cardRoot = null;
|
|
260
320
|
this.pendingCardFocusElement = null;
|
|
321
|
+
this.reactionPicker = null;
|
|
261
322
|
this.layoutFrame = 0;
|
|
262
323
|
this.timeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
263
324
|
day: 'numeric',
|
|
@@ -268,6 +329,9 @@ export class CommentUiController {
|
|
|
268
329
|
this.handleEditorScroll = () => this.scheduleLayoutRefresh();
|
|
269
330
|
this.handlePreviewScroll = () => this.scheduleLayoutRefresh();
|
|
270
331
|
this.handleWindowResize = () => this.scheduleLayoutRefresh();
|
|
332
|
+
this.handleCommentSelectionButtonPointerDown = (event) => {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
};
|
|
271
335
|
this.handleEditorPointerDown = (event) => {
|
|
272
336
|
if (event.button !== 0 || !this.supported || !this.session) {
|
|
273
337
|
return;
|
|
@@ -329,6 +393,11 @@ export class CommentUiController {
|
|
|
329
393
|
this.closeCard();
|
|
330
394
|
};
|
|
331
395
|
this.handleDocumentKeyDown = (event) => {
|
|
396
|
+
if (event.key === 'Escape' && this.reactionPicker) {
|
|
397
|
+
this.reactionPicker = null;
|
|
398
|
+
this.renderCard();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
332
401
|
if (event.key === 'Escape' && this.activeCard) {
|
|
333
402
|
this.closeCard();
|
|
334
403
|
}
|
|
@@ -340,6 +409,7 @@ export class CommentUiController {
|
|
|
340
409
|
}
|
|
341
410
|
};
|
|
342
411
|
|
|
412
|
+
this.commentSelectionButton?.addEventListener('pointerdown', this.handleCommentSelectionButtonPointerDown);
|
|
343
413
|
this.commentSelectionButton?.addEventListener('click', () => {
|
|
344
414
|
this.openComposerForSelection('toolbar');
|
|
345
415
|
});
|
|
@@ -364,6 +434,7 @@ export class CommentUiController {
|
|
|
364
434
|
}
|
|
365
435
|
this.attachSession(null);
|
|
366
436
|
this.previewContainer?.removeEventListener('scroll', this.handlePreviewScroll);
|
|
437
|
+
this.commentSelectionButton?.removeEventListener('pointerdown', this.handleCommentSelectionButtonPointerDown);
|
|
367
438
|
this.editorContainer?.removeEventListener('pointerdown', this.handleEditorPointerDown);
|
|
368
439
|
this.editorContainer?.removeEventListener('focusout', this.handleEditorFocusOut);
|
|
369
440
|
window.removeEventListener('resize', this.handleWindowResize);
|
|
@@ -383,6 +454,7 @@ export class CommentUiController {
|
|
|
383
454
|
this.selectionAnchor = session?.getCurrentSelectionCommentAnchor?.() ?? null;
|
|
384
455
|
this.pendingSelectionAnchor = null;
|
|
385
456
|
this.committedSelectionAnchor = null;
|
|
457
|
+
this.reactionPicker = null;
|
|
386
458
|
this.clearSelectionRevealTimer();
|
|
387
459
|
this.pointerSelecting = false;
|
|
388
460
|
session?.getScrollContainer?.()?.addEventListener('scroll', this.handleEditorScroll, { passive: true });
|
|
@@ -403,6 +475,7 @@ export class CommentUiController {
|
|
|
403
475
|
this.clearSelectionRevealTimer();
|
|
404
476
|
this.pointerSelecting = false;
|
|
405
477
|
this.activeCard = null;
|
|
478
|
+
this.reactionPicker = null;
|
|
406
479
|
}
|
|
407
480
|
if (!this.supported) {
|
|
408
481
|
this.drawerOpen = false;
|
|
@@ -413,6 +486,7 @@ export class CommentUiController {
|
|
|
413
486
|
this.committedSelectionAnchor = null;
|
|
414
487
|
this.clearSelectionRevealTimer();
|
|
415
488
|
this.pointerSelecting = false;
|
|
489
|
+
this.reactionPicker = null;
|
|
416
490
|
}
|
|
417
491
|
this.render();
|
|
418
492
|
}
|
|
@@ -483,6 +557,15 @@ export class CommentUiController {
|
|
|
483
557
|
) {
|
|
484
558
|
this.activeCard = null;
|
|
485
559
|
}
|
|
560
|
+
if (
|
|
561
|
+
this.reactionPicker
|
|
562
|
+
&& !this.threads.some((thread) => (
|
|
563
|
+
thread.id === this.reactionPicker.threadId
|
|
564
|
+
&& thread.messages?.some((message) => message.id === this.reactionPicker.messageId)
|
|
565
|
+
))
|
|
566
|
+
) {
|
|
567
|
+
this.reactionPicker = null;
|
|
568
|
+
}
|
|
486
569
|
this.render();
|
|
487
570
|
}
|
|
488
571
|
|
|
@@ -578,11 +661,26 @@ export class CommentUiController {
|
|
|
578
661
|
quote.className = 'comments-drawer-item-quote';
|
|
579
662
|
quote.textContent = group.anchor.quote || group.anchor.excerpt || 'Source anchored comment';
|
|
580
663
|
|
|
664
|
+
const latestMessage = getLatestGroupMessage(group);
|
|
665
|
+
const preview = createRenderedCommentBody(
|
|
666
|
+
latestMessage?.body || '',
|
|
667
|
+
'comment-markdown comments-drawer-item-preview',
|
|
668
|
+
);
|
|
669
|
+
|
|
581
670
|
const footer = document.createElement('div');
|
|
582
671
|
footer.className = 'comments-drawer-item-footer';
|
|
583
|
-
|
|
672
|
+
const countLabel = document.createElement('span');
|
|
673
|
+
countLabel.textContent = `${group.threads.length} thread${group.threads.length === 1 ? '' : 's'}`;
|
|
584
674
|
|
|
585
|
-
|
|
675
|
+
const updatedLabel = document.createElement('span');
|
|
676
|
+
updatedLabel.className = 'comments-drawer-item-updated';
|
|
677
|
+
updatedLabel.textContent = latestMessage
|
|
678
|
+
? `${latestMessage.userName} • ${this.formatTimestamp(latestMessage.createdAt)}`
|
|
679
|
+
: '';
|
|
680
|
+
|
|
681
|
+
footer.append(countLabel, updatedLabel);
|
|
682
|
+
|
|
683
|
+
button.append(header, quote, preview, footer);
|
|
586
684
|
fragment.appendChild(button);
|
|
587
685
|
});
|
|
588
686
|
|
|
@@ -811,6 +909,7 @@ export class CommentUiController {
|
|
|
811
909
|
}
|
|
812
910
|
|
|
813
911
|
this.selectionAnchor = anchor;
|
|
912
|
+
this.reactionPicker = null;
|
|
814
913
|
const nextOrigin = origin === 'editor' && sourceRect ? 'editor-chip' : origin;
|
|
815
914
|
const nextSourceRect = sourceRect ?? (origin === 'toolbar'
|
|
816
915
|
? this.commentSelectionButton?.getBoundingClientRect?.()
|
|
@@ -826,6 +925,7 @@ export class CommentUiController {
|
|
|
826
925
|
}
|
|
827
926
|
|
|
828
927
|
openThreadGroup(group, { anchor, origin, sourceRect }) {
|
|
928
|
+
this.reactionPicker = null;
|
|
829
929
|
this.activeCard = {
|
|
830
930
|
anchor,
|
|
831
931
|
groupKey: group.key,
|
|
@@ -841,6 +941,7 @@ export class CommentUiController {
|
|
|
841
941
|
closeCard() {
|
|
842
942
|
this.activeCard = null;
|
|
843
943
|
this.pendingCardFocusElement = null;
|
|
944
|
+
this.reactionPicker = null;
|
|
844
945
|
this.renderCard();
|
|
845
946
|
this.scheduleLayoutRefresh();
|
|
846
947
|
}
|
|
@@ -902,6 +1003,21 @@ export class CommentUiController {
|
|
|
902
1003
|
|
|
903
1004
|
const card = document.createElement('section');
|
|
904
1005
|
card.className = 'comment-card';
|
|
1006
|
+
card.addEventListener('click', (event) => {
|
|
1007
|
+
if (
|
|
1008
|
+
!this.reactionPicker
|
|
1009
|
+
|| event.target?.closest?.('.comment-reaction-picker-wrap')
|
|
1010
|
+
) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
this.reactionPicker = null;
|
|
1015
|
+
requestAnimationFrame(() => {
|
|
1016
|
+
if (this.activeCard) {
|
|
1017
|
+
this.renderCard();
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
905
1021
|
|
|
906
1022
|
const header = document.createElement('div');
|
|
907
1023
|
header.className = 'comment-card-header';
|
|
@@ -929,15 +1045,18 @@ export class CommentUiController {
|
|
|
929
1045
|
header.append(titleWrap, closeButton);
|
|
930
1046
|
card.appendChild(header);
|
|
931
1047
|
|
|
1048
|
+
const content = document.createElement('div');
|
|
1049
|
+
content.className = 'comment-card-scroll';
|
|
1050
|
+
|
|
932
1051
|
if (this.activeCard.anchor?.quote) {
|
|
933
1052
|
const quote = document.createElement('p');
|
|
934
1053
|
quote.className = 'comment-card-quote';
|
|
935
1054
|
quote.textContent = this.activeCard.anchor.quote;
|
|
936
|
-
|
|
1055
|
+
content.appendChild(quote);
|
|
937
1056
|
}
|
|
938
1057
|
|
|
939
1058
|
if (this.activeCard.mode === 'create') {
|
|
940
|
-
|
|
1059
|
+
content.appendChild(this.createComposer());
|
|
941
1060
|
} else {
|
|
942
1061
|
const group = this.getThreadGroups().find((entry) => entry.key === this.activeCard.groupKey);
|
|
943
1062
|
if (!group) {
|
|
@@ -946,16 +1065,46 @@ export class CommentUiController {
|
|
|
946
1065
|
}
|
|
947
1066
|
|
|
948
1067
|
group.threads.forEach((thread) => {
|
|
949
|
-
|
|
1068
|
+
content.appendChild(this.createThreadElement(thread));
|
|
950
1069
|
});
|
|
951
1070
|
}
|
|
952
1071
|
|
|
1072
|
+
root.style.visibility = 'hidden';
|
|
1073
|
+
card.appendChild(content);
|
|
953
1074
|
root.appendChild(card);
|
|
1075
|
+
this.updateReactionPickerPosition(card);
|
|
1076
|
+
this.positionCard(card);
|
|
1077
|
+
root.style.visibility = '';
|
|
954
1078
|
this.flushPendingCardFocus();
|
|
955
|
-
root.style.visibility = 'hidden';
|
|
956
1079
|
this.scheduleLayoutRefresh();
|
|
957
1080
|
}
|
|
958
1081
|
|
|
1082
|
+
updateReactionPickerPosition(card) {
|
|
1083
|
+
const bounds = getReactionPickerBounds(card);
|
|
1084
|
+
if (!bounds) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const { picker, scroll, wrap } = bounds;
|
|
1089
|
+
picker.classList.remove('is-upward');
|
|
1090
|
+
picker.style.maxHeight = '';
|
|
1091
|
+
|
|
1092
|
+
const wrapRect = wrap.getBoundingClientRect();
|
|
1093
|
+
const scrollRect = scroll.getBoundingClientRect();
|
|
1094
|
+
const pickerRect = picker.getBoundingClientRect();
|
|
1095
|
+
const safeViewportTop = 12;
|
|
1096
|
+
const safeViewportBottom = window.innerHeight - 12;
|
|
1097
|
+
const lowerBoundary = Math.min(scrollRect.bottom, safeViewportBottom);
|
|
1098
|
+
const upperBoundary = Math.max(scrollRect.top, safeViewportTop);
|
|
1099
|
+
const availableBelow = Math.max(lowerBoundary - wrapRect.bottom - 8, 0);
|
|
1100
|
+
const availableAbove = Math.max(wrapRect.top - upperBoundary - 8, 0);
|
|
1101
|
+
const shouldOpenUpward = pickerRect.height > availableBelow && availableAbove > availableBelow;
|
|
1102
|
+
const maxHeight = Math.max((shouldOpenUpward ? availableAbove : availableBelow), 120);
|
|
1103
|
+
|
|
1104
|
+
picker.classList.toggle('is-upward', shouldOpenUpward);
|
|
1105
|
+
picker.style.maxHeight = `${maxHeight}px`;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
959
1108
|
repositionActiveCard() {
|
|
960
1109
|
const card = this.cardRoot?.firstElementChild;
|
|
961
1110
|
if (!card || !this.activeCard) {
|
|
@@ -1022,6 +1171,9 @@ export class CommentUiController {
|
|
|
1022
1171
|
const header = document.createElement('div');
|
|
1023
1172
|
header.className = 'comment-thread-card-header';
|
|
1024
1173
|
|
|
1174
|
+
const heading = document.createElement('div');
|
|
1175
|
+
heading.className = 'comment-thread-card-heading';
|
|
1176
|
+
|
|
1025
1177
|
const author = document.createElement('span');
|
|
1026
1178
|
author.className = 'comment-thread-card-author';
|
|
1027
1179
|
author.textContent = thread.createdByName;
|
|
@@ -1065,12 +1217,13 @@ export class CommentUiController {
|
|
|
1065
1217
|
});
|
|
1066
1218
|
|
|
1067
1219
|
actions.append(jump, reply, resolve);
|
|
1068
|
-
|
|
1220
|
+
heading.append(author, time);
|
|
1221
|
+
header.append(heading, actions);
|
|
1069
1222
|
|
|
1070
1223
|
article.append(header);
|
|
1071
1224
|
|
|
1072
1225
|
thread.messages.forEach((message) => {
|
|
1073
|
-
article.appendChild(this.createMessageElement(message));
|
|
1226
|
+
article.appendChild(this.createMessageElement(thread, message));
|
|
1074
1227
|
});
|
|
1075
1228
|
|
|
1076
1229
|
if (this.activeCard?.replyThreadId === thread.id) {
|
|
@@ -1080,7 +1233,7 @@ export class CommentUiController {
|
|
|
1080
1233
|
return article;
|
|
1081
1234
|
}
|
|
1082
1235
|
|
|
1083
|
-
createMessageElement(message) {
|
|
1236
|
+
createMessageElement(thread, message) {
|
|
1084
1237
|
const container = document.createElement('div');
|
|
1085
1238
|
container.className = 'comment-message-card';
|
|
1086
1239
|
|
|
@@ -1095,15 +1248,133 @@ export class CommentUiController {
|
|
|
1095
1248
|
time.className = 'comment-message-card-time';
|
|
1096
1249
|
time.textContent = this.formatTimestamp(message.createdAt);
|
|
1097
1250
|
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1251
|
+
const renderedBody = createRenderedCommentBody(
|
|
1252
|
+
message.body,
|
|
1253
|
+
'comment-message-card-body comment-markdown',
|
|
1254
|
+
);
|
|
1101
1255
|
|
|
1102
1256
|
meta.append(author, time);
|
|
1103
|
-
container.append(meta,
|
|
1257
|
+
container.append(meta, renderedBody);
|
|
1258
|
+
container.appendChild(this.createReactionBar(thread, message));
|
|
1104
1259
|
return container;
|
|
1105
1260
|
}
|
|
1106
1261
|
|
|
1262
|
+
createReactionBar(thread, message) {
|
|
1263
|
+
const localUserId = this.session?.getLocalUser?.()?.userId ?? '';
|
|
1264
|
+
const wrap = document.createElement('div');
|
|
1265
|
+
wrap.className = 'comment-reaction-bar';
|
|
1266
|
+
|
|
1267
|
+
const existingReactionEmojis = new Set((message.reactions ?? []).map((reaction) => reaction.emoji));
|
|
1268
|
+
|
|
1269
|
+
const chips = document.createElement('div');
|
|
1270
|
+
chips.className = 'comment-reaction-chips';
|
|
1271
|
+
|
|
1272
|
+
(message.reactions ?? []).forEach((reaction) => {
|
|
1273
|
+
const chip = document.createElement('button');
|
|
1274
|
+
chip.type = 'button';
|
|
1275
|
+
chip.className = 'comment-reaction-chip';
|
|
1276
|
+
chip.classList.toggle('is-active', hasLocalReaction(reaction, localUserId));
|
|
1277
|
+
chip.setAttribute('aria-pressed', String(hasLocalReaction(reaction, localUserId)));
|
|
1278
|
+
chip.title = reaction.users?.map((user) => user.userName).join(', ') || reaction.emoji;
|
|
1279
|
+
|
|
1280
|
+
const emoji = document.createElement('span');
|
|
1281
|
+
emoji.className = 'comment-reaction-chip-emoji';
|
|
1282
|
+
emoji.textContent = reaction.emoji;
|
|
1283
|
+
|
|
1284
|
+
const count = document.createElement('span');
|
|
1285
|
+
count.className = 'comment-reaction-chip-count';
|
|
1286
|
+
count.textContent = formatReactionCount(reaction);
|
|
1287
|
+
|
|
1288
|
+
chip.append(emoji, count);
|
|
1289
|
+
chip.addEventListener('click', async () => {
|
|
1290
|
+
await this.onToggleReaction?.(thread.id, message.id, reaction.emoji);
|
|
1291
|
+
});
|
|
1292
|
+
chips.appendChild(chip);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
const actions = document.createElement('div');
|
|
1296
|
+
actions.className = 'comment-reaction-actions';
|
|
1297
|
+
|
|
1298
|
+
COMMENT_REACTION_PRESET_EMOJIS
|
|
1299
|
+
.filter((emoji) => !existingReactionEmojis.has(emoji))
|
|
1300
|
+
.forEach((emoji) => {
|
|
1301
|
+
actions.appendChild(this.createQuickReactionButton(thread, message, emoji));
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
const pickerWrap = document.createElement('div');
|
|
1305
|
+
pickerWrap.className = 'comment-reaction-picker-wrap';
|
|
1306
|
+
|
|
1307
|
+
const moreButton = document.createElement('button');
|
|
1308
|
+
moreButton.type = 'button';
|
|
1309
|
+
moreButton.className = 'comment-reaction-more-trigger';
|
|
1310
|
+
moreButton.dataset.reactionPickerToggle = 'true';
|
|
1311
|
+
moreButton.setAttribute('aria-expanded', String(
|
|
1312
|
+
isReactionPickerOpen(this.reactionPicker, thread.id, message.id),
|
|
1313
|
+
));
|
|
1314
|
+
moreButton.textContent = 'More';
|
|
1315
|
+
moreButton.addEventListener('click', () => {
|
|
1316
|
+
const isOpen = isReactionPickerOpen(this.reactionPicker, thread.id, message.id);
|
|
1317
|
+
this.reactionPicker = isOpen
|
|
1318
|
+
? null
|
|
1319
|
+
: {
|
|
1320
|
+
messageId: message.id,
|
|
1321
|
+
threadId: thread.id,
|
|
1322
|
+
};
|
|
1323
|
+
this.renderCard();
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
pickerWrap.appendChild(moreButton);
|
|
1327
|
+
|
|
1328
|
+
if (isReactionPickerOpen(this.reactionPicker, thread.id, message.id)) {
|
|
1329
|
+
pickerWrap.appendChild(this.createReactionPicker(thread, message));
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
actions.appendChild(pickerWrap);
|
|
1333
|
+
wrap.append(chips, actions);
|
|
1334
|
+
return wrap;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
createQuickReactionButton(thread, message, emoji) {
|
|
1338
|
+
const button = document.createElement('button');
|
|
1339
|
+
button.type = 'button';
|
|
1340
|
+
button.className = 'comment-reaction-quick-add';
|
|
1341
|
+
button.textContent = emoji;
|
|
1342
|
+
button.title = `React with ${emoji}`;
|
|
1343
|
+
button.addEventListener('click', async () => {
|
|
1344
|
+
await this.onToggleReaction?.(thread.id, message.id, emoji);
|
|
1345
|
+
});
|
|
1346
|
+
return button;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
createReactionPicker(thread, message) {
|
|
1350
|
+
const picker = document.createElement('div');
|
|
1351
|
+
picker.className = 'comment-reaction-picker';
|
|
1352
|
+
const moreGrid = document.createElement('div');
|
|
1353
|
+
moreGrid.className = 'comment-reaction-picker-grid';
|
|
1354
|
+
COMMENT_REACTION_MORE_EMOJIS.forEach((emoji) => {
|
|
1355
|
+
moreGrid.appendChild(this.createReactionPickerButton(thread, message, emoji));
|
|
1356
|
+
});
|
|
1357
|
+
picker.appendChild(moreGrid);
|
|
1358
|
+
|
|
1359
|
+
return picker;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
createReactionPickerButton(thread, message, emoji) {
|
|
1363
|
+
const button = document.createElement('button');
|
|
1364
|
+
button.type = 'button';
|
|
1365
|
+
button.className = 'comment-reaction-picker-btn';
|
|
1366
|
+
button.textContent = emoji;
|
|
1367
|
+
button.title = `React with ${emoji}`;
|
|
1368
|
+
button.addEventListener('click', async () => {
|
|
1369
|
+
const didToggle = await this.onToggleReaction?.(thread.id, message.id, emoji);
|
|
1370
|
+
if (didToggle) {
|
|
1371
|
+
this.reactionPicker = null;
|
|
1372
|
+
this.renderCard();
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
return button;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1107
1378
|
createReplyComposer(thread) {
|
|
1108
1379
|
const form = document.createElement('form');
|
|
1109
1380
|
form.className = 'comment-reply-form';
|
|
@@ -1204,7 +1475,32 @@ export class CommentUiController {
|
|
|
1204
1475
|
}
|
|
1205
1476
|
|
|
1206
1477
|
this.pendingCardFocusElement = null;
|
|
1207
|
-
|
|
1478
|
+
const focusElement = () => {
|
|
1479
|
+
if (!element.isConnected) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const activeElement = document.activeElement;
|
|
1484
|
+
if (activeElement instanceof HTMLElement && this.editorContainer?.contains(activeElement)) {
|
|
1485
|
+
activeElement.blur();
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
element.focus({ preventScroll: true });
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
focusElement();
|
|
1492
|
+
if (document.activeElement === element) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
requestAnimationFrame(() => {
|
|
1497
|
+
focusElement();
|
|
1498
|
+
if (document.activeElement !== element) {
|
|
1499
|
+
setTimeout(() => {
|
|
1500
|
+
focusElement();
|
|
1501
|
+
}, 50);
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1208
1504
|
}
|
|
1209
1505
|
|
|
1210
1506
|
clearSelectionRevealTimer() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isImageAttachmentFilePath } from '../../domain/file-kind.js';
|
|
1
2
|
import { vaultApiClient } from '../domain/vault-api-client.js';
|
|
2
3
|
import { FileActionController } from './file-action-controller.js';
|
|
3
4
|
import { FileTreeState } from './file-tree-state.js';
|
|
@@ -69,6 +70,10 @@ export class FileExplorerController {
|
|
|
69
70
|
return this.state.flatFiles;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
get flatDocumentFiles() {
|
|
74
|
+
return this.state.flatFiles.filter((path) => !isImageAttachmentFilePath(path));
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
renderTree() {
|
|
73
78
|
this.view.render({
|
|
74
79
|
activeFilePath: this.state.activeFilePath,
|
|
@@ -161,12 +161,16 @@ export class FileExplorerView {
|
|
|
161
161
|
const button = document.createElement('button');
|
|
162
162
|
button.className = 'file-tree-item file-tree-file';
|
|
163
163
|
const isExcalidraw = fileType === 'excalidraw';
|
|
164
|
+
const isImage = fileType === 'image';
|
|
164
165
|
const isMermaid = fileType === 'mermaid';
|
|
165
166
|
const isPlantUml = fileType === 'plantuml';
|
|
166
167
|
|
|
167
168
|
if (isExcalidraw) {
|
|
168
169
|
button.classList.add('is-excalidraw');
|
|
169
170
|
}
|
|
171
|
+
if (isImage) {
|
|
172
|
+
button.classList.add('is-image');
|
|
173
|
+
}
|
|
170
174
|
if (isMermaid) {
|
|
171
175
|
button.classList.add('is-mermaid');
|
|
172
176
|
}
|
|
@@ -181,7 +185,7 @@ export class FileExplorerView {
|
|
|
181
185
|
button.dataset.depth = depth;
|
|
182
186
|
button.dataset.path = filePath;
|
|
183
187
|
button.innerHTML = `
|
|
184
|
-
${this.getFileIconSvg({ isExcalidraw, isMermaid, isPlantUml })}
|
|
188
|
+
${this.getFileIconSvg({ isExcalidraw, isImage, isMermaid, isPlantUml })}
|
|
185
189
|
<span class="file-tree-name">${escapeHtml(stripVaultFileExtension(name))}</span>
|
|
186
190
|
`;
|
|
187
191
|
|
|
@@ -196,11 +200,15 @@ export class FileExplorerView {
|
|
|
196
200
|
return button;
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
getFileIconSvg({ isExcalidraw, isMermaid, isPlantUml }) {
|
|
203
|
+
getFileIconSvg({ isExcalidraw, isImage, isMermaid, isPlantUml }) {
|
|
200
204
|
if (isExcalidraw) {
|
|
201
205
|
return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>';
|
|
202
206
|
}
|
|
203
207
|
|
|
208
|
+
if (isImage) {
|
|
209
|
+
return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><path d="m21 16-5-5L7 20"/><path d="m14 14 2 2"/></svg>';
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
if (isMermaid) {
|
|
205
213
|
return '<svg class="file-tree-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7.5c0-1.38 1.12-2.5 2.5-2.5 1.04 0 1.93.64 2.3 1.56A2.5 2.5 0 0 1 14 8.5v1"/><path d="M19 16.5c0 1.38-1.12 2.5-2.5 2.5-1.04 0-1.93-.64-2.3-1.56A2.5 2.5 0 0 1 10 15.5v-1"/><path d="M8 10.5h8"/><path d="M8 13.5h8"/><path d="M10 8.5v7"/><path d="M14 8.5v7"/></svg>';
|
|
206
214
|
}
|
|
@@ -2,7 +2,13 @@ import { normalizeVaultPathInput } from '../domain/vault-paths.js';
|
|
|
2
2
|
|
|
3
3
|
function flattenTree(nodes, files = []) {
|
|
4
4
|
for (const node of nodes) {
|
|
5
|
-
if (
|
|
5
|
+
if (
|
|
6
|
+
node.type === 'file'
|
|
7
|
+
|| node.type === 'excalidraw'
|
|
8
|
+
|| node.type === 'mermaid'
|
|
9
|
+
|| node.type === 'plantuml'
|
|
10
|
+
|| node.type === 'image'
|
|
11
|
+
) {
|
|
6
12
|
files.push(node.path);
|
|
7
13
|
continue;
|
|
8
14
|
}
|