refactor: update file retrieval logic and enhance media handling in SharePage component
This commit is contained in:
		
							parent
							
								
									568a6556af
								
							
						
					
					
						commit
						caedd0ae88
					
				
							
								
								
									
										68
									
								
								src/app/_components/SharePage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/app/_components/SharePage.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
|  | import { useEffect, useState } from "react"; | ||||||
|  | 
 | ||||||
|  | interface SharePageProps { | ||||||
|  |   fileId: string; | ||||||
|  |   fileType: string; // Pass the file type as a prop
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function SharePage({ fileId, fileType }: SharePageProps) { | ||||||
|  |   const [mediaSrc, setMediaSrc] = useState<string | null>(null); | ||||||
|  |   const [error, setError] = useState<string | null>(null); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!fileId) { | ||||||
|  |       setError("File ID is required."); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let objectUrl: string | null = null; | ||||||
|  | 
 | ||||||
|  |     const fetchMedia = async () => { | ||||||
|  |       try { | ||||||
|  |         const response = await fetch(`/api/serv?id=${encodeURIComponent(fileId)}`); | ||||||
|  |         if (!response.ok) { | ||||||
|  |           throw new Error("Failed to fetch media"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const blob = await response.blob(); | ||||||
|  |         objectUrl = URL.createObjectURL(blob); | ||||||
|  |         setMediaSrc(objectUrl); | ||||||
|  |       } catch (err) { | ||||||
|  |         console.error(err); | ||||||
|  |         setError("Failed to load media."); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     fetchMedia(); | ||||||
|  | 
 | ||||||
|  |     return () => { | ||||||
|  |       if (objectUrl) { | ||||||
|  |         URL.revokeObjectURL(objectUrl); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |   }, [fileId]); | ||||||
|  | 
 | ||||||
|  |   if (error) { | ||||||
|  |     return <div className="text-red-500">{error}</div>; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!mediaSrc) { | ||||||
|  |     return <div>Loading...</div>; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (fileType.startsWith("video")) { | ||||||
|  |     return ( | ||||||
|  |       <video | ||||||
|  |         controls | ||||||
|  |         className="max-w-full max-h-96 rounded-lg shadow-md" | ||||||
|  |         src={mediaSrc} | ||||||
|  |       > | ||||||
|  |         Your browser does not support the video tag. | ||||||
|  |       </video> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return <img src={mediaSrc} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />; | ||||||
|  | } | ||||||
| @ -14,7 +14,7 @@ export async function GET(req: Request) { | |||||||
|   try { |   try { | ||||||
|     // Fetch file metadata from the database
 |     // Fetch file metadata from the database
 | ||||||
|     const file = await db.file.findFirst({ |     const file = await db.file.findFirst({ | ||||||
|       where: { name: fileId }, |       where: { id: fileId }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (!file) { |     if (!file) { | ||||||
| @ -22,15 +22,35 @@ export async function GET(req: Request) { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Construct the file path
 |     // Construct the file path
 | ||||||
|     const filePath = path.join(process.cwd(), "uploads", file.name); |     const filePath = path.join(process.cwd(), "uploads", file.id); | ||||||
| 
 | 
 | ||||||
|     // Read the file from the filesystem
 |     // Read the file from the filesystem
 | ||||||
|     const fileBuffer = await fs.readFile(filePath); |     const fileBuffer = await fs.readFile(filePath); | ||||||
| 
 | 
 | ||||||
|  |     const mimeType = file.extension === ".mp4" | ||||||
|  |       ? "video/mp4" | ||||||
|  |       : file.extension === ".webm" | ||||||
|  |       ? "video/webm" | ||||||
|  |       : file.extension === ".ogg" | ||||||
|  |       ? "video/ogg" | ||||||
|  |       : file.extension === ".jpg" || file.extension === ".jpeg" | ||||||
|  |       ? "image/jpeg" | ||||||
|  |       : file.extension === ".png" | ||||||
|  |       ? "image/png" | ||||||
|  |       : file.extension === ".gif" | ||||||
|  |       ? "image/gif" | ||||||
|  |       : file.extension === ".svg" | ||||||
|  |       ? "image/svg+xml" | ||||||
|  |       : file.extension === ".mp3" | ||||||
|  |       ? "audio/mpeg" | ||||||
|  |       : file.extension === ".wav" | ||||||
|  |       ? "audio/wav" | ||||||
|  |       : "application/octet-stream"; | ||||||
|  | 
 | ||||||
|     // Return the file as a binary response
 |     // Return the file as a binary response
 | ||||||
|     return new Response(fileBuffer, { |     return new Response(fileBuffer, { | ||||||
|       headers: { |       headers: { | ||||||
|         "Content-Type": file.extension.startsWith(".png") ? "image/png" : "application/octet-stream", |         "Content-Type": mimeType, | ||||||
|         "Content-Disposition": `inline; filename="${file.name}"`, |         "Content-Disposition": `inline; filename="${file.name}"`, | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -24,7 +24,8 @@ export async function GET(req: Request) { | |||||||
|     return NextResponse.json({ |     return NextResponse.json({ | ||||||
|       name: file.name, |       name: file.name, | ||||||
|       size: file.size, |       size: file.size, | ||||||
|       owner: file.uploadedBy?.name ?? "Unknown", // Use nullish coalescing
 |       owner: file.uploadedBy?.name ?? null, // Use nullish coalescing
 | ||||||
|  |       owneravatar: file.uploadedBy?.image ?? null, | ||||||
|       uploadDate: file.uploadDate, |       uploadDate: file.uploadDate, | ||||||
|       id: file.id, |       id: file.id, | ||||||
|       isOwner: session?.user?.id === file.uploadedById, |       isOwner: session?.user?.id === file.uploadedById, | ||||||
|  | |||||||
| @ -4,15 +4,13 @@ import { Suspense } from "react"; | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { useSearchParams, useRouter } from "next/navigation"; | import { useSearchParams, useRouter } from "next/navigation"; | ||||||
| import toast, { Toaster } from "react-hot-toast"; | import toast, { Toaster } from "react-hot-toast"; | ||||||
| import Head from "next/head"; | import { SharePage } from "~/app/_components/SharePage"; | ||||||
| import { number } from "zod"; |  | ||||||
| 
 |  | ||||||
| // import { SharePage } from "~/components/SharePage";
 |  | ||||||
| 
 | 
 | ||||||
| interface FileDetails { | interface FileDetails { | ||||||
|   name: string; |   name: string; | ||||||
|   size: number; |   size: number; | ||||||
|   owner: string; |   owner: string; | ||||||
|  |   owneravatar: string | null; | ||||||
|   uploadDate: string; |   uploadDate: string; | ||||||
|   id: string; |   id: string; | ||||||
|   isOwner: boolean; |   isOwner: boolean; | ||||||
| @ -20,13 +18,35 @@ interface FileDetails { | |||||||
|   url: string; |   url: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function UploadsPage() { | 
 | ||||||
|  | function Details() { | ||||||
|   const searchParams = useSearchParams(); |   const searchParams = useSearchParams(); | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|   const fileId = searchParams.get("id"); |   const fileId = searchParams.get("id"); | ||||||
|   const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); |   const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); | ||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
|   // const mediasrc = SharePage() as string; // Replace with a valid string URL or logic to generate the URL
 | 
 | ||||||
|  |   // Determine the file type based on the file extension
 | ||||||
|  |   const fileType = fileDetails?.type === ".mp4" | ||||||
|  |     ? "video/mp4" | ||||||
|  |     : fileDetails?.type === ".webm" | ||||||
|  |     ? "video/webm" | ||||||
|  |     : fileDetails?.type === ".ogg" | ||||||
|  |     ? "video/ogg" | ||||||
|  |     : fileDetails?.type === ".jpg" || fileDetails?.type === ".jpeg" | ||||||
|  |     ? "image/jpeg" | ||||||
|  |     : fileDetails?.type === ".png" | ||||||
|  |     ? "image/png" | ||||||
|  |     : fileDetails?.type === ".gif" | ||||||
|  |     ? "image/gif" | ||||||
|  |     : fileDetails?.type === ".svg" | ||||||
|  |     ? "image/svg+xml" | ||||||
|  |     : fileDetails?.type === ".mp3" | ||||||
|  |     ? "audio/mpeg" | ||||||
|  |     : fileDetails?.type === ".wav" | ||||||
|  |     ? "audio/wav" | ||||||
|  |     // if fileType is not one of the above, set it to unknown
 | ||||||
|  |     : "unknown"; | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (!fileId) { |     if (!fileId) { | ||||||
| @ -41,7 +61,7 @@ function UploadsPage() { | |||||||
|           throw new Error("Failed to fetch file details"); |           throw new Error("Failed to fetch file details"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const data: FileDetails = await response.json(); // Explicitly type the response
 |         const data: FileDetails = await response.json(); | ||||||
|         setFileDetails(data); |         setFileDetails(data); | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         console.error(err); |         console.error(err); | ||||||
| @ -49,110 +69,9 @@ function UploadsPage() { | |||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     void fetchFileDetails(); // Use `void` to mark the promise as intentionally ignored
 |     fetchFileDetails(); | ||||||
|   }, [fileId]); |   }, [fileId]); | ||||||
| 
 | 
 | ||||||
|   // set page og meta tags
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (fileDetails) { |  | ||||||
|       const ogTitle = `File Details: ${fileDetails.name}`; |  | ||||||
|       // proper size conversion
 |  | ||||||
|       const sizeInKB = fileDetails.size / 1024; |  | ||||||
|       const sizeInMB = sizeInKB / 1024; |  | ||||||
|       const sizeInGB = sizeInMB / 1024; |  | ||||||
|       let sizeDescription: string = `${sizeInKB.toFixed(2)} KB`; |  | ||||||
|       if (sizeInMB >= 1) { |  | ||||||
|         sizeDescription = `${sizeInMB.toFixed(2)} MB`; |  | ||||||
|       } else if (sizeInGB >= 1) { |  | ||||||
|         sizeDescription = `${sizeInGB.toFixed(2)} GB`; |  | ||||||
|       } |  | ||||||
|       const ogDescription = `File Name: ${fileDetails.name}, Size: ${sizeDescription}, Owner: ${fileDetails.owner}, Upload Date: ${new Date(fileDetails.uploadDate).toLocaleString()}`; |  | ||||||
| 
 |  | ||||||
|       // document.title = ogTitle;
 |  | ||||||
|       // if meta og tags are not present, create them
 |  | ||||||
|       if (!document.querySelector('meta[name="description"]')) { |  | ||||||
|         const metaDescription = document.createElement("meta"); |  | ||||||
|         metaDescription.name = "description"; |  | ||||||
|         document.head.appendChild(metaDescription); |  | ||||||
|       } |  | ||||||
|       if (!document.querySelector('meta[property="og:title"]')) { |  | ||||||
|         const metaOgTitle = document.createElement("meta"); |  | ||||||
|         metaOgTitle.setAttribute("property", "og:title"); |  | ||||||
|         document.head.appendChild(metaOgTitle); |  | ||||||
|       } |  | ||||||
|       if (!document.querySelector('meta[property="og:description"]')) { |  | ||||||
|         const metaOgDescription = document.createElement("meta"); |  | ||||||
|         metaOgDescription.setAttribute("property", "og:description"); |  | ||||||
|         document.head.appendChild(metaOgDescription); |  | ||||||
|       } |  | ||||||
|       document.querySelector('meta[name="description"]')?.setAttribute("content", ogDescription); |  | ||||||
|       document.querySelector('meta[property="og:title"]')?.setAttribute("content", ogTitle); |  | ||||||
|       document.querySelector('meta[property="og:description"]')?.setAttribute("content", ogDescription); |  | ||||||
|        |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
|   }, [fileDetails]); |  | ||||||
| 
 |  | ||||||
|   const handleDownload = async () => { |  | ||||||
|     try { |  | ||||||
|       if (!fileDetails) { |  | ||||||
|         toast.error("File details not available."); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|           const response = await fetch(`/api/files/download?fileId=${encodeURIComponent(fileDetails.id)}&fileName=${encodeURIComponent(fileDetails.name)}`); |  | ||||||
|           if (!response.ok) { |  | ||||||
|             throw new Error("Failed to download file"); |  | ||||||
|           } |  | ||||||
|           // Download the file with the correct filename
 |  | ||||||
|           const blob = await response.blob(); |  | ||||||
|           const url = window.URL.createObjectURL(blob); |  | ||||||
|           const a = document.createElement("a"); |  | ||||||
|           a.href = url; |  | ||||||
|           a.download = fileDetails.name; |  | ||||||
|           document.body.appendChild(a); |  | ||||||
|           a.click(); |  | ||||||
|           a.remove(); |  | ||||||
|           window.URL.revokeObjectURL(url); |  | ||||||
|      |  | ||||||
|           toast.success(`File "${fileDetails.name}" downloaded successfully!`); |  | ||||||
|         } catch (err) { |  | ||||||
|           console.error(err); |  | ||||||
|           toast.error("Failed to download file."); |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|   const handleShare = () => { |  | ||||||
|     if (fileDetails) { |  | ||||||
|       const shareableLink = `${window.location.origin}/share?id=${fileDetails.id}`; |  | ||||||
|       navigator.clipboard |  | ||||||
|         .writeText(shareableLink) |  | ||||||
|         .then(() => toast.success("Shareable link copied to clipboard!")) |  | ||||||
|         .catch(() => toast.error("Failed to copy link.")); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   const handleRemove = async () => { |  | ||||||
|     try { |  | ||||||
|       const response = await fetch(`/api/remove`, { |  | ||||||
|         method: "DELETE", |  | ||||||
|         headers: { |  | ||||||
|           "Content-Type": "application/json", |  | ||||||
|         }, |  | ||||||
|         body: JSON.stringify({ id: fileDetails?.id }), // Use optional chaining
 |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       if (!response.ok) { |  | ||||||
|         throw new Error("Failed to remove file"); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       toast.success("File removed successfully!"); |  | ||||||
|       router.push("/"); |  | ||||||
|     } catch (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       toast.error("Failed to remove file."); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   if (error) { |   if (error) { | ||||||
|     return <div className="text-red-500">{error}</div>; |     return <div className="text-red-500">{error}</div>; | ||||||
|   } |   } | ||||||
| @ -194,58 +113,36 @@ function UploadsPage() { | |||||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details |           <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||||
|         </h1> |         </h1> | ||||||
|         <div className="mt-6"> |         <div className="mt-6"> | ||||||
|           {/* {(fileDetails.type.startsWith(".png") || |           {// if fileType is not ubknown, show the media player
 | ||||||
|             fileDetails.type.startsWith(".jpg") || |             fileType && !fileType.startsWith("unknown") ? ( | ||||||
|             fileDetails.type.startsWith(".jpeg") || |               <SharePage fileId={fileDetails.id} fileType={fileType} /> | ||||||
|             fileDetails.type.startsWith(".gif")) && (mediasrc && <img src={mediasrc} alt="Media preview" />)} |             ) : null} | ||||||
|           {(fileDetails.type.startsWith(".mp4") || |            | ||||||
|             fileDetails.type.startsWith(".webm") || |  | ||||||
|             fileDetails.type.startsWith(".ogg")) && |  | ||||||
|               (mediasrc && |  | ||||||
|               <video controls className="max-w-full max-h-96 rounded-lg shadow-md"> |  | ||||||
|                 <source src={mediasrc} type="video" /> |  | ||||||
|                 Your browser does not support the video tag. |  | ||||||
|               </video> |  | ||||||
|               )} */} |  | ||||||
|         </div> |         </div> | ||||||
|         <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> |         <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> | ||||||
|           <p> |           <p> | ||||||
|             <strong>Name:</strong> {fileDetails.name} |             <strong>Name:</strong> {fileDetails.name} | ||||||
|           </p> |           </p> | ||||||
|           <p> |           <p> | ||||||
|             <strong>Size:</strong> {(fileDetails.size / 1024).toFixed(2)} KB |             <strong>Size:</strong> { // format size
 | ||||||
|  |               fileDetails.size > 1024 * 1024 * 1024 * 1024 | ||||||
|  |                 ? (fileDetails.size / (1024 * 1024 * 1024 * 1024)).toFixed(2) + " TB" | ||||||
|  |                 : fileDetails.size > 1024 * 1024 * 1024 | ||||||
|  |                 ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||||
|  |                 : fileDetails.size > 1024 * 1024 | ||||||
|  |                 ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||||
|  |                 : fileDetails.size > 1024 | ||||||
|  |                 ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||||
|  |                 : fileDetails.size + " Bytes" | ||||||
|  |             } | ||||||
|           </p> |           </p> | ||||||
|           <p> |           <p> | ||||||
|             <strong>Owner:</strong> {fileDetails.owner} |             <strong>Owner:</strong> <img className="rounded-md inline size-5" src={ fileDetails.owneravatar ? ( fileDetails.owneravatar) : ""} alt="owner image" /> {fileDetails.owner} | ||||||
|           </p> |           </p> | ||||||
|           <p> |           <p> | ||||||
|             <strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()} |             <strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()} | ||||||
|           </p> |           </p> | ||||||
|         </div> |         </div> | ||||||
|         <div className="flex gap-4 mt-6"> |  | ||||||
|           <button |  | ||||||
|             onClick={handleDownload} |  | ||||||
|             className="rounded-full bg-blue-500 px-10 py-3 font-semibold no-underline transition hover:bg-blue-600" |  | ||||||
|           > |  | ||||||
|             Download |  | ||||||
|           </button> |  | ||||||
|           {fileDetails.isOwner && ( |  | ||||||
|             <> |  | ||||||
|               <button |  | ||||||
|                 onClick={handleShare} |  | ||||||
|                 className="rounded-full bg-green-500 px-10 py-3 font-semibold no-underline transition hover:bg-green-600" |  | ||||||
|               > |  | ||||||
|                 Share |  | ||||||
|               </button> |  | ||||||
|               <button |  | ||||||
|                 onClick={handleRemove} |  | ||||||
|                 className="rounded-full bg-red-500 px-10 py-3 font-semibold no-underline transition hover:bg-red-600" |  | ||||||
|               > |  | ||||||
|                 Remove |  | ||||||
|               </button> |  | ||||||
|             </> |  | ||||||
|           )} |  | ||||||
|         </div> |  | ||||||
|       </div> |       </div> | ||||||
|     </main> |     </main> | ||||||
|   ); |   ); | ||||||
| @ -254,7 +151,7 @@ function UploadsPage() { | |||||||
| export default function Page() { | export default function Page() { | ||||||
|   return ( |   return ( | ||||||
|     <Suspense fallback={<div>Loading...</div>}> |     <Suspense fallback={<div>Loading...</div>}> | ||||||
|       <UploadsPage /> |       <Details /> | ||||||
|     </Suspense> |     </Suspense> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,52 +0,0 @@ | |||||||
| "use client"; |  | ||||||
| 
 |  | ||||||
| import { useEffect, useState } from "react"; |  | ||||||
| import { useSearchParams } from "next/navigation"; |  | ||||||
| 
 |  | ||||||
| export function SharePage() { |  | ||||||
|   const searchParams = useSearchParams(); |  | ||||||
|   const fileId = searchParams.get("id"); |  | ||||||
|   const [imageSrc, setImageSrc] = useState<string | null>(null); |  | ||||||
|   const [error, setError] = useState<string | null>(null); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     const fetchImage = async () => { |  | ||||||
|       try { |  | ||||||
|         if (!fileId) { |  | ||||||
|           throw new Error("File name is required."); |  | ||||||
|         } |  | ||||||
|         const response = await fetch(`/api/serv?id=${encodeURIComponent(fileId)}`); |  | ||||||
|         if (!response.ok) { |  | ||||||
|           throw new Error("Failed to fetch image"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const blob = await response.blob(); |  | ||||||
|         const objectUrl = URL.createObjectURL(blob); |  | ||||||
|         setImageSrc(objectUrl); |  | ||||||
|       } catch (err) { |  | ||||||
|         console.error(err); |  | ||||||
|         setError("Failed to load image."); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     void fetchImage(); |  | ||||||
| 
 |  | ||||||
|     return () => { |  | ||||||
|       if (imageSrc) { |  | ||||||
|         URL.revokeObjectURL(imageSrc); |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|   }, [fileId, imageSrc]); |  | ||||||
| 
 |  | ||||||
|   if (error) { |  | ||||||
|     return <div className="text-red-500">{error}</div>; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (!imageSrc) { |  | ||||||
|     return <div>Loading...</div>; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|       imageSrc |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user