Merge pull request #335 from ente-io/sharable-url-patches
Sharable url patches
This commit is contained in:
commit
22ae94821c
|
@ -52,6 +52,8 @@ function CollectionShare(props: Props) {
|
||||||
props.collection.key
|
props.collection.key
|
||||||
);
|
);
|
||||||
setPublicShareUrl(t);
|
setPublicShareUrl(t);
|
||||||
|
} else {
|
||||||
|
setPublicShareUrl(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
|
|
|
@ -97,8 +97,8 @@ const BannerContainer = styled.div<{ span: number }>`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ReportAbuseItem = styled.div<{ span: number }>`
|
const ReportAbuseItem = styled.div<{ span: number }>`
|
||||||
flex: 1;
|
display: flex;
|
||||||
text-align: right;
|
justify-content: center;
|
||||||
grid-column: span ${(props) => props.span};
|
grid-column: span ${(props) => props.span};
|
||||||
& > p {
|
& > p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -288,7 +288,7 @@ export function PhotoList({
|
||||||
item: (
|
item: (
|
||||||
<ReportAbuseItem span={columns}>
|
<ReportAbuseItem span={columns}>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ fontSize: '16px' }}
|
style={{ fontSize: '14px' }}
|
||||||
variant={ButtonVariant.danger}
|
variant={ButtonVariant.danger}
|
||||||
onClick={publicCollectionGalleryContext.openReportForm}>
|
onClick={publicCollectionGalleryContext.openReportForm}>
|
||||||
{constants.ABUSE_REPORT_BUTTON_TEXT}
|
{constants.ABUSE_REPORT_BUTTON_TEXT}
|
||||||
|
|
|
@ -226,7 +226,9 @@ export function AbuseReportForm({ show, close, url }: Iprops) {
|
||||||
<Form.Group controlId="reportForm.email">
|
<Form.Group controlId="reportForm.email">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={constants.ENTER_EMAIL}
|
placeholder={
|
||||||
|
constants.ENTER_EMAIL_ADDRESS
|
||||||
|
}
|
||||||
value={values.email}
|
value={values.email}
|
||||||
onChange={handleChange('email')}
|
onChange={handleChange('email')}
|
||||||
isInvalid={Boolean(
|
isInvalid={Boolean(
|
||||||
|
|
|
@ -7,9 +7,7 @@ interface Iprops {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Info = styled.h5`
|
const Info = styled.h5`
|
||||||
padding: 5px 24px;
|
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
border-bottom: 2px solid #5a5858;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function CollectionInfo(props: Iprops) {
|
export function CollectionInfo(props: Iprops) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ const Wrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NoStyleAnchor = styled.a`
|
const NoStyleAnchor = styled.a`
|
||||||
|
color: inherit;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
|
@ -20,7 +21,7 @@ export const ButtonWithLink = ({
|
||||||
href,
|
href,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{ href: string }>) => (
|
}: React.PropsWithChildren<{ href: string }>) => (
|
||||||
<Button variant="outline-success">
|
<Button id="go-to-ente">
|
||||||
<NoStyleAnchor href={href}>{children}</NoStyleAnchor>
|
<NoStyleAnchor href={href}>{children}</NoStyleAnchor>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -232,6 +232,17 @@ const GlobalStyles = createGlobalStyle`
|
||||||
.btn-outline-danger, .btn-outline-secondary, .btn-outline-primary{
|
.btn-outline-danger, .btn-outline-secondary, .btn-outline-primary{
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#go-to-ente{
|
||||||
|
background:none;
|
||||||
|
border-color: #3dbb69;
|
||||||
|
color:#51cd7c;
|
||||||
|
}
|
||||||
|
#go-to-ente:hover, #go-to-ente:focus, #go-to-ente:active {
|
||||||
|
color:#fff;
|
||||||
|
background-color: #44774d;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,7 +178,7 @@ export default function Gallery() {
|
||||||
const loadingBar = useRef(null);
|
const loadingBar = useRef(null);
|
||||||
const [isInSearchMode, setIsInSearchMode] = useState(false);
|
const [isInSearchMode, setIsInSearchMode] = useState(false);
|
||||||
const [searchStats, setSearchStats] = useState(null);
|
const [searchStats, setSearchStats] = useState(null);
|
||||||
const isLoadingBarRunning = useRef(true);
|
const isLoadingBarRunning = useRef(false);
|
||||||
const syncInProgress = useRef(true);
|
const syncInProgress = useRef(true);
|
||||||
const resync = useRef(false);
|
const resync = useRef(false);
|
||||||
const [deleted, setDeleted] = useState<number[]>([]);
|
const [deleted, setDeleted] = useState<number[]>([]);
|
||||||
|
|
|
@ -137,7 +137,7 @@ export default function LandingPage() {
|
||||||
hash: currentURL.hash,
|
hash: currentURL.hash,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setLoading(false);
|
await initLocalForage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNormalRedirect = async () => {
|
const handleNormalRedirect = async () => {
|
||||||
|
@ -145,6 +145,10 @@ export default function LandingPage() {
|
||||||
if (user?.email) {
|
if (user?.email) {
|
||||||
await router.push(PAGES.VERIFY);
|
await router.push(PAGES.VERIFY);
|
||||||
}
|
}
|
||||||
|
await initLocalForage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initLocalForage = async () => {
|
||||||
try {
|
try {
|
||||||
await localForage.ready();
|
await localForage.ready();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function PublicCollectionGallery() {
|
||||||
const token = useRef<string>(null);
|
const token = useRef<string>(null);
|
||||||
const collectionKey = useRef<string>(null);
|
const collectionKey = useRef<string>(null);
|
||||||
const url = useRef<string>(null);
|
const url = useRef<string>(null);
|
||||||
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
|
const [publicFiles, setPublicFiles] = useState<EnteFile[]>([]);
|
||||||
const [publicCollection, setPublicCollection] = useState<Collection>(null);
|
const [publicCollection, setPublicCollection] = useState<Collection>(null);
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
const [abuseReportFormView, setAbuseReportFormView] = useState(false);
|
const [abuseReportFormView, setAbuseReportFormView] = useState(false);
|
||||||
|
@ -42,14 +42,23 @@ export default function PublicCollectionGallery() {
|
||||||
const openReportForm = () => setAbuseReportFormView(true);
|
const openReportForm = () => setAbuseReportFormView(true);
|
||||||
const closeReportForm = () => setAbuseReportFormView(false);
|
const closeReportForm = () => setAbuseReportFormView(false);
|
||||||
const loadingBar = useRef(null);
|
const loadingBar = useRef(null);
|
||||||
|
const [isLoadingBarRunning, setIsLoadingBarRunning] = useState(false);
|
||||||
|
|
||||||
const openMessageDialog = () => setMessageDialogView(true);
|
const openMessageDialog = () => setMessageDialogView(true);
|
||||||
const closeMessageDialog = () => setMessageDialogView(false);
|
const closeMessageDialog = () => setMessageDialogView(false);
|
||||||
|
|
||||||
const startLoading = () => loadingBar.current?.continuousStart();
|
const startLoading = () => {
|
||||||
const finishLoading = () => loadingBar.current?.complete();
|
!isLoadingBarRunning && loadingBar.current?.continuousStart();
|
||||||
|
setIsLoadingBarRunning(true);
|
||||||
|
};
|
||||||
|
const finishLoading = () => {
|
||||||
|
loadingBar.current?.complete();
|
||||||
|
setIsLoadingBarRunning(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
appContext.showNavBar(true);
|
||||||
|
setLoading(false);
|
||||||
const currentURL = new URL(window.location.href);
|
const currentURL = new URL(window.location.href);
|
||||||
if (currentURL.pathname !== PAGES.ROOT) {
|
if (currentURL.pathname !== PAGES.ROOT) {
|
||||||
router.push(
|
router.push(
|
||||||
|
@ -68,32 +77,28 @@ export default function PublicCollectionGallery() {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
url.current = window.location.href;
|
url.current = window.location.href;
|
||||||
const urlS = new URL(url.current);
|
const currentURL = new URL(url.current);
|
||||||
const eToken = urlS.searchParams.get('t');
|
const t = currentURL.searchParams.get('t');
|
||||||
const eCollectionKey = urlS.hash.slice(1);
|
const ck = currentURL.hash.slice(1);
|
||||||
const decodedCollectionKey = await worker.fromHex(eCollectionKey);
|
const dck = await worker.fromHex(ck);
|
||||||
if (!eToken || !decodedCollectionKey) {
|
if (!t || !dck) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
token.current = eToken;
|
token.current = t;
|
||||||
collectionKey.current = decodedCollectionKey;
|
collectionKey.current = dck;
|
||||||
url.current = window.location.href;
|
url.current = window.location.href;
|
||||||
const localCollection = await getLocalPublicCollection(
|
const localCollection = await getLocalPublicCollection(
|
||||||
eCollectionKey
|
collectionKey.current
|
||||||
);
|
);
|
||||||
if (localCollection) {
|
if (localCollection) {
|
||||||
setPublicCollection(localCollection);
|
setPublicCollection(localCollection);
|
||||||
const localPublicFiles = sortFiles(
|
const localPublicFiles = sortFiles(
|
||||||
mergeMetadata(
|
mergeMetadata(await getLocalPublicFiles(localCollection))
|
||||||
await getLocalPublicFiles(`${localCollection.id}`)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
setPublicFiles(localPublicFiles);
|
setPublicFiles(localPublicFiles);
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
syncWithRemote();
|
syncWithRemote();
|
||||||
appContext.showNavBar(true);
|
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -119,7 +124,6 @@ export default function PublicCollectionGallery() {
|
||||||
setPublicFiles(null);
|
setPublicFiles(null);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
|
||||||
finishLoading();
|
finishLoading();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -132,7 +136,7 @@ export default function PublicCollectionGallery() {
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!publicFiles) {
|
if (!isLoadingBarRunning && !publicFiles) {
|
||||||
return <Container>{constants.NOT_FOUND}</Container>;
|
return <Container>{constants.NOT_FOUND}</Container>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -154,7 +158,7 @@ export default function PublicCollectionGallery() {
|
||||||
favItemIds={null}
|
favItemIds={null}
|
||||||
setSelected={() => null}
|
setSelected={() => null}
|
||||||
selected={{ count: 0, collectionID: null }}
|
selected={{ count: 0, collectionID: null }}
|
||||||
isFirstLoad={false}
|
isFirstLoad={true}
|
||||||
openFileUploader={() => null}
|
openFileUploader={() => null}
|
||||||
isInSearchMode={false}
|
isInSearchMode={false}
|
||||||
search={{}}
|
search={{}}
|
||||||
|
|
|
@ -25,8 +25,16 @@ class PublicCollectionDownloadManager {
|
||||||
}
|
}
|
||||||
if (!this.thumbnailObjectURLPromise.get(file.id)) {
|
if (!this.thumbnailObjectURLPromise.get(file.id)) {
|
||||||
const downloadPromise = async () => {
|
const downloadPromise = async () => {
|
||||||
const thumbnailCache = await caches.open('thumbs');
|
const thumbnailCache = await (async () => {
|
||||||
const cacheResp: Response = await thumbnailCache.match(
|
try {
|
||||||
|
return await caches.open('thumbs');
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const cacheResp: Response = await thumbnailCache?.match(
|
||||||
file.id.toString()
|
file.id.toString()
|
||||||
);
|
);
|
||||||
if (cacheResp) {
|
if (cacheResp) {
|
||||||
|
@ -35,7 +43,7 @@ class PublicCollectionDownloadManager {
|
||||||
const thumb = await this.downloadThumb(token, file);
|
const thumb = await this.downloadThumb(token, file);
|
||||||
const thumbBlob = new Blob([thumb]);
|
const thumbBlob = new Blob([thumb]);
|
||||||
try {
|
try {
|
||||||
await thumbnailCache.put(
|
await thumbnailCache?.put(
|
||||||
file.id.toString(),
|
file.id.toString(),
|
||||||
new Response(thumbBlob)
|
new Response(thumbBlob)
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,17 +18,25 @@ const ENDPOINT = getEndpoint();
|
||||||
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
|
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
|
||||||
const PUBLIC_COLLECTIONS_TABLE = 'public-collections';
|
const PUBLIC_COLLECTIONS_TABLE = 'public-collections';
|
||||||
|
|
||||||
const getCollectionUID = (collection: Collection) => `${collection.id}`;
|
const getCollectionUID = (collection: Collection) => `${collection.key}`;
|
||||||
|
const getCollectionSyncTimeUID = (collectionUID: string) =>
|
||||||
|
`public-${collectionUID}-time`;
|
||||||
|
|
||||||
export const getLocalPublicFiles = async (collectionUID: string) => {
|
export const getLocalPublicFiles = async (collection: Collection) => {
|
||||||
const localSavedPublicCollectionFiles = (
|
const localSavedPublicCollectionFiles =
|
||||||
|
(
|
||||||
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>(
|
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>(
|
||||||
PUBLIC_COLLECTION_FILES_TABLE
|
PUBLIC_COLLECTION_FILES_TABLE
|
||||||
)) ?? []
|
)) || []
|
||||||
).find(
|
).find(
|
||||||
(localSavedPublicCollectionFiles) =>
|
(localSavedPublicCollectionFiles) =>
|
||||||
localSavedPublicCollectionFiles.collectionUID === collectionUID
|
localSavedPublicCollectionFiles.collectionUID ===
|
||||||
) || { collectionKey: null, files: [] as EnteFile[] };
|
getCollectionUID(collection)
|
||||||
|
) ||
|
||||||
|
({
|
||||||
|
collectionUID: null,
|
||||||
|
files: [] as EnteFile[],
|
||||||
|
} as LocalSavedPublicCollectionFiles);
|
||||||
return localSavedPublicCollectionFiles.files;
|
return localSavedPublicCollectionFiles.files;
|
||||||
};
|
};
|
||||||
export const savePublicCollectionFiles = async (
|
export const savePublicCollectionFiles = async (
|
||||||
|
@ -38,20 +46,22 @@ export const savePublicCollectionFiles = async (
|
||||||
const publicCollectionFiles =
|
const publicCollectionFiles =
|
||||||
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>(
|
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>(
|
||||||
PUBLIC_COLLECTION_FILES_TABLE
|
PUBLIC_COLLECTION_FILES_TABLE
|
||||||
)) ?? [];
|
)) || [];
|
||||||
await localForage.setItem(PUBLIC_COLLECTION_FILES_TABLE, [
|
await localForage.setItem(
|
||||||
...publicCollectionFiles,
|
PUBLIC_COLLECTION_FILES_TABLE,
|
||||||
|
dedupeCollectionFiles([
|
||||||
{ collectionUID, files },
|
{ collectionUID, files },
|
||||||
]);
|
...publicCollectionFiles,
|
||||||
|
])
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLocalPublicCollection = async (collectionKey: string) => {
|
export const getLocalPublicCollection = async (collectionKey: string) => {
|
||||||
|
const localCollections =
|
||||||
|
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
|
||||||
|
[];
|
||||||
const publicCollection =
|
const publicCollection =
|
||||||
(
|
localCollections.find(
|
||||||
(await localForage.getItem<Collection[]>(
|
|
||||||
PUBLIC_COLLECTIONS_TABLE
|
|
||||||
)) ?? []
|
|
||||||
).find(
|
|
||||||
(localSavedPublicCollection) =>
|
(localSavedPublicCollection) =>
|
||||||
localSavedPublicCollection.key === collectionKey
|
localSavedPublicCollection.key === collectionKey
|
||||||
) || null;
|
) || null;
|
||||||
|
@ -62,19 +72,47 @@ export const savePublicCollection = async (collection: Collection) => {
|
||||||
const publicCollections =
|
const publicCollections =
|
||||||
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ??
|
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ??
|
||||||
[];
|
[];
|
||||||
await localForage.setItem(PUBLIC_COLLECTIONS_TABLE, [
|
await localForage.setItem(
|
||||||
...publicCollections,
|
PUBLIC_COLLECTIONS_TABLE,
|
||||||
collection,
|
dedupeCollections([collection, ...publicCollections])
|
||||||
]);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dedupeCollections = (collections: Collection[]) => {
|
||||||
|
const keySet = new Set([]);
|
||||||
|
return collections.filter((collection) => {
|
||||||
|
if (!keySet.has(collection.key)) {
|
||||||
|
keySet.add(collection.key);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dedupeCollectionFiles = (
|
||||||
|
collectionFiles: LocalSavedPublicCollectionFiles[]
|
||||||
|
) => {
|
||||||
|
const keySet = new Set([]);
|
||||||
|
return collectionFiles.filter(({ collectionUID }) => {
|
||||||
|
if (!keySet.has(collectionUID)) {
|
||||||
|
keySet.add(collectionUID);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPublicCollectionLastSyncTime = async (collectionUID: string) =>
|
const getPublicCollectionLastSyncTime = async (collectionUID: string) =>
|
||||||
(await localForage.getItem<number>(`public-${collectionUID}-time`)) ?? 0;
|
(await localForage.getItem<number>(
|
||||||
|
getCollectionSyncTimeUID(collectionUID)
|
||||||
|
)) ?? 0;
|
||||||
|
|
||||||
const setPublicCollectionLastSyncTime = async (
|
const setPublicCollectionLastSyncTime = async (
|
||||||
collectionUID: string,
|
collectionUID: string,
|
||||||
time: number
|
time: number
|
||||||
) => await localForage.setItem(collectionUID, time);
|
) => await localForage.setItem(getCollectionSyncTimeUID(collectionUID), time);
|
||||||
|
|
||||||
export const syncPublicFiles = async (
|
export const syncPublicFiles = async (
|
||||||
token: string,
|
token: string,
|
||||||
|
@ -83,9 +121,7 @@ export const syncPublicFiles = async (
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let files: EnteFile[] = [];
|
let files: EnteFile[] = [];
|
||||||
const localFiles = await getLocalPublicFiles(
|
const localFiles = await getLocalPublicFiles(collection);
|
||||||
getCollectionUID(collection)
|
|
||||||
);
|
|
||||||
files.push(...localFiles);
|
files.push(...localFiles);
|
||||||
try {
|
try {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
|
@ -627,19 +627,20 @@ const englishConstants = {
|
||||||
MALICIOUS_CONTENT: 'contains malicious content',
|
MALICIOUS_CONTENT: 'contains malicious content',
|
||||||
COPYRIGHT:
|
COPYRIGHT:
|
||||||
'infringes on the copyright of someone I am authorized to represent',
|
'infringes on the copyright of someone I am authorized to represent',
|
||||||
SELECT_REASON: 'select a reason',
|
ENTER_EMAIL_ADDRESS: 'email*',
|
||||||
ENTER_FULL_NAME: 'full name',
|
SELECT_REASON: 'select a reason*',
|
||||||
|
ENTER_FULL_NAME: 'full name*',
|
||||||
ENTER_DIGITAL_SIGNATURE:
|
ENTER_DIGITAL_SIGNATURE:
|
||||||
'typing your full name in this box will act as your digital signature',
|
'typing your full name in this box will act as your digital signature*',
|
||||||
ENTER_ON_BEHALF_OF: 'I am reporting on behalf of',
|
ENTER_ON_BEHALF_OF: 'I am reporting on behalf of*',
|
||||||
ENTER_ADDRESS: 'address',
|
ENTER_ADDRESS: 'address*',
|
||||||
ENTER_JOB_TITLE: 'job title',
|
ENTER_JOB_TITLE: 'job title*',
|
||||||
ENTER_CITY: 'city',
|
ENTER_CITY: 'city*',
|
||||||
ENTER_PHONE: 'phone number',
|
ENTER_PHONE: 'phone number*',
|
||||||
|
|
||||||
ENTER_STATE: 'state',
|
ENTER_STATE: 'state*',
|
||||||
ENTER_POSTAL_CODE: 'zip/postal code',
|
ENTER_POSTAL_CODE: 'zip/postal code*',
|
||||||
ENTER_COUNTRY: 'country',
|
ENTER_COUNTRY: 'country*',
|
||||||
JUDICIAL_DESCRIPTION: () => (
|
JUDICIAL_DESCRIPTION: () => (
|
||||||
<>
|
<>
|
||||||
By checking the following boxes, I state{' '}
|
By checking the following boxes, I state{' '}
|
||||||
|
|
Loading…
Reference in a new issue