Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 133 additions & 29 deletions src/components/community/CommunitySettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from "react";
import { Settings, Upload, X, Share2, Trash2, Clock, Users, Shield, FileText } from "lucide-react";
import { Settings, Upload, X, Share2, Trash2, Clock, Users, Shield, FileText, Crown, UserPlus, UserMinus } from "lucide-react";
import {
Dialog,
DialogContent,
Expand All @@ -16,6 +16,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { useCommunities, type Community } from "@/hooks/useCommunities";
import { useCurrentUser } from "@/hooks/useCurrentUser";
import { useCommunityMembers } from "@/hooks/useCommunityMembers";
Expand All @@ -27,10 +28,11 @@ import { useUploadFile } from "@/hooks/useUploadFile";
import { useNostrPublish } from "@/hooks/useNostrPublish";
import { useToast } from "@/hooks/useToast";
import { useManageMembers } from "@/hooks/useManageMembers";
import { useCommunityRoles } from "@/hooks/useCommunityRoles";
import { genUserName } from "@/lib/genUserName";

import { nip19 } from 'nostr-tools';
import { Copy, Check, QrCode, Download, Crown, AlertTriangle } from 'lucide-react';
import { Copy, Check, QrCode, Download, AlertTriangle } from 'lucide-react';
import QRCode from 'qrcode';

interface CommunitySettingsProps {
Expand Down Expand Up @@ -584,14 +586,19 @@ export function CommunitySettings({ communityId, open, onOpenChange }: Community
Member Management
</CardTitle>
<CardDescription>
Manage community members and their roles
Manage community members and assign moderator roles (owners only)
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{members && members.length > 0 ? (
members.slice(0, 10).map((member) => (
<MemberItem key={member.pubkey} member={member} />
<MemberItem
key={member.pubkey}
member={member}
communityId={communityId}
isOwner={isAdmin}
/>
))
) : (
<div className="text-center py-6 text-muted-foreground">
Expand Down Expand Up @@ -1094,12 +1101,22 @@ function JoinRequestItem({
}

// Helper component for displaying member items
function MemberItem({ member }: { member: { pubkey: string; role: 'owner' | 'moderator' | 'member'; isOnline: boolean } }) {
function MemberItem({
member,
communityId,
isOwner
}: {
member: { pubkey: string; role: 'owner' | 'moderator' | 'member'; isOnline: boolean };
communityId: string;
isOwner: boolean;
}) {
const author = useAuthor(member.pubkey);
const displayName = author.data?.metadata?.name || genUserName(member.pubkey);
const avatar = author.data?.metadata?.picture;


const { toast } = useToast();
const { assignModerator, removeModerator, isAssigningModerator, isRemovingModerator } = useCommunityRoles(communityId);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [pendingAction, setPendingAction] = useState<'assign' | 'remove' | null>(null);

const getRoleIcon = () => {
switch (member.role) {
Expand All @@ -1112,32 +1129,119 @@ function MemberItem({ member }: { member: { pubkey: string; role: 'owner' | 'mod
}
};

const handleRoleChange = (action: 'assign' | 'remove') => {
setPendingAction(action);
setShowConfirmDialog(true);
};

const confirmRoleChange = async () => {
if (!pendingAction) return;

try {
if (pendingAction === 'assign') {
await assignModerator({ userPubkey: member.pubkey });
toast({
title: "Success",
description: `${displayName} has been promoted to moderator`,
});
} else {
await removeModerator({ userPubkey: member.pubkey });
toast({
title: "Success",
description: `${displayName} has been removed as moderator`,
});
}
} catch (error) {
toast({
title: "Error",
description: error instanceof Error ? error.message : "Failed to update role",
variant: "destructive",
});
} finally {
setShowConfirmDialog(false);
setPendingAction(null);
}
};

const getActionText = () => {
if (pendingAction === 'assign') {
return `promote ${displayName} to moderator`;
} else if (pendingAction === 'remove') {
return `remove ${displayName} as moderator`;
}
return '';
};

const isProcessing = isAssigningModerator || isRemovingModerator;

return (
<div className="flex items-center justify-between p-3 border rounded-lg bg-card">
<div className="flex items-center gap-3">
<Avatar className="w-8 h-8">
<AvatarImage src={avatar} alt={displayName} />
<AvatarFallback>
{displayName.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{displayName}</p>
{member.isOnline && (
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" title="Online" />
)}
</div>
<div className="flex gap-1">
<Badge variant={member.role === 'owner' ? 'secondary' : 'outline'} className="flex items-center gap-1">
{getRoleIcon()}
{member.role.charAt(0).toUpperCase() + member.role.slice(1)}
</Badge>
<>
<div className="flex items-center justify-between p-3 border rounded-lg bg-card">
<div className="flex items-center gap-3">
<Avatar className="w-8 h-8">
<AvatarImage src={avatar} alt={displayName} />
<AvatarFallback>
{displayName.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{displayName}</p>
{member.isOnline && (
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" title="Online" />
)}
</div>
<div className="flex gap-1">
<Badge variant={member.role === 'owner' ? 'secondary' : 'outline'} className="flex items-center gap-1">
{getRoleIcon()}
{member.role.charAt(0).toUpperCase() + member.role.slice(1)}
</Badge>
</div>
</div>
</div>

{isOwner && member.role !== 'owner' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="outline" disabled={isProcessing}>
Manage
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{member.role === 'member' && (
<DropdownMenuItem onClick={() => handleRoleChange('assign')} className="text-green-600">
<UserPlus className="w-4 h-4 mr-2" />
Promote to Moderator
</DropdownMenuItem>
)}
{member.role === 'moderator' && (
<DropdownMenuItem onClick={() => handleRoleChange('remove')} className="text-red-600">
<UserMinus className="w-4 h-4 mr-2" />
Remove as Moderator
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<Button size="sm" variant="outline">Manage</Button>
</div>

<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Role Change</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to {getActionText()}? This will affect their permissions in the community.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmRoleChange}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

Expand Down
Loading