diff --git a/public/favicon.png b/public/favicon.png index 8dc2c05..d0d2087 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/logotext.png b/public/logotext.png new file mode 100644 index 0000000..494d5b1 Binary files /dev/null and b/public/logotext.png differ diff --git a/src/app/[handle]/page.tsx b/src/app/[handle]/page.tsx index 845d2bc..a043e62 100644 --- a/src/app/[handle]/page.tsx +++ b/src/app/[handle]/page.tsx @@ -81,13 +81,15 @@ export default function ProfilePage() { const [user, setUser] = useState(null); const [posts, setPosts] = useState([]); + const [likedPosts, setLikedPosts] = useState([]); const [currentUser, setCurrentUser] = useState<{ id: string; handle: string } | null>(null); const [isFollowing, setIsFollowing] = useState(false); const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState<'posts' | 'followers' | 'following'>('posts'); + const [activeTab, setActiveTab] = useState<'posts' | 'likes' | 'followers' | 'following'>('posts'); const [followers, setFollowers] = useState([]); const [following, setFollowing] = useState([]); const [postsLoading, setPostsLoading] = useState(true); + const [likesLoading, setLikesLoading] = useState(false); const [followersLoading, setFollowersLoading] = useState(false); const [followingLoading, setFollowingLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -106,6 +108,7 @@ export default function ProfilePage() { setSaveError(null); setFollowers([]); setFollowing([]); + setLikedPosts([]); // Get current user fetch('/api/auth/me') .then(res => res.json()) @@ -197,6 +200,15 @@ export default function ProfilePage() { .catch(() => setFollowing([])) .finally(() => setFollowingLoading(false)); } + + if (activeTab === 'likes') { + setLikesLoading(true); + fetch(`/api/users/${handle}/likes`) + .then(res => res.json()) + .then(data => setLikedPosts(data.posts || [])) + .catch(() => setLikedPosts([])) + .finally(() => setLikesLoading(false)); + } }, [activeTab, handle]); const handleFollow = async () => { @@ -617,7 +629,10 @@ export default function ProfilePage() { {/* Tabs */}
- {(['posts', 'followers', 'following'] as const).map(tab => ( + {(user?.isBot + ? ['posts', 'followers', 'following'] as const + : ['posts', 'likes', 'followers', 'following'] as const + ).map(tab => ( +
- {trendingPosts.map((post) => ( + {fediversePosts.map((post) => ( ))}
diff --git a/src/app/guide/page.tsx b/src/app/guide/page.tsx deleted file mode 100644 index a9ae33e..0000000 --- a/src/app/guide/page.tsx +++ /dev/null @@ -1,189 +0,0 @@ -'use client'; - -import Link from 'next/link'; -import { ArrowLeftIcon } from '@/components/Icons'; -import { Rocket } from 'lucide-react'; - -export default function GuidePage() { - return ( -
-
- - - -
-

Synapsis Guide

-

- Understanding the federated social network -

-
-
- - {/* Table of Contents */} - - - {/* Section 1 */} -
-

- 1. What is the Fediverse? -

-

- The Fediverse (federated universe) is a network of interconnected social platforms that can talk to each other. Unlike centralized platforms like Twitter or Facebook, the Fediverse is made up of thousands of independent servers (called "instances" or "nodes") that share a common protocol. -

-

- Think of it like email: you can send an email from Gmail to Outlook because they speak the same language. Similarly, you can follow someone on a Mastodon server from your Synapsis account because they both speak ActivityPub. -

-
-
Key Terms
-
    -
  • Node / Instance: An independent server running social software (like this Synapsis node).
  • -
  • ActivityPub: The protocol that allows different servers to communicate.
  • -
  • Handle: Your username, including your server (e.g., @alice@mynode.com).
  • -
-
-
- - {/* Section 2 */} -
-

- 2. How Synapsis is Different -

-

- While Synapsis uses ActivityPub like Mastodon, it introduces a key difference: Decentralized Identifiers (DIDs). -

- -

- The Problem with Traditional Federation -

-

- On Mastodon, your identity is @username@server.com. If your server shuts down, or you want to move to a different one, you effectively lose your identity. Your followers have to re-follow your new account, and your post history doesn't come with you. -

- -

- The Synapsis Approach: DIDs -

-

- When you create an account on Synapsis, you're assigned a unique DID (Decentralized Identifier) — a cryptographic ID like did:key:z6Mk.... This DID is your true identity. Your human-readable handle (@alice) is simply a friendly pointer to that DID. -

-
-
What this means for you
-
    -
  • You own your identity. Your DID is generated from a cryptographic key pair that only you control.
  • -
  • Authenticity. Every post you make is cryptographically signed, proving it came from you.
  • -
-
-
- - {/* Section 3 */} -
-

- 3. Following Users on Other Servers -

- -

- Following Someone on Another Synapsis Node -

-

- To follow a user on a different Synapsis node, use the Explore / Search feature. Enter their full handle in the format: -

-
- @username@other-node.com -
-

- Synapsis uses WebFinger to discover the user's profile on the remote server, then sends a Follow request via ActivityPub. -

- -

- Following Someone on Mastodon (or other Fediverse platforms) -

-

- The process is identical! Enter their full handle: -

-
- @user@mastodon.social -
-

- Because ActivityPub is an open standard, Synapsis can communicate with Mastodon, Pleroma, Misskey, and other compatible platforms. -

-
- - {/* Section 4 */} -
-

- 4. How Others Follow You -

-

- To let someone on another server follow you, share your full handle. It includes your username and this node's domain: -

-
- @your-username@{typeof window !== 'undefined' ? window.location.host : 'this-node.com'} -
-

- When they search for your handle on their platform (Mastodon, another Synapsis node, etc.), their server will use WebFinger to find your profile and send a Follow request. -

-

- You can find your full handle on your Profile page. -

-
- - {/* Section 5 */} -
-

- 5. Portable Identity & Account Migration -

-

- Synapsis offers true account portability powered by DIDs. You can migrate your entire account — identity, posts, and media — to another Synapsis node. -

- -

- What Gets Migrated -

-
    -
  • Your DID & Keys: Your cryptographic identity stays exactly the same
  • -
  • Posts & Media: Your entire post history and uploaded media
  • -
  • Following List: Who you follow
  • -
  • Synapsis Followers: Automatically migrated (they recognize your DID)
  • -
  • Fediverse Followers: Notified of your move (they can re-follow)
  • -
- -

- How to Migrate -

-
    -
  1. Go to Settings → Account Migration
  2. -
  3. Click Export Account and enter your password
  4. -
  5. Download your export file (keep it safe!)
  6. -
  7. On the new node, go to Settings → Account Migration → Import
  8. -
  9. Upload your export file and choose a handle
  10. -
- -
-
- The Synapsis Advantage -
-

- Unlike Mastodon where followers must manually re-follow you, Synapsis followers are automatically migrated because they follow your DID, not just a server-specific account. This is true account portability. -

-
-
- -
-

Have questions? Reach out to the node administrator or contribute to Synapsis on GitHub.

-
-
- ); -} diff --git a/src/app/settings/bots/page.tsx b/src/app/settings/bots/page.tsx index 7b6d338..5256968 100644 --- a/src/app/settings/bots/page.tsx +++ b/src/app/settings/bots/page.tsx @@ -19,6 +19,7 @@ interface BotData { name: string; handle: string; bio: string; + avatarUrl: string | null; isActive: boolean; isSuspended: boolean; autonomousMode: boolean; @@ -108,42 +109,65 @@ export default function BotsPage() { onClick={() => router.push(`/settings/bots/${bot.id}`)} >
-
-
-

{bot.name}

- {bot.autonomousMode && ( - + e.stopPropagation()} + className="avatar" + style={{ + width: '48px', + height: '48px', + flexShrink: 0, + fontSize: '18px', + }} + > + {bot.avatarUrl ? ( + {bot.name} + ) : ( + bot.name.charAt(0).toUpperCase() + )} + +
+
+

{bot.name}

+ {bot.autonomousMode && ( + + + Auto + + )} +
+ e.stopPropagation()} + style={{ fontSize: '13px', color: 'var(--foreground-tertiary)' }} + > + @{bot.handle} + + {bot.bio && ( +

- - Auto - + {bot.bio} +

)}
-

- @{bot.handle} -

- {bot.bio && ( -

- {bot.bio} -

- )}
{bot.isSuspended ? ( diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 57c42d0..e22f957 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -48,15 +48,27 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, isDetail setReposted(post.isReposted || false); }, [post.isLiked, post.isReposted, post.id]); - const formatTime = (dateStr: string) => { + const formatTime = (dateStr: string | Date) => { const date = new Date(dateStr); + + if (isNaN(date.getTime())) { + return ''; + } + const now = new Date(); const diff = now.getTime() - date.getTime(); - const minutes = Math.floor(diff / 60000); + + // If post is in the future (minor clock skew), show "now" + if (diff < 0) { + return 'now'; + } + + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); - if (minutes < 1) return 'now'; + if (seconds < 60) return 'now'; if (minutes < 60) return `${minutes}m`; if (hours < 24) return `${hours}h`; if (days < 7) return `${days}d`; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index fa8d97d..eeb50a2 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,9 +1,10 @@ 'use client'; import Link from 'next/link'; +import Image from 'next/image'; import { usePathname } from 'next/navigation'; import { useAuth } from '@/lib/contexts/AuthContext'; -import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SynapsisLogo, BookOpenIcon, SettingsIcon, BotIcon } from './Icons'; +import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SettingsIcon, BotIcon } from './Icons'; import { formatFullHandle } from '@/lib/utils/handle'; export function Sidebar() { @@ -16,8 +17,7 @@ export function Sidebar() { return (