Skip to content
Draft
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
467 changes: 251 additions & 216 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@lexical/utils": "^0.34.0",
"@monaco-editor/react": "^4.7.0",
"@permaweb/aoconnect": "0.0.93",
"@permaweb/libs": "0.0.86",
"@permaweb/libs": "file:permaweb-libs-0.0.87.tgz",
"@stripe/react-stripe-js": "^3.5.1",
"@stripe/stripe-js": "^6.1.0",
"@wanderapp/connect": "^0.1.13",
Expand Down
Binary file added permaweb-libs-0.0.87.tgz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { currentPostUpdate } from 'editor/store/post';
import { Button } from 'components/atoms/Button';
import { Checkbox } from 'components/atoms/Checkbox';
import { FormField } from 'components/atoms/FormField';
import { ICONS } from 'helpers/config';
import { Select } from 'components/atoms/Select';
import { ICONS, TIP_TOKEN_OPTIONS } from 'helpers/config';
import { CommentRulesType, PortalUserType } from 'helpers/types';
import { useArweaveProvider } from 'providers/ArweaveProvider';
import { useLanguageProvider } from 'providers/LanguageProvider';
Expand Down Expand Up @@ -58,6 +59,20 @@ export default function ArticlePostCommentRules() {
}, [assetId, portalProvider.current?.assets, currentPost.data?.commentsId]);

const disabled = currentPost.editor?.loading?.active || !hasPermission;
const tipTokenOptions = TIP_TOKEN_OPTIONS.map((token) => ({
id: token.tokenAddress,
label: token.label,
}));
const selectedTipAssetId = currentPost.data.commentRules?.tipAssetId ?? '';
const hasSelectedPreset = tipTokenOptions.some((option) => option.id === selectedTipAssetId);
const tipAssetSelectOptions =
!selectedTipAssetId || hasSelectedPreset
? tipTokenOptions
: [{ id: selectedTipAssetId, label: `Custom token (${selectedTipAssetId.slice(0, 8)}...)` }, ...tipTokenOptions];
const activeTipAssetOption =
tipAssetSelectOptions.find((option) => option.id === selectedTipAssetId) ||
tipTokenOptions.find((option) => option.label.toUpperCase() === 'AR') ||
tipTokenOptions[0];

