Fix federation flows and add versioned updater
This commit is contained in:
+169
-26
@@ -38,6 +38,22 @@ export default function AdminPage() {
|
||||
const [logoUploadError, setLogoUploadError] = useState<string | null>(null);
|
||||
const [isUploadingFavicon, setIsUploadingFavicon] = useState(false);
|
||||
const [faviconUploadError, setFaviconUploadError] = useState<string | null>(null);
|
||||
const [updateStatus, setUpdateStatus] = useState<{
|
||||
current: { version: string; commit: string | null; buildDate: string | null };
|
||||
latest: { version: string; commit: string | null; buildDate: string | null } | null;
|
||||
updateAvailable: boolean;
|
||||
updater: {
|
||||
available: boolean;
|
||||
status: string;
|
||||
message?: string;
|
||||
lastStartedAt?: string | null;
|
||||
lastFinishedAt?: string | null;
|
||||
lastExitCode?: number | null;
|
||||
lastError?: string | null;
|
||||
};
|
||||
} | null>(null);
|
||||
const [loadingUpdateStatus, setLoadingUpdateStatus] = useState(false);
|
||||
const [triggeringUpdate, setTriggeringUpdate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/admin/me')
|
||||
@@ -71,12 +87,42 @@ export default function AdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadUpdateStatus = async () => {
|
||||
setLoadingUpdateStatus(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/update', { cache: 'no-store' });
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to load update status');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setUpdateStatus(data);
|
||||
} catch {
|
||||
setUpdateStatus(null);
|
||||
} finally {
|
||||
setLoadingUpdateStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdmin) {
|
||||
loadNodeSettings();
|
||||
loadUpdateStatus();
|
||||
}
|
||||
}, [isAdmin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
loadUpdateStatus();
|
||||
}, 30000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [isAdmin]);
|
||||
|
||||
const handleSaveSettings = async (override?: typeof nodeSettings) => {
|
||||
const payload = override ?? nodeSettings;
|
||||
setSavingSettings(true);
|
||||
@@ -182,6 +228,25 @@ export default function AdminPage() {
|
||||
setBannerPromptError('');
|
||||
};
|
||||
|
||||
const handleTriggerUpdate = async () => {
|
||||
setTriggeringUpdate(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/update', { method: 'POST' });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to start update');
|
||||
}
|
||||
|
||||
showToast(data.message || 'Update started. Synapsis will restart shortly.', 'success');
|
||||
await loadUpdateStatus();
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : 'Failed to start update', 'error');
|
||||
} finally {
|
||||
setTriggeringUpdate(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = '';
|
||||
@@ -276,6 +341,13 @@ export default function AdminPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const formatTimestamp = (value?: string | null) => {
|
||||
if (!value) return 'Never';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header style={{
|
||||
@@ -438,33 +510,19 @@ export default function AdminPage() {
|
||||
</div>
|
||||
{nodeSettings.bannerUrl && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', marginBottom: '8px' }}>
|
||||
Sidebar preview
|
||||
</div>
|
||||
<div
|
||||
className="card"
|
||||
<img
|
||||
src={nodeSettings.bannerUrl}
|
||||
alt="Banner preview"
|
||||
style={{
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
maxWidth: '360px',
|
||||
width: '100%',
|
||||
maxWidth: '520px',
|
||||
maxHeight: '220px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid var(--border)',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '140px',
|
||||
background: `url(${nodeSettings.bannerUrl}) center/cover no-repeat`,
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: '16px' }}>
|
||||
<h3 style={{ fontWeight: 600, marginBottom: '12px' }}>
|
||||
Welcome to {nodeSettings.name || 'Your Node'}
|
||||
</h3>
|
||||
<p style={{ color: 'var(--foreground-secondary)', fontSize: '14px', lineHeight: 1.6 }}>
|
||||
{nodeSettings.description || 'A brief tagline for your node.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -604,7 +662,92 @@ export default function AdminPage() {
|
||||
<button className="btn btn-primary" onClick={() => handleSaveSettings()} disabled={savingSettings}>
|
||||
{savingSettings ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
background: 'var(--background-secondary)',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '16px', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '6px' }}>System Update</h2>
|
||||
<p style={{ fontSize: '13px', color: 'var(--foreground-secondary)', margin: 0 }}>
|
||||
Keep this node on the latest published Synapsis build.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleTriggerUpdate}
|
||||
disabled={
|
||||
triggeringUpdate ||
|
||||
loadingUpdateStatus ||
|
||||
updateStatus?.updater.available === false ||
|
||||
updateStatus?.updater.status === 'updating' ||
|
||||
!updateStatus?.updateAvailable
|
||||
}
|
||||
>
|
||||
{triggeringUpdate || updateStatus?.updater.status === 'updating' ? 'Updating...' : 'Update Now'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '8px', marginTop: '16px', fontSize: '13px' }}>
|
||||
<div>
|
||||
<strong>Current build:</strong>{' '}
|
||||
{updateStatus?.current?.version || 'Unknown'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Latest build:</strong>{' '}
|
||||
{updateStatus?.latest?.version || 'Unavailable'}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Status:</strong>{' '}
|
||||
{loadingUpdateStatus ? 'Checking…' : updateStatus?.updater.message || 'Ready'}
|
||||
</div>
|
||||
{updateStatus?.updater.lastStartedAt && (
|
||||
<div>
|
||||
<strong>Last started:</strong>{' '}
|
||||
{formatTimestamp(updateStatus.updater.lastStartedAt)}
|
||||
</div>
|
||||
)}
|
||||
{updateStatus?.updater.lastFinishedAt && (
|
||||
<div>
|
||||
<strong>Last finished:</strong>{' '}
|
||||
{formatTimestamp(updateStatus.updater.lastFinishedAt)}
|
||||
</div>
|
||||
)}
|
||||
{typeof updateStatus?.updater.lastExitCode === 'number' && (
|
||||
<div>
|
||||
<strong>Last exit code:</strong>{' '}
|
||||
{updateStatus.updater.lastExitCode}
|
||||
</div>
|
||||
)}
|
||||
{updateStatus?.updater.lastError && (
|
||||
<div style={{ color: 'var(--danger)' }}>
|
||||
<strong>Last error:</strong>{' '}
|
||||
{updateStatus.updater.lastError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!updateStatus?.updater.available && (
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--background)',
|
||||
fontSize: '12px',
|
||||
color: 'var(--foreground-secondary)',
|
||||
}}>
|
||||
One-click updates are unavailable on this host. Use:
|
||||
<div style={{ marginTop: '8px', fontFamily: 'monospace', color: 'var(--foreground)' }}>
|
||||
curl -fsSL https://synapsis.social/update.sh | bash
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { requireAdmin } from '@/lib/auth/admin';
|
||||
import { getCurrentBuildInfo, getLatestPublishedBuild, compareBuildVersions } from '@/lib/version';
|
||||
import { getHostUpdaterStatus, triggerHostUpdate } from '@/lib/host-updater';
|
||||
|
||||
function isUpdateAvailable(currentVersion: string, latestVersion: string | null | undefined) {
|
||||
if (!latestVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compareBuildVersions(currentVersion, latestVersion) < 0;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAdmin();
|
||||
|
||||
const [latest, updater] = await Promise.all([
|
||||
getLatestPublishedBuild(),
|
||||
getHostUpdaterStatus(),
|
||||
]);
|
||||
|
||||
const current = getCurrentBuildInfo();
|
||||
|
||||
return NextResponse.json({
|
||||
current,
|
||||
latest,
|
||||
updateAvailable: isUpdateAvailable(current.version, latest?.version),
|
||||
updater,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Admin required') {
|
||||
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
|
||||
}
|
||||
|
||||
console.error('[Admin Update] Status error:', error);
|
||||
return NextResponse.json({ error: 'Failed to get update status' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
await requireAdmin();
|
||||
|
||||
const result = await triggerHostUpdate();
|
||||
return NextResponse.json(result, { status: 202 });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Admin required') {
|
||||
return NextResponse.json({ error: 'Admin required' }, { status: 403 });
|
||||
}
|
||||
|
||||
console.error('[Admin Update] Trigger error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to trigger update' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,26 +7,56 @@ import { verifySwarmRequest } from '@/lib/swarm/signature';
|
||||
import { fetchAndCacheRemoteKey, logKeyChange } from '@/lib/swarm/identity-cache';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Schema for direct signed action (legacy)
|
||||
const chatReceiveSchema = z.object({
|
||||
const signedChatActionSchema = z.object({
|
||||
action: z.string().min(1),
|
||||
did: z.string().regex(/^did:/, 'Must be a valid DID'),
|
||||
handle: z.string().min(3).max(30),
|
||||
ts: z.number(),
|
||||
nonce: z.string().min(1),
|
||||
sig: z.string().min(1),
|
||||
data: z.object({
|
||||
recipientDid: z.string().regex(/^did:/, 'Must be a valid DID'),
|
||||
content: z.string().min(1).max(5000),
|
||||
}),
|
||||
});
|
||||
|
||||
// Backward compatibility for older nodes that sent legacy field names.
|
||||
const legacyChatActionSchema = z.object({
|
||||
did: z.string().regex(/^did:/, 'Must be a valid DID'),
|
||||
handle: z.string().min(3).max(30),
|
||||
data: z.object({
|
||||
recipientDid: z.string().regex(/^did:/, 'Must be a valid DID'),
|
||||
content: z.string().min(1).max(5000),
|
||||
}),
|
||||
signature: z.string(),
|
||||
timestamp: z.number().optional(),
|
||||
signature: z.string().min(1),
|
||||
timestamp: z.number(),
|
||||
});
|
||||
|
||||
// Schema for federated envelope
|
||||
const incomingChatActionSchema = z.union([signedChatActionSchema, legacyChatActionSchema]);
|
||||
|
||||
const federatedEnvelopeSchema = z.object({
|
||||
userAction: chatReceiveSchema,
|
||||
userAction: incomingChatActionSchema,
|
||||
fullSenderHandle: z.string().min(3).max(60),
|
||||
sourceDomain: z.string().min(1),
|
||||
ts: z.number(),
|
||||
});
|
||||
|
||||
function normalizeSignedAction(action: z.infer<typeof incomingChatActionSchema>): SignedAction {
|
||||
if ('sig' in action) {
|
||||
return action;
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'chat',
|
||||
data: action.data,
|
||||
did: action.did,
|
||||
handle: action.handle,
|
||||
ts: action.timestamp,
|
||||
nonce: `legacy:${action.did}:${action.timestamp}`,
|
||||
sig: action.signature,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/chat/receive
|
||||
* Endpoint for receiving federated chat messages from other nodes.
|
||||
@@ -67,19 +97,19 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Extract user's signed action and full handle from envelope
|
||||
signedAction = body.userAction;
|
||||
signedAction = normalizeSignedAction(body.userAction);
|
||||
fullSenderHandle = body.fullSenderHandle;
|
||||
console.log(`[Chat Receive] Federated envelope from node: ${sourceDomain}, full handle: ${fullSenderHandle}`);
|
||||
} else {
|
||||
// Legacy format - direct user signed action
|
||||
const actionValidation = chatReceiveSchema.safeParse(body);
|
||||
const actionValidation = incomingChatActionSchema.safeParse(body);
|
||||
if (!actionValidation.success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid action payload', details: actionValidation.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
signedAction = body;
|
||||
signedAction = normalizeSignedAction(actionValidation.data);
|
||||
}
|
||||
|
||||
const { did, handle, data } = signedAction;
|
||||
|
||||
@@ -79,12 +79,9 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
actorDisplayName: user.displayName || user.handle,
|
||||
actorAvatarUrl: user.avatarUrl || undefined,
|
||||
actorNodeDomain: nodeDomain,
|
||||
actorDid: user.did,
|
||||
actorPublicKey: user.publicKey,
|
||||
interactionId: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
userSignature: signedAction.sig,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
@@ -181,12 +178,9 @@ export async function POST(request: Request, context: RouteContext) {
|
||||
actorDisplayName: user.displayName || user.handle,
|
||||
actorAvatarUrl: user.avatarUrl || undefined,
|
||||
actorNodeDomain: nodeDomain,
|
||||
actorDid: user.did,
|
||||
actorPublicKey: user.publicKey,
|
||||
interactionId: crypto.randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
userSignature: signedAction.sig,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function GET(request: NextRequest) {
|
||||
// Local posts may have apId if they've been federated, so we check nodeId instead
|
||||
let whereCondition = and(
|
||||
isNull(posts.replyToId), // Not a reply
|
||||
isNull(posts.swarmReplyToId), // Not a swarm reply
|
||||
eq(posts.isRemoved, false), // Not removed
|
||||
isNull(users.nodeId) // Local user (not from another swarm node)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, posts, users, likes } from '@/db';
|
||||
import { eq, desc, and, inArray, lt } from 'drizzle-orm';
|
||||
import { eq, desc, and, inArray, lt, sql } from 'drizzle-orm';
|
||||
import { fetchSwarmUserProfile, isSwarmNode } from '@/lib/swarm/interactions';
|
||||
import { discoverNode } from '@/lib/swarm/discovery';
|
||||
|
||||
@@ -51,8 +51,8 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
displayName: profile.displayName || profile.handle,
|
||||
avatarUrl: profile.avatarUrl,
|
||||
};
|
||||
|
||||
const posts = profileData.posts.map((post: any) => ({
|
||||
|
||||
const remotePosts = profileData.posts.map((post: any) => ({
|
||||
id: post.id,
|
||||
content: post.content,
|
||||
createdAt: post.createdAt,
|
||||
@@ -70,7 +70,7 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
originalPostId: post.id,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ posts, nextCursor: null });
|
||||
return NextResponse.json({ posts: remotePosts, nextCursor: null });
|
||||
}
|
||||
|
||||
return NextResponse.json({ posts: [] });
|
||||
@@ -108,13 +108,35 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
avatarUrl: profile.avatarUrl,
|
||||
};
|
||||
|
||||
const posts = profileData.posts.map((post: any) => ({
|
||||
const swarmPostIds = profileData.posts.map((post: any) => `swarm:${remote.domain}:${post.id}`);
|
||||
let localReplyCounts = new Map<string, number>();
|
||||
|
||||
if (swarmPostIds.length > 0) {
|
||||
const counts = await db.select({
|
||||
swarmReplyToId: posts.swarmReplyToId,
|
||||
replyCount: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(posts)
|
||||
.where(and(
|
||||
inArray(posts.swarmReplyToId, swarmPostIds),
|
||||
eq(posts.isRemoved, false)
|
||||
))
|
||||
.groupBy(posts.swarmReplyToId);
|
||||
|
||||
localReplyCounts = new Map(
|
||||
counts
|
||||
.filter(row => row.swarmReplyToId)
|
||||
.map(row => [row.swarmReplyToId as string, row.replyCount])
|
||||
);
|
||||
}
|
||||
|
||||
const remotePosts = profileData.posts.map((post: any) => ({
|
||||
id: post.id,
|
||||
content: post.content,
|
||||
createdAt: post.createdAt,
|
||||
likesCount: post.likesCount || 0,
|
||||
repostsCount: post.repostsCount || 0,
|
||||
repliesCount: post.repliesCount || 0,
|
||||
repliesCount: (post.repliesCount || 0) + (localReplyCounts.get(`swarm:${remote.domain}:${post.id}`) || 0),
|
||||
author,
|
||||
media: post.media || [],
|
||||
linkPreviewUrl: post.linkPreviewUrl || null,
|
||||
@@ -126,7 +148,7 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
originalPostId: post.id,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ posts, nextCursor: null });
|
||||
return NextResponse.json({ posts: remotePosts, nextCursor: null });
|
||||
}
|
||||
|
||||
return NextResponse.json({ posts: [] });
|
||||
|
||||
@@ -1,59 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { execSync } from 'child_process';
|
||||
import { getCurrentBuildInfo } from '@/lib/version';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Get the total number of commits
|
||||
const commitCount = execSync('git rev-list --count HEAD', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'] // Ignore stderr
|
||||
}).trim();
|
||||
|
||||
// Get the short hash for reference
|
||||
const commitHash = execSync('git rev-parse --short HEAD', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
const fullHash = execSync('git rev-parse HEAD', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
// Try to get the GitHub repo URL
|
||||
let githubUrl = null;
|
||||
try {
|
||||
const remoteUrl = execSync('git config --get remote.origin.url', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
|
||||
// Convert git URL to GitHub web URL
|
||||
// Handle both SSH (git@github.com:user/repo.git) and HTTPS (https://github.com/user/repo.git)
|
||||
if (remoteUrl.includes('github.com')) {
|
||||
let cleanUrl = remoteUrl
|
||||
.replace('git@github.com:', 'https://github.com/')
|
||||
.replace(/\.git$/, '');
|
||||
|
||||
githubUrl = `${cleanUrl}/commit/${fullHash}`;
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't get the remote URL, that's okay
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
count: parseInt(commitCount, 10),
|
||||
hash: commitHash,
|
||||
fullHash: fullHash,
|
||||
githubUrl: githubUrl
|
||||
});
|
||||
} catch (error) {
|
||||
// If git is not available or not a git repo, return unknown
|
||||
return NextResponse.json({
|
||||
count: null,
|
||||
hash: 'unknown',
|
||||
fullHash: null,
|
||||
githubUrl: null
|
||||
});
|
||||
}
|
||||
return NextResponse.json(getCurrentBuildInfo());
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Post } from '@/lib/types';
|
||||
import { useFormattedHandle } from '@/lib/utils/handle';
|
||||
import { Bot, Network, Server, EyeOff } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
import { signedAPI } from '@/lib/api/signed-fetch';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -81,7 +82,7 @@ interface SwarmPost {
|
||||
}
|
||||
|
||||
export default function ExplorePage() {
|
||||
const { user } = useAuth();
|
||||
const { user, did, handle } = useAuth();
|
||||
const [query, setQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'node' | 'swarm' | 'users' | 'search'>('node');
|
||||
const [nodePosts, setNodePosts] = useState<Post[]>([]);
|
||||
@@ -268,8 +269,13 @@ export default function ExplorePage() {
|
||||
};
|
||||
|
||||
const handleLike = async (postId: string, currentLiked: boolean) => {
|
||||
const method = currentLiked ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/posts/${postId}/like`, { method });
|
||||
if (!did || !handle) {
|
||||
throw new Error('Please log in again.');
|
||||
}
|
||||
|
||||
const res = currentLiked
|
||||
? await signedAPI.unlikePost(postId, did, handle)
|
||||
: await signedAPI.likePost(postId, did, handle);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -278,8 +284,13 @@ export default function ExplorePage() {
|
||||
};
|
||||
|
||||
const handleRepost = async (postId: string, currentReposted: boolean) => {
|
||||
const method = currentReposted ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/posts/${postId}/repost`, { method });
|
||||
if (!did || !handle) {
|
||||
throw new Error('Please log in again.');
|
||||
}
|
||||
|
||||
const res = currentReposted
|
||||
? await signedAPI.unrepostPost(postId, did, handle)
|
||||
: await signedAPI.repostPost(postId, did, handle);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
+17
-4
@@ -7,6 +7,8 @@ import { useFormattedHandle } from '@/lib/utils/handle';
|
||||
import { PostCard } from '@/components/PostCard';
|
||||
import { Post } from '@/lib/types';
|
||||
import { Bot } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
import { signedAPI } from '@/lib/api/signed-fetch';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -132,6 +134,7 @@ export default function SearchPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const initialQuery = searchParams.get('q') || '';
|
||||
const { did, handle } = useAuth();
|
||||
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
@@ -181,8 +184,13 @@ export default function SearchPage() {
|
||||
};
|
||||
|
||||
const handleLike = async (postId: string, currentLiked: boolean) => {
|
||||
const method = currentLiked ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/posts/${postId}/like`, { method });
|
||||
if (!did || !handle) {
|
||||
throw new Error('Please log in again.');
|
||||
}
|
||||
|
||||
const res = currentLiked
|
||||
? await signedAPI.unlikePost(postId, did, handle)
|
||||
: await signedAPI.likePost(postId, did, handle);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -191,8 +199,13 @@ export default function SearchPage() {
|
||||
};
|
||||
|
||||
const handleRepost = async (postId: string, currentReposted: boolean) => {
|
||||
const method = currentReposted ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/posts/${postId}/repost`, { method });
|
||||
if (!did || !handle) {
|
||||
throw new Error('Please log in again.');
|
||||
}
|
||||
|
||||
const res = currentReposted
|
||||
? await signedAPI.unrepostPost(postId, did, handle)
|
||||
: await signedAPI.repostPost(postId, did, handle);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Rocket, MoreHorizontal, Mail } from 'lucide-react';
|
||||
import { useFormattedHandle } from '@/lib/utils/handle';
|
||||
import { Bot } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
import { signedAPI } from '@/lib/api/signed-fetch';
|
||||
|
||||
interface BotOwner {
|
||||
id: string;
|
||||
@@ -81,7 +82,7 @@ export default function ProfilePage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const handle = (params.handle as string)?.replace(/^@/, '') || '';
|
||||
const { isIdentityUnlocked, signUserAction } = useAuth();
|
||||
const { did, handle: currentHandle, isIdentityUnlocked, signUserAction } = useAuth();
|
||||
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const userFullHandle = user ? useFormattedHandle(user.handle) : '';
|
||||
@@ -207,8 +208,13 @@ export default function ProfilePage() {
|
||||
}, [activeTab, postsCursor, repliesCursor, postsLoadingMore, repliesLoadingMore, loadMorePosts, loadMoreReplies]);
|
||||
|
||||
const handleLike = async (postId: string, currentLiked: boolean) => {
|
||||
const method = currentLiked ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/posts/${postId}/like`, { method });
|
||||
if (!did || !currentHandle) {
|
||||
throw new Error('Please log in again.');
|
||||
}
|
||||
|
||||
const res = currentLiked
|
||||
? await signedAPI.unlikePost(postId, did, currentHandle)
|
||||
: await signedAPI.likePost(postId, did, currentHandle);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -217,8 +223,13 @@ export default function ProfilePage() {
|
||||
};
|
||||
|
||||
const handleRepost = async (postId: string, currentReposted: boolean) => {
|
||||
const method = currentReposted ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/posts/${postId}/repost`, { method });
|
||||
if (!did || !currentHandle) {
|
||||
throw new Error('Please log in again.');
|
||||
}
|
||||
|
||||
const res = currentReposted
|
||||
? await signedAPI.unrepostPost(postId, did, currentHandle)
|
||||
: await signedAPI.repostPost(postId, did, currentHandle);
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
@@ -322,8 +333,14 @@ export default function ProfilePage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const method = isFollowing ? 'DELETE' : 'POST';
|
||||
const res = await fetch(`/api/users/${handle}/follow`, { method });
|
||||
if (!did || !currentHandle) {
|
||||
alert('Session expired. Please log in again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = isFollowing
|
||||
? await signedAPI.unfollowUser(handle, did, currentHandle)
|
||||
: await signedAPI.followUser(handle, did, currentHandle);
|
||||
|
||||
if (res.ok && user) {
|
||||
setIsFollowing(!isFollowing);
|
||||
|
||||
Reference in New Issue
Block a user