Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions app/hashtag/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useAuth } from '@/contexts/auth-context'
import { useSettingsStore } from '@/lib/store'
import { checkBlockedForAuthors } from '@/hooks/use-block'
import { isCashtagStorage, cashtagStorageToDisplay } from '@/lib/post-helpers'
import { LegacyYapprLink } from '@/components/ui/legacy-yappr-link'

function HashtagPageContent() {
const router = useRouter()
Expand Down Expand Up @@ -163,6 +164,7 @@ function HashtagPageContent() {
<p className="text-gray-500 mb-4">
Be the first to post with {tagSymbol}{displayTag}
</p>
<LegacyYapprLink />
</div>
) : (
posts.map((post, index) => (
Expand Down
20 changes: 16 additions & 4 deletions app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
UserPlusIcon,
LockClosedIcon,
CloudArrowUpIcon,
NoSymbolIcon,
} from '@heroicons/react/24/outline'
import { Sidebar } from '@/components/layout/sidebar'
import { RightSidebar } from '@/components/layout/right-sidebar'
Expand All @@ -41,14 +42,18 @@ import { BlockListSettings } from '@/components/settings/block-list-settings'
import { SavedAddressesSettings } from '@/components/settings/saved-addresses-settings'
import { StorachaSettings } from '@/components/settings/storacha-settings'
import { PinataSettings } from '@/components/settings/pinata-settings'
import { ModerationSettings } from '@/components/settings/moderation-settings'
import { YAPP_TOKEN_AUTHORITY_ID } from '@/lib/constants'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { useDashPayContactsModal } from '@/hooks/use-dashpay-contacts-modal'
import { useSettingsStore } from '@/lib/store'
import { CORS_PROXY_INFO } from '@/hooks/use-link-preview'
import { UsernameModal } from '@/components/dpns/username-modal'

type SettingsSection = 'main' | 'account' | 'contacts' | 'notifications' | 'privacy' | 'privateFeed' | 'storage' | 'appearance' | 'about'
const VALID_SECTIONS: SettingsSection[] = ['main', 'account', 'contacts', 'notifications', 'privacy', 'privateFeed', 'storage', 'appearance', 'about']
type SettingsSection = 'main' | 'account' | 'contacts' | 'notifications' | 'privacy' | 'privateFeed' | 'storage' | 'appearance' | 'about' | 'moderation'
const VALID_SECTIONS: SettingsSection[] = ['main', 'account', 'contacts', 'notifications', 'privacy', 'privateFeed', 'storage', 'appearance', 'about', 'moderation']

const MODERATION_SECTION = { id: 'moderation', label: 'Moderation', icon: NoSymbolIcon, description: 'Freeze or slash YAPP balances (token authority only)' }

const settingsSections = [
{ id: 'account', label: 'Account', icon: UserIcon, description: 'Manage your account details' },
Expand Down Expand Up @@ -85,6 +90,7 @@ function SettingsPage() {
const router = useRouter()
const searchParams = useSearchParams()
const { user, logout } = useAuth()
const isAuthority = user?.identityId === YAPP_TOKEN_AUTHORITY_ID
const { theme, setTheme } = useTheme()
const linkPreviewsChoice = useSettingsStore((s) => s.linkPreviewsChoice)
const setLinkPreviewsChoice = useSettingsStore((s) => s.setLinkPreviewsChoice)
Expand Down Expand Up @@ -206,7 +212,7 @@ function SettingsPage() {

const renderMainSettings = () => (
<div className="divide-y divide-gray-200 dark:divide-gray-800">
{settingsSections.map((section) => (
{(isAuthority ? [...settingsSections, MODERATION_SECTION] : settingsSections).map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id as SettingsSection)}
Expand Down Expand Up @@ -734,14 +740,20 @@ function SettingsPage() {
return renderAppearanceSettings()
case 'about':
return renderAboutSettings()
case 'moderation':
return isAuthority ? <ModerationSettings /> : renderMainSettings()
default:
return renderMainSettings()
}
}

const getSectionTitle = () => {
if (activeSection === 'main') return 'Settings'
const section = settingsSections.find(s => s.id === activeSection)
// Only surface the Moderation title to the authority (matches renderSection's
// gate) so a non-authority hitting ?section=moderation doesn't see a
// "Moderation" header over the fallback main menu.
const sections = isAuthority ? [...settingsSections, MODERATION_SECTION] : settingsSections
const section = sections.find(s => s.id === activeSection)
return section?.label || 'Settings'
}

Expand Down
5 changes: 4 additions & 1 deletion components/compose/compose-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { extractAllTags, extractMentions } from '@/lib/post-helpers'
import { hashtagService } from '@/lib/services/hashtag-service'
import { mentionService } from '@/lib/services/mention-service'
import { extractErrorMessage, isTimeoutError, categorizeError } from '@/lib/error-utils'
import { handleInsufficientYapp } from '@/hooks/use-buy-yapp-modal'
import {
PostingProgress,
PostButtonContent,
Expand Down Expand Up @@ -902,7 +903,9 @@ export function ComposeModal() {
}
} catch (error) {
logger.error('Failed to create post:', error)
toast.error(categorizeError(error))
if (!handleInsufficientYapp(error, 'You need YAPP to post. Buy some to continue.')) {
toast.error(categorizeError(error))
}
} finally {
setIsPosting(false)
setPostingProgress(null)
Expand Down
9 changes: 9 additions & 0 deletions components/feed/feed-post-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Post } from '@/lib/types';
import ErrorBoundary from '@/components/error-boundary';
import { LoadingState } from '@/components/ui/loading-state';
import { LegacyYapprLink } from '@/components/ui/legacy-yappr-link';
import { PostCard } from '@/components/post/post-card';

interface FeedPostListProps {
Expand Down Expand Up @@ -66,6 +67,7 @@ export function FeedPostList({
? 'Follow some people to see their posts here!'
: 'Be the first to share something!'
}
emptyAction={<LegacyYapprLink />}
>
<div>
{posts?.map((post) => (
Expand All @@ -89,6 +91,13 @@ export function FeedPostList({
</button>
</div>
)}

{!hasMore && posts && posts.length > 0 && (
<div className="p-6 flex flex-col items-center gap-2 border-t border-gray-200 dark:border-gray-800 text-center">
<p className="text-sm text-gray-500">You&apos;ve reached the end.</p>
<LegacyYapprLink />
</div>
)}
</div>
</LoadingState>
</ErrorBoundary>
Expand Down
4 changes: 3 additions & 1 deletion components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { useAppStore } from '@/lib/store'
import { useNotificationStore } from '@/lib/stores/notification-store'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { UserAvatar } from '@/components/ui/avatar-image'
import { YappBalanceItem } from '@/components/token/yapp-balance-item'
import { useAuth } from '@/contexts/auth-context'
import { notificationService } from '@/lib/services'
import { useLoginModal } from '@/hooks/use-login-modal'
Expand Down Expand Up @@ -320,7 +321,8 @@ export function Sidebar() {
</div>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-gray-200 dark:bg-gray-800 my-1" />
<DropdownMenu.Item
<YappBalanceItem />
<DropdownMenu.Item
className="px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-900 cursor-pointer outline-none flex items-center gap-2"
onClick={logout}
>
Expand Down
9 changes: 7 additions & 2 deletions components/post/post-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { EmbeddedPostCard, EmbeddedPostSkeleton } from './embedded-post-card'
import { EmbeddedBlogPostCard, isEmbeddedBlogPostLike } from '@/components/blog/embedded-blog-post-card'
import { ProfileHoverCard } from '@/components/profile/profile-hover-card'
import { useTipModal } from '@/hooks/use-tip-modal'
import { handleInsufficientYapp } from '@/hooks/use-buy-yapp-modal'
import { useBlock } from '@/hooks/use-block'
import { useFollow } from '@/hooks/use-follow'
import { useHashtagValidation } from '@/hooks/use-hashtag-validation'
Expand Down Expand Up @@ -364,7 +365,9 @@ export function PostCard({ post, hideAvatar = false, isOwnPost: isOwnPostProp, e
setLiked(wasLiked)
setLikes(prevLikes)
logger.error('Like error:', error)
toast.error('Failed to update like. Please try again.')
if (!handleInsufficientYapp(error, 'You need YAPP to like posts. Buy some to continue.')) {
toast.error('Failed to update like. Please try again.')
}
} finally {
setLikeLoading(false)
}
Expand Down Expand Up @@ -397,7 +400,9 @@ export function PostCard({ post, hideAvatar = false, isOwnPost: isOwnPostProp, e
setReposted(wasReposted)
setReposts(prevReposts)
logger.error('Repost error:', error)
toast.error('Failed to update repost. Please try again.')
if (!handleInsufficientYapp(error, 'You need YAPP to repost. Buy some to continue.')) {
toast.error('Failed to update repost. Please try again.')
}
} finally {
setRepostLoading(false)
}
Expand Down
8 changes: 8 additions & 0 deletions components/post/tip-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as Dialog from '@radix-ui/react-dialog'
import { XMarkIcon, CurrencyDollarIcon, QrCodeIcon, WalletIcon, BookmarkIcon } from '@heroicons/react/24/outline'
import { CheckCircleIcon, ExclamationCircleIcon } from '@heroicons/react/24/solid'
import { motion, AnimatePresence } from 'framer-motion'
import toast from 'react-hot-toast'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
import { useTipModal } from '@/hooks/use-tip-modal'
Expand Down Expand Up @@ -204,6 +205,13 @@ export function TipModal() {
// Update global balance in auth context (persists to localStorage)
refreshBalance().catch(err => logger.error('Failed to refresh balance:', err))

// The tip (credit transfer) succeeded, but the public "tip" reply is a
// YAPP-costed doc — tell the user if it couldn't be posted rather than
// letting it vanish silently.
if (result.announcementPosted === false) {
toast('Tip sent — but the public tip note couldn’t be posted (needs YAPP).', { icon: '⚠️', duration: 5000 })
}
Comment thread
thepastaclaw marked this conversation as resolved.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// If key was manually entered and not already saved, offer to save
if (keySource === 'manual' && usedTransferKeyRef.current && !hasTransferKey(user.identityId)) {
setState('save-prompt')
Expand Down
2 changes: 2 additions & 0 deletions components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MentionRecoveryModal } from '@/components/post/mention-recovery-modal'
import { DeleteConfirmationModal } from '@/components/post/delete-confirmation-modal'
import { DashPayContactsModal } from '@/components/contacts/dashpay-contacts-modal'
import { EncryptionKeyModal } from '@/components/auth/encryption-key-modal'
import { BuyYappModal } from '@/components/token/buy-yapp-modal'

export function Providers({ children }: { children: React.ReactNode }) {
return (
Expand All @@ -26,6 +27,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
<UsernameModalProvider />
<KeyBackupModal />
<TipModal />
<BuyYappModal />
<HashtagRecoveryModal />
<MentionRecoveryModal />
<DeleteConfirmationModal />
Expand Down
97 changes: 97 additions & 0 deletions components/settings/moderation-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client'

import { useState } from 'react'
import toast from 'react-hot-toast'
import { NoSymbolIcon, ArrowUturnLeftIcon, FireIcon } from '@heroicons/react/24/outline'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { useAuth } from '@/contexts/auth-context'
import { tokenService } from '@/lib/services/token-service'

/**
* Owner-only YAPP moderation: freeze (suspend), unfreeze (reinstate), and
* destroyFrozen (slash the staked balance). Rendered only for the token
* authority identity (gated in the settings page).
*/
export function ModerationSettings() {
const { user } = useAuth()
const [targetId, setTargetId] = useState('')
const [note, setNote] = useState('')
const [busy, setBusy] = useState<null | 'freeze' | 'unfreeze' | 'destroyFrozen'>(null)

const run = async (action: 'freeze' | 'unfreeze' | 'destroyFrozen') => {
if (!user) return
const id = targetId.trim()
if (!id) {
toast.error('Enter the identity ID to moderate')
return
}
if (action === 'destroyFrozen' && !window.confirm(
`Permanently burn ${id}'s YAPP balance? This slashes their stake and cannot be undone. The account must already be frozen.`
)) return

setBusy(action)
const fn = action === 'freeze' ? tokenService.freeze
: action === 'unfreeze' ? tokenService.unfreeze
: tokenService.destroyFrozen
const result = await fn.call(tokenService, user.identityId, id, note.trim() || undefined)
setBusy(null)

if (result.success) {
toast.success(
action === 'freeze' ? 'Identity frozen (suspended)'
: action === 'unfreeze' ? 'Identity unfrozen (reinstated)'
: 'Frozen balance destroyed (slashed)'
)
} else {
toast.error(result.error || 'Action failed')
}
}

return (
<Card>
<CardHeader>
<CardTitle>YAPP Moderation</CardTitle>
<CardDescription>
Freeze suspends an account (blocks posting and transfers immediately). Slash permanently
burns a frozen account&apos;s YAPP stake. Use unfreeze to reinstate.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label htmlFor="moderation-target-id" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Identity ID</label>
<input
id="moderation-target-id"
type="text"
value={targetId}
onChange={(e) => setTargetId(e.target.value)}
placeholder="Base58 identity ID to moderate"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-neutral-800 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-yappr-500"
/>
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div>
<label htmlFor="moderation-note" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Public note (optional)</label>
<input
id="moderation-note"
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Reason recorded on-chain"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-yappr-500"
/>
</div>
<div className="flex flex-wrap gap-2">
<Button variant="outline" disabled={busy !== null} onClick={() => run('freeze')} className="gap-2">
<NoSymbolIcon className="h-4 w-4" /> {busy === 'freeze' ? 'Freezing…' : 'Freeze'}
</Button>
<Button variant="outline" disabled={busy !== null} onClick={() => run('unfreeze')} className="gap-2">
<ArrowUturnLeftIcon className="h-4 w-4" /> {busy === 'unfreeze' ? 'Unfreezing…' : 'Unfreeze'}
</Button>
<Button variant="destructive" disabled={busy !== null} onClick={() => run('destroyFrozen')} className="gap-2">
<FireIcon className="h-4 w-4" /> {busy === 'destroyFrozen' ? 'Slashing…' : 'Slash'}
</Button>
</div>
</CardContent>
</Card>
)
}
Loading
Loading