Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1113,10 +1113,10 @@ jobs:
runs-on: ubuntu-latest
needs: [job_docker_build, job_setup]
strategy:
fail-fast: false
fail-fast: true
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions apps/admin-x-settings/src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const Sidebar: React.FC = () => {
<Icon className='absolute left-3 top-3 z-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
<TextField
autoComplete="off"
autoCorrect="off"
className='mr-12 flex h-10 w-full items-center rounded-lg border border-transparent bg-white px-[33px] py-1.5 text-[14px] shadow-[0_0_1px_rgba(21,23,26,0.25),0_1px_3px_rgba(0,0,0,0.03),0_8px_10px_-12px_rgba(0,0,0,.1)] transition-colors hover:shadow-sm focus:border-green focus:bg-white focus:shadow-[0_0_0_2px_rgba(48,207,67,0.25)] focus:outline-2 tablet:mr-0 dark:border-transparent dark:bg-grey-925 dark:text-white dark:placeholder:text-grey-800 dark:focus:border-green dark:focus:bg-grey-950'
containerClassName='w-100'
inputRef={searchInputRef}
Expand Down
11 changes: 9 additions & 2 deletions apps/comments-ui/src/app-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type Member = {
uuid: string,
name: string,
avatar_image: string,
expertise: string
expertise: string,
can_comment?: boolean
}

export type Comment = {
Expand Down Expand Up @@ -86,7 +87,13 @@ export type EditableAppContext = {
commentsIsLoading?: boolean,
commentIdToHighlight: string | null,
commentIdToScrollTo: string | null,
pageUrl: string
pageUrl: string,
supportEmail: string | null,
isMember: boolean,
isAdmin: boolean,
isPaidOnly: boolean,
hasRequiredTier: boolean,
isCommentingDisabled: boolean
}

export type TranslationFunction = (key: string, replacements?: Record<string, string | number>) => string;
Expand Down
26 changes: 22 additions & 4 deletions apps/comments-ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ const App: React.FC<AppProps> = ({scriptTag, initialCommentId, pageUrl}) => {
commentsIsLoading: false,
commentIdToHighlight: null,
commentIdToScrollTo: initialCommentId,
pageUrl
pageUrl,
supportEmail: null,
isMember: false,
isAdmin: false,
isPaidOnly: false,
hasRequiredTier: true,
isCommentingDisabled: false
});

const iframeRef = React.createRef<HTMLIFrameElement>();
Expand Down Expand Up @@ -149,7 +155,8 @@ const App: React.FC<AppProps> = ({scriptTag, initialCommentId, pageUrl}) => {

setState({
adminApi,
admin
admin,
isAdmin: !!admin
});
} catch (e) {
/* eslint-disable no-console */
Expand Down Expand Up @@ -278,7 +285,7 @@ const App: React.FC<AppProps> = ({scriptTag, initialCommentId, pageUrl}) => {
/** Initialize comments setup once in viewport, fetch data and setup state */
const initSetup = async () => {
try {
const {member, labs} = await api.init();
const {member, labs, supportEmail} = await api.init();
const {count, comments: initialComments, pagination: initialPagination} = await fetchComments();

let comments = initialComments;
Expand All @@ -296,6 +303,12 @@ const App: React.FC<AppProps> = ({scriptTag, initialCommentId, pageUrl}) => {
}
}

// Compute tier access values
const isMember = !!member;
const isPaidOnly = options.commentsEnabled === 'paid';
const isPaidMember = !!member?.paid;
const hasRequiredTier = isPaidMember || !isPaidOnly;

setState({
member,
initStatus: 'success',
Expand All @@ -306,7 +319,12 @@ const App: React.FC<AppProps> = ({scriptTag, initialCommentId, pageUrl}) => {
labs: labs,
commentsIsLoading: false,
commentIdToHighlight: null,
commentIdToScrollTo: scrollTargetFound ? initialCommentId : null
commentIdToScrollTo: scrollTargetFound ? initialCommentId : null,
supportEmail,
isMember,
isPaidOnly,
hasRequiredTier,
isCommentingDisabled: member?.can_comment === false
});
} catch (e) {
console.error(`[Comments] Failed to initialize:`, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ type Props = {
comment: Comment;
};
const LikeButton: React.FC<Props> = ({comment}) => {
const {dispatchAction, member, commentsEnabled} = useAppContext();
const {dispatchAction, isMember, hasRequiredTier} = useAppContext();
const [animationClass, setAnimation] = useState('');
const [disabled, setDisabled] = useState(false);

const paidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
const canLike = member && (isPaidMember || !paidOnly);
const canLike = isMember && hasRequiredTier;

const toggleLike = async () => {
if (!canLike) {
Expand Down
26 changes: 26 additions & 0 deletions apps/comments-ui/src/components/content/buttons/like-count.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {ReactComponent as LikeIcon} from '../../../images/icons/like.svg';

type Props = {
count: number;
liked: boolean;
};

const LikeCount: React.FC<Props> = ({count, liked}) => {
return (
<span
className={`flex items-center font-sans text-base sm:text-sm ${
liked ? 'text-black/90 dark:text-white/90' : 'text-black/50 dark:text-white/60'
}`}
data-testid="like-count"
>
<LikeIcon
className={`mr-[6px] ${
liked ? 'fill-black stroke-black dark:fill-white dark:stroke-white' : 'stroke-black/50 dark:stroke-white/60'
}`}
/>
{count}
</span>
);
};

export default LikeCount;
10 changes: 1 addition & 9 deletions apps/comments-ui/src/components/content/buttons/more-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CommentContextMenu from '../context-menus/comment-context-menu';
import {Comment, useAppContext} from '../../../app-context';
import {Comment} from '../../../app-context';
import {ReactComponent as MoreIcon} from '../../../images/icons/more.svg';
import {useState} from 'react';

Expand All @@ -10,8 +10,6 @@ type Props = {

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

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

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

if (!show) {
return null;
}

return (
<div data-testid="more-button">
<button className="outline-0" type="button" onClick={toggleContextMenu}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ type Props = {
};

const ReplyButton: React.FC<Props> = ({disabled, isReplying, openReplyForm}) => {
const {member, t, dispatchAction, commentsEnabled} = useAppContext();
const {t, dispatchAction, isMember, hasRequiredTier} = useAppContext();

const paidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
const canReply = member && (isPaidMember || !paidOnly);
const canReply = isMember && hasRequiredTier;

const handleClick = () => {
if (!canReply) {
Expand Down
51 changes: 32 additions & 19 deletions apps/comments-ui/src/components/content/comment.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import EditForm from './forms/edit-form';
import LikeButton from './buttons/like-button';
import LikeCount from './buttons/like-count';
import MoreButton from './buttons/more-button';
import React, {useCallback} from 'react';
import Replies, {RepliesProps} from './replies';
Expand Down Expand Up @@ -39,8 +40,8 @@ const AnimatedComment: React.FC<AnimatedCommentProps> = ({comment, parent}) => {
};

export const CommentComponent: React.FC<CommentProps> = ({comment, parent}) => {
const {dispatchAction, admin} = useAppContext();
const {showDeletedMessage, showHiddenMessage, showCommentContent} = useCommentVisibility(comment, admin);
const {dispatchAction, isAdmin} = useAppContext();
const {showDeletedMessage, showHiddenMessage, showCommentContent} = useCommentVisibility(comment, isAdmin);

const openEditMode = useCallback(() => {
const newForm: OpenCommentForm = {
Expand Down Expand Up @@ -82,10 +83,10 @@ type PublishedCommentProps = CommentProps & {
openEditMode: () => void;
}
const PublishedComment: React.FC<PublishedCommentProps> = ({comment, parent, openEditMode}) => {
const {dispatchAction, openCommentForms, admin, commentIdToHighlight} = useAppContext();
const {dispatchAction, openCommentForms, isAdmin, commentIdToHighlight} = useAppContext();

// Determine if the comment should be displayed with reduced opacity
const isHidden = admin && comment.status === 'hidden';
const isHidden = isAdmin && comment.status === 'hidden';
const hiddenClass = isHidden ? 'opacity-30' : '';

// Check if this comment is being edited
Expand Down Expand Up @@ -159,9 +160,9 @@ type UnpublishedCommentProps = {
openEditMode: () => void;
}
const UnpublishedComment: React.FC<UnpublishedCommentProps> = ({comment, openEditMode}) => {
const {admin, openCommentForms, t} = useAppContext();
const {isAdmin, openCommentForms, t} = useAppContext();

const avatar = (admin && comment.status !== 'deleted')
const avatar = (isAdmin && comment.status !== 'deleted')
? <Avatar member={comment.member} />
: <BlankAvatar />;
const hasReplies = comment.replies && comment.replies.length > 0;
Expand All @@ -179,7 +180,7 @@ const UnpublishedComment: React.FC<UnpublishedCommentProps> = ({comment, openEdi
const displayReplyForm = openForm && (!openForm.parent_id || openForm.parent_id === comment.id);

// Only show MoreButton for hidden (not deleted) comments when admin
const showMoreButton = admin && comment.status === 'hidden';
const showMoreButton = isAdmin && comment.status === 'hidden';

return (
<CommentLayout avatar={avatar} hasReplies={hasReplies}>
Expand Down Expand Up @@ -402,28 +403,40 @@ type CommentMenuProps = {
openReplyForm: () => void;
highlightReplyButton: boolean;
openEditMode: () => void;
parent?: Comment;
className?: string;
};
const CommentMenu: React.FC<CommentMenuProps> = ({comment, openReplyForm, highlightReplyButton, openEditMode, className = ''}) => {
const {admin, t} = useAppContext();
const {member, t, isMember, isAdmin, isCommentingDisabled} = useAppContext();

const isPublished = comment.status === 'published';
const isOwnComment = member && comment.member?.uuid === member?.uuid;

// Visibility decisions
const showLikeButton = !isCommentingDisabled;
const showReplyButton = !isCommentingDisabled;
const shouldShowMoreButton = isAdmin || (isMember && isPublished);
const shouldHideMoreButton = isCommentingDisabled && isOwnComment;
const showMoreButton = shouldShowMoreButton && !shouldHideMoreButton;

if (admin && comment.status === 'hidden') {
if (isAdmin && comment.status === 'hidden') {
return (
<div className={`flex items-center gap-4 ${className}`}>
<span className="font-sans text-base leading-snug text-red-600 sm:text-sm">{t('Hidden for members')}</span>
{<MoreButton comment={comment} toggleEdit={openEditMode} />}
</div>
);
} else {
return (
<div className={`flex items-center gap-4 ${className}`}>
{<LikeButton comment={comment} />}
{<ReplyButton isReplying={highlightReplyButton} openReplyForm={openReplyForm} />}
{<MoreButton comment={comment} toggleEdit={openEditMode} />}
<MoreButton comment={comment} toggleEdit={openEditMode} />
</div>
);
}

return (
<div className={`flex items-center gap-4 ${className}`}>
{showLikeButton
? <LikeButton comment={comment} />
: <LikeCount count={comment.count.likes} liked={comment.liked} />
}
{showReplyButton && <ReplyButton isReplying={highlightReplyButton} openReplyForm={openReplyForm} />}
{showMoreButton && <MoreButton comment={comment} toggleEdit={openEditMode} />}
</div>
);
};

//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {useAppContext} from '../../app-context';

const CommentingDisabledBox: React.FC = () => {
const {accentColor, supportEmail, t} = useAppContext();

const linkStyle = {
color: accentColor
};

return (
<>
<h1 className="mb-[8px] text-center font-sans text-2xl font-semibold tracking-tight text-black dark:text-[rgba(255,255,255,0.85)]">
{t('Commenting disabled')}
</h1>
<p className="mb-[28px] w-full px-0 text-center font-sans text-lg leading-normal text-neutral-600 sm:max-w-screen-sm sm:px-8 dark:text-[rgba(255,255,255,0.85)]">
{supportEmail ? (
<>
{t('You can\'t post comments in this publication.')}{' '}
<a className="font-semibold hover:opacity-90" href={`mailto:${supportEmail}`} style={linkStyle}>
{t('Contact support')}
</a>{' '}
{t('for more information.')}
</>
) : (
t('You can\'t post comments in this publication.')
)}
</p>
</>
);
};

export default CommentingDisabledBox;
23 changes: 16 additions & 7 deletions apps/comments-ui/src/components/content/content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CTABox from './cta-box';
import Comment from './comment';
import CommentingDisabledBox from './commenting-disabled-box';
import ContentTitle from './content-title';
import MainForm from './forms/main-form';
import Pagination from './pagination';
Expand Down Expand Up @@ -72,7 +73,7 @@ function onIframeResize(

const Content = () => {
const labs = useLabs();
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, commentsIsLoading, t, dispatchAction, commentIdToScrollTo} = useAppContext();
const {pagination, comments, commentCount, title, showCount, commentsIsLoading, t, dispatchAction, commentIdToScrollTo, isMember, isPaidOnly, hasRequiredTier, isCommentingDisabled} = useAppContext();
const containerRef = useRef<HTMLDivElement>(null);

const scrollToComment = useCallback((element: HTMLElement, commentId: string) => {
Expand Down Expand Up @@ -140,19 +141,27 @@ const Content = () => {
scrollToComment(element, commentIdToScrollTo);
}, [commentIdToScrollTo, commentsIsLoading, comments, scrollToComment]);

const isPaidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
const isFirst = pagination?.total === 0;
const canComment = isMember && hasRequiredTier && !isCommentingDisabled;

const commentsComponents = comments.slice().map(comment => <Comment key={comment.id} comment={comment} />);
// Explicit form/box visibility states
const showMainForm = canComment;
const showDisabledBox = !canComment && isCommentingDisabled;
const showCtaBox = !canComment && !isCommentingDisabled;

const commentsComponents = comments.map(comment => <Comment key={comment.id} comment={comment} />);

return (
<>
<ContentTitle count={commentCount} showCount={showCount} title={title}/>
<div>
{(member && (isPaidMember || !isPaidOnly)) ? (
<MainForm commentsCount={comments.length} />
) : (
{showMainForm && <MainForm commentsCount={comments.length} />}
{showDisabledBox && (
<section className="flex flex-col items-center py-6 sm:px-8 sm:py-10" data-testid="commenting-disabled-box">
<CommentingDisabledBox />
</section>
)}
{showCtaBox && (
<section className="flex flex-col items-center py-6 sm:px-8 sm:py-10" data-testid="cta-box">
<CTABox isFirst={isFirst} isPaid={isPaidOnly} />
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ type Props = {
toggleEdit: () => void;
};
const CommentContextMenu: React.FC<Props> = ({comment, close, toggleEdit}) => {
const {member, admin} = useAppContext();
const {member, isAdmin} = useAppContext();
const isAuthor = member && comment.member?.uuid === member?.uuid;
const isAdmin = !!admin;
const element = useRef<HTMLDivElement>(null);
const innerElement = useRef<HTMLDivElement>(null);

Expand Down
Loading
Loading