0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Restricted actions for logged in members and paid members

refs https://github.com/TryGhost/Team/issues/1693

- Added a new data attribute to the injected stript tag: `data-comments-enabled`. This contains the commentsEnabled setting, and can be 'all' or 'paid' (when it is off the comments section is never injected).
- Added a new component `<NotPaidBox>`, which is visible when a member is signed in but doesn't have paid access to comment
- Prevented clicking the reply and like buttons when a member doesn't have access
This commit is contained in:
Simon Backx 2022-07-20 10:16:50 +02:00
parent dd5a4bb35e
commit 2657af11f6
7 changed files with 72 additions and 25 deletions

View file

@ -282,6 +282,7 @@ export default class App extends React.Component {
colorScheme: this.props.colorScheme,
avatarSaturation: this.props.avatarSaturation,
accentColor: this.props.accentColor,
commentsEnabled: this.props.commentsEnabled,
dispatchAction: (_action, data) => this.dispatchAction(_action, data),
/**

View file

@ -21,7 +21,7 @@ const Comment = (props) => {
setIsInReplyMode(current => !current);
};
const {admin, avatarSaturation} = useContext(AppContext);
const {admin, avatarSaturation, member, commentsEnabled} = useContext(AppContext);
const comment = props.comment;
const hasReplies = comment.replies && comment.replies.length > 0;
const isNotPublished = comment.status !== 'published';
@ -35,6 +35,10 @@ const Comment = (props) => {
}
}
const paidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
const canReply = member && (isPaidMember || !paidOnly);
if (isInEditMode) {
return (
<Form comment={comment} toggle={toggleEditMode} parent={props.parent} isEdit={true} avatarSaturation={props.avatarSaturation} />
@ -55,7 +59,7 @@ const Comment = (props) => {
</div>
<div className="ml-14 flex gap-5 items-center">
<Like comment={comment} />
{(isNotPublished || !props.parent) && <Reply comment={comment} toggleReply={toggleReplyMode} isReplying={isInReplyMode} />}
{canReply && (isNotPublished || !props.parent) && <Reply comment={comment} toggleReply={toggleReplyMode} isReplying={isInReplyMode} />}
<div className="text-sm text-neutral-400 dark:text-[rgba(255,255,255,0.5)] font-sans">{formatRelativeTime(comment.created_at)}</div>
<More comment={comment} toggleEdit={toggleEditMode} />
</div>

View file

@ -5,6 +5,7 @@ import Loading from './Loading';
import Form from './Form';
import Comment from './Comment';
import Pagination from './Pagination';
import NotPaidBox from './NotPaidBox';
const CommentsBox = (props) => {
const luminance = (r, g, b) => {
@ -40,7 +41,7 @@ const CommentsBox = (props) => {
}
};
const {accentColor, pagination, member, comments} = useContext(AppContext);
const {accentColor, pagination, member, comments, commentsEnabled} = useContext(AppContext);
const commentsElements = comments.slice().reverse().map(comment => <Comment comment={comment} key={comment.id} avatarSaturation={props.avatarSaturation} />);
@ -50,6 +51,9 @@ const CommentsBox = (props) => {
'--gh-accent-color': accentColor ?? 'blue'
};
const paidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
return (
<section className={'ghost-display ' + containerClass} style={style}>
<Pagination />
@ -59,7 +63,7 @@ const CommentsBox = (props) => {
{commentsElements}
</div>
<div>
{ member ? <Form commentsCount={commentsCount} avatarSaturation={props.avatarSaturation} /> : <NotSignedInBox /> }
{ member ? (isPaidMember || !paidOnly ? <Form commentsCount={commentsCount} avatarSaturation={props.avatarSaturation} /> : <NotPaidBox />) : <NotSignedInBox /> }
</div>
</>
: <Loading />}

View file

@ -3,33 +3,42 @@ import {ReactComponent as LikeIcon} from '../images/icons/like.svg';
import AppContext from '../AppContext';
function Like(props) {
const {onAction, member} = useContext(AppContext);
const {dispatchAction, member, commentsEnabled} = useContext(AppContext);
const [animationClass, setAnimation] = useState('');
let likeCursor = 'cursor-pointer';
if (!member) {
likeCursor = 'cursor-text';
}
const paidOnly = commentsEnabled === 'paid';
const isPaidMember = member && !!member.paid;
const canLike = member && (isPaidMember || !paidOnly);
const toggleLike = () => {
if (member) {
if (!props.comment.liked) {
onAction('likeComment', props.comment);
setAnimation('animate-heartbeat');
setTimeout(() => {
setAnimation('');
}, 400);
} else {
onAction('unlikeComment', props.comment);
}
if (!canLike) {
return;
}
if (!props.comment.liked) {
dispatchAction('likeComment', props.comment);
setAnimation('animate-heartbeat');
setTimeout(() => {
setAnimation('');
}, 400);
} else {
dispatchAction('unlikeComment', props.comment);
}
};
// If can like: use <button> element, otherwise use a <span>
const CustomTag = canLike ? `button` : `span`;
let likeCursor = 'cursor-pointer';
if (!canLike) {
likeCursor = 'cursor-text';
}
return (
<button className={`flex font-sans items-center text-sm ${props.comment.liked ? 'text-neutral-900 dark:text-[rgba(255,255,255,0.9)]' : 'text-neutral-400 dark:text-[rgba(255,255,255,0.5)]'} ${likeCursor}`} onClick={toggleLike}>
<CustomTag className={`flex font-sans items-center text-sm ${props.comment.liked ? 'text-neutral-900 dark:text-[rgba(255,255,255,0.9)]' : 'text-neutral-400 dark:text-[rgba(255,255,255,0.5)]'} ${likeCursor}`} onClick={toggleLike}>
<LikeIcon className={animationClass + ` mr-[6px] ${props.comment.liked ? 'fill-neutral-900 stroke-neutral-900 dark:fill-white dark:stroke-white' : 'stroke-neutral-400 dark:stroke-[rgba(255,255,255,0.5)]'}`} />
{props.comment.likes_count}
</button>
</CustomTag>
);
}

View file

@ -0,0 +1,28 @@
import {useContext} from 'react';
import AppContext from '../AppContext';
const NotPaidBox = (props) => {
const {accentColor} = useContext(AppContext);
const boxStyle = {
background: accentColor
};
const buttonStyle = {
color: accentColor
};
return (
<section className="text-center mb-1 bg-neutral-900 rounded-lg pt-12 pb-10 px-8" style={boxStyle}>
<h1 className="text-center text-white text-[28px] font-sans font-semibold mb-6 tracking-tight">Want to join the discussion?</h1>
<a className="bg-white font-sans py-3 px-4 mb-6 rounded inline-block font-medium" style={buttonStyle} href="#/portal/signup">
Subscribe now
</a>
<p className="font-sans text-center text-white">
You need to be subscribed to a paid plan to be able to join the discussion.
</p>
</section>
);
};
export default NotPaidBox;

View file

@ -37,8 +37,9 @@ function getSiteData() {
const avatarSaturation = scriptTag.dataset.avatarSaturation;
const accentColor = scriptTag.dataset.accentColor;
const appVersion = scriptTag.dataset.appVersion;
const commentsEnabled = scriptTag.dataset.commentsEnabled;
return {siteUrl, apiKey, apiUrl, sentryDsn, postId, adminUrl, colorScheme, avatarSaturation, accentColor, appVersion};
return {siteUrl, apiKey, apiUrl, sentryDsn, postId, adminUrl, colorScheme, avatarSaturation, accentColor, appVersion, commentsEnabled};
}
return {};
}
@ -58,13 +59,13 @@ function setup({siteUrl}) {
function init() {
// const customSiteUrl = getSiteUrl();
const {siteUrl: customSiteUrl, sentryDsn, postId, adminUrl, colorScheme, avatarSaturation, accentColor, appVersion} = getSiteData();
const {siteUrl: customSiteUrl, ...siteData} = getSiteData();
const siteUrl = customSiteUrl || window.location.origin;
setup({siteUrl});
ReactDOM.render(
<React.StrictMode>
{<App appVersion={appVersion} adminUrl={adminUrl} siteUrl={siteUrl} customSiteUrl={customSiteUrl} sentryDsn={sentryDsn} postId={postId} colorScheme={colorScheme} avatarSaturation={avatarSaturation} accentColor={accentColor} />}
{<App siteUrl={siteUrl} customSiteUrl={customSiteUrl} {...siteData} />}
</React.StrictMode>,
document.getElementById(ROOT_DIV_ID)
);

View file

@ -109,6 +109,6 @@ export function getBundledCssLink({appVersion}) {
if (process.env.NODE_ENV === 'production' && appVersion) {
return `https://unpkg.com/@tryghost/comments-ui@~${appVersion}/umd/main.css`;
} else {
return 'http://localhost:4000/main.css';
return 'https://comments.localhost/main.css';
}
}