@@ -100,7 +28,12 @@ export function AuthorDisplay({
variant="secondary"
className="text-xs px-1.5 py-0.5 h-auto bg-green-900/20 border-green-500/30 text-green-400"
>
- {hasENS ? (
+ {hasCallSign ? (
+ <>
+
+ Call Sign
+ >
+ ) : hasENS ? (
<>
ENS
diff --git a/src/components/ui/call-sign-setup-dialog.tsx b/src/components/ui/call-sign-setup-dialog.tsx
new file mode 100644
index 0000000..1be826e
--- /dev/null
+++ b/src/components/ui/call-sign-setup-dialog.tsx
@@ -0,0 +1,215 @@
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { Loader2, User, Hash } from 'lucide-react';
+import { useAuth } from '@/contexts/useAuth';
+import { useForum } from '@/contexts/useForum';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ FormDescription,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { useToast } from '@/hooks/use-toast';
+import { DisplayPreference } from '@/types/identity';
+
+const formSchema = z.object({
+ callSign: z
+ .string()
+ .min(3, 'Call sign must be at least 3 characters')
+ .max(20, 'Call sign must be less than 20 characters')
+ .regex(
+ /^[a-zA-Z0-9_-]+$/,
+ 'Only letters, numbers, hyphens, and underscores allowed'
+ )
+ .refine(val => !/[-_]{2,}/.test(val), 'No consecutive special characters'),
+ displayPreference: z.nativeEnum(DisplayPreference),
+});
+
+interface CallSignSetupDialogProps {
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
+
+export function CallSignSetupDialog({
+ open: externalOpen,
+ onOpenChange,
+}: CallSignSetupDialogProps = {}) {
+ const { currentUser } = useAuth();
+ const { userIdentityService, refreshData } = useForum();
+ const { toast } = useToast();
+ const [internalOpen, setInternalOpen] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const open = externalOpen ?? internalOpen;
+ const setOpen = onOpenChange ?? setInternalOpen;
+
+ const form = useForm
>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ callSign: currentUser?.callSign || '',
+ displayPreference:
+ currentUser?.displayPreference || DisplayPreference.WALLET_ADDRESS,
+ },
+ });
+
+ const onSubmit = async (values: z.infer) => {
+ if (!currentUser || !userIdentityService) {
+ toast({
+ title: 'Error',
+ description: 'User not authenticated or identity service not available',
+ variant: 'destructive',
+ });
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const success = await userIdentityService.updateUserProfile(
+ currentUser.address,
+ values.callSign,
+ values.displayPreference
+ );
+
+ if (success) {
+ // Refresh the forum state to get the updated profile
+ await refreshData();
+
+ toast({
+ title: 'Profile Updated',
+ description:
+ 'Your call sign and display preferences have been updated successfully.',
+ });
+ setOpen(false);
+ form.reset();
+ } else {
+ toast({
+ title: 'Update Failed',
+ description: 'Failed to update profile. Please try again.',
+ variant: 'destructive',
+ });
+ }
+ } catch (error) {
+ console.error('Error updating profile:', error);
+ toast({
+ title: 'Error',
+ description: 'An unexpected error occurred. Please try again.',
+ variant: 'destructive',
+ });
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (!currentUser) return null;
+
+ return (
+
+ );
+}
+
+export default CallSignSetupDialog;
diff --git a/src/components/ui/verification-step.tsx b/src/components/ui/verification-step.tsx
index 97a1978..64fbbb3 100644
--- a/src/components/ui/verification-step.tsx
+++ b/src/components/ui/verification-step.tsx
@@ -198,18 +198,8 @@ export function VerificationStep({
{currentUser && (
- {walletType === 'bitcoin' && currentUser.ordinalDetails && (
-
- Ordinal ID:{' '}
- {typeof currentUser.ordinalDetails === 'object'
- ? currentUser.ordinalDetails.ordinalId
- : 'Verified'}
-
- )}
- {walletType === 'ethereum' &&
- currentUser.ensDetails?.ensName && (
-
ENS Name: {currentUser.ensDetails.ensName}
- )}
+ {walletType === 'bitcoin' &&
Ordinal ID: Verified
}
+ {walletType === 'ethereum' &&
ENS Name: Verified
}
)}
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index e28720a..ac715bf 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -246,6 +246,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
const chainName = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
+ // Note: We can't use useUserDisplay hook here since this is not a React component
+ // This is just for toast messages, so simple truncation is acceptable
const displayName = `${address.slice(0, 6)}...${address.slice(-4)}`;
toast({
diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx
index 66a171c..1404f22 100644
--- a/src/contexts/ForumContext.tsx
+++ b/src/contexts/ForumContext.tsx
@@ -20,8 +20,8 @@ import { getDataFromCache } from '@/lib/forum/transformers';
import { RelevanceCalculator } from '@/lib/forum/RelevanceCalculator';
import { UserVerificationStatus } from '@/types/forum';
import { DelegationManager } from '@/lib/delegation';
-import { getEnsName } from '@wagmi/core';
-import { config } from '@/lib/wallet/config';
+import { UserIdentityService } from '@/lib/services/UserIdentityService';
+import { MessageService } from '@/lib/services/MessageService';
interface ForumContextType {
cells: Cell[];
@@ -29,6 +29,8 @@ interface ForumContextType {
comments: Comment[];
// User verification status for display
userVerificationStatus: UserVerificationStatus;
+ // User identity service for profile management
+ userIdentityService: UserIdentityService | null;
// Granular loading states
isInitialLoading: boolean;
isPostingCell: boolean;
@@ -99,6 +101,14 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
const { currentUser, isAuthenticated } = useAuth();
const delegationManager = useMemo(() => new DelegationManager(), []);
+ const messageService = useMemo(
+ () => new MessageService(delegationManager),
+ [delegationManager]
+ );
+ const userIdentityService = useMemo(
+ () => new UserIdentityService(messageService),
+ [messageService]
+ );
const forumActions = useMemo(
() => new ForumActions(delegationManager),
[delegationManager]
@@ -134,35 +144,60 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
userAddresses.add(vote.author);
});
- // Create user objects for verification status
- Array.from(userAddresses).forEach(address => {
- // Check if this address matches the current user's address
- if (currentUser && currentUser.address === address) {
- // Use the current user's actual verification status
- allUsers.push({
- address,
- walletType: currentUser.walletType,
- verificationStatus: currentUser.verificationStatus,
- displayPreference: currentUser.displayPreference,
- ensDetails: currentUser.ensDetails,
- ordinalDetails: currentUser.ordinalDetails,
- lastChecked: currentUser.lastChecked,
- });
- } else {
- // Create generic user object for other addresses
- allUsers.push({
- address,
- walletType: address.startsWith('0x') ? 'ethereum' : 'bitcoin',
- verificationStatus: EVerificationStatus.UNVERIFIED,
- displayPreference: DisplayPreference.WALLET_ADDRESS,
- });
+ // Create user objects for verification status using UserIdentityService
+ const userIdentityPromises = Array.from(userAddresses).map(
+ async address => {
+ // Check if this address matches the current user's address
+ if (currentUser && currentUser.address === address) {
+ // Use the current user's actual verification status
+ return {
+ address,
+ walletType: currentUser.walletType,
+ verificationStatus: currentUser.verificationStatus,
+ displayPreference: currentUser.displayPreference,
+ ensDetails: currentUser.ensDetails,
+ ordinalDetails: currentUser.ordinalDetails,
+ lastChecked: currentUser.lastChecked,
+ };
+ } else {
+ // Use UserIdentityService to get identity information
+ const identity = await userIdentityService.getUserIdentity(address);
+ if (identity) {
+ return {
+ address,
+ walletType: address.startsWith('0x')
+ ? ('ethereum' as const)
+ : ('bitcoin' as const),
+ verificationStatus: identity.verificationStatus,
+ displayPreference: identity.displayPreference,
+ ensDetails: identity.ensName
+ ? { ensName: identity.ensName }
+ : undefined,
+ ordinalDetails: identity.ordinalDetails,
+ lastChecked: identity.lastUpdated,
+ };
+ } else {
+ // Fallback to generic user object
+ return {
+ address,
+ walletType: address.startsWith('0x')
+ ? ('ethereum' as const)
+ : ('bitcoin' as const),
+ verificationStatus: EVerificationStatus.UNVERIFIED,
+ displayPreference: DisplayPreference.WALLET_ADDRESS,
+ };
+ }
+ }
}
- });
+ );
+
+ const resolvedUsers = await Promise.all(userIdentityPromises);
+ allUsers.push(...resolvedUsers);
const initialStatus =
relevanceCalculator.buildUserVerificationStatus(allUsers);
- // Transform data with relevance calculation (initial pass)
+ // Transform data with relevance calculation
const { cells, posts, comments } = await getDataFromCache(
verifyFn,
initialStatus
@@ -172,50 +207,7 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
setPosts(posts);
setComments(comments);
setUserVerificationStatus(initialStatus);
-
- // Enrich: resolve ENS for ethereum addresses asynchronously and update
- (async () => {
- const targets = allUsers.filter(
- u => u.walletType === 'ethereum' && !u.ensDetails
- );
- if (targets.length === 0) return;
- const lookups = await Promise.all(
- targets.map(async u => {
- try {
- const name = await getEnsName(config, {
- address: u.address as `0x${string}`,
- });
- return { address: u.address, ensName: name || undefined };
- } catch {
- return { address: u.address, ensName: undefined };
- }
- })
- );
- const ensByAddress = new Map