feat: Comments (#4911)

* Comment model

* Framework, model, policy, presenter, api endpoint etc

* Iteration, first pass of UI

* fixes, refactors

* Comment commands

* comment socket support

* typing indicators

* comment component, styling

* wip

* right sidebar resize

* fix: CMD+Enter submit

* Add usePersistedState
fix: Main page scrolling on comment highlight

* drafts

* Typing indicator

* refactor

* policies

* Click thread to highlight
Improve comment timestamps

* padding

* Comment menu v1

* Change comments to use editor

* Basic comment editing

* fix: Hide commenting button when disabled at team level

* Enable opening sidebar without mark

* Move selected comment to location state

* Add comment delete confirmation

* Add comment count to document meta

* fix: Comment sidebar togglable
Add copy link to comment

* stash

* Restore History changes

* Refactor right sidebar to allow for comment animation

* Update to new router best practices

* stash

* Various improvements

* stash

* Handle click outside

* Fix incorrect placeholder in input
fix: Input box appearing on other sessions erroneously

* stash

* fix: Don't leave orphaned child comments

* styling

* stash

* Enable comment toggling again

* Edit styling, merge conflicts

* fix: Cannot navigate from insights to comments

* Remove draft comment mark on click outside

* Fix: Empty comment sidebar, tsc

* Remove public toggle

* fix: All comments are recessed
fix: Comments should not be printed

* fix: Associated mark should be removed on comment delete

* Revert unused changes

* Empty state, basic RTL support

* Create dont toggle comment mark

* Make it feel more snappy

* Highlight active comment in text

* fix animation

* RTL support

* Add reply CTA

* Translations
This commit is contained in:
Tom Moor
2023-02-25 15:03:05 -05:00
committed by GitHub
parent 59e25a0ef0
commit fc8c20149f
89 changed files with 2909 additions and 315 deletions

View File

@@ -103,12 +103,13 @@ export default abstract class BaseStore<T extends BaseModel> {
save(
params: Partial<T>,
options?: Record<string, string | boolean | number | undefined>
options: Record<string, string | boolean | number | undefined> = {}
): Promise<T> {
if (params.id) {
return this.update(params, options);
const { isNew, ...rest } = options;
if (isNew || !params.id) {
return this.create(params, rest);
}
return this.create(params, options);
return this.update(params, rest);
}
get(id: string): T | undefined {
@@ -171,6 +172,10 @@ export default abstract class BaseStore<T extends BaseModel> {
throw new Error(`Cannot delete ${this.modelName}`);
}
if (item.isNew) {
return this.remove(item.id);
}
this.isSaving = true;
try {

101
app/stores/CommentsStore.ts Normal file
View File

@@ -0,0 +1,101 @@
import invariant from "invariant";
import { filter, orderBy } from "lodash";
import { action, runInAction, computed } from "mobx";
import Comment from "~/models/Comment";
import Document from "~/models/Document";
import { PaginationParams } from "~/types";
import { client } from "~/utils/ApiClient";
import BaseStore from "./BaseStore";
import RootStore from "./RootStore";
export default class CommentsStore extends BaseStore<Comment> {
apiEndpoint = "comments";
constructor(rootStore: RootStore) {
super(rootStore, Comment);
}
/**
* Returns a list of comments in a document that are not replies to other
* comments.
*
* @param documentId ID of the document to get comments for
* @returns Array of comments
*/
threadsInDocument(documentId: string): Comment[] {
return this.inDocument(documentId).filter(
(comment) => !comment.parentCommentId
);
}
/**
* Returns a list of comments that are replies to the given comment.
*
* @param commentId ID of the comment to get replies for
* @returns Array of comments
*/
inThread(threadId: string): Comment[] {
return filter(
this.orderedData,
(comment) =>
comment.parentCommentId === threadId ||
(comment.id === threadId && !comment.isNew)
);
}
/**
* Returns a list of comments in a document.
*
* @param documentId ID of the document to get comments for
* @returns Array of comments
*/
inDocument(documentId: string): Comment[] {
return filter(
this.orderedData,
(comment) => comment.documentId === documentId
);
}
@action
setTyping({
commentId,
userId,
}: {
commentId: string;
userId: string;
}): void {
const comment = this.get(commentId);
if (comment) {
comment.typingUsers.set(userId, new Date());
}
}
@action
fetchDocumentComments = async (
documentId: string,
options?: PaginationParams | undefined
): Promise<Document[]> => {
this.isFetching = true;
try {
const res = await client.post(`/comments.list`, {
documentId,
...options,
});
invariant(res && res.data, "Comment list not available");
runInAction("CommentsStore#fetchDocumentComments", () => {
res.data.forEach(this.add);
this.addPolicies(res.policies);
});
return res.data;
} finally {
this.isFetching = false;
}
};
@computed
get orderedData(): Comment[] {
return orderBy(Array.from(this.data.values()), "createdAt", "asc");
}
}

View File

@@ -3,6 +3,7 @@ import AuthStore from "./AuthStore";
import AuthenticationProvidersStore from "./AuthenticationProvidersStore";
import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore";
import CollectionsStore from "./CollectionsStore";
import CommentsStore from "./CommentsStore";
import DialogsStore from "./DialogsStore";
import DocumentPresenceStore from "./DocumentPresenceStore";
import DocumentsStore from "./DocumentsStore";
@@ -32,6 +33,7 @@ export default class RootStore {
authenticationProviders: AuthenticationProvidersStore;
collections: CollectionsStore;
collectionGroupMemberships: CollectionGroupMembershipsStore;
comments: CommentsStore;
dialogs: DialogsStore;
documents: DocumentsStore;
events: EventsStore;
@@ -63,6 +65,7 @@ export default class RootStore {
this.auth = new AuthStore(this);
this.collections = new CollectionsStore(this);
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
this.comments = new CommentsStore(this);
this.dialogs = new DialogsStore();
this.documents = new DocumentsStore(this);
this.events = new EventsStore(this);
@@ -92,6 +95,7 @@ export default class RootStore {
// this.auth omitted for reasons...
this.collections.clear();
this.collectionGroupMemberships.clear();
this.comments.clear();
this.documents.clear();
this.events.clear();
this.groups.clear();

View File

@@ -57,9 +57,15 @@ class UiStore {
@observable
sidebarWidth: number;
@observable
sidebarRightWidth: number;
@observable
sidebarCollapsed = false;
@observable
commentsCollapsed = false;
@observable
sidebarIsResizing = false;
@@ -91,6 +97,8 @@ class UiStore {
this.languagePromptDismissed = data.languagePromptDismissed;
this.sidebarCollapsed = !!data.sidebarCollapsed;
this.sidebarWidth = data.sidebarWidth || defaultTheme.sidebarWidth;
this.sidebarRightWidth =
data.sidebarRightWidth || defaultTheme.sidebarWidth;
this.tocVisible = !!data.tocVisible;
this.theme = data.theme || Theme.System;
@@ -153,8 +161,8 @@ class UiStore {
};
@action
setSidebarWidth = (sidebarWidth: number): void => {
this.sidebarWidth = sidebarWidth;
setSidebarWidth = (width: number): void => {
this.sidebarWidth = width;
};
@action
@@ -168,6 +176,21 @@ class UiStore {
this.sidebarCollapsed = false;
};
@action
collapseComments = () => {
this.commentsCollapsed = true;
};
@action
expandComments = () => {
this.commentsCollapsed = false;
};
@action
toggleComments = () => {
this.commentsCollapsed = !this.commentsCollapsed;
};
@action
toggleCollapsedSidebar = () => {
sidebarHidden = false;
@@ -239,6 +262,7 @@ class UiStore {
tocVisible: this.tocVisible,
sidebarCollapsed: this.sidebarCollapsed,
sidebarWidth: this.sidebarWidth,
sidebarRightWidth: this.sidebarRightWidth,
languagePromptDismissed: this.languagePromptDismissed,
theme: this.theme,
};