mirror of
https://github.com/smolgrrr/TAO.git
synced 2024-09-20 01:11:25 +00:00
various tings alright
This commit is contained in:
parent
23da7698c8
commit
80a2b3fc29
@ -15,8 +15,10 @@
|
||||
"nostr-tools": "latest",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-pull-to-refresh": "^2.0.1",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-swipe": "^6.0.4",
|
||||
"react-swipeable-views": "^0.14.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-background-sync": "^6.6.0",
|
||||
|
@ -2,7 +2,7 @@ import { PropsWithChildren } from 'react';
|
||||
|
||||
export default function CardContainer({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="card bg-gradient-to-r from-black to-neutral-900 shadow-lg shadow-black">
|
||||
<div className="card bg-gradient-to-r from-black to-neutral-950 shadow-lg shadow-black">
|
||||
<div className="card-body p-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,14 +1,16 @@
|
||||
import CardContainer from './CardContainer';
|
||||
import { ArrowUpTrayIcon, CpuChipIcon } from '@heroicons/react/24/outline';
|
||||
import { useState } from 'react';
|
||||
import { Event, generatePrivateKey, getPublicKey, finishEvent, relayInit} from 'nostr-tools';
|
||||
import { Event, generatePrivateKey, getPublicKey, finishEvent, relayInit } from 'nostr-tools';
|
||||
import { minePow } from '../../utils/mine';
|
||||
import { publish } from '../../utils/relays';
|
||||
import NostrImg from '../../utils/ImgUpload';
|
||||
|
||||
const difficulty = 20
|
||||
|
||||
const NewThreadCard: React.FC = () => {
|
||||
const [comment, setComment] = useState("");
|
||||
const [file, setFile] = useState("");
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@ -18,73 +20,98 @@ const NewThreadCard: React.FC = () => {
|
||||
const event = minePow({
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: comment,
|
||||
content: comment + " " + file,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: getPublicKey(sk),
|
||||
}, difficulty);
|
||||
|
||||
const signedEvent = finishEvent(event, sk);
|
||||
await publish(signedEvent);
|
||||
console.log(signedEvent.id);
|
||||
|
||||
setComment("")
|
||||
setFile("")
|
||||
} catch (error) {
|
||||
setComment(comment + " " + error);
|
||||
}
|
||||
};
|
||||
|
||||
// async function attachFile(file_input: File | null) {
|
||||
// try {
|
||||
// if (file_input) {
|
||||
// const rx = await NostrImg(file_input);
|
||||
// if (rx.url) {
|
||||
// setComment(comment + " " + rx.url);
|
||||
// } else if (rx?.error) {
|
||||
// setComment(comment + " " + rx.error);
|
||||
// }
|
||||
// }
|
||||
// } catch (error: unknown) {
|
||||
// if (error instanceof Error) {
|
||||
// setComment(comment + " " + error?.message);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
async function attachFile(file_input: File | null) {
|
||||
try {
|
||||
if (file_input) {
|
||||
const rx = await NostrImg(file_input);
|
||||
if (rx.url) {
|
||||
setFile(rx.url);
|
||||
} else if (rx?.error) {
|
||||
setFile(rx.error);
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
setFile(error?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContainer>
|
||||
<form
|
||||
name="post"
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
|
||||
<div id="togglePostFormLink" className="text-lg font-semibold">
|
||||
Start a New Thread
|
||||
<form
|
||||
name="post"
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
|
||||
<div id="togglePostFormLink" className="text-lg font-semibold">
|
||||
Start a New Thread
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
name="com"
|
||||
wrap="soft"
|
||||
className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none placeholder-blue-300"
|
||||
placeholder='Shitpost here...'
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{file !== "" && (
|
||||
<div className="file m-0.5">
|
||||
<img
|
||||
src={file}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
name="com"
|
||||
wrap="soft"
|
||||
className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none placeholder-blue-300"
|
||||
placeholder='Shitpost here...'
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<ArrowUpTrayIcon
|
||||
className="h-6 w-6 text-white cursor-pointer"
|
||||
onClick={() => document.getElementById('file_input')?.click()}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
name="file_input"
|
||||
id="file_input"
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => {
|
||||
const file_input = e.target.files?.[0];
|
||||
if (file_input) {
|
||||
attachFile(file_input);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<ArrowUpTrayIcon className="h-6 w-6 text-white" />
|
||||
<input type="file" className="hidden" />
|
||||
</div>
|
||||
<span className="flex items-center"><CpuChipIcon className="h-6 w-6 text-white" />: {difficulty}</span>
|
||||
<button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
</form>
|
||||
<span className="flex items-center"><CpuChipIcon className="h-6 w-6 text-white" />: {difficulty}</span>
|
||||
<button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
</form>
|
||||
</CardContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -94,14 +94,14 @@ const PostCard = ({ event, metadata, replyCount }: { event: Event, metadata: Eve
|
||||
<div className="mr-2 flex flex-col break-words">
|
||||
{comment}
|
||||
</div>
|
||||
{/* {file !== "" && (
|
||||
{file !== "" && (
|
||||
<div className="file">
|
||||
<img
|
||||
src={file}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</CardContainer>
|
||||
|
@ -1,56 +1,55 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {generatePrivateKey, getPublicKey} from 'nostr-tools';
|
||||
// import {powEvent} from './system';
|
||||
// import {publish} from './relays';
|
||||
|
||||
const Settings = () => {
|
||||
// State variables to hold the settings
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const [filterDifficulty, setFilterDifficulty] = useState(localStorage.getItem('filterDifficulty') || 20);
|
||||
const [difficulty, setDifficulty] = useState(localStorage.getItem('difficulty') || 20);
|
||||
|
||||
// Mimic fetching existing settings from an API or local storage
|
||||
useEffect(() => {
|
||||
// Simulate fetching existing settings
|
||||
const existingSettings = {
|
||||
isDarkMode: false, // replace with actual value
|
||||
username: '' // replace with actual value
|
||||
};
|
||||
setIsDarkMode(existingSettings.isDarkMode);
|
||||
setUsername(existingSettings.username);
|
||||
}, []);
|
||||
|
||||
// Function to save changes (simulate API call or local storage update)
|
||||
const saveChanges = () => {
|
||||
// Replace this with an actual API call or local storage update
|
||||
console.log('Dark Mode:', isDarkMode);
|
||||
console.log('Username:', username);
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
localStorage.setItem('filterDifficulty', String(filterDifficulty));
|
||||
localStorage.setItem('difficulty', String(difficulty));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<h1>Settings</h1>
|
||||
|
||||
<div className="setting-item">
|
||||
<label>
|
||||
Dark Mode
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isDarkMode}
|
||||
onChange={(e) => setIsDarkMode(e.target.checked)}
|
||||
/>
|
||||
<div className="settings-page bg-black text-white min-h-screen p-8">
|
||||
<h1 className="text-lg font-semibold mb-4">Settings</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="flex flex-wrap -mx-2 mb-4">
|
||||
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
|
||||
<label className="block mb-2" htmlFor="filterDifficulty">
|
||||
Filter Difficulty:
|
||||
</label>
|
||||
<input
|
||||
id="filterDifficulty"
|
||||
type="number"
|
||||
value={filterDifficulty}
|
||||
onChange={e => setFilterDifficulty(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="setting-item">
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<div className="w-full md:w-1/3 px-2 mb-4 md:mb-0">
|
||||
<label className="block mb-2" htmlFor="difficulty">
|
||||
Post Difficulty:
|
||||
</label>
|
||||
<input
|
||||
id="difficulty"
|
||||
type="number"
|
||||
value={difficulty}
|
||||
onChange={e => setDifficulty(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onClick={saveChanges}>Save Changes</button>
|
||||
</div>
|
||||
<button className="bg-gradient-to-r from-blue-900 to-cyan-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
Save Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -10,6 +10,7 @@ import { ArrowUpTrayIcon, CpuChipIcon } from '@heroicons/react/24/outline';
|
||||
import { generatePrivateKey, getPublicKey, finishEvent, relayInit } from 'nostr-tools';
|
||||
import { minePow } from '../../utils/mine';
|
||||
import { publish } from '../../utils/relays';
|
||||
import ThreadPost from './ThreadPost';
|
||||
|
||||
|
||||
const difficulty = 20
|
||||
@ -20,6 +21,7 @@ const Thread = () => {
|
||||
let decodeResult = nip19.decode(id as string);
|
||||
const [comment, setComment] = useState("");
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [postType, setPostType] = useState("");
|
||||
|
||||
|
||||
|
||||
@ -80,7 +82,7 @@ const Thread = () => {
|
||||
|
||||
if (!uniqEvents[0]) {
|
||||
return (
|
||||
<main className="bg-black text-white min-h-screen">
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
<div className="border border-blue-300 shadow rounded-md p-4 max-w-sm w-full mx-auto">
|
||||
<div className="animate-pulse flex space-x-4">
|
||||
@ -98,7 +100,7 @@ const Thread = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
@ -107,44 +109,24 @@ const Thread = () => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
<PostCard event={uniqEvents[0]} metadata={getMetadataEvent(uniqEvents[0])} replyCount={countReplies(uniqEvents[0])} />
|
||||
<div className="col-span-full flex justify-center space-x-36 ">
|
||||
<DocumentTextIcon className="h-5 w-5 text-gray-200" onClick={() => setShowForm(prevShowForm => !prevShowForm)} />
|
||||
<FolderPlusIcon className="h-5 w-5 text-gray-200" onClick={() => setShowForm(prevShowForm => !prevShowForm)} />
|
||||
<DocumentTextIcon
|
||||
className="h-5 w-5 text-gray-200"
|
||||
onClick={() => {
|
||||
setShowForm(prevShowForm => !prevShowForm);
|
||||
setPostType('r');
|
||||
}}
|
||||
/>
|
||||
|
||||
<FolderPlusIcon
|
||||
className="h-5 w-5 text-gray-200"
|
||||
onClick={() => {
|
||||
setShowForm(prevShowForm => !prevShowForm);
|
||||
setPostType('q');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{showForm && (
|
||||
<form
|
||||
name="post"
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
|
||||
<div id="togglePostFormLink" className="text-lg font-semibold">
|
||||
Reply to note
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
name="com"
|
||||
wrap="soft"
|
||||
className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none placeholder-blue-300"
|
||||
placeholder='Shitpost here...'
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<ArrowUpTrayIcon className="h-6 w-6 text-white" />
|
||||
<input type="file" className="hidden" />
|
||||
</div>
|
||||
<span className="flex items-center"><CpuChipIcon className="h-6 w-6 text-white" />: {difficulty}</span>
|
||||
<button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
</form>)}
|
||||
<ThreadPost state={showForm} type={postType} />
|
||||
</div>
|
||||
<div className="col-span-full h-0.5 bg-neutral-900"></div> {/* This is the white line separator */}
|
||||
{uniqEvents
|
||||
|
133
client/src/components/Thread/ThreadPost.tsx
Normal file
133
client/src/components/Thread/ThreadPost.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useState } from "react";
|
||||
import { ArrowUpTrayIcon, CpuChipIcon } from '@heroicons/react/24/outline';
|
||||
import { generatePrivateKey, getPublicKey, finishEvent, relayInit } from 'nostr-tools';
|
||||
import { minePow } from '../../utils/mine';
|
||||
import { publish } from '../../utils/relays';
|
||||
import NostrImg from '../../utils/ImgUpload';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
|
||||
const difficulty = 5
|
||||
|
||||
const ThreadPost = ({ state, type }: { state: Boolean, type: String }) => {
|
||||
const { id} = useParams();
|
||||
const [comment, setComment] = useState("");
|
||||
const [file, setFile] = useState("");
|
||||
const [tags, setTags] = useState([['']]);
|
||||
|
||||
let decodeResult = nip19.decode(id as string);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
let sk = generatePrivateKey();
|
||||
let id = decodeResult.data as string
|
||||
|
||||
if (type === 'r') {
|
||||
tags.push(["e", id as string])
|
||||
} else if (type === 'q') {
|
||||
tags.push(["q", id as string])
|
||||
setComment(comment + ' nostr:' + id)
|
||||
}
|
||||
|
||||
try {
|
||||
const event = minePow({
|
||||
kind: 1,
|
||||
tags,
|
||||
content: comment + " " + file,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: getPublicKey(sk),
|
||||
}, difficulty);
|
||||
|
||||
const signedEvent = finishEvent(event, sk);
|
||||
await publish(signedEvent);
|
||||
console.log(signedEvent.id);
|
||||
|
||||
} catch (error) {
|
||||
setComment(comment + " " + error);
|
||||
}
|
||||
};
|
||||
|
||||
async function attachFile(file_input: File | null) {
|
||||
try {
|
||||
if (file_input) {
|
||||
const rx = await NostrImg(file_input);
|
||||
if (rx.url) {
|
||||
setFile(rx.url);
|
||||
} else if (rx?.error) {
|
||||
setFile(rx.error);
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
setFile(error?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{state && (
|
||||
<form
|
||||
name="post"
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
className=""
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<input type="hidden" name="MAX_FILE_SIZE" defaultValue={4194304} />
|
||||
<div id="togglePostFormLink" className="text-lg font-semibold">
|
||||
{type === 'r' ? <span>Reply To Post</span> : <span>Quote Post</span>}
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
name="com"
|
||||
wrap="soft"
|
||||
className="w-full p-2 rounded bg-gradient-to-r from-blue-900 to-cyan-500 text-white border-none placeholder-blue-300"
|
||||
placeholder='Shitpost here...'
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{file !== "" && (
|
||||
<div className="file m-0.5">
|
||||
<img
|
||||
src={file}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<ArrowUpTrayIcon
|
||||
className="h-6 w-6 text-white cursor-pointer"
|
||||
onClick={() => document.getElementById('file_input')?.click()}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
name="file_input"
|
||||
id="file_input"
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => {
|
||||
const file_input = e.target.files?.[0];
|
||||
if (file_input) {
|
||||
attachFile(file_input);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="flex items-center"><CpuChipIcon className="h-6 w-6 text-white" />: {difficulty}</span>
|
||||
<button type="submit" className="px-4 py-2 bg-gradient-to-r from-cyan-900 to-blue-500 rounded text-white font-semibold">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<div id="postFormError" className="text-red-500" />
|
||||
</form>)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadPost;
|
@ -9,6 +9,7 @@ body {
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -9,27 +9,27 @@ export interface UploadResult {
|
||||
*/
|
||||
|
||||
export default async function NostrImg(file: File ): Promise<UploadResult> {
|
||||
const buf = await file.arrayBuffer();
|
||||
const fd = new FormData();
|
||||
fd.append("image", file);
|
||||
|
||||
const req = await fetch("https://void.cat/upload", {
|
||||
body: buf,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"V-Content-Type": file.type, // Extracting the mime type
|
||||
"V-Filename": file.name, // Extracting the filename
|
||||
"V-Description": "Upload from https://tao-green.vercel.app/",
|
||||
},
|
||||
});
|
||||
if (req.ok) {
|
||||
let rsp: VoidUploadResponse = await req.json();
|
||||
const fileExtension = file.name.split('.').pop(); // Extracting the file extension
|
||||
const resultUrl = `https://void.cat/d/${rsp.file?.id}.${fileExtension}`;
|
||||
return {url: resultUrl};
|
||||
const req = await fetch("https://nostrimg.com/api/upload", {
|
||||
body: fd,
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
if (req.ok) {
|
||||
const data: UploadResponse = await req.json();
|
||||
if (typeof data?.imageUrl === "string" && data.success) {
|
||||
return {
|
||||
url: new URL(data.imageUrl).toString(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: "Upload failed",
|
||||
};
|
||||
}
|
||||
return {
|
||||
error: "Upload failed",
|
||||
};
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
@ -43,6 +43,22 @@ export type VoidUploadResponse = {
|
||||
errorMessage?: string,
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
fileID?: string;
|
||||
fileName?: string;
|
||||
imageUrl?: string;
|
||||
lightningDestination?: string;
|
||||
lightningPaymentLink?: string;
|
||||
message?: string;
|
||||
route?: string;
|
||||
status: number;
|
||||
success: boolean;
|
||||
url?: string;
|
||||
data?: {
|
||||
url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type VoidFile = {
|
||||
id: string,
|
||||
meta?: VoidFileMeta
|
||||
|
Loading…
Reference in New Issue
Block a user