feat: Implement identity lock screen for sensitive user actions and introduce avatar generation.
This commit is contained in:
@@ -6,6 +6,7 @@ import { Post, Attachment } from '@/lib/types';
|
||||
import { ImageIcon, AlertTriangle, Film } from 'lucide-react';
|
||||
import { VideoEmbed } from '@/components/VideoEmbed';
|
||||
import { formatFullHandle } from '@/lib/utils/handle';
|
||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
|
||||
interface MediaAttachment extends Attachment {
|
||||
mimeType?: string;
|
||||
@@ -20,6 +21,7 @@ interface ComposeProps {
|
||||
}
|
||||
|
||||
export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What's happening?", isReply }: ComposeProps) {
|
||||
const { isIdentityUnlocked, setShowUnlockPrompt } = useAuth();
|
||||
const [content, setContent] = useState('');
|
||||
const [isPosting, setIsPosting] = useState(false);
|
||||
const [attachments, setAttachments] = useState<MediaAttachment[]>([]);
|
||||
@@ -43,8 +45,8 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
setCanPostNsfw(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
.catch(() => { });
|
||||
|
||||
fetch('/api/node')
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
.then(data => {
|
||||
@@ -52,7 +54,7 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
setIsNsfwNode(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
}, []);
|
||||
|
||||
// Detect URLs in content
|
||||
@@ -89,6 +91,12 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!content.trim() || isPosting || isUploading) return;
|
||||
|
||||
if (!isIdentityUnlocked) {
|
||||
setShowUnlockPrompt(true, () => handleSubmit());
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPosting(true);
|
||||
await onPost(content, attachments.map((item) => item.id).filter(Boolean), linkPreview, replyingTo?.id, isNsfw);
|
||||
setContent('');
|
||||
@@ -237,7 +245,16 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
||||
)}
|
||||
</div>
|
||||
<div className="compose-actions">
|
||||
<label className="compose-media-button" title="Add media">
|
||||
<label
|
||||
className="compose-media-button"
|
||||
title="Add media"
|
||||
onClick={(e) => {
|
||||
if (!isIdentityUnlocked) {
|
||||
e.preventDefault();
|
||||
setShowUnlockPrompt(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isUploading ? '...' : <ImageIcon size={20} />}
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { Lock, Shield } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
|
||||
interface IdentityLockScreenProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function IdentityLockScreen({
|
||||
title = 'Identity Required',
|
||||
description = 'Accessing settings requires your identity to be unlocked. Your private keys are used to sign changes to prove they came from you.',
|
||||
icon
|
||||
}: IdentityLockScreenProps) {
|
||||
const { setShowUnlockPrompt } = useAuth();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '16px',
|
||||
padding: '24px'
|
||||
}}>
|
||||
{icon || <Lock size={48} style={{ color: 'var(--accent)' }} />}
|
||||
|
||||
<h2 style={{ fontSize: '20px', fontWeight: 600 }}>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<p style={{
|
||||
color: 'var(--foreground-secondary)',
|
||||
maxWidth: '400px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setShowUnlockPrompt(true)}
|
||||
className="btn btn-primary"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||
>
|
||||
<Shield size={16} />
|
||||
Unlock Identity
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ interface PostCardProps {
|
||||
}
|
||||
|
||||
export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide, isDetail, showThread = true, isThreadParent, parentPostAuthorId }: PostCardProps) {
|
||||
const { user: currentUser, did, handle: currentUserHandle } = useAuth();
|
||||
const { user: currentUser, did, handle: currentUserHandle, isIdentityUnlocked, setShowUnlockPrompt } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const router = useRouter();
|
||||
const [liked, setLiked] = useState(post.isLiked || false);
|
||||
@@ -89,6 +89,12 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
|
||||
const handleLike = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isIdentityUnlocked) {
|
||||
setShowUnlockPrompt(true, () => handleLike(e));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLiked = liked;
|
||||
setLiked(!currentLiked);
|
||||
onLike?.(post.id, currentLiked); // Pass current state before toggle
|
||||
@@ -97,6 +103,12 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
|
||||
const handleRepost = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isIdentityUnlocked) {
|
||||
setShowUnlockPrompt(true, () => handleRepost(e));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentReposted = reposted;
|
||||
setReposted(!currentReposted);
|
||||
onRepost?.(post.id, currentReposted); // Pass current state before toggle
|
||||
|
||||
@@ -107,17 +107,15 @@ export function Sidebar() {
|
||||
{user && (
|
||||
<Link href="/notifications" className={`nav-item ${pathname?.startsWith('/notifications') ? 'active' : ''}`} title="Notifications">
|
||||
<BellIcon />
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', position: 'relative' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span className="nav-label">Notifications</span>
|
||||
{unreadCount > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
top: '-4px',
|
||||
right: '-4px',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
background: 'var(--error)',
|
||||
borderRadius: '50%',
|
||||
flexShrink: 0
|
||||
}} className="notification-dot" />
|
||||
)}
|
||||
</span>
|
||||
@@ -128,17 +126,15 @@ export function Sidebar() {
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', position: 'relative' }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<span className="nav-label">Chat</span>
|
||||
{unreadChatCount > 0 && (
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
top: '-4px',
|
||||
right: '-4px',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
background: 'var(--error)',
|
||||
borderRadius: '50%',
|
||||
flexShrink: 0
|
||||
}} className="notification-dot" />
|
||||
)}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user