cc702027b0
Install @capacitor/haptics and add a centralized haptics utility (src/lib/haptics.ts) that uses the native taptic engine on iOS/Android and falls back to navigator.vibrate() on web. Haptics added to: - Switch component (covers 36+ toggle switches app-wide) - PullToRefresh threshold (covers 15+ pages) - MobileBottomNav tab taps - ReactionButton (like/unlike, double-click heart) - RepostMenu (repost/undo repost) - ZapDialog button press + payment success (NWC and WebLN) - FollowButton and ProfilePage follow toggle - ComposeBox (post, voice message, and poll publish success) - NoteMoreMenu (bookmark, pin, mute) - VinesFeedPage reaction and repost buttons - ProfileReactionButton and ExternalReactionButton - NoteCard share button - BlobbiRoomShell swipe navigation Replaces raw navigator.vibrate() calls in GameControls and SendAnimation with the new cross-platform haptics utility, fixing haptic feedback on iOS where the Vibration API is not available.
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { useCallback, useMemo } from 'react';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { useFollowList, useFollowActions } from '@/hooks/useFollowActions';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { impactMedium } from '@/lib/haptics';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface FollowButtonProps {
|
|
/** The pubkey of the user to follow/unfollow. */
|
|
pubkey: string;
|
|
/** Optional class name overrides. */
|
|
className?: string;
|
|
/** Button size variant. Defaults to "sm". */
|
|
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
}
|
|
|
|
/**
|
|
* Reusable follow / unfollow button.
|
|
*
|
|
* Hides itself when the target is the logged-in user or when no user is logged in.
|
|
*/
|
|
export function FollowButton({ pubkey, className, size = 'sm' }: FollowButtonProps) {
|
|
const { user } = useCurrentUser();
|
|
const { data: followData } = useFollowList();
|
|
const { isPending, follow, unfollow } = useFollowActions();
|
|
const { toast } = useToast();
|
|
|
|
const isFollowing = useMemo(() => {
|
|
if (!followData?.pubkeys) return false;
|
|
return followData.pubkeys.includes(pubkey);
|
|
}, [pubkey, followData]);
|
|
|
|
const handleToggleFollow = useCallback(async (e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!user) return;
|
|
|
|
try {
|
|
if (isFollowing) {
|
|
await unfollow(pubkey);
|
|
impactMedium();
|
|
toast({ title: 'Unfollowed' });
|
|
} else {
|
|
await follow(pubkey);
|
|
impactMedium();
|
|
toast({ title: 'Followed' });
|
|
}
|
|
} catch (err) {
|
|
console.error('Follow toggle failed:', err);
|
|
toast({ title: 'Failed to update follow list', variant: 'destructive' });
|
|
}
|
|
}, [user, pubkey, isFollowing, follow, unfollow, toast]);
|
|
|
|
// Don't render for own profile or when logged out
|
|
if (!user || user.pubkey === pubkey) return null;
|
|
|
|
return (
|
|
<Button
|
|
type="button"
|
|
size={size}
|
|
variant={isFollowing ? 'outline' : 'default'}
|
|
className={cn(
|
|
'rounded-full font-bold',
|
|
isFollowing && 'bg-background border border-border text-foreground hover:bg-destructive hover:text-destructive-foreground hover:border-destructive',
|
|
className,
|
|
)}
|
|
onClick={handleToggleFollow}
|
|
disabled={isPending}
|
|
>
|
|
{isPending ? '...' : isFollowing ? 'Unfollow' : 'Follow'}
|
|
</Button>
|
|
);
|
|
}
|