feat: Implement identity lock screen for sensitive user actions and introduce avatar generation.

This commit is contained in:
Christomatt
2026-01-30 04:44:57 +01:00
parent 4c9afa42fe
commit 10a54a0ea9
16 changed files with 519 additions and 134 deletions
+21 -4
View File
@@ -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"
+53
View 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>
);
}
+13 -1
View File
@@ -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
+4 -8
View File
@@ -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>