feat(pdf-marking): binds text to marks and saves with signatures
This commit is contained in:
parent
296b135c06
commit
4a932ffe03
@ -66,9 +66,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&.nonEditable {
|
&.nonEditable {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resizeHandle {
|
.resizeHandle {
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { PdfFile } from '../../types/drawing.ts'
|
import { PdfFile } from '../../types/drawing.ts'
|
||||||
import { Mark, MarkConfigDetails } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark, MarkConfigDetails } from '../../types/mark.ts'
|
||||||
import PdfPageItem from './PdfPageItem.tsx';
|
import PdfPageItem from './PdfPageItem.tsx';
|
||||||
|
|
||||||
interface PdfItemProps {
|
interface PdfItemProps {
|
||||||
pdfFile: PdfFile
|
pdfFile: PdfFile
|
||||||
marks: Mark[]
|
marks: Mark[]
|
||||||
handleMarkClick: (id: number) => void
|
handleMarkClick: (id: number) => void
|
||||||
|
currentMarkValue: string
|
||||||
|
currentUserMark: CurrentUserMark | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => {
|
const PdfItem = ({ pdfFile, marks, handleMarkClick, currentMarkValue, currentUserMark }: PdfItemProps) => {
|
||||||
const filterByPage = (marks: Mark[], page: number): Mark[] => {
|
const filterByPage = (marks: Mark[], page: number): Mark[] => {
|
||||||
return marks.filter((mark) => mark.location.page === page);
|
return marks.filter((mark) => mark.location.page === page);
|
||||||
}
|
}
|
||||||
@ -20,6 +22,8 @@ const PdfItem = ({ pdfFile, marks, handleMarkClick }: PdfItemProps) => {
|
|||||||
key={i}
|
key={i}
|
||||||
marks={filterByPage(marks, i)}
|
marks={filterByPage(marks, i)}
|
||||||
handleMarkClick={handleMarkClick}
|
handleMarkClick={handleMarkClick}
|
||||||
|
currentMarkValue={currentMarkValue}
|
||||||
|
currentUserMark={currentUserMark}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Mark, MarkLocation } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark, MarkLocation } from '../../types/mark.ts'
|
||||||
import styles from '../DrawPDFFields/style.module.scss'
|
import styles from '../DrawPDFFields/style.module.scss'
|
||||||
import { inPx } from '../../utils/pdf.ts'
|
import { inPx } from '../../utils/pdf.ts'
|
||||||
|
|
||||||
@ -6,10 +6,17 @@ interface PdfMarkItemProps {
|
|||||||
mark: Mark
|
mark: Mark
|
||||||
handleMarkClick: (id: number) => void
|
handleMarkClick: (id: number) => void
|
||||||
isEditable: boolean
|
isEditable: boolean
|
||||||
|
currentMarkValue: string
|
||||||
|
currentUserMark: CurrentUserMark | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PdfMarkItem = ({ mark, handleMarkClick, isEditable }: PdfMarkItemProps) => {
|
const PdfMarkItem = ({ mark, handleMarkClick, isEditable, currentMarkValue, currentUserMark }: PdfMarkItemProps) => {
|
||||||
const handleClick = () => isEditable && handleMarkClick(mark.id);
|
const handleClick = () => isEditable && handleMarkClick(mark.id);
|
||||||
|
const getMarkValue = () => (
|
||||||
|
currentUserMark?.mark.id === mark.id
|
||||||
|
? currentMarkValue
|
||||||
|
: mark.value
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
@ -20,7 +27,7 @@ const PdfMarkItem = ({ mark, handleMarkClick, isEditable }: PdfMarkItemProps) =>
|
|||||||
width: inPx(mark.location.width),
|
width: inPx(mark.location.width),
|
||||||
height: inPx(mark.location.height)
|
height: inPx(mark.location.height)
|
||||||
}}
|
}}
|
||||||
/>
|
>{getMarkValue()}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import styles from '../DrawPDFFields/style.module.scss'
|
import styles from '../DrawPDFFields/style.module.scss'
|
||||||
import { PdfPage } from '../../types/drawing.ts'
|
import { PdfPage } from '../../types/drawing.ts'
|
||||||
import { Mark, MarkConfigDetails } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark, MarkConfigDetails } from '../../types/mark.ts'
|
||||||
import PdfMarkItem from './PdfMarkItem.tsx'
|
import PdfMarkItem from './PdfMarkItem.tsx'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { State } from '../../store/rootReducer.ts'
|
import { State } from '../../store/rootReducer.ts'
|
||||||
@ -9,9 +9,11 @@ interface PdfPageProps {
|
|||||||
page: PdfPage
|
page: PdfPage
|
||||||
marks: Mark[]
|
marks: Mark[]
|
||||||
handleMarkClick: (id: number) => void
|
handleMarkClick: (id: number) => void
|
||||||
|
currentMarkValue: string
|
||||||
|
currentUserMark: CurrentUserMark | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PdfPageItem = ({ page, marks, handleMarkClick }: PdfPageProps) => {
|
const PdfPageItem = ({ page, marks, handleMarkClick, currentMarkValue, currentUserMark }: PdfPageProps) => {
|
||||||
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)
|
||||||
const isEditable = (mark: Mark): boolean => {
|
const isEditable = (mark: Mark): boolean => {
|
||||||
if (!usersPubkey) return false;
|
if (!usersPubkey) return false;
|
||||||
@ -39,6 +41,8 @@ const PdfPageItem = ({ page, marks, handleMarkClick }: PdfPageProps) => {
|
|||||||
mark={mark}
|
mark={mark}
|
||||||
isEditable={isEditable(mark)}
|
isEditable={isEditable(mark)}
|
||||||
handleMarkClick={handleMarkClick}
|
handleMarkClick={handleMarkClick}
|
||||||
|
currentMarkValue={currentMarkValue}
|
||||||
|
currentUserMark={currentUserMark}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { PdfFile } from '../../types/drawing.ts'
|
import { PdfFile } from '../../types/drawing.ts'
|
||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
import PdfItem from './PdfItem.tsx'
|
import PdfItem from './PdfItem.tsx'
|
||||||
import { Mark, MarkConfigDetails } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark, MarkConfigDetails } from '../../types/mark.ts'
|
||||||
|
|
||||||
interface PdfViewProps {
|
interface PdfViewProps {
|
||||||
files: { [filename: string]: PdfFile },
|
files: { [filename: string]: PdfFile },
|
||||||
fileHashes: { [key: string]: string | null },
|
fileHashes: { [key: string]: string | null },
|
||||||
marks: Mark[],
|
marks: Mark[],
|
||||||
handleMarkClick: (id: number) => void
|
handleMarkClick: (id: number) => void
|
||||||
|
currentMarkValue: string
|
||||||
|
currentUserMark: CurrentUserMark | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) => {
|
const PdfView = ({ files, fileHashes, marks, handleMarkClick, currentMarkValue, currentUserMark }: PdfViewProps) => {
|
||||||
const filterByFile = (marks: Mark[], fileHash: string): Mark[] => {
|
const filterByFile = (marks: Mark[], fileHash: string): Mark[] => {
|
||||||
return marks.filter((mark) => mark.pdfFileHash === fileHash);
|
return marks.filter((mark) => mark.pdfFileHash === fileHash);
|
||||||
}
|
}
|
||||||
@ -23,6 +25,8 @@ const PdfView = ({ files, fileHashes, marks, handleMarkClick }: PdfViewProps) =>
|
|||||||
key={i}
|
key={i}
|
||||||
marks={filterByFile(marks, fileHashes[name] ?? "")}
|
marks={filterByFile(marks, fileHashes[name] ?? "")}
|
||||||
handleMarkClick={handleMarkClick}
|
handleMarkClick={handleMarkClick}
|
||||||
|
currentMarkValue={currentMarkValue}
|
||||||
|
currentUserMark={currentUserMark}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
import { CurrentUserMark, Mark } from '../../types/mark.ts'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Box, Button, TextField } from '@mui/material'
|
import { Box, Button, TextField } from '@mui/material'
|
||||||
|
import { MarkTypeTranslation } from './const.ts'
|
||||||
|
|
||||||
interface MarkFormFieldProps {
|
interface MarkFormFieldProps {
|
||||||
handleSubmit: (event: any) => void
|
handleSubmit: (event: any) => void
|
||||||
@ -17,7 +18,7 @@ const MarkFormField = (props: MarkFormFieldProps) => {
|
|||||||
<Box component="form" onSubmit={handleSubmit}>
|
<Box component="form" onSubmit={handleSubmit}>
|
||||||
<TextField
|
<TextField
|
||||||
id="mark-value"
|
id="mark-value"
|
||||||
label={currentMark.mark.type}
|
label={MarkTypeTranslation[currentMark.mark.type.valueOf()]}
|
||||||
value={currentMarkValue}
|
value={currentMarkValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
5
src/pages/sign/const.ts
Normal file
5
src/pages/sign/const.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { MarkType } from '../../types/drawing.ts'
|
||||||
|
|
||||||
|
export const MarkTypeTranslation: { [key: string]: string } = {
|
||||||
|
[MarkType.FULLNAME.valueOf()]: "Full Name"
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { LoadingSpinner } from '../../components/LoadingSpinner'
|
|||||||
import { NostrController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPublicRoutes } from '../../routes'
|
import { appPublicRoutes } from '../../routes'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
import { CreateSignatureEventContent, Meta, SignedEvent } from '../../types'
|
import { CreateSignatureEventContent, Meta, SignedEvent, UserRole } from '../../types'
|
||||||
import {
|
import {
|
||||||
decryptArrayBuffer,
|
decryptArrayBuffer,
|
||||||
encryptArrayBuffer,
|
encryptArrayBuffer,
|
||||||
@ -239,6 +239,7 @@ export const SignPage = () => {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
handleArrayBufferFromBlossom(res.data, encryptionKey)
|
handleArrayBufferFromBlossom(res.data, encryptionKey)
|
||||||
setMeta(metaInNavState)
|
setMeta(metaInNavState)
|
||||||
|
console.log('meta in nav state: ', metaInNavState)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(`error occurred in getting file from ${zipUrl}`, err)
|
console.error(`error occurred in getting file from ${zipUrl}`, err)
|
||||||
@ -490,6 +491,8 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.log('parsed meta: ', parsedMetaJson)
|
||||||
|
|
||||||
setMeta(parsedMetaJson)
|
setMeta(parsedMetaJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,7 +517,10 @@ export const SignPage = () => {
|
|||||||
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
|
const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!))
|
||||||
if (!prevSig) return
|
if (!prevSig) return
|
||||||
|
|
||||||
const signedEvent = await signEventForMeta(prevSig)
|
const marks = getSignerMarksForMeta()
|
||||||
|
if (!marks) return
|
||||||
|
|
||||||
|
const signedEvent = await signEventForMeta({ prevSig, marks })
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return
|
||||||
|
|
||||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
||||||
@ -528,14 +534,19 @@ export const SignPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sign the event for the meta file
|
// Sign the event for the meta file
|
||||||
const signEventForMeta = async (prevSig: string) => {
|
const signEventForMeta = async (signerContent: { prevSig: string, marks: Mark[] }) => {
|
||||||
return await signEventForMetaFile(
|
return await signEventForMetaFile(
|
||||||
JSON.stringify({ prevSig }),
|
JSON.stringify(signerContent),
|
||||||
nostrController,
|
nostrController,
|
||||||
setIsLoading
|
setIsLoading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSignerMarksForMeta = (): Mark[] | undefined => {
|
||||||
|
if (currentUserMarks.length === 0) return;
|
||||||
|
return currentUserMarks.map(( { mark }: CurrentUserMark) => mark);
|
||||||
|
}
|
||||||
|
|
||||||
const handleMarkClick = (id: number) => {
|
const handleMarkClick = (id: number) => {
|
||||||
const nextMark = currentUserMarks.find(mark => mark.mark.id === id)
|
const nextMark = currentUserMarks.find(mark => mark.mark.id === id)
|
||||||
setCurrentUserMark(nextMark!)
|
setCurrentUserMark(nextMark!)
|
||||||
@ -595,6 +606,7 @@ export const SignPage = () => {
|
|||||||
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
[hexToNpub(signedEvent.pubkey)]: JSON.stringify(signedEvent, null, 2)
|
||||||
}
|
}
|
||||||
metaCopy.modifiedAt = now()
|
metaCopy.modifiedAt = now()
|
||||||
|
console.log('meta copy: ', metaCopy);
|
||||||
return metaCopy
|
return metaCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,6 +731,7 @@ export const SignPage = () => {
|
|||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Notifications sent successfully')
|
toast.success('Notifications sent successfully')
|
||||||
|
console.log('meta: ', meta);
|
||||||
setMeta(meta)
|
setMeta(meta)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -948,6 +961,34 @@ export const SignPage = () => {
|
|||||||
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
return <LoadingSpinner desc={loadingSpinnerDesc} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isMarksCompleted) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box className={styles.container}>
|
||||||
|
{
|
||||||
|
marks.length > 0 && (
|
||||||
|
<PdfView
|
||||||
|
files={files}
|
||||||
|
marks={marks}
|
||||||
|
fileHashes={currentFileHashes}
|
||||||
|
handleMarkClick={handleMarkClick}
|
||||||
|
currentMarkValue={currentMarkValue}
|
||||||
|
currentUserMark={currentUserMark}
|
||||||
|
/>)}
|
||||||
|
{
|
||||||
|
currentUserMark !== null && (
|
||||||
|
<MarkFormField
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleChange={handleChange}
|
||||||
|
currentMark={currentUserMark}
|
||||||
|
currentMarkValue={currentMarkValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box className={styles.container}>
|
<Box className={styles.container}>
|
||||||
@ -976,67 +1017,46 @@ export const SignPage = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*{submittedBy && Object.entries(files).length > 0 && meta && (*/}
|
{submittedBy && Object.entries(files).length > 0 && meta && (
|
||||||
{/* <>*/}
|
<>
|
||||||
{/* <DisplayMeta*/}
|
<DisplayMeta
|
||||||
{/* meta={meta}*/}
|
meta={meta}
|
||||||
{/* files={files}*/}
|
files={files}
|
||||||
{/* submittedBy={submittedBy}*/}
|
submittedBy={submittedBy}
|
||||||
{/* signers={signers}*/}
|
signers={signers}
|
||||||
{/* viewers={viewers}*/}
|
viewers={viewers}
|
||||||
{/* creatorFileHashes={creatorFileHashes}*/}
|
creatorFileHashes={creatorFileHashes}
|
||||||
{/* currentFileHashes={currentFileHashes}*/}
|
currentFileHashes={currentFileHashes}
|
||||||
{/* signedBy={signedBy}*/}
|
signedBy={signedBy}
|
||||||
{/* nextSigner={nextSinger}*/}
|
nextSigner={nextSinger}
|
||||||
{/* getPrevSignersSig={getPrevSignersSig}*/}
|
getPrevSignersSig={getPrevSignersSig}
|
||||||
{/* />*/}
|
|
||||||
|
|
||||||
{/* {signedStatus === SignedStatus.Fully_Signed && (*/}
|
|
||||||
{/* <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>*/}
|
|
||||||
{/* <Button onClick={handleExport} variant="contained">*/}
|
|
||||||
{/* Export*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/* </Box>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
|
|
||||||
{/* {signedStatus === SignedStatus.User_Is_Next_Signer && (*/}
|
|
||||||
{/* <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>*/}
|
|
||||||
{/* <Button onClick={handleSign} variant="contained">*/}
|
|
||||||
{/* Sign*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/* </Box>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
|
|
||||||
{/* {isSignerOrCreator && (*/}
|
|
||||||
{/* <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>*/}
|
|
||||||
{/* <Button onClick={handleExportSigit} variant="contained">*/}
|
|
||||||
{/* Export Sigit*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/* </Box>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/* </>*/}
|
|
||||||
{/*)}*/}
|
|
||||||
{
|
|
||||||
!isMarksCompleted && marks.length > 0 && (
|
|
||||||
<PdfView
|
|
||||||
files={files}
|
|
||||||
marks={marks}
|
|
||||||
fileHashes={currentFileHashes}
|
|
||||||
handleMarkClick={handleMarkClick}
|
|
||||||
/>
|
/>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{signedStatus === SignedStatus.Fully_Signed && (
|
||||||
!isMarksCompleted && currentUserMark !== null && <MarkFormField
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
handleSubmit={handleSubmit}
|
<Button onClick={handleExport} variant="contained">
|
||||||
handleChange={handleChange}
|
Export
|
||||||
currentMark={currentUserMark}
|
</Button>
|
||||||
currentMarkValue={currentMarkValue}
|
</Box>
|
||||||
/>
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
{ isMarksCompleted && <p>Ready to Sign!</p>}
|
{signedStatus === SignedStatus.User_Is_Next_Signer && (
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button onClick={handleSign} variant="contained">
|
||||||
|
Sign
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSignerOrCreator && (
|
||||||
|
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Button onClick={handleExportSigit} variant="contained">
|
||||||
|
Export Sigit
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -55,13 +55,14 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
height: 100px;
|
||||||
border-top: 1px solid #ccc;
|
border-top: 1px solid #ccc;
|
||||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
//z-index: 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixedBottomForm input[type="text"] {
|
.fixedBottomForm input[type="text"] {
|
||||||
|
@ -30,6 +30,7 @@ export interface CreateSignatureEventContent {
|
|||||||
|
|
||||||
export interface SignedEventContent {
|
export interface SignedEventContent {
|
||||||
prevSig: string
|
prevSig: string
|
||||||
|
markConfig: Mark[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sigit {
|
export interface Sigit {
|
||||||
|
Loading…
Reference in New Issue
Block a user