Skip to content

Commit

Permalink
Merge branch 'main' into patch-7
Browse files Browse the repository at this point in the history
  • Loading branch information
alexrodba authored Dec 11, 2024
2 parents 408d500 + 712ae40 commit 001b7c9
Show file tree
Hide file tree
Showing 95 changed files with 2,088 additions and 314 deletions.
5 changes: 2 additions & 3 deletions apps/comments-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,12 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
admin = await adminApi.getUser();
if (admin && state.labs.commentImprovements) {
// this is a bit of a hack, but we need to fetch the comments fully populated if the user is an admin
const adminComments = await adminApi.browse({page: 1, postId: options.postId, order: state.order});
const adminComments = await adminApi.browse({page: 1, postId: options.postId, order: state.order, memberUuid: state.member?.uuid});
setState({
...state,
adminApi: adminApi,
comments: adminComments.comments,
pagination: adminComments.meta.pagination,
commentCount: adminComments.meta.pagination.total
pagination: adminComments.meta.pagination
});
}
} catch (e) {
Expand Down
89 changes: 61 additions & 28 deletions apps/comments-ui/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function loadMoreComments({state, api, options, order}: {state: EditableAp
}
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements) {
data = await state.adminApi.browse({page, postId: options.postId, order: order || state.order});
data = await state.adminApi.browse({page, postId: options.postId, order: order || state.order, memberUuid: state.member?.uuid});
} else {
data = await api.comments.browse({page, postId: options.postId, order: order || state.order});
}
Expand All @@ -22,13 +22,19 @@ async function loadMoreComments({state, api, options, order}: {state: EditableAp
};
}

async function setOrder({state, data: {order}, options, api}: {state: EditableAppContext, data: {order: string}, options: CommentsOptions, api: GhostApi}) {
state.commentsIsLoading = true;
function setCommentsIsLoading({data: isLoading}: {data: boolean | null}) {
return {
commentsIsLoading: isLoading
};
}

async function setOrder({state, data: {order}, options, api, dispatchAction}: {state: EditableAppContext, data: {order: string}, options: CommentsOptions, api: GhostApi, dispatchAction: DispatchActionType}) {
dispatchAction('setCommentsIsLoading', true);

try {
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements) {
data = await state.adminApi.browse({page: 1, postId: options.postId, order});
data = await state.adminApi.browse({page: 1, postId: options.postId, order, memberUuid: state.member?.uuid});
} else {
data = await api.comments.browse({page: 1, postId: options.postId, order});
}
Expand All @@ -49,7 +55,7 @@ async function setOrder({state, data: {order}, options, api}: {state: EditableAp
async function loadMoreReplies({state, api, data: {comment, limit}, isReply}: {state: EditableAppContext, api: GhostApi, data: {comment: any, limit?: number | 'all'}, isReply: boolean}): Promise<Partial<EditableAppContext>> {
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements && !isReply) { // we don't want the admin api to load reply data for replying to a reply, so we pass isReply: true
data = await state.adminApi.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit});
data = await state.adminApi.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit, memberUuid: state.member?.uuid});
} else {
data = await api.comments.replies({commentId: comment.id, afterReplyId: comment.replies[comment.replies.length - 1]?.id, limit});
}
Expand Down Expand Up @@ -144,13 +150,13 @@ async function hideComment({state, data: comment}: {state: EditableAppContext, a

async function showComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, adminApi: any, data: {id: string}}) {
if (state.adminApi) {
await state.adminApi.showComment(comment.id);
await state.adminApi.showComment({id: comment.id});
}
// We need to refetch the comment, to make sure we have an up to date HTML content
// + all relations are loaded as the current member (not the admin)
let data;
if (state.admin && state.adminApi && state.labs.commentImprovements) {
data = await state.adminApi.read({commentId: comment.id});
data = await state.adminApi.read({commentId: comment.id, memberUuid: state.member?.uuid});
} else {
data = await api.comments.read(comment.id);
}
Expand Down Expand Up @@ -273,34 +279,60 @@ async function deleteComment({state, api, data: comment}: {state: EditableAppCon
}
});

return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
if (state.labs.commentImprovements) {
return {
comments: state.comments.map((c) => {
// If the comment has replies we want to keep it so the replies are
// still visible, but mark the comment as deleted. Otherwise remove it.
if (c.id === comment.id) {
if (c.replies.length > 0) {
return {
...c,
status: 'deleted'
};
} else {
return null; // Will be filtered out later
}
}

const updatedReplies = c.replies.filter(r => r.id !== comment.id);
return {
...c,
replies: updatedReplies
};
}).filter(Boolean),
commentCount: state.commentCount - 1
};
} else {
return {
comments: state.comments.map((c) => {
const replies = c.replies.map((r) => {
if (r.id === comment.id) {
return {
...r,
status: 'deleted'
};
}

return r;
});

if (c.id === comment.id) {
return {
...r,
status: 'deleted'
...c,
status: 'deleted',
replies
};
}

return r;
});

if (c.id === comment.id) {
return {
...c,
status: 'deleted',
replies
};
}

return {
...c,
replies
};
}),
commentCount: state.commentCount - 1
};
}),
commentCount: state.commentCount - 1
};
}
}