// Load existing rules when component mounts
React.useEffect(() => {
Expand All @@ -68,13 +83,28 @@ export default function ArticlePostCommentRules() {
permawebProvider.libs
.getRules({ commentsId })
.then((rules: any) => {
console.log('[comment-rules] fetched rules raw:', {
commentsId,
rules,
});
if (rules) {
const requireProfileThumbnail = rules.requireProfileThumbnail ?? rules.RequireProfileThumbnail;
const enableTipping = rules.enableTipping ?? rules.EnableTipping;
const requireTipToComment = rules.requireTipToComment ?? rules.RequireTipToComment;
const showPaidTab = rules.showPaidTab ?? rules.ShowPaidTab;
const highlightPaidComments = rules.highlightPaidComments ?? rules.HighlightPaidComments;
const normalizedRules: CommentRulesType = {
profileAgeRequired: rules.profileAgeRequired ?? rules.ProfileAgeRequired,
mutedWords: rules.mutedWords ?? rules.MutedWords,
requireProfileThumbnail: requireProfileThumbnail === 'true' || requireProfileThumbnail === true,
enableTipping: enableTipping === 'true' || enableTipping === true,
requireTipToComment: requireTipToComment === 'true' || requireTipToComment === true,
tipAssetId: rules.tipAssetId ?? rules.TipAssetId ?? '',
minTipAmount: rules.minTipAmount ?? rules.MinTipAmount ?? '0',
showPaidTab: showPaidTab === 'true' || showPaidTab === true,
highlightPaidComments: highlightPaidComments === 'true' || highlightPaidComments === true,
};
console.log('[comment-rules] normalized rules from process:', normalizedRules);
handleCurrentPostUpdate({ field: 'commentRules', value: normalizedRules });
}
setHasLoaded(true);
Expand All @@ -89,10 +119,21 @@ export default function ArticlePostCommentRules() {
}, [commentsId, permawebProvider.libs, hasLoaded, hasPermission]);

const handleRuleChange = (field: keyof CommentRulesType, value: any) => {
const updatedRules = {
const updatedRules: CommentRulesType = {
...currentPost.data.commentRules,
[field]: value,
};
// Ensure tipping always has a token selected so updates do not fail server-side.
if (field === 'enableTipping' && value === true && !updatedRules.tipAssetId) {
updatedRules.tipAssetId = activeTipAssetOption?.id || 'AR';
}
if (field === 'enableTipping') {
console.log('[comment-rules] enableTipping changed:', {
prev: currentPost.data.commentRules?.enableTipping,
next: value,
tipAssetIdAfterChange: updatedRules.tipAssetId,
});
}
handleCurrentPostUpdate({ field: 'commentRules', value: updatedRules });
};

Expand Down Expand Up @@ -130,13 +171,43 @@ export default function ArticlePostCommentRules() {
if (commentsId && arProvider.wallet && portalProvider.current?.id && currentPost.data.commentRules) {
setUpdating(true);
try {
const normalizedTipAssetId =
currentPost.data.commentRules.enableTipping && !(currentPost.data.commentRules.tipAssetId || '').trim()
? activeTipAssetOption?.id || 'AR'
: currentPost.data.commentRules.tipAssetId ?? '';
const normalizedRulesForState: CommentRulesType = {
profileAgeRequired: currentPost.data.commentRules.profileAgeRequired ?? 0,
mutedWords: currentPost.data.commentRules.mutedWords ?? [],
requireProfileThumbnail: !!currentPost.data.commentRules.requireProfileThumbnail,
enableTipping: !!currentPost.data.commentRules.enableTipping,
requireTipToComment: !!currentPost.data.commentRules.requireTipToComment,
tipAssetId: normalizedTipAssetId,
minTipAmount: currentPost.data.commentRules.minTipAmount ?? '0',
showPaidTab: !!currentPost.data.commentRules.showPaidTab,
highlightPaidComments: !!currentPost.data.commentRules.highlightPaidComments,
};
const rulesToSend = {
ProfileAgeRequired: currentPost.data.commentRules.profileAgeRequired ?? 0,
MutedWords: currentPost.data.commentRules.mutedWords ?? [],
RequireProfileThumbnail: !!currentPost.data.commentRules.requireProfileThumbnail,
ProfileAgeRequired: normalizedRulesForState.profileAgeRequired ?? 0,
MutedWords: normalizedRulesForState.mutedWords ?? [],
RequireProfileThumbnail: !!normalizedRulesForState.requireProfileThumbnail,
EnableTipping: !!normalizedRulesForState.enableTipping,
RequireTipToComment: !!normalizedRulesForState.requireTipToComment,
TipAssetId: normalizedRulesForState.tipAssetId ?? '',
MinTipAmount: normalizedRulesForState.minTipAmount ?? '0',
ShowPaidTab: !!normalizedRulesForState.showPaidTab,
HighlightPaidComments: !!normalizedRulesForState.highlightPaidComments,
};
console.log('[comment-rules] saving rules payload:', {
commentsId,
portalId: portalProvider.current.id,
rulesToSend,
});

await permawebProvider.libs.sendMessage({
// Keep local state in sync with normalized payload used for persistence.
handleCurrentPostUpdate({ field: 'commentRules', value: normalizedRulesForState });

// Update rules via portal forwarding (canonical Run-Action format).
const forwardResult = await permawebProvider.libs.sendMessage({
processId: portalProvider.current.id,
wallet: arProvider.wallet,
action: 'Run-Action',
Expand All @@ -145,11 +216,29 @@ export default function ArticlePostCommentRules() {
{ name: 'Forward-Action', value: 'Update-Rules' },
],
data: { Input: rulesToSend },
returnResult: true,
});
console.log('[comment-rules] forwarded updateRules result:', forwardResult);
const hadUpdateRulesError = (forwardResult?.Messages || []).some((message: any) => {
const tags = message?.Tags || [];
const actionTag = tags.find((tag: any) => tag?.name === 'Action')?.value;
return actionTag === 'Update-Rules-Error';
});
if (hadUpdateRulesError) {
const errorTag = (forwardResult?.Messages || [])
.flatMap((message: any) => message?.Tags || [])
.find((tag: any) => tag?.name === 'Error')?.value;
throw new Error(errorTag || 'Update-Rules-Error');
}

addNotification(`${language?.commentRulesUpdated || 'Comment rules updated'}!`, 'success');
} catch (e: any) {
console.error('Error updating comment rules:', e);
console.error('[comment-rules] save failed context:', {
commentsId,
portalId: portalProvider.current?.id,
currentRules: currentPost.data.commentRules,
});
addNotification(e.message ?? language?.errorUpdatingCommentRules, 'warning');
}
setUpdating(false);
Expand All @@ -176,6 +265,7 @@ export default function ArticlePostCommentRules() {
const mutedWords = currentPost.data.commentRules?.mutedWords || [];
const profileAgeRequired = currentPost.data.commentRules?.profileAgeRequired || 0;
const profileAgeDays = Math.floor(profileAgeRequired / (24 * 60 * 60 * 1000));
const tippingEnabled = currentPost.data.commentRules?.enableTipping ?? false;

return (
<S.Wrapper>
Expand Down Expand Up @@ -269,6 +359,109 @@ export default function ArticlePostCommentRules() {
)}
</S.TagsWrapper>
</S.Section>

<S.SectionDivider />

<S.Section>
<S.RuleItem>
<S.RuleLabel>{language?.enableTipping || 'Enable Tipping'}</S.RuleLabel>
<Checkbox
checked={tippingEnabled}
handleSelect={() => handleRuleChange('enableTipping', !tippingEnabled)}
disabled={disabled || updating}
/>
</S.RuleItem>
<S.RuleDescription>
{language?.enableTippingDescription || 'Allow users to attach tips to their comments'}
</S.RuleDescription>
</S.Section>

{tippingEnabled && (
<>
<S.Section>
<S.RuleItem>
<S.RuleLabel>{language?.requireTipToComment || 'Require Tip to Comment'}</S.RuleLabel>
<Checkbox
checked={currentPost.data.commentRules?.requireTipToComment ?? false}
handleSelect={() =>
handleRuleChange('requireTipToComment', !currentPost.data.commentRules?.requireTipToComment)
}
disabled={disabled || updating}
/>
</S.RuleItem>
<S.RuleDescription>
{language?.requireTipToCommentDescription || 'Users must attach a tip to post a comment'}
</S.RuleDescription>
</S.Section>

<S.Section>
<Select
label={language?.tipAssetId || 'Tip Asset ID'}
activeOption={activeTipAssetOption}
setActiveOption={(option) => handleRuleChange('tipAssetId', option.id)}
options={tipAssetSelectOptions}
disabled={disabled || updating}
/>
<S.RuleDescription>
{language?.tipAssetIdDescription || 'Select the token used for comment tips'}
</S.RuleDescription>
</S.Section>

<S.Section>
<FormField
label={language?.minTipAmount || 'Minimum Tip Amount'}
placeholder={'0'}
value={currentPost.data.commentRules?.minTipAmount ?? '0'}
onChange={(e) => handleRuleChange('minTipAmount', e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.nativeEvent.stopImmediatePropagation();
}
}}
disabled={disabled || updating}
invalid={{ status: false, message: null }}
hideErrorMessage
sm
noMargin
/>
<S.RuleDescription>
{language?.minTipAmountDescription || 'Minimum tip amount in base units'}
</S.RuleDescription>
</S.Section>

<S.Section>
<S.RuleItem>
<S.RuleLabel>{language?.showPaidTab || 'Show Paid Tab'}</S.RuleLabel>
<Checkbox
checked={currentPost.data.commentRules?.showPaidTab ?? false}
handleSelect={() => handleRuleChange('showPaidTab', !currentPost.data.commentRules?.showPaidTab)}
disabled={disabled || updating}
/>
</S.RuleItem>
<S.RuleDescription>
{language?.showPaidTabDescription || 'Show a separate tab for paid/tipped comments'}
</S.RuleDescription>
</S.Section>

<S.Section>
<S.RuleItem>
<S.RuleLabel>{language?.highlightPaidComments || 'Highlight Paid Comments'}</S.RuleLabel>
<Checkbox
checked={currentPost.data.commentRules?.highlightPaidComments ?? false}
handleSelect={() =>
handleRuleChange('highlightPaidComments', !currentPost.data.commentRules?.highlightPaidComments)
}
disabled={disabled || updating}
/>
</S.RuleItem>
<S.RuleDescription>
{language?.highlightPaidCommentsDescription || 'Show top paid comments in a highlighted strip'}
</S.RuleDescription>
</S.Section>
</>
)}

<S.ActionWrapper>
<Button
type={'alt1'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export const Tag = styled.div`
}
`;

export const SectionDivider = styled.hr`
width: 100%;
border: none;
border-top: 1px solid ${(props) => props.theme.colors.border.primary};
margin: 0;
`;

export const HeaderWrapper = styled.div`
width: 100%;
display: flex;
Expand Down
21 changes: 17 additions & 4 deletions src/apps/engine/builder/blocks/comments/comment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ export default function Comment(props: any) {
prevReplyCountRef.current = currentReplyCount;
}, [data]);

const userIsAdmin = ['Admin'].some((r) => user?.roles?.includes(r));
const userIsModerator = ['Moderator'].some((r) => user?.roles?.includes(r));
const userRoles = React.useMemo(() => {
if (Array.isArray(user?.roles)) return user.roles;
if (typeof user?.roles === 'string') return [user.roles];
return [];
}, [user?.roles]);

const userIsAdmin = userRoles.includes('Admin');
const userIsModerator = userRoles.includes('Moderator');
// const userIsContributor = ['Contributor'].some((r) => user?.roles?.includes(r));

const commentAuthorPortalUser = portal?.Users?.find((u: any) => u.address === commentData.creator);
Expand Down Expand Up @@ -282,13 +288,15 @@ export default function Comment(props: any) {
}
}

const isPaidComment = !!commentData.tipReceiptId;

if (!commentData) return null;
const hasModPermission = user?.owner && user?.roles && ['Admin', 'Moderator'].some((r) => user.roles?.includes(r));
const hasModPermission = !!user?.owner && ['Admin', 'Moderator'].some((r) => userRoles.includes(r));
if (commentData.status !== 'active' && !hasModPermission) return null;

return (
<>
<S.Comment $level={level} $status={commentData.status}>
<S.Comment $level={level} $status={commentData.status} $isPaid={isPaidComment}>
{(isUpdating || isEditSubmitting) && (
<S.LoadingOverlay>
<S.Spinner />
Expand Down Expand Up @@ -346,6 +354,11 @@ export default function Comment(props: any) {
Blocked User
</S.HiddenIndicator>
)}
{isPaidComment && commentData.tipAmount && (
<S.PaidIndicator>
{commentData.tipAmount} {commentData.tipAssetSymbol || 'Tip'}
</S.PaidIndicator>
)}
<S.Date>
{!commentData?.dateCreated ? (
<Placeholder />
Expand Down
Loading