async function editComment({state, api, data: {comment, parent}}: {state: EditableAppContext, api: GhostApi, data: {comment: Partial<Comment> & {id: string}, parent?: Comment}}) {
Expand Down Expand Up @@ -474,7 +506,8 @@ export const Actions = {
setOrder,
openCommentForm,
highlightComment,
setHighlightComment
setHighlightComment,
setCommentsIsLoading
};

export type ActionType = keyof typeof Actions;
Expand Down
4 changes: 2 additions & 2 deletions apps/comments-ui/src/components/content/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const BlankAvatar = () => {
const dimensionClasses = getDimensionClasses();
return (
<figure className={`relative ${dimensionClasses}`}>
<div className={`flex items-center justify-center rounded-full bg-black/10 dark:bg-white/15 ${dimensionClasses}`}>
<AvatarIcon className="stroke-white opacity-80" />
<div className={`flex items-center justify-center rounded-full bg-black/5 text-neutral-900/25 dark:bg-white/15 dark:text-white/30 ${dimensionClasses}`}>
<AvatarIcon className="h-7 w-7 opacity-80" />
</div>
</figure>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/comments-ui/src/components/content/Comment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ const RepliesLine: React.FC<{hasReplies: boolean}> = ({hasReplies}) => {
return null;
}

return (<div className="mb-2 h-full w-px grow rounded bg-gradient-to-b from-neutral-900/10 via-neutral-900/10 to-transparent dark:from-white/10 dark:via-white/10" />);
return (<div className="mb-2 h-full w-px grow rounded bg-gradient-to-b from-neutral-900/10 via-neutral-900/10 to-transparent dark:from-white/10 dark:via-white/10" data-testid="replies-line" />);
};

type CommentLayoutProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {

const MoreButton: React.FC<Props> = ({comment, toggleEdit}) => {
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const {member, admin, pagination, comments} = useAppContext();
const {member, admin} = useAppContext();
const isAdmin = !!admin;

const toggleContextMenu = () => {
Expand All @@ -21,10 +21,6 @@ const MoreButton: React.FC<Props> = ({comment, toggleEdit}) => {
setIsContextMenuOpen(false);
};

// Check if this is the last comment and there's no more pagination
const isLastComment = (!pagination || pagination.total <= pagination.page * pagination.limit) &&
comments[comments.length - 1]?.id === comment.id;

const show = (!!member && comment.status === 'published') || isAdmin;

if (!show) {
Expand All @@ -36,7 +32,7 @@ const MoreButton: React.FC<Props> = ({comment, toggleEdit}) => {
<button className="outline-0" type="button" onClick={toggleContextMenu}>
<MoreIcon className={`duration-50 gh-comments-icon gh-comments-icon-more outline-0 transition ease-linear hover:fill-black/75 dark:hover:fill-white/75 ${isContextMenuOpen ? 'fill-black/75 dark:fill-white/75' : 'fill-black/50 dark:fill-white/60'}`} />
</button>
{isContextMenuOpen ? <CommentContextMenu close={closeContextMenu} comment={comment} isLastComment={isLastComment} toggleEdit={toggleEdit} /> : null}
{isContextMenuOpen ? <CommentContextMenu close={closeContextMenu} comment={comment} toggleEdit={toggleEdit} /> : null}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const AuthorContextMenu: React.FC<Props> = ({comment, close, toggleEdit}) => {
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700" data-testid="edit" type="button" onClick={toggleEdit}>
{t('Edit')}
</button>
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] text-red-600 transition-colors hover:bg-neutral-100 dark:text-red-500 dark:hover:bg-neutral-700" type="button" onClick={deleteComment}>
<button className="w-full rounded px-2.5 py-1.5 text-left text-[14px] text-red-600 transition-colors hover:bg-neutral-100 dark:text-red-500 dark:hover:bg-neutral-700" data-testid="delete" type="button" onClick={deleteComment}>
{t('Delete')}
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import CommentContextMenu from './CommentContextMenu';
import React from 'react';
import sinon from 'sinon';
import {AppContext} from '../../../AppContext';
import {buildComment} from '../../../../test/utils/fixtures';
import {render, screen} from '@testing-library/react';

const contextualRender = (ui, {appContext, ...renderOptions}) => {
const contextWithDefaults = {
member: null,
dispatchAction: () => {},
t: str => str,
...appContext
};

return render(
<AppContext.Provider value={contextWithDefaults}>{ui}</AppContext.Provider>,
renderOptions
);
};

describe('<CommentContextMenu>', () => {
afterEach(() => {
sinon.restore();
});

it('has display-below classes when in viewport', () => {
const comment = buildComment();
contextualRender(<CommentContextMenu comment={comment} />, {appContext: {admin: true, labs: {commentImprovements: true}}});
expect(screen.getByTestId('comment-context-menu-inner')).toHaveClass('top-0');
});

it('has display-above classes when bottom is out of viewport', () => {
sinon.stub(HTMLElement.prototype, 'getBoundingClientRect').returns({bottom: 2000});

const comment = buildComment();
contextualRender(<CommentContextMenu comment={comment} />, {appContext: {admin: true, labs: {commentImprovements: true}}});
expect(screen.getByTestId('comment-context-menu-inner')).toHaveClass('bottom-full', 'mb-6');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@ import AuthorContextMenu from './AuthorContextMenu';
import NotAuthorContextMenu from './NotAuthorContextMenu';
import {Comment, useAppContext, useLabs} from '../../../AppContext';
import {useEffect, useRef} from 'react';
import {useOutOfViewportClasses} from '../../../utils/hooks';

type Props = {
comment: Comment;
close: () => void;
toggleEdit: () => void;
isLastComment?: boolean;
};
const CommentContextMenu: React.FC<Props> = ({comment, close, toggleEdit, isLastComment}) => {
const CommentContextMenu: React.FC<Props> = ({comment, close, toggleEdit}) => {
const {member, admin} = useAppContext();
const isAuthor = member && comment.member?.uuid === member?.uuid;
const isAdmin = !!admin;
const element = useRef<HTMLDivElement>(null);
const innerElement = useRef<HTMLDivElement>(null);
const labs = useLabs();


// By default display dropdown below but move above if that renders off-screen
// NOTE: innerElement ref is only set when commentImprovements flag is enabled
useOutOfViewportClasses(innerElement, {
bottom: {
default: 'top-0',
outOfViewport: 'bottom-full mb-6'
}
});

useEffect(() => {
const listener = () => {
close();
Expand Down Expand Up @@ -79,8 +89,8 @@ const CommentContextMenu: React.FC<Props> = ({comment, close, toggleEdit, isLast

return (
labs.commentImprovements ? (
<div ref={element} className="relative" onClick={stopPropagation}>
<div className={`absolute z-10 min-w-min whitespace-nowrap rounded bg-white p-1 font-sans text-sm shadow-lg outline-0 sm:min-w-[80px] dark:bg-neutral-800 dark:text-white ${isLastComment ? 'bottom-full mb-6' : 'top-0'}`}>
<div ref={element} className="relative" data-testid="comment-context-menu" onClick={stopPropagation}>
<div ref={innerElement} className={`absolute z-10 min-w-min whitespace-nowrap rounded bg-white p-1 font-sans text-sm shadow-lg outline-0 sm:min-w-[80px] dark:bg-neutral-800 dark:text-white`} data-testid="comment-context-menu-inner">
{contextMenu}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {useAppContext} from '../../../AppContext';
import {Comment, useAppContext} from '../../../AppContext';

type Props = {
comment: Comment;
Expand Down
13 changes: 5 additions & 8 deletions apps/comments-ui/src/components/content/forms/EditForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {Comment, OpenCommentForm, useAppContext} from '../../../AppContext';
import {Form} from './Form';
import {getEditorConfig} from '../../../utils/editor';
import {isMobile} from '../../../utils/helpers';
import {useCallback, useEffect} from 'react';
import {useEditor} from '@tiptap/react';
import {useCallback, useEffect, useMemo} from 'react';
import {useEditor} from '../../../utils/hooks';

type Props = {
openForm: OpenCommentForm;
Expand All @@ -14,17 +13,15 @@ type Props = {
const EditForm: React.FC<Props> = ({comment, openForm, parent}) => {
const {dispatchAction, t} = useAppContext();

const config = {
const editorConfig = useMemo(() => ({
placeholder: t('Edit this comment'),
// warning: we cannot use autofocus on the edit field, because that sets
// the cursor position at the beginning of the text field instead of the end
autofocus: false,
content: comment.html
};
}), [comment]);

const editor = useEditor({
...getEditorConfig(config)
});
const {editor} = useEditor(editorConfig);

// Instead of autofocusing, we focus and jump to end manually
useEffect(() => {
Expand Down
Loading

0 comments on commit 001b7c9

Please sign in to comment.