mirror of
https://github.com/Qortal/q-tube.git
synced 2025-02-11 17:55:51 +00:00
Merge pull request #1 from QortalSeth/main
Superlike Dialog allows optional donation to DevFund.
This commit is contained in:
commit
3a453210fd
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
@ -43,11 +43,15 @@ import {
|
||||
setEditPlaylist,
|
||||
} from "../../state/features/videoSlice";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE, categories, subCategories } from "../../constants";
|
||||
import { categories, subCategories } from "../../constants/Categories.ts";
|
||||
import { Playlists } from "../Playlists/Playlists";
|
||||
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import {
|
||||
QTUBE_PLAYLIST_BASE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
} from "../../constants/Identifiers.ts";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -87,17 +91,17 @@ export const EditPlaylist = () => {
|
||||
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
|
||||
useState<any>(null);
|
||||
|
||||
const isNew = useMemo(()=> {
|
||||
return editVideoProperties?.mode === 'new'
|
||||
}, [editVideoProperties])
|
||||
const isNew = useMemo(() => {
|
||||
return editVideoProperties?.mode === "new";
|
||||
}, [editVideoProperties]);
|
||||
|
||||
useEffect(()=> {
|
||||
if(isNew){
|
||||
useEffect(() => {
|
||||
if (isNew) {
|
||||
setPlaylistData({
|
||||
videos: []
|
||||
})
|
||||
videos: [],
|
||||
});
|
||||
}
|
||||
}, [isNew])
|
||||
}, [isNew]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (editVideoProperties) {
|
||||
@ -145,7 +149,7 @@ export const EditPlaylist = () => {
|
||||
// }
|
||||
// }, [editVideoProperties]);
|
||||
|
||||
const checkforPlaylist = React.useCallback(async (videoList) => {
|
||||
const checkforPlaylist = React.useCallback(async videoList => {
|
||||
try {
|
||||
const combinedData: any = {};
|
||||
const videos = [];
|
||||
@ -174,21 +178,19 @@ export const EditPlaylist = () => {
|
||||
useEffect(() => {
|
||||
if (editVideoProperties) {
|
||||
setTitle(editVideoProperties?.title || "");
|
||||
|
||||
if(editVideoProperties?.htmlDescription){
|
||||
|
||||
if (editVideoProperties?.htmlDescription) {
|
||||
setDescription(editVideoProperties?.htmlDescription);
|
||||
|
||||
} else if(editVideoProperties?.description) {
|
||||
const paragraph = `<p>${editVideoProperties?.description}</p>`
|
||||
} else if (editVideoProperties?.description) {
|
||||
const paragraph = `<p>${editVideoProperties?.description}</p>`;
|
||||
setDescription(paragraph);
|
||||
|
||||
}
|
||||
setCoverImage(editVideoProperties?.image || "");
|
||||
setVideos(editVideoProperties?.videos || []);
|
||||
|
||||
if (editVideoProperties?.category) {
|
||||
const selectedOption = categories.find(
|
||||
(option) => option.id === +editVideoProperties.category
|
||||
option => option.id === +editVideoProperties.category
|
||||
);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
}
|
||||
@ -200,7 +202,7 @@ export const EditPlaylist = () => {
|
||||
) {
|
||||
const selectedOption = subCategories[
|
||||
+editVideoProperties?.category
|
||||
]?.find((option) => option.id === +editVideoProperties.subcategory);
|
||||
]?.find(option => option.id === +editVideoProperties.subcategory);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
}
|
||||
|
||||
@ -211,24 +213,22 @@ export const EditPlaylist = () => {
|
||||
}, [editVideoProperties]);
|
||||
|
||||
const onClose = () => {
|
||||
setTitle("")
|
||||
setDescription("")
|
||||
setVideos([])
|
||||
setPlaylistData(null)
|
||||
setSelectedCategoryVideos(null)
|
||||
setSelectedSubCategoryVideos(null)
|
||||
setCoverImage("")
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setVideos([]);
|
||||
setPlaylistData(null);
|
||||
setSelectedCategoryVideos(null);
|
||||
setSelectedSubCategoryVideos(null);
|
||||
setCoverImage("");
|
||||
dispatch(setEditPlaylist(null));
|
||||
|
||||
};
|
||||
|
||||
async function publishQDNResource() {
|
||||
try {
|
||||
|
||||
if(!title) throw new Error('Please enter a title')
|
||||
if(!description) throw new Error('Please enter a description')
|
||||
if(!coverImage) throw new Error('Please select cover image')
|
||||
if(!selectedCategoryVideos) throw new Error('Please select a category')
|
||||
if (!title) throw new Error("Please enter a title");
|
||||
if (!description) throw new Error("Please enter a description");
|
||||
if (!coverImage) throw new Error("Please select cover image");
|
||||
if (!selectedCategoryVideos) throw new Error("Please select a category");
|
||||
|
||||
if (!editVideoProperties) return;
|
||||
if (!userAddress) throw new Error("Unable to locate user address");
|
||||
@ -258,7 +258,7 @@ export const EditPlaylist = () => {
|
||||
const category = selectedCategoryVideos.id;
|
||||
const subcategory = selectedSubCategoryVideos?.id || "";
|
||||
|
||||
const videoStructured = playlistData.videos.map((item) => {
|
||||
const videoStructured = playlistData.videos.map(item => {
|
||||
const descriptionVid = item?.metadata?.description;
|
||||
if (!descriptionVid) throw new Error("cannot find video code");
|
||||
|
||||
@ -286,13 +286,12 @@ export const EditPlaylist = () => {
|
||||
});
|
||||
const id = uid();
|
||||
|
||||
let commentsId = editVideoProperties?.id
|
||||
|
||||
if(isNew){
|
||||
commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}`
|
||||
}
|
||||
const stringDescription = extractTextFromHTML(description)
|
||||
let commentsId = editVideoProperties?.id;
|
||||
|
||||
if (isNew) {
|
||||
commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}`;
|
||||
}
|
||||
const stringDescription = extractTextFromHTML(description);
|
||||
|
||||
const playlistObject: any = {
|
||||
title,
|
||||
@ -303,10 +302,13 @@ export const EditPlaylist = () => {
|
||||
videos: videoStructured,
|
||||
commentsId: commentsId,
|
||||
category,
|
||||
subcategory
|
||||
subcategory,
|
||||
};
|
||||
|
||||
const codes = videoStructured.map((item) => `c:${item.code};`).slice(0,10).join("");
|
||||
const codes = videoStructured
|
||||
.map(item => `c:${item.code};`)
|
||||
.slice(0, 10)
|
||||
.join("");
|
||||
let metadescription =
|
||||
`**category:${category};subcategory:${subcategory};${codes}**` +
|
||||
stringDescription.slice(0, 120);
|
||||
@ -314,15 +316,18 @@ export const EditPlaylist = () => {
|
||||
const crowdfundObjectToBase64 = await objectToBase64(playlistObject);
|
||||
// Description is obtained from raw data
|
||||
|
||||
let identifier = editVideoProperties?.id
|
||||
let identifier = editVideoProperties?.id;
|
||||
const sanitizeTitle = title
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if(isNew){
|
||||
identifier = `${QTUBE_PLAYLIST_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
|
||||
if (isNew) {
|
||||
identifier = `${QTUBE_PLAYLIST_BASE}${sanitizeTitle.slice(
|
||||
0,
|
||||
30
|
||||
)}_${id}`;
|
||||
}
|
||||
const requestBodyJson: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
@ -336,21 +341,17 @@ export const EditPlaylist = () => {
|
||||
};
|
||||
|
||||
await qortalRequest(requestBodyJson);
|
||||
if(isNew){
|
||||
if (isNew) {
|
||||
const objectToStore = {
|
||||
title: title.slice(0, 50),
|
||||
description: metadescription,
|
||||
id: identifier,
|
||||
service: "PLAYLIST",
|
||||
user: username,
|
||||
...playlistObject
|
||||
}
|
||||
dispatch(
|
||||
updateVideo(objectToStore)
|
||||
);
|
||||
dispatch(
|
||||
updateInHashMap(objectToStore)
|
||||
);
|
||||
...playlistObject,
|
||||
};
|
||||
dispatch(updateVideo(objectToStore));
|
||||
dispatch(updateInHashMap(objectToStore));
|
||||
} else {
|
||||
dispatch(
|
||||
updateVideo({
|
||||
@ -365,7 +366,7 @@ export const EditPlaylist = () => {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Playlist published",
|
||||
@ -399,13 +400,11 @@ export const EditPlaylist = () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleOptionCategoryChangeVideos = (
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find((option) => option.id === +optionId);
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChangeVideos = (
|
||||
@ -414,19 +413,18 @@ export const EditPlaylist = () => {
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
(option) => option.id === +optionId
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
};
|
||||
|
||||
|
||||
const removeVideo = (index) => {
|
||||
const removeVideo = index => {
|
||||
const copyData = structuredClone(playlistData);
|
||||
copyData.videos.splice(index, 1);
|
||||
setPlaylistData(copyData);
|
||||
};
|
||||
|
||||
const addVideo = (data) => {
|
||||
const addVideo = data => {
|
||||
const copyData = structuredClone(playlistData);
|
||||
copyData.videos = [...copyData.videos, { ...data }];
|
||||
setPlaylistData(copyData);
|
||||
@ -449,10 +447,8 @@ export const EditPlaylist = () => {
|
||||
>
|
||||
{isNew ? (
|
||||
<NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle>
|
||||
|
||||
) : (
|
||||
<NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle>
|
||||
|
||||
<NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle>
|
||||
)}
|
||||
</Box>
|
||||
<>
|
||||
@ -471,7 +467,7 @@ export const EditPlaylist = () => {
|
||||
value={selectedCategoryVideos?.id || ""}
|
||||
onChange={handleOptionCategoryChangeVideos}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -486,20 +482,18 @@ export const EditPlaylist = () => {
|
||||
labelId="Sub-Category"
|
||||
input={<OutlinedInput label="Select a Sub-Category" />}
|
||||
value={selectedSubCategoryVideos?.id || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
subCategories[selectedCategoryVideos?.id]
|
||||
)
|
||||
}
|
||||
>
|
||||
{subCategories[selectedCategoryVideos.id].map(
|
||||
(option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
{subCategories[selectedCategoryVideos.id].map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
@ -533,9 +527,12 @@ export const EditPlaylist = () => {
|
||||
label="Title of playlist"
|
||||
variant="filled"
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
const formattedValue = value.replace(/[^a-zA-Z0-9\s-_!?]/g, "");
|
||||
const formattedValue = value.replace(
|
||||
/[^a-zA-Z0-9\s-_!?]/g,
|
||||
""
|
||||
);
|
||||
setTitle(formattedValue);
|
||||
}}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
@ -552,12 +549,19 @@ export const EditPlaylist = () => {
|
||||
maxRows={3}
|
||||
required
|
||||
/> */}
|
||||
<Typography sx={{
|
||||
fontSize: '18px'
|
||||
}}>Description of playlist</Typography>
|
||||
<TextEditor inlineContent={description} setInlineContent={(value)=> {
|
||||
setDescription(value)
|
||||
}} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
}}
|
||||
>
|
||||
Description of playlist
|
||||
</Typography>
|
||||
<TextEditor
|
||||
inlineContent={description}
|
||||
setInlineContent={value => {
|
||||
setDescription(value);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
||||
<PlaylistListEdit
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Compressor from 'compressorjs'
|
||||
import Compressor from "compressorjs";
|
||||
|
||||
import {
|
||||
AddCoverImageButton,
|
||||
@ -14,7 +14,7 @@ import {
|
||||
NewCrowdfundTitle,
|
||||
StyledButton,
|
||||
TimesIcon,
|
||||
} from "./Upload-styles";
|
||||
} from "./EditVideo-styles.tsx";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
|
||||
import {
|
||||
@ -46,12 +46,14 @@ import {
|
||||
updateInHashMap,
|
||||
} from "../../state/features/videoSlice";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import { QTUBE_VIDEO_BASE, categories, subCategories } from "../../constants";
|
||||
import { categories, subCategories } from "../../constants/Categories.ts";
|
||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import { toBase64 } from "../UploadVideo/UploadVideo";
|
||||
import { toBase64 } from "../PublishVideo/PublishVideo.tsx";
|
||||
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
|
||||
import { titleFormatter } from "../../constants/Misc.ts";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -94,8 +96,7 @@ export const EditVideo = () => {
|
||||
useState<any>(null);
|
||||
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
|
||||
useState<any>(null);
|
||||
const [imageExtracts, setImageExtracts] = useState<any>([])
|
||||
|
||||
const [imageExtracts, setImageExtracts] = useState<any>([]);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
@ -111,7 +112,7 @@ export const EditVideo = () => {
|
||||
let errorString = null;
|
||||
|
||||
rejectedFiles.forEach(({ file, errors }) => {
|
||||
errors.forEach((error) => {
|
||||
errors.forEach(error => {
|
||||
if (error.code === "file-too-large") {
|
||||
errorString = "File must be under 400mb";
|
||||
}
|
||||
@ -178,19 +179,17 @@ export const EditVideo = () => {
|
||||
useEffect(() => {
|
||||
if (editVideoProperties) {
|
||||
setTitle(editVideoProperties?.title || "");
|
||||
if(editVideoProperties?.htmlDescription){
|
||||
if (editVideoProperties?.htmlDescription) {
|
||||
setDescription(editVideoProperties?.htmlDescription);
|
||||
|
||||
} else if(editVideoProperties?.fullDescription) {
|
||||
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`
|
||||
} else if (editVideoProperties?.fullDescription) {
|
||||
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`;
|
||||
setDescription(paragraph);
|
||||
|
||||
}
|
||||
setCoverImage(editVideoProperties?.videoImage || "");
|
||||
|
||||
if (editVideoProperties?.category) {
|
||||
const selectedOption = categories.find(
|
||||
(option) => option.id === +editVideoProperties.category
|
||||
option => option.id === +editVideoProperties.category
|
||||
);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
}
|
||||
@ -202,7 +201,7 @@ export const EditVideo = () => {
|
||||
) {
|
||||
const selectedOption = subCategories[
|
||||
+editVideoProperties?.category
|
||||
]?.find((option) => option.id === +editVideoProperties.subcategory);
|
||||
]?.find(option => option.id === +editVideoProperties.subcategory);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
}
|
||||
}
|
||||
@ -213,7 +212,7 @@ export const EditVideo = () => {
|
||||
setVideoPropertiesToSetToRedux(null);
|
||||
setFile(null);
|
||||
setTitle("");
|
||||
setImageExtracts([])
|
||||
setImageExtracts([]);
|
||||
setDescription("");
|
||||
setCoverImage("");
|
||||
};
|
||||
@ -253,7 +252,7 @@ export const EditVideo = () => {
|
||||
const category = selectedCategoryVideos.id;
|
||||
const subcategory = selectedSubCategoryVideos?.id || "";
|
||||
|
||||
const fullDescription = extractTextFromHTML(description)
|
||||
const fullDescription = extractTextFromHTML(description);
|
||||
let fileExtension = "mp4";
|
||||
const fileExtensionSplit = file?.name?.split(".");
|
||||
if (fileExtensionSplit?.length > 1) {
|
||||
@ -285,15 +284,13 @@ export const EditVideo = () => {
|
||||
subcategory,
|
||||
code: editVideoProperties.code,
|
||||
videoType: file?.type || "video/mp4",
|
||||
filename: `${alphanumericString.trim()}.${fileExtension}`
|
||||
filename: `${alphanumericString.trim()}.${fileExtension}`,
|
||||
};
|
||||
|
||||
let metadescription =
|
||||
`**category:${category};subcategory:${subcategory};code:${editVideoProperties.code}**` +
|
||||
description.slice(0, 150);
|
||||
|
||||
|
||||
|
||||
const crowdfundObjectToBase64 = await objectToBase64(videoObject);
|
||||
// Description is obtained from raw data
|
||||
const requestBodyJson: any = {
|
||||
@ -319,7 +316,7 @@ export const EditVideo = () => {
|
||||
description: metadescription,
|
||||
identifier: editVideoProperties.videoReference?.identifier,
|
||||
tag1: QTUBE_VIDEO_BASE,
|
||||
filename: `${alphanumericString.trim()}.${fileExtension}`
|
||||
filename: `${alphanumericString.trim()}.${fileExtension}`,
|
||||
};
|
||||
|
||||
listOfPublishes.push(requestBodyVideo);
|
||||
@ -356,13 +353,11 @@ export const EditVideo = () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleOptionCategoryChangeVideos = (
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find((option) => option.id === +optionId);
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChangeVideos = (
|
||||
@ -371,48 +366,45 @@ export const EditVideo = () => {
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
(option) => option.id === +optionId
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
};
|
||||
|
||||
const onFramesExtracted = async (imgs)=> {
|
||||
const onFramesExtracted = async imgs => {
|
||||
try {
|
||||
let imagesExtracts = []
|
||||
|
||||
for (const img of imgs){
|
||||
let imagesExtracts = [];
|
||||
|
||||
for (const img of imgs) {
|
||||
try {
|
||||
let compressedFile
|
||||
const image = img
|
||||
await new Promise<void>((resolve) => {
|
||||
let compressedFile;
|
||||
const image = img;
|
||||
await new Promise<void>(resolve => {
|
||||
new Compressor(image, {
|
||||
quality: .8,
|
||||
quality: 0.8,
|
||||
maxWidth: 750,
|
||||
mimeType: 'image/webp',
|
||||
mimeType: "image/webp",
|
||||
success(result) {
|
||||
const file = new File([result], 'name', {
|
||||
type: 'image/webp'
|
||||
})
|
||||
compressedFile = file
|
||||
resolve()
|
||||
const file = new File([result], "name", {
|
||||
type: "image/webp",
|
||||
});
|
||||
compressedFile = file;
|
||||
resolve();
|
||||
},
|
||||
error(err) {}
|
||||
})
|
||||
})
|
||||
if (!compressedFile) continue
|
||||
const base64Img = await toBase64(compressedFile)
|
||||
imagesExtracts.push(base64Img)
|
||||
|
||||
error(err) {},
|
||||
});
|
||||
});
|
||||
if (!compressedFile) continue;
|
||||
const base64Img = await toBase64(compressedFile);
|
||||
imagesExtracts.push(base64Img);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
setImageExtracts(imagesExtracts)
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
setImageExtracts(imagesExtracts);
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -467,7 +459,7 @@ export const EditVideo = () => {
|
||||
value={selectedCategoryVideos?.id || ""}
|
||||
onChange={handleOptionCategoryChangeVideos}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -482,26 +474,27 @@ export const EditVideo = () => {
|
||||
labelId="Sub-Category"
|
||||
input={<OutlinedInput label="Select a Sub-Category" />}
|
||||
value={selectedSubCategoryVideos?.id || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
subCategories[selectedCategoryVideos?.id]
|
||||
)
|
||||
}
|
||||
>
|
||||
{subCategories[selectedCategoryVideos.id].map(
|
||||
(option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
{subCategories[selectedCategoryVideos.id].map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Box>
|
||||
{file && (
|
||||
<FrameExtractor videoFile={file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs)}/>
|
||||
<FrameExtractor
|
||||
videoFile={file}
|
||||
onFramesExtracted={imgs => onFramesExtracted(imgs)}
|
||||
/>
|
||||
)}
|
||||
<React.Fragment>
|
||||
{!coverImage ? (
|
||||
@ -532,23 +525,27 @@ export const EditVideo = () => {
|
||||
label="Title of video"
|
||||
variant="filled"
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
const formattedValue = value.replace(
|
||||
/[^a-zA-Z0-9\s-_!?]/g,
|
||||
""
|
||||
);
|
||||
const formattedValue = value.replace(titleFormatter, "");
|
||||
setTitle(formattedValue);
|
||||
}}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
required
|
||||
/>
|
||||
<Typography sx={{
|
||||
fontSize: '18px'
|
||||
}}>Description of video</Typography>
|
||||
<TextEditor inlineContent={description} setInlineContent={(value)=> {
|
||||
setDescription(value)
|
||||
}} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
}}
|
||||
>
|
||||
Description of video
|
||||
</Typography>
|
||||
<TextEditor
|
||||
inlineContent={description}
|
||||
setInlineContent={value => {
|
||||
setDescription(value);
|
||||
}}
|
||||
/>
|
||||
{/* <CustomInputField
|
||||
name="description"
|
||||
label="Describe your video in a few words"
|
||||
@ -588,8 +585,8 @@ export const EditVideo = () => {
|
||||
disabled={file && imageExtracts.length === 0}
|
||||
>
|
||||
{file && imageExtracts.length === 0 && (
|
||||
<CircularProgress color="secondary" size={14} />
|
||||
)}
|
||||
<CircularProgress color="secondary" size={14} />
|
||||
)}
|
||||
Publish
|
||||
</CrowdfundActionButton>
|
||||
</Box>
|
||||
|
@ -3,208 +3,208 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles"
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../UploadVideo/Upload-styles";
|
||||
} from "../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { Box, Button, Input, Typography, useTheme } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import { removeVideo } from "../../state/features/videoSlice";
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../state/store";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
|
||||
export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
|
||||
const [searchResults, setSearchResults] = useState([])
|
||||
const [filterSearch, setFilterSearch] = useState("")
|
||||
const search = async()=> {
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QTUBE_VIDEO_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [filterSearch, setFilterSearch] = useState("");
|
||||
const search = async () => {
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QTUBE_VIDEO_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseDataSearchVid = await response.json()
|
||||
setSearchResults(responseDataSearchVid)
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearchVid = await response.json();
|
||||
setSearchResults(responseDataSearchVid);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
maxWidth: "300px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CardContentContainerComment
|
||||
sx={{
|
||||
marginTop: "25px",
|
||||
height: "450px",
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{playlistData?.videos?.map((vid, index) => {
|
||||
return (
|
||||
<Box
|
||||
key={vid?.identifier}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{vid?.metadata?.title}
|
||||
</Typography>
|
||||
<DeleteOutlineIcon
|
||||
onClick={() => {
|
||||
removeVideo(index);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</CardContentContainerComment>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
maxWidth: "300px",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CardContentContainerComment
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "25px",
|
||||
height: "450px",
|
||||
overflow: 'auto'
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
maxWidth: "300px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={(e) => {
|
||||
setFilterSearch(e.target.value);
|
||||
}}
|
||||
value={filterSearch}
|
||||
placeholder="Search by title"
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CardContentContainerComment
|
||||
sx={{
|
||||
marginTop: "25px",
|
||||
height: "450px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{playlistData?.videos?.map((vid, index) => {
|
||||
return (
|
||||
<Box
|
||||
key={vid?.identifier}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{vid?.metadata?.title}
|
||||
</Typography>
|
||||
<DeleteOutlineIcon
|
||||
onClick={() => {
|
||||
removeVideo(index);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</CardContentContainerComment>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
maxWidth: "300px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CardContentContainerComment
|
||||
sx={{
|
||||
marginTop: "25px",
|
||||
height: "450px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: "1px solid white",
|
||||
"&&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:hover:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
fontSize: "18px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
search();
|
||||
}}
|
||||
|
||||
variant="contained"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{searchResults?.map((vid, index) => {
|
||||
return (
|
||||
<Box
|
||||
key={vid?.identifier}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
userSelect: "none",
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={e => {
|
||||
setFilterSearch(e.target.value);
|
||||
}}
|
||||
value={filterSearch}
|
||||
placeholder="Search by title"
|
||||
sx={{
|
||||
borderBottom: "1px solid white",
|
||||
"&&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:hover:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
fontSize: "18px",
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
search();
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
<Typography
|
||||
Search
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{searchResults?.map((vid, index) => {
|
||||
return (
|
||||
<Box
|
||||
key={vid?.identifier}
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{vid?.metadata?.title}
|
||||
</Typography>
|
||||
<AddIcon
|
||||
onClick={() => {
|
||||
addVideo(vid);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</CardContentContainerComment>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{vid?.metadata?.title}
|
||||
</Typography>
|
||||
<AddIcon
|
||||
onClick={() => {
|
||||
addVideo(vid);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</CardContentContainerComment>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -1,66 +1,83 @@
|
||||
import React from 'react'
|
||||
import { CardContentContainerComment } from '../common/Comments/Comments-styles'
|
||||
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from '../UploadVideo/Upload-styles'
|
||||
import { Box, Typography, useTheme } from '@mui/material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const Playlists = ({playlistData, currentVideoIdentifier, onClick}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate()
|
||||
import React from "react";
|
||||
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { Box, Typography, useTheme } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const Playlists = ({
|
||||
playlistData,
|
||||
currentVideoIdentifier,
|
||||
onClick,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
maxWidth: '400px',
|
||||
width: '100%'
|
||||
}}>
|
||||
<CrowdfundSubTitleRow >
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
maxWidth: "400px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CardContentContainerComment sx={{
|
||||
marginTop: '25px',
|
||||
height: '450px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{playlistData?.videos?.map((vid, index)=> {
|
||||
const isCurrentVidPlayling = vid?.identifier === currentVideoIdentifier;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Box key={vid?.identifier} sx={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
background: isCurrentVidPlayling && theme.palette.primary.main,
|
||||
alignItems: 'center',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
cursor: isCurrentVidPlayling ? 'default' : 'pointer',
|
||||
userSelect: 'none'
|
||||
<CardContentContainerComment
|
||||
sx={{
|
||||
marginTop: "25px",
|
||||
height: "450px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{playlistData?.videos?.map((vid, index) => {
|
||||
const isCurrentVidPlayling =
|
||||
vid?.identifier === currentVideoIdentifier;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={vid?.identifier}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
background: isCurrentVidPlayling && theme.palette.primary.main,
|
||||
alignItems: "center",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
cursor: isCurrentVidPlayling ? "default" : "pointer",
|
||||
userSelect: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isCurrentVidPlayling) return;
|
||||
onClick(vid.name, vid.identifier);
|
||||
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
onClick={()=> {
|
||||
if(isCurrentVidPlayling) return
|
||||
onClick(vid.name, vid.identifier)
|
||||
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||
>
|
||||
{index + 1}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{
|
||||
fontSize: '14px'
|
||||
}}>{index + 1}</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: '18px',
|
||||
wordBreak: 'break-word'
|
||||
}}>{vid?.metadata?.title}</Typography>
|
||||
|
||||
</Box>
|
||||
)
|
||||
>
|
||||
{vid?.metadata?.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</CardContentContainerComment>
|
||||
</CardContentContainerComment>
|
||||
</Box>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Compressor from 'compressorjs'
|
||||
import Compressor from "compressorjs";
|
||||
import {
|
||||
AddCoverImageButton,
|
||||
AddLogoIcon,
|
||||
@ -13,7 +13,7 @@ import {
|
||||
NewCrowdfundTitle,
|
||||
StyledButton,
|
||||
TimesIcon,
|
||||
} from "./Upload-styles";
|
||||
} from "./PublishVideo-styles.tsx";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
|
||||
import {
|
||||
@ -45,12 +45,7 @@ import {
|
||||
upsertVideos,
|
||||
} from "../../state/features/videoSlice";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import {
|
||||
QTUBE_PLAYLIST_BASE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
categories,
|
||||
subCategories,
|
||||
} from "../../constants";
|
||||
import { categories, subCategories } from "../../constants/Categories.ts";
|
||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
@ -59,18 +54,28 @@ import {
|
||||
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import { FiltersCheckbox, FiltersRow, FiltersSubContainer } from "../../pages/Home/VideoList-styles";
|
||||
import {
|
||||
FiltersCheckbox,
|
||||
FiltersRow,
|
||||
FiltersSubContainer,
|
||||
} from "../../pages/Home/VideoList-styles";
|
||||
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
|
||||
import {
|
||||
QTUBE_PLAYLIST_BASE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
} from "../../constants/Identifiers.ts";
|
||||
import { titleFormatter } from "../../constants/Misc.ts";
|
||||
import { getFileName } from "../../utils/stringFunctions.ts";
|
||||
|
||||
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = error => {
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -90,7 +95,7 @@ interface VideoFile {
|
||||
description: string;
|
||||
coverImage?: string;
|
||||
}
|
||||
export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||
@ -113,6 +118,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
useState<any>(null);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [filterSearch, setFilterSearch] = useState("");
|
||||
const [titlesPrefix, setTitlesPrefix] = useState("");
|
||||
const [playlistTitle, setPlaylistTitle] = useState<string>("");
|
||||
const [playlistDescription, setPlaylistDescription] = useState<string>("");
|
||||
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
||||
@ -125,39 +131,37 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
|
||||
const [publishes, setPublishes] = useState<any[]>([]);
|
||||
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false)
|
||||
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false)
|
||||
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false)
|
||||
const [imageExtracts, setImageExtracts] = useState<any>({})
|
||||
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(true);
|
||||
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(true);
|
||||
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] =
|
||||
useState(false);
|
||||
const [imageExtracts, setImageExtracts] = useState<any>({});
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
"video/*": [],
|
||||
},
|
||||
maxSize: 419430400, // 400 MB in bytes
|
||||
onDrop: (acceptedFiles, rejectedFiles) => {
|
||||
const formatArray = acceptedFiles.map((item) => {
|
||||
const formatArray = acceptedFiles.map(item => {
|
||||
let filteredTitle = "";
|
||||
|
||||
let formatTitle = ''
|
||||
if(isCheckTitleByFile && item?.name){
|
||||
const fileExtensionSplit = item?.name?.split(".");
|
||||
if (fileExtensionSplit?.length > 1) {
|
||||
formatTitle = fileExtensionSplit[0]
|
||||
}
|
||||
formatTitle = (formatTitle || "").replace(/[^a-zA-Z0-9\s-_!?]/g, "");
|
||||
if (isCheckTitleByFile) {
|
||||
const fileName = getFileName(item?.name || "");
|
||||
filteredTitle = (titlesPrefix + fileName).replace(titleFormatter, "");
|
||||
}
|
||||
return {
|
||||
file: item,
|
||||
title: formatTitle,
|
||||
title: filteredTitle || "",
|
||||
description: "",
|
||||
coverImage: "",
|
||||
};
|
||||
});
|
||||
|
||||
setFiles((prev) => [...prev, ...formatArray]);
|
||||
setFiles(prev => [...prev, ...formatArray]);
|
||||
|
||||
let errorString = null;
|
||||
rejectedFiles.forEach(({ file, errors }) => {
|
||||
errors.forEach((error) => {
|
||||
errors.forEach(error => {
|
||||
if (error.code === "file-too-large") {
|
||||
errorString = "File must be under 400mb";
|
||||
}
|
||||
@ -204,8 +208,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (!playlistCoverImage) throw new Error("Please select cover image");
|
||||
if (!selectedCategory) throw new Error("Please select a category");
|
||||
}
|
||||
if(files?.length === 0) throw new Error("Please select at least one file");
|
||||
if(isCheckSameCoverImage && !coverImageForAll) throw new Error("Please select cover image");
|
||||
if (files?.length === 0)
|
||||
throw new Error("Please select at least one file");
|
||||
if (isCheckSameCoverImage && !coverImageForAll)
|
||||
throw new Error("Please select cover image");
|
||||
if (!userAddress) throw new Error("Unable to locate user address");
|
||||
let errorMsg = "";
|
||||
let name = "";
|
||||
@ -234,12 +240,16 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
let listOfPublishes = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const publish = files[i]
|
||||
const publish = files[i];
|
||||
const title = publish.title;
|
||||
const description = isCheckDescriptionIsTitle ? publish.title : publish.description;
|
||||
const description = isCheckDescriptionIsTitle
|
||||
? publish.title
|
||||
: publish.description;
|
||||
const category = selectedCategoryVideos.id;
|
||||
const subcategory = selectedSubCategoryVideos?.id || "";
|
||||
const coverImage = isCheckSameCoverImage ? coverImageForAll : publish.coverImage;
|
||||
const coverImage = isCheckSameCoverImage
|
||||
? coverImageForAll
|
||||
: publish.coverImage;
|
||||
const file = publish.file;
|
||||
const sanitizeTitle = title
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||
@ -255,7 +265,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
: `${QTUBE_VIDEO_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
|
||||
|
||||
const code = shortuid();
|
||||
const fullDescription = extractTextFromHTML(description)
|
||||
const fullDescription = extractTextFromHTML(description);
|
||||
|
||||
let fileExtension = "mp4";
|
||||
const fileExtensionSplit = file?.name?.split(".");
|
||||
@ -292,15 +302,13 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
subcategory,
|
||||
code,
|
||||
videoType: file?.type || "video/mp4",
|
||||
filename: `${alphanumericString.trim()}.${fileExtension}`
|
||||
filename: `${alphanumericString.trim()}.${fileExtension}`,
|
||||
};
|
||||
|
||||
let metadescription =
|
||||
`**category:${category};subcategory:${subcategory};code:${code}**` +
|
||||
fullDescription.slice(0, 150);
|
||||
|
||||
|
||||
|
||||
const crowdfundObjectToBase64 = await objectToBase64(videoObject);
|
||||
// Description is obtained from raw data
|
||||
const requestBodyJson: any = {
|
||||
@ -335,7 +343,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (isNewPlaylist) {
|
||||
const title = playlistTitle;
|
||||
const description = playlistDescription;
|
||||
const stringDescription = extractTextFromHTML(description)
|
||||
const stringDescription = extractTextFromHTML(description);
|
||||
const category = selectedCategory.id;
|
||||
const subcategory = selectedSubCategory?.id || "";
|
||||
const coverImage = playlistCoverImage;
|
||||
@ -354,10 +362,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const videos = listOfPublishes
|
||||
.filter(
|
||||
(item) =>
|
||||
item =>
|
||||
item.service === "DOCUMENT" && item.tag1 === QTUBE_VIDEO_BASE
|
||||
)
|
||||
.map((vid) => {
|
||||
.map(vid => {
|
||||
return {
|
||||
identifier: vid.identifier,
|
||||
service: vid.service,
|
||||
@ -378,7 +386,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
subcategory,
|
||||
};
|
||||
|
||||
const codes = videos.map((item) => `c:${item.code};`).slice(0,10).join("");
|
||||
const codes = videos
|
||||
.map(item => `c:${item.code};`)
|
||||
.slice(0, 10)
|
||||
.join("");
|
||||
|
||||
let metadescription =
|
||||
`**category:${category};subcategory:${subcategory};${codes}**` +
|
||||
@ -413,10 +424,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (responseData && !responseData.error) {
|
||||
const videos = listOfPublishes
|
||||
.filter(
|
||||
(item) =>
|
||||
item =>
|
||||
item.service === "DOCUMENT" && item.tag1 === QTUBE_VIDEO_BASE
|
||||
)
|
||||
.map((vid) => {
|
||||
.map(vid => {
|
||||
return {
|
||||
identifier: vid.identifier,
|
||||
service: vid.service,
|
||||
@ -431,7 +442,8 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
videos: videosInPlaylist,
|
||||
};
|
||||
const codes = videosInPlaylist
|
||||
.map((item) => `c:${item.code};`).slice(0,10)
|
||||
.map(item => `c:${item.code};`)
|
||||
.slice(0, 10)
|
||||
.join("");
|
||||
|
||||
let metadescription =
|
||||
@ -485,10 +497,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
}
|
||||
|
||||
const handleOnchange = (index: number, type: string, value: string) => {
|
||||
setFiles((prev) => {
|
||||
setFiles(prev => {
|
||||
let formattedValue = value;
|
||||
if (type === "title") {
|
||||
formattedValue = value.replace(/[^a-zA-Z0-9\s-_!?]/g, "");
|
||||
formattedValue = value.replace(titleFormatter, "");
|
||||
}
|
||||
const copyFiles = [...prev];
|
||||
copyFiles[index] = {
|
||||
@ -501,7 +513,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const handleOptionCategoryChange = (event: SelectChangeEvent<string>) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find((option) => option.id === +optionId);
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategory(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChange = (
|
||||
@ -510,7 +522,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
(option) => option.id === +optionId
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategory(selectedOption || null);
|
||||
};
|
||||
@ -519,7 +531,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find((option) => option.id === +optionId);
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChangeVideos = (
|
||||
@ -528,20 +540,24 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
(option) => option.id === +optionId
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
try {
|
||||
if(isCheckSameCoverImage && !coverImageForAll) throw new Error("Please select cover image");
|
||||
if(files?.length === 0) throw new Error("Please select at least one file");
|
||||
if (isCheckSameCoverImage && !coverImageForAll)
|
||||
throw new Error("Please select cover image");
|
||||
if (files?.length === 0)
|
||||
throw new Error("Please select at least one file");
|
||||
if (!selectedCategoryVideos) throw new Error("Please select a category");
|
||||
files.forEach((file) => {
|
||||
files.forEach(file => {
|
||||
if (!file.title) throw new Error("Please enter a title");
|
||||
if (!isCheckTitleByFile && !file.description) throw new Error("Please enter a description");
|
||||
if (!isCheckSameCoverImage && !file.coverImage) throw new Error("Please select cover image");
|
||||
if (!isCheckTitleByFile && !file.description)
|
||||
throw new Error("Please enter a description");
|
||||
if (!isCheckSameCoverImage && !file.coverImage)
|
||||
throw new Error("Please select cover image");
|
||||
});
|
||||
|
||||
setStep("playlist");
|
||||
@ -555,48 +571,45 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onFramesExtracted = async (imgs, index)=> {
|
||||
const onFramesExtracted = async (imgs, index) => {
|
||||
try {
|
||||
let imagesExtracts = []
|
||||
|
||||
for (const img of imgs){
|
||||
let imagesExtracts = [];
|
||||
|
||||
for (const img of imgs) {
|
||||
try {
|
||||
let compressedFile
|
||||
const image = img
|
||||
await new Promise<void>((resolve) => {
|
||||
let compressedFile;
|
||||
const image = img;
|
||||
await new Promise<void>(resolve => {
|
||||
new Compressor(image, {
|
||||
quality: .8,
|
||||
quality: 0.8,
|
||||
maxWidth: 750,
|
||||
mimeType: 'image/webp',
|
||||
mimeType: "image/webp",
|
||||
success(result) {
|
||||
const file = new File([result], 'name', {
|
||||
type: 'image/webp'
|
||||
})
|
||||
compressedFile = file
|
||||
resolve()
|
||||
const file = new File([result], "name", {
|
||||
type: "image/webp",
|
||||
});
|
||||
compressedFile = file;
|
||||
resolve();
|
||||
},
|
||||
error(err) {}
|
||||
})
|
||||
})
|
||||
if (!compressedFile) continue
|
||||
const base64Img = await toBase64(compressedFile)
|
||||
imagesExtracts.push(base64Img)
|
||||
|
||||
error(err) {},
|
||||
});
|
||||
});
|
||||
if (!compressedFile) continue;
|
||||
const base64Img = await toBase64(compressedFile);
|
||||
imagesExtracts.push(base64Img);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
setImageExtracts((prev)=> {
|
||||
setImageExtracts(prev => {
|
||||
return {
|
||||
...prev,
|
||||
[index]: imagesExtracts
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
[index]: imagesExtracts,
|
||||
};
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -638,38 +651,49 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
{step === "videos" && (
|
||||
<>
|
||||
<FiltersSubContainer>
|
||||
<FiltersRow>
|
||||
Populate Titles by filename (when the files are picked)
|
||||
<FiltersCheckbox
|
||||
checked={isCheckTitleByFile}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckTitleByFile(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
<FiltersSubContainer>
|
||||
<FiltersRow>
|
||||
Populate Titles by filename (when the files are picked)
|
||||
<FiltersCheckbox
|
||||
checked={isCheckTitleByFile}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckTitleByFile(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
All videos use the same Cover Image
|
||||
<FiltersCheckbox
|
||||
checked={isCheckSameCoverImage}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckSameCoverImage(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
Populate all descriptions by Title
|
||||
<FiltersCheckbox
|
||||
checked={isCheckDescriptionIsTitle}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckDescriptionIsTitle(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
</FiltersSubContainer>
|
||||
<CustomInputField
|
||||
name="prefix"
|
||||
label="Titles Prefix"
|
||||
variant="filled"
|
||||
value={titlesPrefix}
|
||||
onChange={e =>
|
||||
setTitlesPrefix(e.target.value.replace(titleFormatter, ""))
|
||||
}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
required
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
All videos use the same Cover Image
|
||||
<FiltersCheckbox
|
||||
checked={isCheckSameCoverImage}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckSameCoverImage(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
Populate all descriptions by Title
|
||||
<FiltersCheckbox
|
||||
checked={isCheckDescriptionIsTitle}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsCheckDescriptionIsTitle(e.target.checked);
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
</FiltersSubContainer>
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
@ -685,7 +709,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
Drag and drop a video files here or click to select files
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -703,7 +727,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
value={selectedCategoryVideos?.id || ""}
|
||||
onChange={handleOptionCategoryChangeVideos}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -722,7 +746,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
<OutlinedInput label="Select a Sub-Category" />
|
||||
}
|
||||
value={selectedSubCategoryVideos?.id || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
subCategories[selectedCategoryVideos?.id]
|
||||
@ -730,7 +754,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
}
|
||||
>
|
||||
{subCategories[selectedCategoryVideos.id].map(
|
||||
(option) => (
|
||||
option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -743,83 +767,85 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
)}
|
||||
</Box>
|
||||
{files?.length > 0 && isCheckSameCoverImage && (
|
||||
<>
|
||||
{!coverImageForAll ? (
|
||||
<ImageUploader
|
||||
onPick={(img: string) =>
|
||||
setCoverImageForAll(img)
|
||||
}
|
||||
>
|
||||
<AddCoverImageButton variant="contained">
|
||||
Add Cover Image
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddCoverImageButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<CoverImagePreview src={coverImageForAll} alt="logo" />
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() =>
|
||||
setCoverImageForAll(null)
|
||||
}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{!coverImageForAll ? (
|
||||
<ImageUploader
|
||||
onPick={(img: string) => setCoverImageForAll(img)}
|
||||
>
|
||||
<AddCoverImageButton variant="contained">
|
||||
Add Cover Image
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddCoverImageButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<CoverImagePreview src={coverImageForAll} alt="logo" />
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() => setCoverImageForAll(null)}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{files.map((file, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<FrameExtractor videoFile={file.file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs, index)}/>
|
||||
<FrameExtractor
|
||||
videoFile={file.file}
|
||||
onFramesExtracted={imgs => onFramesExtracted(imgs, index)}
|
||||
/>
|
||||
<Typography>{file?.file?.name}</Typography>
|
||||
{!isCheckSameCoverImage && (
|
||||
<>
|
||||
{!file?.coverImage ? (
|
||||
<ImageUploader
|
||||
onPick={(img: string) =>
|
||||
handleOnchange(index, "coverImage", img)
|
||||
}
|
||||
>
|
||||
<AddCoverImageButton variant="contained">
|
||||
Add Cover Image
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddCoverImageButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<CoverImagePreview src={file?.coverImage} alt="logo" />
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() =>
|
||||
handleOnchange(index, "coverImage", "")
|
||||
}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
{!file?.coverImage ? (
|
||||
<ImageUploader
|
||||
onPick={(img: string) =>
|
||||
handleOnchange(index, "coverImage", img)
|
||||
}
|
||||
>
|
||||
<AddCoverImageButton variant="contained">
|
||||
Add Cover Image
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddCoverImageButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<CoverImagePreview
|
||||
src={file?.coverImage}
|
||||
alt="logo"
|
||||
/>
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() =>
|
||||
handleOnchange(index, "coverImage", "")
|
||||
}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
<CustomInputField
|
||||
name="title"
|
||||
label="Title of video"
|
||||
variant="filled"
|
||||
value={file.title}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleOnchange(index, "title", e.target.value)
|
||||
}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
@ -827,15 +853,22 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
/>
|
||||
{!isCheckDescriptionIsTitle && (
|
||||
<>
|
||||
<Typography sx={{
|
||||
fontSize: '18px'
|
||||
}}>Description of video</Typography>
|
||||
<TextEditor inlineContent={file?.description} setInlineContent={(value)=> {
|
||||
handleOnchange(index, "description", value)
|
||||
}} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
}}
|
||||
>
|
||||
Description of video
|
||||
</Typography>
|
||||
<TextEditor
|
||||
inlineContent={file?.description}
|
||||
setInlineContent={value => {
|
||||
handleOnchange(index, "description", value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* <CustomInputField
|
||||
name="description"
|
||||
label="Describe your video in a few words"
|
||||
@ -962,7 +995,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
setFilterSearch(e.target.value);
|
||||
}}
|
||||
value={filterSearch}
|
||||
@ -1072,11 +1105,11 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
label="Title of playlist"
|
||||
variant="filled"
|
||||
value={playlistTitle}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
let formattedValue: string = value;
|
||||
|
||||
formattedValue = value.replace(/[^a-zA-Z0-9\s-_!?]/g, "");
|
||||
formattedValue = value.replace(titleFormatter, "");
|
||||
|
||||
setPlaylistTitle(formattedValue);
|
||||
}}
|
||||
@ -1095,12 +1128,19 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
required
|
||||
/> */}
|
||||
|
||||
<Typography sx={{
|
||||
fontSize: '18px'
|
||||
}}>Description of playlist</Typography>
|
||||
<TextEditor inlineContent={playlistDescription} setInlineContent={(value)=> {
|
||||
setPlaylistDescription(value)
|
||||
}} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
}}
|
||||
>
|
||||
Description of playlist
|
||||
</Typography>
|
||||
<TextEditor
|
||||
inlineContent={playlistDescription}
|
||||
setInlineContent={value => {
|
||||
setPlaylistDescription(value);
|
||||
}}
|
||||
/>
|
||||
<FormControl fullWidth sx={{ marginBottom: 2, marginTop: 2 }}>
|
||||
<InputLabel id="Category">Select a Category</InputLabel>
|
||||
<Select
|
||||
@ -1109,7 +1149,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
value={selectedCategory?.id || ""}
|
||||
onChange={handleOptionCategoryChange}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -1125,14 +1165,14 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
labelId="Sub-Category"
|
||||
input={<OutlinedInput label="Select a Sub-Category" />}
|
||||
value={selectedSubCategory?.id || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChange(
|
||||
e,
|
||||
subCategories[selectedCategory?.id]
|
||||
)
|
||||
}
|
||||
>
|
||||
{subCategories[selectedCategory.id].map((option) => (
|
||||
{subCategories[selectedCategory.id].map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -1186,37 +1226,41 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
) : (
|
||||
<CrowdfundActionButton
|
||||
variant="contained"
|
||||
disabled={files?.length !== Object.keys(imageExtracts)?.length}
|
||||
disabled={
|
||||
files?.length !== Object.keys(imageExtracts)?.length
|
||||
}
|
||||
onClick={() => {
|
||||
next();
|
||||
}}
|
||||
>
|
||||
{files?.length !== Object.keys(imageExtracts)?.length ? 'Generating image extracts' : ''}
|
||||
{files?.length !== Object.keys(imageExtracts)?.length
|
||||
? "Generating image extracts"
|
||||
: ""}
|
||||
{files?.length !== Object.keys(imageExtracts)?.length && (
|
||||
<CircularProgress color="secondary" size={14} />
|
||||
)}
|
||||
Next
|
||||
Next
|
||||
</CrowdfundActionButton>
|
||||
)}
|
||||
</Box>
|
||||
</CrowdfundActionButtonRow>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
||||
|
||||
{isOpenMultiplePublish && (
|
||||
<MultiplePublish
|
||||
isOpen={isOpenMultiplePublish}
|
||||
onSubmit={() => {
|
||||
setIsOpenMultiplePublish(false);
|
||||
setIsOpen(false);
|
||||
setImageExtracts({})
|
||||
setImageExtracts({});
|
||||
setFiles([]);
|
||||
setStep("videos");
|
||||
setPlaylistCoverImage(null);
|
||||
setPlaylistTitle("");
|
||||
setPlaylistDescription("");
|
||||
setSelectedCategory(null);
|
||||
setCoverImageForAll(null)
|
||||
setCoverImageForAll(null);
|
||||
setSelectedSubCategory(null);
|
||||
setSelectedCategoryVideos(null);
|
||||
setSelectedSubCategoryVideos(null);
|
@ -11,7 +11,8 @@ import {
|
||||
CommentInputContainer,
|
||||
SubmitCommentButton,
|
||||
} from "./Comments-styles";
|
||||
import { COMMENT_BASE } from "../../../constants";
|
||||
|
||||
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||
const uid = new ShortUniqueId();
|
||||
|
||||
const notification = localforage.createInstance({
|
||||
|
@ -14,8 +14,11 @@ import {
|
||||
LoadMoreCommentsButtonRow,
|
||||
NoCommentsRow,
|
||||
} from "./Comments-styles";
|
||||
import { COMMENT_BASE } from "../../../constants";
|
||||
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from "../../UploadVideo/Upload-styles";
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||
|
||||
interface CommentSectionProps {
|
||||
postId: string;
|
||||
@ -218,11 +221,10 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Panel>
|
||||
<CrowdfundSubTitleRow >
|
||||
<CrowdfundSubTitle>Comments</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle>Comments</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CommentsContainer>
|
||||
{loadingComments ? (
|
||||
<NoCommentsRow>
|
||||
|
@ -8,13 +8,13 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import React, { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { ModalBody } from "../../UploadVideo/Upload-styles";
|
||||
import { ModalBody } from "../../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
|
||||
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
|
||||
|
||||
export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
const theme = useTheme();
|
||||
const listOfSuccessfulPublishesRef = useRef([])
|
||||
const listOfSuccessfulPublishesRef = useRef([]);
|
||||
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
|
||||
any[]
|
||||
>([]);
|
||||
@ -23,7 +23,7 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
const publish = useCallback(async (pub: any) => {
|
||||
await qortalRequest(pub);
|
||||
}, []);
|
||||
const [isPublishing, setIsPublishing] = useState(true)
|
||||
const [isPublishing, setIsPublishing] = useState(true);
|
||||
|
||||
const handlePublish = useCallback(
|
||||
async (pub: any) => {
|
||||
@ -33,10 +33,13 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
await publish(pub);
|
||||
|
||||
setListOfSuccessfulPublishes((prev: any) => [...prev, pub?.identifier]);
|
||||
listOfSuccessfulPublishesRef.current = [...listOfSuccessfulPublishesRef.current, pub?.identifier]
|
||||
listOfSuccessfulPublishesRef.current = [
|
||||
...listOfSuccessfulPublishesRef.current,
|
||||
pub?.identifier,
|
||||
];
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
await new Promise<void>((res) => {
|
||||
await new Promise<void>(res => {
|
||||
setTimeout(() => {
|
||||
res();
|
||||
}, 5000);
|
||||
@ -49,17 +52,18 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
|
||||
const startPublish = useCallback(
|
||||
async (pubs: any) => {
|
||||
setIsPublishing(true)
|
||||
const filterPubs = pubs.filter((pub)=> !listOfSuccessfulPublishesRef.current.includes(pub.identifier))
|
||||
setIsPublishing(true);
|
||||
const filterPubs = pubs.filter(
|
||||
pub => !listOfSuccessfulPublishesRef.current.includes(pub.identifier)
|
||||
);
|
||||
for (const pub of filterPubs) {
|
||||
await handlePublish(pub);
|
||||
|
||||
}
|
||||
|
||||
if(listOfSuccessfulPublishesRef.current.length === pubs.length){
|
||||
onSubmit()
|
||||
|
||||
if (listOfSuccessfulPublishesRef.current.length === pubs.length) {
|
||||
onSubmit();
|
||||
}
|
||||
setIsPublishing(false)
|
||||
setIsPublishing(false);
|
||||
},
|
||||
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
|
||||
);
|
||||
@ -71,7 +75,6 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
}
|
||||
}, [startPublish, publishes, listOfSuccessfulPublishes]);
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
@ -118,18 +121,28 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{!isPublishing && listOfSuccessfulPublishes.length !== publishes.length && (
|
||||
<>
|
||||
<Typography sx={{
|
||||
marginTop: '20px',
|
||||
fontSize: '16px'
|
||||
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography>
|
||||
<Button onClick={()=> {
|
||||
startPublish(publishes)
|
||||
}}>Try again</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPublishing &&
|
||||
listOfSuccessfulPublishes.length !== publishes.length && (
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
Some files were not published. Please try again. It's important
|
||||
that all the files get published. Maybe wait a couple minutes if
|
||||
the error keeps occurring
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
startPublish(publishes);
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -1,324 +1,347 @@
|
||||
import { Badge, Box, Button, List, ListItem, ListItemText, Popover, Typography } from '@mui/material'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../state/store'
|
||||
import { FOR, FOR_SUPER_LIKE, SUPER_LIKE_BASE, minPriceSuperlike } from '../../../constants'
|
||||
import NotificationsIcon from '@mui/icons-material/Notifications'
|
||||
import { formatDate } from '../../../utils/time'
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Popover,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "../../../state/store";
|
||||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||||
import { formatDate } from "../../../utils/time";
|
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
|
||||
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from '../../../pages/VideoContent/VideoContent'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
extractSigValue,
|
||||
getPaymentInfo,
|
||||
isTimestampWithinRange,
|
||||
} from "../../../pages/VideoContent/VideoContent";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import localForage from "localforage";
|
||||
import moment from 'moment'
|
||||
import moment from "moment";
|
||||
import {
|
||||
FOR,
|
||||
FOR_SUPER_LIKE,
|
||||
SUPER_LIKE_BASE,
|
||||
} from "../../../constants/Identifiers.ts";
|
||||
import { minPriceSuperlike } from "../../../constants/Misc.ts";
|
||||
|
||||
const generalLocal = localForage.createInstance({
|
||||
name: "q-tube-general",
|
||||
});
|
||||
name: "q-tube-general",
|
||||
});
|
||||
export function extractIdValue(metadescription) {
|
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) {
|
||||
const match = str.match(/\*\*(.*?)\*\*/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) {
|
||||
const regex = /id:(.*?)(;|$)/;
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription);
|
||||
|
||||
if (relevantSubstring) {
|
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) {
|
||||
const match = str.match(/\*\*(.*?)\*\*/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) {
|
||||
const regex = /id:(.*?)(;|$)/;
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription);
|
||||
|
||||
if (relevantSubstring) {
|
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const Notifications = () => {
|
||||
const dispatch = useDispatch()
|
||||
const [anchorElNotification, setAnchorElNotification] = useState<HTMLButtonElement | null>(null)
|
||||
const [notifications, setNotifications] = useState<any[]>([])
|
||||
const [notificationTimestamp, setNotificationTimestamp] = useState<null | number>(null)
|
||||
const dispatch = useDispatch();
|
||||
const [anchorElNotification, setAnchorElNotification] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [notifications, setNotifications] = useState<any[]>([]);
|
||||
const [notificationTimestamp, setNotificationTimestamp] = useState<
|
||||
null | number
|
||||
>(null);
|
||||
|
||||
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
const usernameAddress = useSelector((state: RootState) => state.auth?.user?.address);
|
||||
const navigate = useNavigate();
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
const usernameAddress = useSelector(
|
||||
(state: RootState) => state.auth?.user?.address
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const interval = useRef<any>(null)
|
||||
const interval = useRef<any>(null);
|
||||
|
||||
const getInitialTimestamp = async ()=> {
|
||||
const timestamp: undefined | number = await generalLocal.getItem("notification-timestamp");
|
||||
if(timestamp){
|
||||
setNotificationTimestamp(timestamp)
|
||||
}
|
||||
const getInitialTimestamp = async () => {
|
||||
const timestamp: undefined | number = await generalLocal.getItem(
|
||||
"notification-timestamp"
|
||||
);
|
||||
if (timestamp) {
|
||||
setNotificationTimestamp(timestamp);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
getInitialTimestamp()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getInitialTimestamp();
|
||||
}, []);
|
||||
|
||||
const openNotificationPopover = (event: any) => {
|
||||
const target = event.currentTarget as unknown as HTMLButtonElement | null
|
||||
setAnchorElNotification(target)
|
||||
}
|
||||
const closeNotificationPopover = () => {
|
||||
setAnchorElNotification(null)
|
||||
}
|
||||
const fullNotifications = useMemo(() => {
|
||||
return [...notifications].sort(
|
||||
(a, b) => b.created - a.created
|
||||
)
|
||||
}, [notifications])
|
||||
const notificationBadgeLength = useMemo(()=> {
|
||||
if(!notificationTimestamp) return fullNotifications.length
|
||||
return fullNotifications?.filter((item)=> item.created > notificationTimestamp).length
|
||||
}, [fullNotifications, notificationTimestamp])
|
||||
const openNotificationPopover = (event: any) => {
|
||||
const target = event.currentTarget as unknown as HTMLButtonElement | null;
|
||||
setAnchorElNotification(target);
|
||||
};
|
||||
const closeNotificationPopover = () => {
|
||||
setAnchorElNotification(null);
|
||||
};
|
||||
const fullNotifications = useMemo(() => {
|
||||
return [...notifications].sort((a, b) => b.created - a.created);
|
||||
}, [notifications]);
|
||||
const notificationBadgeLength = useMemo(() => {
|
||||
if (!notificationTimestamp) return fullNotifications.length;
|
||||
return fullNotifications?.filter(
|
||||
item => item.created > notificationTimestamp
|
||||
).length;
|
||||
}, [fullNotifications, notificationTimestamp]);
|
||||
|
||||
const checkNotifications = useCallback(async (username: string) => {
|
||||
try {
|
||||
// let notificationComments: Item[] =
|
||||
// (await notification.getItem('comments')) || []
|
||||
// notificationComments = notificationComments
|
||||
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
||||
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
||||
const checkNotifications = useCallback(async (username: string) => {
|
||||
try {
|
||||
// let notificationComments: Item[] =
|
||||
// (await notification.getItem('comments')) || []
|
||||
// notificationComments = notificationComments
|
||||
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
||||
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
||||
|
||||
const timestamp = await generalLocal.getItem("notification-timestamp");
|
||||
|
||||
const after = timestamp || moment().subtract(5, 'days').valueOf();
|
||||
const timestamp = await generalLocal.getItem("notification-timestamp");
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
let notifys = []
|
||||
for (const comment of responseDataSearch) {
|
||||
if (comment.identifier && comment.name && comment?.metadata?.description) {
|
||||
|
||||
|
||||
try {
|
||||
const result = extractSigValue(comment?.metadata?.description)
|
||||
if(!result) continue
|
||||
const res = await getPaymentInfo(result);
|
||||
if(+res?.amount >= minPriceSuperlike && res.recipient === usernameAddress && isTimestampWithinRange(res?.timestamp, comment.created)){
|
||||
|
||||
let urlReference = null
|
||||
try {
|
||||
let idForUrl = extractIdValue(comment?.metadata?.description)
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`;
|
||||
const response2 = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseSearch = await response2.json();
|
||||
if(responseSearch.length > 0){
|
||||
urlReference = responseSearch[0]
|
||||
}
|
||||
const after = timestamp || moment().subtract(5, "days").valueOf();
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||
// const response = await fetch(url, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// });
|
||||
// if(!response.ok) continue
|
||||
// const responseData2 = await response.text();
|
||||
|
||||
notifys = [...notifys, {
|
||||
...comment,
|
||||
amount: res.amount,
|
||||
urlReference: urlReference || null
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
setNotifications((prev) => {
|
||||
const allNotifications = [...notifys, ...prev];
|
||||
const uniqueNotifications = Array.from(new Map(allNotifications.map(notif => [notif.identifier, notif])).values());
|
||||
return uniqueNotifications.slice(0, 20);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log({ error })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const checkNotificationsFunc = useCallback(
|
||||
(username: string) => {
|
||||
let isCalling = false
|
||||
interval.current = setInterval(async () => {
|
||||
if (isCalling) return
|
||||
isCalling = true
|
||||
const res = await checkNotifications(username)
|
||||
isCalling = false
|
||||
}, 60000)
|
||||
checkNotifications(username)
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
[checkNotifications])
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
let notifys = [];
|
||||
for (const comment of responseDataSearch) {
|
||||
if (
|
||||
comment.identifier &&
|
||||
comment.name &&
|
||||
comment?.metadata?.description
|
||||
) {
|
||||
try {
|
||||
const result = extractSigValue(comment?.metadata?.description);
|
||||
if (!result) continue;
|
||||
const res = await getPaymentInfo(result);
|
||||
if (
|
||||
+res?.amount >= minPriceSuperlike &&
|
||||
res.recipient === usernameAddress &&
|
||||
isTimestampWithinRange(res?.timestamp, comment.created)
|
||||
) {
|
||||
let urlReference = null;
|
||||
try {
|
||||
let idForUrl = extractIdValue(comment?.metadata?.description);
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`;
|
||||
const response2 = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseSearch = await response2.json();
|
||||
if (responseSearch.length > 0) {
|
||||
urlReference = responseSearch[0];
|
||||
}
|
||||
} catch (error) {}
|
||||
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||
// const response = await fetch(url, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// });
|
||||
// if(!response.ok) continue
|
||||
// const responseData2 = await response.text();
|
||||
|
||||
useEffect(() => {
|
||||
if (!username) return
|
||||
checkNotificationsFunc(username)
|
||||
|
||||
|
||||
|
||||
return () => {
|
||||
if (interval?.current) {
|
||||
clearInterval(interval.current)
|
||||
}
|
||||
notifys = [
|
||||
...notifys,
|
||||
{
|
||||
...comment,
|
||||
amount: res.amount,
|
||||
urlReference: urlReference || null,
|
||||
},
|
||||
];
|
||||
}
|
||||
}, [checkNotificationsFunc, username])
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
setNotifications(prev => {
|
||||
const allNotifications = [...notifys, ...prev];
|
||||
const uniqueNotifications = Array.from(
|
||||
new Map(
|
||||
allNotifications.map(notif => [notif.identifier, notif])
|
||||
).values()
|
||||
);
|
||||
return uniqueNotifications.slice(0, 20);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openPopover = Boolean(anchorElNotification)
|
||||
const checkNotificationsFunc = useCallback(
|
||||
(username: string) => {
|
||||
let isCalling = false;
|
||||
interval.current = setInterval(async () => {
|
||||
if (isCalling) return;
|
||||
isCalling = true;
|
||||
const res = await checkNotifications(username);
|
||||
isCalling = false;
|
||||
}, 60000);
|
||||
checkNotifications(username);
|
||||
},
|
||||
[checkNotifications]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!username) return;
|
||||
checkNotificationsFunc(username);
|
||||
|
||||
return () => {
|
||||
if (interval?.current) {
|
||||
clearInterval(interval.current);
|
||||
}
|
||||
};
|
||||
}, [checkNotificationsFunc, username]);
|
||||
|
||||
const openPopover = Boolean(anchorElNotification);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
|
||||
<Badge
|
||||
badgeContent={notificationBadgeLength}
|
||||
color="primary"
|
||||
sx={{
|
||||
margin: '0px 12px'
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
openNotificationPopover(e)
|
||||
generalLocal.setItem("notification-timestamp", Date.now());
|
||||
setNotificationTimestamp(Date.now)
|
||||
}}
|
||||
<Badge
|
||||
badgeContent={notificationBadgeLength}
|
||||
color="primary"
|
||||
sx={{
|
||||
margin: '0px',
|
||||
padding: '0px',
|
||||
height: 'auto',
|
||||
width: 'auto',
|
||||
minWidth: 'unset'
|
||||
margin: "0px 12px",
|
||||
}}
|
||||
>
|
||||
<NotificationsIcon color="action" />
|
||||
</Button>
|
||||
</Badge>
|
||||
<Popover
|
||||
id={'simple-popover-notification'}
|
||||
open={openPopover}
|
||||
anchorEl={anchorElNotification}
|
||||
onClose={closeNotificationPopover}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<List
|
||||
<Button
|
||||
onClick={e => {
|
||||
openNotificationPopover(e);
|
||||
generalLocal.setItem("notification-timestamp", Date.now());
|
||||
setNotificationTimestamp(Date.now);
|
||||
}}
|
||||
sx={{
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto'
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
height: "auto",
|
||||
width: "auto",
|
||||
minWidth: "unset",
|
||||
}}
|
||||
>
|
||||
<NotificationsIcon color="action" />
|
||||
</Button>
|
||||
</Badge>
|
||||
<Popover
|
||||
id={"simple-popover-notification"}
|
||||
open={openPopover}
|
||||
anchorEl={anchorElNotification}
|
||||
onClose={closeNotificationPopover}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<List
|
||||
sx={{
|
||||
maxHeight: "300px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{fullNotifications.length === 0 && (
|
||||
<ListItem
|
||||
|
||||
<ListItem>
|
||||
<ListItemText primary="No new notifications"></ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
{fullNotifications.map((notification: any, index: number) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
divider
|
||||
sx={{
|
||||
cursor: notification?.urlReference ? "pointer" : "default",
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (notification?.urlReference) {
|
||||
navigate(
|
||||
`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary="No new notifications">
|
||||
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
{fullNotifications.map((notification: any, index: number) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
divider
|
||||
sx={{
|
||||
cursor: notification?.urlReference ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={async () => {
|
||||
if(notification?.urlReference){
|
||||
navigate(`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px'
|
||||
}}>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body1"
|
||||
color="textPrimary"
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body1"
|
||||
color="textPrimary"
|
||||
>
|
||||
Super Like
|
||||
|
||||
</Typography>
|
||||
<ThumbUpIcon
|
||||
style={{
|
||||
color: "gold",
|
||||
|
||||
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<React.Fragment>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '16px'
|
||||
}}
|
||||
color="textSecondary"
|
||||
>
|
||||
{formatDate(notification.created)}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '16px'
|
||||
}}
|
||||
color="textSecondary"
|
||||
>
|
||||
{` from ${notification.name}`}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Typography>
|
||||
<ThumbUpIcon
|
||||
style={{
|
||||
color: "gold",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<React.Fragment>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
color="textSecondary"
|
||||
>
|
||||
{formatDate(notification.created)}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
color="textSecondary"
|
||||
>
|
||||
{` from ${notification.name}`}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
|
||||
import {
|
||||
Box,
|
||||
@ -7,10 +7,13 @@ import {
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Select,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import qortImg from "../../../assets/img/qort.png";
|
||||
@ -19,13 +22,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { setNotification } from "../../../state/features/notificationsSlice";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { objectToBase64 } from "../../../utils/toBase64";
|
||||
import {
|
||||
FOR,
|
||||
FOR_SUPER_LIKE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
SUPER_LIKE_BASE,
|
||||
minPriceSuperlike,
|
||||
} from "../../../constants";
|
||||
import { minPriceSuperlike } from "../../../constants/Misc.ts";
|
||||
import { CommentInput } from "../Comments/Comments-styles";
|
||||
import {
|
||||
CrowdfundActionButton,
|
||||
@ -33,9 +30,18 @@ import {
|
||||
ModalBody,
|
||||
NewCrowdfundTitle,
|
||||
Spacer,
|
||||
} from "../../UploadVideo/Upload-styles";
|
||||
} from "../../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor";
|
||||
import { RootState } from "../../../state/store";
|
||||
import {
|
||||
FOR,
|
||||
FOR_SUPER_LIKE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
SUPER_LIKE_BASE,
|
||||
} from "../../../constants/Identifiers.ts";
|
||||
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx";
|
||||
import { numberToInt, truncateNumber } from "../../../utils/numberFunctions.ts";
|
||||
import { getUserBalance } from "../../../utils/qortalRequestFunctions.ts";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 4 });
|
||||
|
||||
@ -48,17 +54,21 @@ export const SuperLike = ({
|
||||
numberOfSuperlikes,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [amount, setAmount] = useState<number>(10);
|
||||
|
||||
const [superlikeDonationAmount, setSuperlikeDonationAmount] =
|
||||
useState<number>(10);
|
||||
const [qortalDevDonationAmount, setQortalDevDonationAmount] =
|
||||
useState<number>(0);
|
||||
const [currentBalance, setCurrentBalance] = useState<string>("");
|
||||
|
||||
const [comment, setComment] = useState<string>("");
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
|
||||
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||
const [publishes, setPublishes] = useState<any[]>([]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const resetValues = () => {
|
||||
setAmount(0);
|
||||
setSuperlikeDonationAmount(0);
|
||||
setComment("");
|
||||
setPublishes([]);
|
||||
};
|
||||
@ -71,6 +81,15 @@ export const SuperLike = ({
|
||||
try {
|
||||
if (!username) throw new Error("You need a name to publish");
|
||||
if (!name) throw new Error("Could not retrieve content creator's name");
|
||||
const estimatedTransactionFees = 0.1;
|
||||
const donationExceedsBalance =
|
||||
superlikeDonationAmount +
|
||||
qortalDevDonationAmount +
|
||||
estimatedTransactionFees >=
|
||||
+currentBalance;
|
||||
if (donationExceedsBalance) {
|
||||
throw new Error("Total donations exceeds current balance");
|
||||
}
|
||||
|
||||
let resName = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
@ -83,7 +102,10 @@ export const SuperLike = ({
|
||||
|
||||
if (!address)
|
||||
throw new Error("Could not retrieve content creator's address");
|
||||
if (!amount || amount < minPriceSuperlike)
|
||||
if (
|
||||
!superlikeDonationAmount ||
|
||||
superlikeDonationAmount < minPriceSuperlike
|
||||
)
|
||||
throw new Error(
|
||||
`The amount needs to be at least ${minPriceSuperlike} QORT`
|
||||
);
|
||||
@ -94,9 +116,26 @@ export const SuperLike = ({
|
||||
action: "SEND_COIN",
|
||||
coin: "QORT",
|
||||
destinationAddress: address,
|
||||
amount: amount,
|
||||
amount: superlikeDonationAmount,
|
||||
});
|
||||
|
||||
const devDonation = qortalDevDonationAmount > 0;
|
||||
if (devDonation) {
|
||||
const devFundName = "DevFund";
|
||||
|
||||
let devFundNameData = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: devFundName,
|
||||
});
|
||||
|
||||
const devFundAddress = devFundNameData.owner;
|
||||
const resDevFund = await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "QORT",
|
||||
destinationAddress: devFundAddress,
|
||||
amount: qortalDevDonationAmount,
|
||||
});
|
||||
}
|
||||
let metadescription = `**sig:${
|
||||
res.signature
|
||||
};${FOR}:${name}_${FOR_SUPER_LIKE};nm:${name.slice(
|
||||
@ -119,7 +158,7 @@ export const SuperLike = ({
|
||||
for: `${name}_${FOR_SUPER_LIKE}`,
|
||||
},
|
||||
about:
|
||||
"Super likes are a way to suppert your favorite content creators. Attach a message to the Super like and have your message seen before normal comments. There is a minimum amount for a Super like. Each Super like is verified before displaying to make there aren't any non-paid Super likes",
|
||||
"Super likes are a way to suppert your favorite content creators. Attach a message to the Super like and have your message seen before normal comments. There is a minimum superLikeAmount for a Super like. Each Super like is verified before displaying to make there aren't any non-paid Super likes",
|
||||
});
|
||||
// Description is obtained from raw data
|
||||
// const base64 = utf8ToBase64(comment);
|
||||
@ -164,6 +203,14 @@ export const SuperLike = ({
|
||||
throw new Error("Failed to publish Super Like");
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUserBalance().then(foundBalance => {
|
||||
setCurrentBalance(truncateNumber(foundBalance, 2));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const textFieldWidth = "350px";
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
@ -245,42 +292,44 @@ export const SuperLike = ({
|
||||
<NewCrowdfundTitle>Super Like</NewCrowdfundTitle>
|
||||
</Box>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: "300px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<InputLabel htmlFor="standard-adornment-amount">
|
||||
Amount in QORT (min 10 QORT)
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="standard-adornment-amount"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(+e.target.value)}
|
||||
startAdornment={
|
||||
<Box>
|
||||
<InputLabel htmlFor="standard-adornment-amount">
|
||||
Amount in QORT (min 10 QORT)
|
||||
</InputLabel>
|
||||
<BoundedNumericTextField
|
||||
minValue={10}
|
||||
initialValue={minPriceSuperlike.toString()}
|
||||
maxValue={numberToInt(+currentBalance)}
|
||||
allowDecimals={false}
|
||||
allowNegatives={false}
|
||||
id="standard-adornment-amount"
|
||||
value={superlikeDonationAmount}
|
||||
afterChange={(e: string) => setSuperlikeDonationAmount(+e)}
|
||||
InputProps={{
|
||||
style: { fontSize: 30, width: textFieldWidth },
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<img
|
||||
style={{
|
||||
height: "15px",
|
||||
width: "15px",
|
||||
height: "40px",
|
||||
width: "40px",
|
||||
}}
|
||||
src={qortImg}
|
||||
alt={"Qort Icon"}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="25px" />
|
||||
<Box>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>Current QORT Balance is: {currentBalance}</div>
|
||||
<Spacer height="25px" />
|
||||
|
||||
<CommentInput
|
||||
id="standard-multiline-flexible"
|
||||
label="Your comment"
|
||||
multiline
|
||||
minRows={8}
|
||||
maxRows={8}
|
||||
variant="filled"
|
||||
value={comment}
|
||||
@ -288,7 +337,37 @@ export const SuperLike = ({
|
||||
maxLength: 500,
|
||||
}}
|
||||
InputLabelProps={{ style: { fontSize: "18px" } }}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
/>
|
||||
<Spacer height="50px" />
|
||||
<InputLabel
|
||||
htmlFor="standard-adornment-amount"
|
||||
style={{ paddingBottom: "10px" }}
|
||||
>
|
||||
Would you like to donate to Qortal Development?
|
||||
</InputLabel>
|
||||
<BoundedNumericTextField
|
||||
minValue={0}
|
||||
initialValue={""}
|
||||
maxValue={numberToInt(+currentBalance)}
|
||||
allowDecimals={false}
|
||||
value={superlikeDonationAmount}
|
||||
afterChange={(e: string) => setQortalDevDonationAmount(+e)}
|
||||
InputProps={{
|
||||
style: { fontSize: 30, width: textFieldWidth },
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<img
|
||||
style={{
|
||||
height: "40px",
|
||||
width: "40px",
|
||||
}}
|
||||
src={qortImg}
|
||||
alt={"Qort Icon"}
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
@ -332,7 +411,7 @@ export const SuperLike = ({
|
||||
message: comment,
|
||||
service,
|
||||
identifier,
|
||||
amount: +amount,
|
||||
amount: +superlikeDonationAmount,
|
||||
created: Date.now(),
|
||||
});
|
||||
setIsOpenMultiplePublish(false);
|
||||
|
@ -11,8 +11,8 @@ import {
|
||||
CommentInputContainer,
|
||||
SubmitCommentButton,
|
||||
} from "./Comments-styles";
|
||||
import { COMMENT_BASE } from "../../../constants";
|
||||
import { addtoHashMapSuperlikes } from "../../../state/features/videoSlice";
|
||||
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||
const uid = new ShortUniqueId();
|
||||
|
||||
const notification = localforage.createInstance({
|
||||
@ -84,9 +84,9 @@ interface CommentEditorProps {
|
||||
commentId?: string;
|
||||
isEdit?: boolean;
|
||||
commentMessage?: string;
|
||||
isSuperLike?: boolean
|
||||
isSuperLike?: boolean;
|
||||
comment?: any;
|
||||
hasHash?: boolean
|
||||
hasHash?: boolean;
|
||||
}
|
||||
|
||||
export function utf8ToBase64(inputString: string): string {
|
||||
@ -111,7 +111,7 @@ export const CommentEditor = ({
|
||||
commentMessage,
|
||||
isSuperLike,
|
||||
comment,
|
||||
hasHash
|
||||
hasHash,
|
||||
}: CommentEditorProps) => {
|
||||
const [value, setValue] = useState<string>("");
|
||||
const dispatch = useDispatch();
|
||||
@ -156,34 +156,41 @@ export const CommentEditor = ({
|
||||
}
|
||||
|
||||
try {
|
||||
let data64 = null
|
||||
let description = ""
|
||||
let tag1 = ""
|
||||
let superObj = {}
|
||||
if(isSuperLike){
|
||||
if(!comment?.metadata?.description || !comment?.metadata?.tags[0] || !comment?.transactionReference || !comment?.notificationInformation || !comment?.about) throw new Error('unable to edit Super like')
|
||||
description = comment?.metadata?.description
|
||||
tag1 = comment?.metadata?.tags[0]
|
||||
superObj = {
|
||||
let data64 = null;
|
||||
let description = "";
|
||||
let tag1 = "";
|
||||
let superObj = {};
|
||||
if (isSuperLike) {
|
||||
if (
|
||||
!comment?.metadata?.description ||
|
||||
!comment?.metadata?.tags[0] ||
|
||||
!comment?.transactionReference ||
|
||||
!comment?.notificationInformation ||
|
||||
!comment?.about
|
||||
)
|
||||
throw new Error("unable to edit Super like");
|
||||
description = comment?.metadata?.description;
|
||||
tag1 = comment?.metadata?.tags[0];
|
||||
superObj = {
|
||||
comment: value,
|
||||
transactionReference: comment.transactionReference,
|
||||
notificationInformation: comment.notificationInformation,
|
||||
about: comment.about
|
||||
}
|
||||
about: comment.about,
|
||||
};
|
||||
const superLikeToBase64 = await objectToBase64(superObj);
|
||||
data64 = superLikeToBase64
|
||||
data64 = superLikeToBase64;
|
||||
}
|
||||
if(isSuperLike && !data64) throw new Error('unable to edit Super like')
|
||||
if (isSuperLike && !data64) throw new Error("unable to edit Super like");
|
||||
|
||||
const base64 = utf8ToBase64(value);
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: name,
|
||||
service: "BLOG_COMMENT",
|
||||
service: "BLOG_COMMENT",
|
||||
data64: isSuperLike ? data64 : base64,
|
||||
identifier: identifier,
|
||||
description,
|
||||
tag1
|
||||
tag1,
|
||||
});
|
||||
dispatch(
|
||||
setNotification({
|
||||
@ -192,13 +199,14 @@ export const CommentEditor = ({
|
||||
})
|
||||
);
|
||||
|
||||
if(isSuperLike){
|
||||
dispatch(addtoHashMapSuperlikes({
|
||||
...superObj,
|
||||
...comment,
|
||||
message: value
|
||||
}))
|
||||
|
||||
if (isSuperLike) {
|
||||
dispatch(
|
||||
addtoHashMapSuperlikes({
|
||||
...superObj,
|
||||
...comment,
|
||||
message: value,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (idForNotification) {
|
||||
addItem({
|
||||
@ -240,7 +248,7 @@ export const CommentEditor = ({
|
||||
|
||||
let identifier = `${COMMENT_BASE}${postId.slice(-12)}_base_${id}`;
|
||||
let idForNotification = identifier;
|
||||
let service = 'BLOG_COMMENT'
|
||||
let service = "BLOG_COMMENT";
|
||||
if (isReply && commentId) {
|
||||
const removeBaseCommentId = commentId;
|
||||
removeBaseCommentId.replace("_base_", "");
|
||||
@ -252,10 +260,10 @@ export const CommentEditor = ({
|
||||
if (isEdit && commentId) {
|
||||
identifier = commentId;
|
||||
}
|
||||
|
||||
|
||||
await publishComment(identifier, idForNotification);
|
||||
if(isSuperLike){
|
||||
onSubmit({})
|
||||
if (isSuperLike) {
|
||||
onSubmit({});
|
||||
} else {
|
||||
onSubmit({
|
||||
created: Date.now(),
|
||||
@ -265,7 +273,7 @@ export const CommentEditor = ({
|
||||
name: user?.name,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setValue("");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -14,14 +14,17 @@ import {
|
||||
LoadMoreCommentsButtonRow,
|
||||
NoCommentsRow,
|
||||
} from "./Comments-styles";
|
||||
import { COMMENT_BASE } from "../../../constants";
|
||||
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from "../../UploadVideo/Upload-styles";
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||
|
||||
interface CommentSectionProps {
|
||||
postId: string;
|
||||
postName: string;
|
||||
superlikes: any[];
|
||||
getMore: ()=> void;
|
||||
getMore: () => void;
|
||||
loadingSuperLikes: boolean;
|
||||
}
|
||||
|
||||
@ -49,7 +52,13 @@ const Panel = styled("div")`
|
||||
background-color: #555;
|
||||
}
|
||||
`;
|
||||
export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postName, getMore }: CommentSectionProps) => {
|
||||
export const SuperLikesSection = ({
|
||||
loadingSuperLikes,
|
||||
superlikes,
|
||||
postId,
|
||||
postName,
|
||||
getMore,
|
||||
}: CommentSectionProps) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [listComments, setListComments] = useState<any[]>([]);
|
||||
@ -59,7 +68,7 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
|
||||
const [loadingComments, setLoadingComments] = useState<boolean>(null);
|
||||
const hashMapSuperlikes = useSelector(
|
||||
(state: RootState) => state.video.hashMapSuperlikes
|
||||
)
|
||||
);
|
||||
const onSubmit = (obj?: any, isEdit?: boolean) => {
|
||||
if (isEdit) {
|
||||
setListComments((prev: any[]) => {
|
||||
@ -147,32 +156,28 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
|
||||
[postId]
|
||||
);
|
||||
|
||||
const getComments = useCallback(
|
||||
async (superlikes, postId) => {
|
||||
try {
|
||||
setLoadingComments(true);
|
||||
|
||||
let comments: any[] = [];
|
||||
for (const comment of superlikes) {
|
||||
comments.push(comment);
|
||||
const res = await getReplies(comment.identifier, postId);
|
||||
comments = [...comments, ...res];
|
||||
}
|
||||
|
||||
setListComments(comments);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingComments(false);
|
||||
const getComments = useCallback(async (superlikes, postId) => {
|
||||
try {
|
||||
setLoadingComments(true);
|
||||
|
||||
let comments: any[] = [];
|
||||
for (const comment of superlikes) {
|
||||
comments.push(comment);
|
||||
const res = await getReplies(comment.identifier, postId);
|
||||
comments = [...comments, ...res];
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
setListComments(comments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingComments(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if(postId){
|
||||
getComments(superlikes, postId)
|
||||
if (postId) {
|
||||
getComments(superlikes, postId);
|
||||
}
|
||||
}, [getComments, superlikes, postId]);
|
||||
|
||||
@ -191,41 +196,44 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
|
||||
}, []);
|
||||
}, [listComments]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Panel>
|
||||
<CrowdfundSubTitleRow >
|
||||
<CrowdfundSubTitle sx={{
|
||||
fontSize: '18px',
|
||||
color: 'gold'
|
||||
}}>Super Likes</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitleRow>
|
||||
<CrowdfundSubTitle
|
||||
sx={{
|
||||
fontSize: "18px",
|
||||
color: "gold",
|
||||
}}
|
||||
>
|
||||
Super Likes
|
||||
</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CommentsContainer>
|
||||
{(loadingComments || loadingSuperLikes) ? (
|
||||
{loadingComments || loadingSuperLikes ? (
|
||||
<NoCommentsRow>
|
||||
<CircularProgress />
|
||||
</NoCommentsRow>
|
||||
) : listComments.length === 0 ? (
|
||||
) : listComments.length === 0 ? (
|
||||
<NoCommentsRow>
|
||||
There are no super likes yet. Be the first!
|
||||
</NoCommentsRow>
|
||||
) : (
|
||||
<CommentContainer>
|
||||
{structuredCommentList.map((comment: any) => {
|
||||
let hasHash = false
|
||||
let message = {...comment}
|
||||
let hash = {}
|
||||
if(hashMapSuperlikes[comment?.identifier]){
|
||||
message.message = hashMapSuperlikes[comment?.identifier]?.comment || ""
|
||||
hasHash = true
|
||||
hash = hashMapSuperlikes[comment?.identifier]
|
||||
let hasHash = false;
|
||||
let message = { ...comment };
|
||||
let hash = {};
|
||||
if (hashMapSuperlikes[comment?.identifier]) {
|
||||
message.message =
|
||||
hashMapSuperlikes[comment?.identifier]?.comment || "";
|
||||
hasHash = true;
|
||||
hash = hashMapSuperlikes[comment?.identifier];
|
||||
}
|
||||
return (
|
||||
<Comment
|
||||
key={comment?.identifier}
|
||||
comment={{...message, ...hash}}
|
||||
comment={{ ...message, ...hash }}
|
||||
onSubmit={onSubmit}
|
||||
postId={postId}
|
||||
postName={postName}
|
||||
@ -241,7 +249,7 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
|
||||
<LoadMoreCommentsButtonRow>
|
||||
<LoadMoreCommentsButton
|
||||
onClick={() => {
|
||||
getMore()
|
||||
getMore();
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
|
@ -35,8 +35,8 @@ import {
|
||||
} from "../../../state/features/videoSlice";
|
||||
import { RootState } from "../../../state/store";
|
||||
import { useWindowSize } from "../../../hooks/useWindowSize";
|
||||
import { UploadVideo } from "../../UploadVideo/UploadVideo";
|
||||
import { StyledButton } from "../../UploadVideo/Upload-styles";
|
||||
import { PublishVideo } from "../../PublishVideo/PublishVideo.tsx";
|
||||
import { StyledButton } from "../../PublishVideo/PublishVideo-styles.tsx";
|
||||
import { Notifications } from "../../common/Notifications/Notifications";
|
||||
interface Props {
|
||||
isAuthenticated: boolean;
|
||||
@ -279,10 +279,10 @@ const NavBar: React.FC<Props> = ({
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
inputRef={inputRef}
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
searchValRef.current = e.target.value;
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
onKeyDown={event => {
|
||||
if (event.key === "Enter" || event.keyCode === 13) {
|
||||
if (!searchValRef.current) {
|
||||
dispatch(setIsFiltering(false));
|
||||
@ -355,9 +355,7 @@ const NavBar: React.FC<Props> = ({
|
||||
/>
|
||||
</Box>
|
||||
</Popover>
|
||||
{isAuthenticated && userName && (
|
||||
<Notifications />
|
||||
)}
|
||||
{isAuthenticated && userName && <Notifications />}
|
||||
|
||||
<DownloadTaskManager />
|
||||
{isAuthenticated && userName && (
|
||||
@ -393,20 +391,18 @@ const NavBar: React.FC<Props> = ({
|
||||
<AvatarContainer>
|
||||
{isAuthenticated && userName && (
|
||||
<>
|
||||
<UploadVideo />
|
||||
<StyledButton
|
||||
color="primary"
|
||||
startIcon={<AddBoxIcon />}
|
||||
onClick={() => {
|
||||
dispatch(setEditPlaylist({mode: 'new'}))
|
||||
}}
|
||||
>
|
||||
create playlist
|
||||
</StyledButton>
|
||||
<PublishVideo />
|
||||
<StyledButton
|
||||
color="primary"
|
||||
startIcon={<AddBoxIcon />}
|
||||
onClick={() => {
|
||||
dispatch(setEditPlaylist({ mode: "new" }));
|
||||
}}
|
||||
>
|
||||
create playlist
|
||||
</StyledButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
</AvatarContainer>
|
||||
|
||||
<Popover
|
||||
|
107
src/constants/Categories.ts
Normal file
107
src/constants/Categories.ts
Normal file
@ -0,0 +1,107 @@
|
||||
interface SubCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CategoryMap {
|
||||
[key: number]: SubCategory[];
|
||||
}
|
||||
|
||||
const sortCategory = (a: SubCategory, b: SubCategory) => {
|
||||
if (a.name === "Other") return 1;
|
||||
else if (b.name === "Other") return -1;
|
||||
else return a.name.localeCompare(b.name);
|
||||
};
|
||||
export const categories = [
|
||||
{ id: 1, name: "Movies" },
|
||||
{ id: 2, name: "Series" },
|
||||
{ id: 3, name: "Music" },
|
||||
{ id: 4, name: "Education" },
|
||||
{ id: 5, name: "Lifestyle" },
|
||||
{ id: 6, name: "Gaming" },
|
||||
{ id: 7, name: "Technology" },
|
||||
{ id: 8, name: "Sports" },
|
||||
{ id: 9, name: "News & Politics" },
|
||||
{ id: 10, name: "Cooking & Food" },
|
||||
{ id: 11, name: "Animation" },
|
||||
{ id: 12, name: "Science" },
|
||||
{ id: 13, name: "Health & Wellness" },
|
||||
{ id: 14, name: "DIY & Crafts" },
|
||||
{ id: 15, name: "Kids & Family" },
|
||||
{ id: 16, name: "Comedy" },
|
||||
{ id: 17, name: "Travel & Adventure" },
|
||||
{ id: 18, name: "Art & Design" },
|
||||
{ id: 19, name: "Nature & Environment" },
|
||||
{ id: 20, name: "Business & Finance" },
|
||||
{ id: 21, name: "Personal Development" },
|
||||
{ id: 22, name: "Other" },
|
||||
{ id: 23, name: "History" },
|
||||
{ id: 24, name: "Anime" },
|
||||
{ id: 25, name: "Cartoons" },
|
||||
{ id: 26, name: "Qortal" },
|
||||
].sort(sortCategory);
|
||||
|
||||
export const subCategories: CategoryMap = {
|
||||
1: [
|
||||
// Movies
|
||||
{ id: 101, name: "Action & Adventure" },
|
||||
{ id: 102, name: "Comedy" },
|
||||
{ id: 103, name: "Drama" },
|
||||
{ id: 104, name: "Fantasy & Science Fiction" },
|
||||
{ id: 105, name: "Horror & Thriller" },
|
||||
{ id: 106, name: "Documentaries" },
|
||||
{ id: 107, name: "Animated" },
|
||||
{ id: 108, name: "Family & Kids" },
|
||||
{ id: 109, name: "Romance" },
|
||||
{ id: 110, name: "Mystery & Crime" },
|
||||
{ id: 111, name: "Historical & War" },
|
||||
{ id: 112, name: "Musicals & Music Films" },
|
||||
{ id: 113, name: "Indie Films" },
|
||||
{ id: 114, name: "International Films" },
|
||||
{ id: 115, name: "Biographies & True Stories" },
|
||||
{ id: 116, name: "Other" },
|
||||
].sort(sortCategory),
|
||||
2: [
|
||||
// Series
|
||||
{ id: 201, name: "Dramas" },
|
||||
{ id: 202, name: "Comedies" },
|
||||
{ id: 203, name: "Reality & Competition" },
|
||||
{ id: 204, name: "Documentaries & Docuseries" },
|
||||
{ id: 205, name: "Sci-Fi & Fantasy" },
|
||||
{ id: 206, name: "Crime & Mystery" },
|
||||
{ id: 207, name: "Animated Series" },
|
||||
{ id: 208, name: "Kids & Family" },
|
||||
{ id: 209, name: "Historical & Period Pieces" },
|
||||
{ id: 210, name: "Action & Adventure" },
|
||||
{ id: 211, name: "Horror & Thriller" },
|
||||
{ id: 212, name: "Romance" },
|
||||
{ id: 213, name: "Anthologies" },
|
||||
{ id: 214, name: "International Series" },
|
||||
{ id: 215, name: "Miniseries" },
|
||||
{ id: 216, name: "Other" },
|
||||
].sort(sortCategory),
|
||||
4: [
|
||||
// Education
|
||||
{ id: 400, name: "Tutorial" },
|
||||
{ id: 401, name: "Documentary" },
|
||||
{ id: 401, name: "Qortal" },
|
||||
{ id: 402, name: "Other" },
|
||||
].sort(sortCategory),
|
||||
|
||||
24: [
|
||||
{ id: 2401, name: "Kodomomuke" },
|
||||
{ id: 2402, name: "Shonen" },
|
||||
{ id: 2403, name: "Shoujo" },
|
||||
{ id: 2404, name: "Seinen" },
|
||||
{ id: 2405, name: "Josei" },
|
||||
{ id: 2406, name: "Mecha" },
|
||||
{ id: 2407, name: "Mahou Shoujo" },
|
||||
{ id: 2408, name: "Isekai" },
|
||||
{ id: 2409, name: "Yaoi" },
|
||||
{ id: 2410, name: "Yuri" },
|
||||
{ id: 2411, name: "Harem" },
|
||||
{ id: 2412, name: "Ecchi" },
|
||||
{ id: 2413, name: "Idol" },
|
||||
{ id: 2414, name: "Other" },
|
||||
].sort(sortCategory),
|
||||
};
|
15
src/constants/Identifiers.ts
Normal file
15
src/constants/Identifiers.ts
Normal file
@ -0,0 +1,15 @@
|
||||
const useTestIdentifiers = false;
|
||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers
|
||||
? "MYTEST_vid_"
|
||||
: "qtube_vid_";
|
||||
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers
|
||||
? "MYTEST_playlist_"
|
||||
: "qtube_playlist_";
|
||||
export const SUPER_LIKE_BASE = useTestIdentifiers
|
||||
? "MYTEST_superlike_"
|
||||
: "qtube_superlike_";
|
||||
export const COMMENT_BASE = useTestIdentifiers
|
||||
? "qcomment_v1_MYTEST_"
|
||||
: "qcomment_v1_qtube_";
|
||||
export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962";
|
||||
export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`;
|
2
src/constants/Misc.ts
Normal file
2
src/constants/Misc.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const minPriceSuperlike = 10;
|
||||
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^&*+=]/g;
|
@ -1,121 +0,0 @@
|
||||
const useTestIdentifiers = true;
|
||||
|
||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers
|
||||
? "MYTEST_vid_"
|
||||
: "qtube_vid_";
|
||||
|
||||
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers
|
||||
? "MYTEST_playlist_"
|
||||
: "qtube_playlist_";
|
||||
|
||||
export const SUPER_LIKE_BASE = useTestIdentifiers
|
||||
? "MYTEST_superlike_"
|
||||
: "qtube_superlike_";
|
||||
|
||||
export const COMMENT_BASE = useTestIdentifiers
|
||||
? "qcomment_v1_MYTEST_"
|
||||
: "qcomment_v1_qtube_";
|
||||
|
||||
export const FOR = useTestIdentifiers
|
||||
? "FORTEST5"
|
||||
: "FOR0962";
|
||||
|
||||
export const FOR_SUPER_LIKE = useTestIdentifiers
|
||||
? "MYTEST_sl"
|
||||
: `qtube_sl`;
|
||||
|
||||
export const minPriceSuperlike = 10
|
||||
|
||||
interface SubCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CategoryMap {
|
||||
[key: number]: SubCategory[];
|
||||
}
|
||||
|
||||
|
||||
export const categories = [
|
||||
{"id": 1, "name": "Movies"},
|
||||
{"id": 2, "name": "Series"},
|
||||
{"id": 3, "name": "Music"},
|
||||
{"id": 4, "name": "Education"},
|
||||
{"id": 5, "name": "Lifestyle"},
|
||||
{"id": 6, "name": "Gaming"},
|
||||
{"id": 7, "name": "Technology"},
|
||||
{"id": 8, "name": "Sports"},
|
||||
{"id": 9, "name": "News & Politics"},
|
||||
{"id": 10, "name": "Cooking & Food"},
|
||||
{"id": 11, "name": "Animation"},
|
||||
{"id": 12, "name": "Science"},
|
||||
{"id": 13, "name": "Health & Wellness"},
|
||||
{"id": 14, "name": "DIY & Crafts"},
|
||||
{"id": 15, "name": "Kids & Family"},
|
||||
{"id": 16, "name": "Comedy"},
|
||||
{"id": 17, "name": "Travel & Adventure"},
|
||||
{"id": 18, "name": "Art & Design"},
|
||||
{"id": 19, "name": "Nature & Environment"},
|
||||
{"id": 20, "name": "Business & Finance"},
|
||||
{"id": 21, "name": "Personal Development"},
|
||||
{"id": 22, "name": "Other"},
|
||||
{"id": 23, "name": "History"},
|
||||
{"id": 24, "name": "Anime"},
|
||||
{"id": 25, "name": "Cartoons"}
|
||||
]
|
||||
|
||||
|
||||
export const subCategories: CategoryMap = {
|
||||
1: [ // Movies
|
||||
{"id": 101, "name": "Action & Adventure"},
|
||||
{"id": 102, "name": "Comedy"},
|
||||
{"id": 103, "name": "Drama"},
|
||||
{"id": 104, "name": "Fantasy & Science Fiction"},
|
||||
{"id": 105, "name": "Horror & Thriller"},
|
||||
{"id": 106, "name": "Documentaries"},
|
||||
{"id": 107, "name": "Animated"},
|
||||
{"id": 108, "name": "Family & Kids"},
|
||||
{"id": 109, "name": "Romance"},
|
||||
{"id": 110, "name": "Mystery & Crime"},
|
||||
{"id": 111, "name": "Historical & War"},
|
||||
{"id": 112, "name": "Musicals & Music Films"},
|
||||
{"id": 113, "name": "Indie Films"},
|
||||
{"id": 114, "name": "International Films"},
|
||||
{"id": 115, "name": "Biographies & True Stories"},
|
||||
{"id": 116, "name": "Other"}
|
||||
],
|
||||
2: [ // Series
|
||||
{"id": 201, "name": "Dramas"},
|
||||
{"id": 202, "name": "Comedies"},
|
||||
{"id": 203, "name": "Reality & Competition"},
|
||||
{"id": 204, "name": "Documentaries & Docuseries"},
|
||||
{"id": 205, "name": "Sci-Fi & Fantasy"},
|
||||
{"id": 206, "name": "Crime & Mystery"},
|
||||
{"id": 207, "name": "Animated Series"},
|
||||
{"id": 208, "name": "Kids & Family"},
|
||||
{"id": 209, "name": "Historical & Period Pieces"},
|
||||
{"id": 210, "name": "Action & Adventure"},
|
||||
{"id": 211, "name": "Horror & Thriller"},
|
||||
{"id": 212, "name": "Romance"},
|
||||
{"id": 213, "name": "Anthologies"},
|
||||
{"id": 214, "name": "International Series"},
|
||||
{"id": 215, "name": "Miniseries"},
|
||||
{"id": 216, "name": "Other"}
|
||||
],
|
||||
24: [
|
||||
{"id": 2401, "name": "Kodomomuke"},
|
||||
{"id": 2402, "name": "Shonen"},
|
||||
{"id": 2403, "name": "Shoujo"},
|
||||
{"id": 2404, "name": "Seinen"},
|
||||
{"id": 2405, "name": "Josei"},
|
||||
{"id": 2406, "name": "Mecha"},
|
||||
{"id": 2407, "name": "Mahou Shoujo"},
|
||||
{"id": 2408, "name": "Isekai"},
|
||||
{"id": 2409, "name": "Yaoi"},
|
||||
{"id": 2410, "name": "Yuri"},
|
||||
{"id": 2411, "name": "Harem"},
|
||||
{"id": 2412, "name": "Ecchi"},
|
||||
{"id": 2413, "name": "Idol"},
|
||||
{"id": 2414, "name": "Other"}
|
||||
]
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import React from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
addVideos,
|
||||
addToHashMap,
|
||||
@ -7,103 +7,108 @@ import {
|
||||
upsertVideos,
|
||||
upsertVideosBeginning,
|
||||
Video,
|
||||
upsertFilteredVideos
|
||||
} from '../state/features/videoSlice'
|
||||
upsertFilteredVideos,
|
||||
} from "../state/features/videoSlice";
|
||||
import {
|
||||
setIsLoadingGlobal, setUserAvatarHash
|
||||
} from '../state/features/globalSlice'
|
||||
import { RootState } from '../state/store'
|
||||
import { fetchAndEvaluateVideos } from '../utils/fetchVideos'
|
||||
import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE } from '../constants'
|
||||
import { RequestQueue } from '../utils/queue'
|
||||
import { queue } from '../wrappers/GlobalWrapper'
|
||||
|
||||
|
||||
setIsLoadingGlobal,
|
||||
setUserAvatarHash,
|
||||
} from "../state/features/globalSlice";
|
||||
import { RootState } from "../state/store";
|
||||
import { fetchAndEvaluateVideos } from "../utils/fetchVideos";
|
||||
import { RequestQueue } from "../utils/queue";
|
||||
import { queue } from "../wrappers/GlobalWrapper";
|
||||
import {
|
||||
QTUBE_PLAYLIST_BASE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
} from "../constants/Identifiers.ts";
|
||||
|
||||
export const useFetchVideos = () => {
|
||||
const dispatch = useDispatch()
|
||||
const dispatch = useDispatch();
|
||||
const hashMapVideos = useSelector(
|
||||
(state: RootState) => state.video.hashMapVideos
|
||||
)
|
||||
const videos = useSelector((state: RootState) => state.video.videos)
|
||||
);
|
||||
const videos = useSelector((state: RootState) => state.video.videos);
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
)
|
||||
);
|
||||
const filteredVideos = useSelector(
|
||||
(state: RootState) => state.video.filteredVideos
|
||||
)
|
||||
);
|
||||
|
||||
const checkAndUpdateVideo = React.useCallback(
|
||||
(video: Video) => {
|
||||
const existingVideo = hashMapVideos[video.id]
|
||||
const existingVideo = hashMapVideos[video.id];
|
||||
if (!existingVideo) {
|
||||
return true
|
||||
return true;
|
||||
} else if (
|
||||
video?.updated &&
|
||||
existingVideo?.updated &&
|
||||
(!existingVideo?.updated || video?.updated) > existingVideo?.updated
|
||||
) {
|
||||
return true
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[hashMapVideos]
|
||||
)
|
||||
);
|
||||
|
||||
const getAvatar = React.useCallback(async (author: string) => {
|
||||
try {
|
||||
let url = await qortalRequest({
|
||||
action: 'GET_QDN_RESOURCE_URL',
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
name: author,
|
||||
service: 'THUMBNAIL',
|
||||
identifier: 'qortal_avatar'
|
||||
})
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar",
|
||||
});
|
||||
|
||||
dispatch(setUserAvatarHash({
|
||||
name: author,
|
||||
url
|
||||
}))
|
||||
} catch (error) { }
|
||||
}, [])
|
||||
dispatch(
|
||||
setUserAvatarHash({
|
||||
name: author,
|
||||
url,
|
||||
})
|
||||
);
|
||||
} catch (error) {}
|
||||
}, []);
|
||||
|
||||
const getVideo = async (user: string, videoId: string, content: any, retries: number = 0) => {
|
||||
const getVideo = async (
|
||||
user: string,
|
||||
videoId: string,
|
||||
content: any,
|
||||
retries: number = 0
|
||||
) => {
|
||||
try {
|
||||
const res = await fetchAndEvaluateVideos({
|
||||
user,
|
||||
videoId,
|
||||
content
|
||||
})
|
||||
|
||||
dispatch(addToHashMap(res))
|
||||
content,
|
||||
});
|
||||
|
||||
dispatch(addToHashMap(res));
|
||||
} catch (error) {
|
||||
retries= retries + 1
|
||||
if (retries < 2) { // 3 is the maximum number of retries here, you can adjust it to your needs
|
||||
retries = retries + 1;
|
||||
if (retries < 2) {
|
||||
// 3 is the maximum number of retries here, you can adjust it to your needs
|
||||
queue.push(() => getVideo(user, videoId, content, retries + 1));
|
||||
} else {
|
||||
console.error('Failed to get video after 3 attempts', error);
|
||||
console.error("Failed to get video after 3 attempts", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const getNewVideos = React.useCallback(async () => {
|
||||
try {
|
||||
dispatch(setIsLoadingGlobal(true))
|
||||
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
// const responseData = await qortalRequest({
|
||||
// action: "SEARCH_QDN_RESOURCES",
|
||||
// mode: "ALL",
|
||||
@ -116,16 +121,16 @@ export const useFetchVideos = () => {
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const latestVideo = videos[0]
|
||||
if (!latestVideo) return
|
||||
const latestVideo = videos[0];
|
||||
if (!latestVideo) return;
|
||||
const findVideo = responseData?.findIndex(
|
||||
(item: any) => item?.identifier === latestVideo?.id
|
||||
)
|
||||
let fetchAll = responseData
|
||||
let willFetchAll = true
|
||||
);
|
||||
let fetchAll = responseData;
|
||||
let willFetchAll = true;
|
||||
if (findVideo !== -1) {
|
||||
willFetchAll = false
|
||||
fetchAll = responseData.slice(0, findVideo)
|
||||
willFetchAll = false;
|
||||
fetchAll = responseData.slice(0, findVideo);
|
||||
}
|
||||
|
||||
const structureData = fetchAll.map((video: any): Video => {
|
||||
@ -138,22 +143,22 @@ export const useFetchVideos = () => {
|
||||
created: video?.created,
|
||||
updated: video?.updated,
|
||||
user: video.name,
|
||||
videoImage: '',
|
||||
id: video.identifier
|
||||
}
|
||||
})
|
||||
videoImage: "",
|
||||
id: video.identifier,
|
||||
};
|
||||
});
|
||||
if (!willFetchAll) {
|
||||
dispatch(upsertVideosBeginning(structureData))
|
||||
dispatch(upsertVideosBeginning(structureData));
|
||||
}
|
||||
if (willFetchAll) {
|
||||
dispatch(addVideos(structureData))
|
||||
dispatch(addVideos(structureData));
|
||||
}
|
||||
setTimeout(()=> {
|
||||
dispatch(setCountNewVideos(0))
|
||||
}, 1000)
|
||||
setTimeout(() => {
|
||||
dispatch(setCountNewVideos(0));
|
||||
}, 1000);
|
||||
for (const content of structureData) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateVideo(content)
|
||||
const res = checkAndUpdateVideo(content);
|
||||
if (res) {
|
||||
queue.push(() => getVideo(content.user, content.id, content));
|
||||
}
|
||||
@ -161,182 +166,185 @@ export const useFetchVideos = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false))
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
}, [videos, hashMapVideos])
|
||||
}, [videos, hashMapVideos]);
|
||||
|
||||
const getVideos = React.useCallback(async (filters = {}, reset?:boolean, resetFilers?: boolean,limit?: number) => {
|
||||
try {
|
||||
const {name = '',
|
||||
category = '',
|
||||
subcategory = '',
|
||||
keywords = '',
|
||||
type = '' }: any = resetFilers ? {} : filters
|
||||
let offset = videos.length
|
||||
if(reset){
|
||||
offset = 0
|
||||
}
|
||||
const videoLimit = limit || 20
|
||||
const getVideos = React.useCallback(
|
||||
async (
|
||||
filters = {},
|
||||
reset?: boolean,
|
||||
resetFilers?: boolean,
|
||||
limit?: number
|
||||
) => {
|
||||
try {
|
||||
const {
|
||||
name = "",
|
||||
category = "",
|
||||
subcategory = "",
|
||||
keywords = "",
|
||||
type = "",
|
||||
}: any = resetFilers ? {} : filters;
|
||||
let offset = videos.length;
|
||||
if (reset) {
|
||||
offset = 0;
|
||||
}
|
||||
const videoLimit = limit || 20;
|
||||
|
||||
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`
|
||||
|
||||
|
||||
if(name){
|
||||
defaultUrl = defaultUrl + `&name=${name}`
|
||||
}
|
||||
if(category){
|
||||
if(!subcategory){
|
||||
defaultUrl = defaultUrl + `&description=category:${category}`
|
||||
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
|
||||
|
||||
if (name) {
|
||||
defaultUrl = defaultUrl + `&name=${name}`;
|
||||
}
|
||||
if (category) {
|
||||
if (!subcategory) {
|
||||
defaultUrl = defaultUrl + `&description=category:${category}`;
|
||||
} else {
|
||||
defaultUrl =
|
||||
defaultUrl +
|
||||
`&description=category:${category};subcategory:${subcategory}`;
|
||||
}
|
||||
}
|
||||
if (keywords) {
|
||||
defaultUrl = defaultUrl + `&query=${keywords}`;
|
||||
}
|
||||
if (type === "playlists") {
|
||||
defaultUrl = defaultUrl + `&service=PLAYLIST`;
|
||||
defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`;
|
||||
} else {
|
||||
defaultUrl = defaultUrl + `&description=category:${category};subcategory:${subcategory}`
|
||||
defaultUrl = defaultUrl + `&service=DOCUMENT`;
|
||||
defaultUrl = defaultUrl + `&identifier=${QTUBE_VIDEO_BASE}`;
|
||||
}
|
||||
}
|
||||
if(keywords){
|
||||
defaultUrl = defaultUrl + `&query=${keywords}`
|
||||
}
|
||||
if(type === 'playlists'){
|
||||
defaultUrl = defaultUrl + `&service=PLAYLIST`
|
||||
defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`
|
||||
|
||||
} else {
|
||||
defaultUrl = defaultUrl + `&service=DOCUMENT`
|
||||
defaultUrl = defaultUrl + `&identifier=${QTUBE_VIDEO_BASE}`
|
||||
}
|
||||
|
||||
// const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
|
||||
const url = defaultUrl
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
|
||||
const url = defaultUrl;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
// const responseData = await qortalRequest({
|
||||
// action: "SEARCH_QDN_RESOURCES",
|
||||
// mode: "ALL",
|
||||
// service: "DOCUMENT",
|
||||
// query: "${QTUBE_VIDEO_BASE}",
|
||||
// limit: 20,
|
||||
// includeMetadata: true,
|
||||
// offset: offset,
|
||||
// reverse: true,
|
||||
// excludeBlocked: true,
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const structureData = responseData.map((video: any): Video => {
|
||||
return {
|
||||
title: video?.metadata?.title,
|
||||
service: video?.service,
|
||||
category: video?.metadata?.category,
|
||||
categoryName: video?.metadata?.categoryName,
|
||||
tags: video?.metadata?.tags || [],
|
||||
description: video?.metadata?.description,
|
||||
created: video?.created,
|
||||
updated: video?.updated,
|
||||
user: video.name,
|
||||
videoImage: "",
|
||||
id: video.identifier,
|
||||
};
|
||||
});
|
||||
if (reset) {
|
||||
dispatch(addVideos(structureData));
|
||||
} else {
|
||||
dispatch(upsertVideos(structureData));
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
|
||||
|
||||
// const responseData = await qortalRequest({
|
||||
// action: "SEARCH_QDN_RESOURCES",
|
||||
// mode: "ALL",
|
||||
// service: "DOCUMENT",
|
||||
// query: "${QTUBE_VIDEO_BASE}",
|
||||
// limit: 20,
|
||||
// includeMetadata: true,
|
||||
// offset: offset,
|
||||
// reverse: true,
|
||||
// excludeBlocked: true,
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const structureData = responseData.map((video: any): Video => {
|
||||
return {
|
||||
title: video?.metadata?.title,
|
||||
service: video?.service,
|
||||
category: video?.metadata?.category,
|
||||
categoryName: video?.metadata?.categoryName,
|
||||
tags: video?.metadata?.tags || [],
|
||||
description: video?.metadata?.description,
|
||||
created: video?.created,
|
||||
updated: video?.updated,
|
||||
user: video.name,
|
||||
videoImage: '',
|
||||
id: video.identifier
|
||||
}
|
||||
})
|
||||
if(reset){
|
||||
dispatch(addVideos(structureData))
|
||||
|
||||
} else {
|
||||
dispatch(upsertVideos(structureData))
|
||||
|
||||
}
|
||||
for (const content of structureData) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateVideo(content)
|
||||
if (res) {
|
||||
for (const content of structureData) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateVideo(content);
|
||||
if (res) {
|
||||
queue.push(() => getVideo(content.user, content.id, content));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
} finally {
|
||||
}
|
||||
} catch (error) {
|
||||
console.log({error})
|
||||
} finally {
|
||||
|
||||
}
|
||||
}, [videos, hashMapVideos])
|
||||
},
|
||||
[videos, hashMapVideos]
|
||||
);
|
||||
|
||||
const getVideosFiltered = React.useCallback(async (filterValue: string) => {
|
||||
try {
|
||||
const offset = filteredVideos.length
|
||||
const replaceSpacesWithUnderscore = filterValue.replace(/ /g, '_');
|
||||
const getVideosFiltered = React.useCallback(
|
||||
async (filterValue: string) => {
|
||||
try {
|
||||
const offset = filteredVideos.length;
|
||||
const replaceSpacesWithUnderscore = filterValue.replace(/ /g, "_");
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${replaceSpacesWithUnderscore}&identifier=${QTUBE_VIDEO_BASE}&limit=10&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
|
||||
// const responseData = await qortalRequest({
|
||||
// action: "SEARCH_QDN_RESOURCES",
|
||||
// mode: "ALL",
|
||||
// service: "DOCUMENT",
|
||||
// query: replaceSpacesWithUnderscore,
|
||||
// identifier: "${QTUBE_VIDEO_BASE}",
|
||||
// limit: 20,
|
||||
// includeMetadata: true,
|
||||
// offset: offset,
|
||||
// reverse: true,
|
||||
// excludeBlocked: true,
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const structureData = responseData.map((video: any): Video => {
|
||||
return {
|
||||
title: video?.metadata?.title,
|
||||
category: video?.metadata?.category,
|
||||
categoryName: video?.metadata?.categoryName,
|
||||
tags: video?.metadata?.tags || [],
|
||||
description: video?.metadata?.description,
|
||||
created: video?.created,
|
||||
updated: video?.updated,
|
||||
user: video.name,
|
||||
videoImage: '',
|
||||
id: video.identifier
|
||||
}
|
||||
})
|
||||
dispatch(upsertFilteredVideos(structureData))
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${replaceSpacesWithUnderscore}&identifier=${QTUBE_VIDEO_BASE}&limit=10&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
|
||||
for (const content of structureData) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateVideo(content)
|
||||
if (res) {
|
||||
queue.push(() => getVideo(content.user, content.id, content));
|
||||
// const responseData = await qortalRequest({
|
||||
// action: "SEARCH_QDN_RESOURCES",
|
||||
// mode: "ALL",
|
||||
// service: "DOCUMENT",
|
||||
// query: replaceSpacesWithUnderscore,
|
||||
// identifier: "${QTUBE_VIDEO_BASE}",
|
||||
// limit: 20,
|
||||
// includeMetadata: true,
|
||||
// offset: offset,
|
||||
// reverse: true,
|
||||
// excludeBlocked: true,
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const structureData = responseData.map((video: any): Video => {
|
||||
return {
|
||||
title: video?.metadata?.title,
|
||||
category: video?.metadata?.category,
|
||||
categoryName: video?.metadata?.categoryName,
|
||||
tags: video?.metadata?.tags || [],
|
||||
description: video?.metadata?.description,
|
||||
created: video?.created,
|
||||
updated: video?.updated,
|
||||
user: video.name,
|
||||
videoImage: "",
|
||||
id: video.identifier,
|
||||
};
|
||||
});
|
||||
dispatch(upsertFilteredVideos(structureData));
|
||||
|
||||
for (const content of structureData) {
|
||||
if (content.user && content.id) {
|
||||
const res = checkAndUpdateVideo(content);
|
||||
if (res) {
|
||||
queue.push(() => getVideo(content.user, content.id, content));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
|
||||
}
|
||||
}, [filteredVideos, hashMapVideos])
|
||||
},
|
||||
[filteredVideos, hashMapVideos]
|
||||
);
|
||||
|
||||
const checkNewVideos = React.useCallback(async () => {
|
||||
try {
|
||||
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
// const responseData = await qortalRequest({
|
||||
// action: "SEARCH_QDN_RESOURCES",
|
||||
// mode: "ALL",
|
||||
@ -349,21 +357,20 @@ export const useFetchVideos = () => {
|
||||
// exactMatchNames: true,
|
||||
// name: names
|
||||
// })
|
||||
const latestVideo = videos[0]
|
||||
if (!latestVideo) return
|
||||
const latestVideo = videos[0];
|
||||
if (!latestVideo) return;
|
||||
const findVideo = responseData?.findIndex(
|
||||
(item: any) => item?.identifier === latestVideo?.id
|
||||
)
|
||||
);
|
||||
if (findVideo === -1) {
|
||||
dispatch(setCountNewVideos(responseData.length))
|
||||
return
|
||||
dispatch(setCountNewVideos(responseData.length));
|
||||
return;
|
||||
}
|
||||
const newArray = responseData.slice(0, findVideo)
|
||||
dispatch(setCountNewVideos(newArray.length))
|
||||
return
|
||||
const newArray = responseData.slice(0, findVideo);
|
||||
dispatch(setCountNewVideos(newArray.length));
|
||||
return;
|
||||
} catch (error) {}
|
||||
}, [videos])
|
||||
|
||||
}, [videos]);
|
||||
|
||||
return {
|
||||
getVideos,
|
||||
@ -372,6 +379,6 @@ export const useFetchVideos = () => {
|
||||
hashMapVideos,
|
||||
getNewVideos,
|
||||
checkNewVideos,
|
||||
getVideosFiltered
|
||||
}
|
||||
}
|
||||
getVideosFiltered,
|
||||
};
|
||||
};
|
||||
|
@ -58,11 +58,11 @@ import {
|
||||
setEditPlaylist,
|
||||
setEditVideo,
|
||||
} from "../../state/features/videoSlice";
|
||||
import { categories, subCategories } from "../../constants";
|
||||
import { categories, subCategories } from "../../constants/Categories.ts";
|
||||
import { Playlists } from "../../components/Playlists/Playlists";
|
||||
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
|
||||
import BlockIcon from "@mui/icons-material/Block";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import { LiskSuperLikeContainer } from "../../components/common/ListSuperLikes/LiskSuperLikeContainer";
|
||||
import { VideoCardImageContainer } from "./VideoCardImageContainer";
|
||||
|
||||
@ -83,19 +83,19 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
|
||||
const filterType = useSelector((state: RootState) => state.video.filterType);
|
||||
|
||||
const setFilterType = (payload) => {
|
||||
const setFilterType = payload => {
|
||||
dispatch(changeFilterType(payload));
|
||||
};
|
||||
const filterSearch = useSelector(
|
||||
(state: RootState) => state.video.filterSearch
|
||||
);
|
||||
|
||||
const setFilterSearch = (payload) => {
|
||||
const setFilterSearch = payload => {
|
||||
dispatch(changefilterSearch(payload));
|
||||
};
|
||||
const filterName = useSelector((state: RootState) => state.video.filterName);
|
||||
|
||||
const setFilterName = (payload) => {
|
||||
const setFilterName = payload => {
|
||||
dispatch(changefilterName(payload));
|
||||
};
|
||||
|
||||
@ -103,14 +103,14 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
(state: RootState) => state.video.selectedCategoryVideos
|
||||
);
|
||||
|
||||
const setSelectedCategoryVideos = (payload) => {
|
||||
const setSelectedCategoryVideos = payload => {
|
||||
dispatch(changeSelectedCategoryVideos(payload));
|
||||
};
|
||||
const selectedSubCategoryVideos = useSelector(
|
||||
(state: RootState) => state.video.selectedSubCategoryVideos
|
||||
);
|
||||
|
||||
const setSelectedSubCategoryVideos = (payload) => {
|
||||
const setSelectedSubCategoryVideos = payload => {
|
||||
dispatch(changeSelectedSubCategoryVideos(payload));
|
||||
};
|
||||
|
||||
@ -144,8 +144,6 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
|
||||
const getVideosHandler = React.useCallback(
|
||||
async (reset?: boolean, resetFilers?: boolean) => {
|
||||
|
||||
|
||||
if (!firstFetch.current || !afterFetch.current) return;
|
||||
if (isFetching.current) return;
|
||||
isFetching.current = true;
|
||||
@ -259,7 +257,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find((option) => option.id === +optionId);
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChangeVideos = (
|
||||
@ -268,7 +266,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
(option) => option.id === +optionId
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
};
|
||||
@ -284,24 +282,24 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
});
|
||||
|
||||
if (response === true) {
|
||||
dispatch(blockUser(user))
|
||||
dispatch(blockUser(user));
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
getVideosHandler(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container sx={{ width: "100%" }}>
|
||||
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3} >
|
||||
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}>
|
||||
<FiltersContainer>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
setFilterSearch(e.target.value);
|
||||
}}
|
||||
value={filterSearch}
|
||||
@ -329,11 +327,11 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
/>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={(e) => {
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value);
|
||||
}}
|
||||
value={filterName}
|
||||
placeholder="User's name"
|
||||
placeholder="User's Name (Exact)"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
@ -406,7 +404,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{categories.map((option) => (
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -428,7 +426,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
labelId="Sub-Category"
|
||||
input={<OutlinedInput label="Sub-Category" />}
|
||||
value={selectedSubCategoryVideos?.id || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
subCategories[selectedCategoryVideos?.id]
|
||||
@ -453,7 +451,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
}}
|
||||
>
|
||||
{subCategories[selectedCategoryVideos.id].map(
|
||||
(option) => (
|
||||
option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
@ -521,73 +519,171 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
</FiltersCol>
|
||||
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
|
||||
<ProductManagerRow>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<SubtitleContainer
|
||||
<Box
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
paddingLeft: "15px",
|
||||
width: "100%",
|
||||
maxWidth: "1400px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
|
||||
</SubtitleContainer>
|
||||
|
||||
<VideoCardContainer >
|
||||
{videos.map((video: any, index: number) => {
|
||||
const existingVideo = hashMapVideos[video?.id];
|
||||
let hasHash = false;
|
||||
let videoObj = video;
|
||||
if (existingVideo) {
|
||||
videoObj = existingVideo;
|
||||
hasHash = true;
|
||||
}
|
||||
<SubtitleContainer
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
paddingLeft: "15px",
|
||||
width: "100%",
|
||||
maxWidth: "1400px",
|
||||
}}
|
||||
></SubtitleContainer>
|
||||
|
||||
let avatarUrl = "";
|
||||
if (userAvatarHash[videoObj?.user]) {
|
||||
avatarUrl = userAvatarHash[videoObj?.user];
|
||||
}
|
||||
<VideoCardContainer>
|
||||
{videos.map((video: any, index: number) => {
|
||||
const existingVideo = hashMapVideos[video?.id];
|
||||
let hasHash = false;
|
||||
let videoObj = video;
|
||||
if (existingVideo) {
|
||||
videoObj = existingVideo;
|
||||
hasHash = true;
|
||||
}
|
||||
|
||||
if (hasHash && !videoObj?.videoImage && !videoObj?.image) {
|
||||
return null;
|
||||
}
|
||||
const isPlaylist = videoObj?.service === "PLAYLIST";
|
||||
let avatarUrl = "";
|
||||
if (userAvatarHash[videoObj?.user]) {
|
||||
avatarUrl = userAvatarHash[videoObj?.user];
|
||||
}
|
||||
|
||||
if (hasHash && !videoObj?.videoImage && !videoObj?.image) {
|
||||
return null;
|
||||
}
|
||||
const isPlaylist = videoObj?.service === "PLAYLIST";
|
||||
|
||||
if (isPlaylist) {
|
||||
return (
|
||||
<VideoCardCol
|
||||
onMouseEnter={() => setShowIcons(videoObj.id)}
|
||||
onMouseLeave={() => setShowIcons(null)}
|
||||
key={videoObj.id}
|
||||
>
|
||||
<IconsBox
|
||||
sx={{
|
||||
opacity: showIcons === videoObj.id ? 1 : 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{videoObj?.user === username && (
|
||||
<Tooltip title="Edit playlist" placement="top">
|
||||
<BlockIconContainer>
|
||||
<EditIcon
|
||||
onClick={() => {
|
||||
dispatch(setEditPlaylist(videoObj));
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip title="Block user content" placement="top">
|
||||
<BlockIconContainer>
|
||||
<BlockIcon
|
||||
onClick={() => {
|
||||
blockUserFunc(videoObj?.user);
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
</IconsBox>
|
||||
<VideoCard
|
||||
sx={{
|
||||
cursor: !hasHash && "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!hasHash) return;
|
||||
navigate(
|
||||
`/playlist/${videoObj?.user}/${videoObj?.id}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ResponsiveImage
|
||||
src={videoObj?.image}
|
||||
width={266}
|
||||
height={150}
|
||||
style={{
|
||||
maxHeight: "50%",
|
||||
}}
|
||||
/>
|
||||
<VideoCardTitle>{videoObj?.title}</VideoCardTitle>
|
||||
<BottomParent>
|
||||
<NameContainer
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
navigate(`/channel/${videoObj?.user}`);
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{ height: 24, width: 24 }}
|
||||
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
|
||||
alt={`${videoObj?.user}'s avatar`}
|
||||
/>
|
||||
<VideoCardName
|
||||
sx={{
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{videoObj?.user}
|
||||
</VideoCardName>
|
||||
</NameContainer>
|
||||
|
||||
{videoObj?.created && (
|
||||
<VideoUploadDate>
|
||||
{formatDate(videoObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
bottom: "5px",
|
||||
right: "5px",
|
||||
}}
|
||||
>
|
||||
<PlaylistSVG
|
||||
color={theme.palette.text.primary}
|
||||
height="36px"
|
||||
width="36px"
|
||||
/>
|
||||
</Box>
|
||||
</BottomParent>
|
||||
</VideoCard>
|
||||
</VideoCardCol>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPlaylist) {
|
||||
return (
|
||||
<VideoCardCol
|
||||
|
||||
key={videoObj.id}
|
||||
onMouseEnter={() => setShowIcons(videoObj.id)}
|
||||
onMouseLeave={() => setShowIcons(null)}
|
||||
key={videoObj.id}
|
||||
>
|
||||
|
||||
<IconsBox
|
||||
sx={{
|
||||
opacity: showIcons === videoObj.id ? 1 : 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{videoObj?.user === username && (
|
||||
<Tooltip title="Edit playlist" placement="top">
|
||||
{videoObj?.user === username && (
|
||||
<Tooltip title="Edit video properties" placement="top">
|
||||
<BlockIconContainer>
|
||||
<EditIcon
|
||||
onClick={() => {
|
||||
dispatch(setEditPlaylist(videoObj));
|
||||
dispatch(setEditVideo(videoObj));
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
|
||||
<Tooltip title="Block user content" placement="top">
|
||||
<BlockIconContainer>
|
||||
<BlockIcon
|
||||
@ -599,28 +695,25 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
</Tooltip>
|
||||
</IconsBox>
|
||||
<VideoCard
|
||||
sx={{
|
||||
cursor: !hasHash && 'default'
|
||||
}}
|
||||
onClick={() => {
|
||||
if(!hasHash) return
|
||||
navigate(
|
||||
`/playlist/${videoObj?.user}/${videoObj?.id}`
|
||||
);
|
||||
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
|
||||
}}
|
||||
>
|
||||
<ResponsiveImage
|
||||
src={videoObj?.image}
|
||||
<VideoCardImageContainer
|
||||
width={266}
|
||||
height={150}
|
||||
style={{
|
||||
maxHeight: '50%'
|
||||
}}
|
||||
videoImage={videoObj.videoImage}
|
||||
frameImages={videoObj?.extracts || []}
|
||||
/>
|
||||
<VideoCardTitle>{videoObj?.title}</VideoCardTitle>
|
||||
{/* <ResponsiveImage
|
||||
src={videoObj.videoImage}
|
||||
width={266}
|
||||
height={150}
|
||||
/> */}
|
||||
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
|
||||
<BottomParent>
|
||||
<NameContainer
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
navigate(`/channel/${videoObj?.user}`);
|
||||
}}
|
||||
@ -646,115 +739,18 @@ export const VideoList = ({ mode }: VideoListProps) => {
|
||||
{formatDate(videoObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
bottom: "5px",
|
||||
right: "5px",
|
||||
}}
|
||||
>
|
||||
<PlaylistSVG
|
||||
color={theme.palette.text.primary}
|
||||
height="36px"
|
||||
width="36px"
|
||||
/>
|
||||
</Box>
|
||||
</BottomParent>
|
||||
</VideoCard>
|
||||
</VideoCardCol>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</VideoCardContainer>
|
||||
|
||||
return (
|
||||
<VideoCardCol
|
||||
|
||||
key={videoObj.id}
|
||||
onMouseEnter={() => setShowIcons(videoObj.id)}
|
||||
onMouseLeave={() => setShowIcons(null)}
|
||||
>
|
||||
<IconsBox
|
||||
sx={{
|
||||
opacity: showIcons === videoObj.id ? 1 : 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{videoObj?.user === username && (
|
||||
<Tooltip title="Edit video properties" placement="top">
|
||||
<BlockIconContainer>
|
||||
<EditIcon
|
||||
onClick={() => {
|
||||
dispatch(setEditVideo(videoObj));
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
|
||||
)}
|
||||
|
||||
<Tooltip title="Block user content" placement="top">
|
||||
<BlockIconContainer>
|
||||
<BlockIcon
|
||||
onClick={() => {
|
||||
blockUserFunc(videoObj?.user);
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
</IconsBox>
|
||||
<VideoCard
|
||||
onClick={() => {
|
||||
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
|
||||
}}
|
||||
>
|
||||
<VideoCardImageContainer width={266}
|
||||
height={150} videoImage={videoObj.videoImage} frameImages={videoObj?.extracts || []} />
|
||||
{/* <ResponsiveImage
|
||||
src={videoObj.videoImage}
|
||||
width={266}
|
||||
height={150}
|
||||
/> */}
|
||||
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
|
||||
<BottomParent>
|
||||
<NameContainer
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/channel/${videoObj?.user}`);
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{ height: 24, width: 24 }}
|
||||
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
|
||||
alt={`${videoObj?.user}'s avatar`}
|
||||
/>
|
||||
<VideoCardName
|
||||
sx={{
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{videoObj?.user}
|
||||
</VideoCardName>
|
||||
</NameContainer>
|
||||
|
||||
{videoObj?.created && (
|
||||
<VideoUploadDate>
|
||||
{formatDate(videoObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
</BottomParent>
|
||||
</VideoCard>
|
||||
</VideoCardCol>
|
||||
);
|
||||
})}
|
||||
</VideoCardContainer>
|
||||
|
||||
<LazyLoad
|
||||
onLoadMore={getVideosHandler}
|
||||
isLoading={isLoading}
|
||||
></LazyLoad>
|
||||
</Box>
|
||||
<LazyLoad
|
||||
onLoadMore={getVideosHandler}
|
||||
isLoading={isLoading}
|
||||
></LazyLoad>
|
||||
</Box>
|
||||
</ProductManagerRow>
|
||||
</Grid>
|
||||
<FiltersCol item xs={0} lg={3} xl={2}>
|
||||
|
@ -21,8 +21,8 @@ import ResponsiveImage from "../../components/ResponsiveImage";
|
||||
import { formatDate, formatTimestampSeconds } from "../../utils/time";
|
||||
import { Video } from "../../state/features/videoSlice";
|
||||
import { queue } from "../../wrappers/GlobalWrapper";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants";
|
||||
import { VideoCardImageContainer } from "./VideoCardImageContainer";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
|
||||
|
||||
interface VideoListProps {
|
||||
mode?: string;
|
||||
@ -80,7 +80,7 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
|
||||
const copiedVideos: Video[] = [...videos];
|
||||
structureData.forEach((video: Video) => {
|
||||
const index = videos.findIndex((p) => p.id === video.id);
|
||||
const index = videos.findIndex(p => p.id === video.id);
|
||||
if (index !== -1) {
|
||||
copiedVideos[index] = video;
|
||||
} else {
|
||||
|
@ -38,12 +38,7 @@ import { CommentSection } from "../../components/common/Comments/CommentSection"
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../../components/UploadVideo/Upload-styles";
|
||||
import {
|
||||
QTUBE_VIDEO_BASE,
|
||||
SUPER_LIKE_BASE,
|
||||
minPriceSuperlike,
|
||||
} from "../../constants";
|
||||
} from "../../components/PublishVideo/PublishVideo-styles.tsx";
|
||||
import { Playlists } from "../../components/Playlists/Playlists";
|
||||
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||
import FileElement from "../../components/common/FileElement";
|
||||
@ -55,6 +50,11 @@ import {
|
||||
isTimestampWithinRange,
|
||||
} from "../VideoContent/VideoContent";
|
||||
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
|
||||
import {
|
||||
QTUBE_VIDEO_BASE,
|
||||
SUPER_LIKE_BASE,
|
||||
} from "../../constants/Identifiers.ts";
|
||||
import { minPriceSuperlike } from "../../constants/Misc.ts";
|
||||
|
||||
export const PlaylistContent = () => {
|
||||
const { name, id } = useParams();
|
||||
@ -82,7 +82,7 @@ export const PlaylistContent = () => {
|
||||
|
||||
const [nameAddress, setNameAddress] = useState<string>("");
|
||||
|
||||
const getAddressName = async (name) => {
|
||||
const getAddressName = async name => {
|
||||
const response = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name,
|
||||
@ -297,7 +297,7 @@ export const PlaylistContent = () => {
|
||||
|
||||
const nextVideo = useMemo(() => {
|
||||
const currentVideoIndex = playlistData?.videos?.findIndex(
|
||||
(item) => item?.identifier === videoData?.id
|
||||
item => item?.identifier === videoData?.id
|
||||
);
|
||||
if (currentVideoIndex !== -1) {
|
||||
const nextVideoIndex = currentVideoIndex + 1;
|
||||
@ -318,7 +318,7 @@ export const PlaylistContent = () => {
|
||||
|
||||
const onEndVideo = useCallback(() => {
|
||||
const currentVideoIndex = playlistData?.videos?.findIndex(
|
||||
(item) => item?.identifier === videoData?.id
|
||||
item => item?.identifier === videoData?.id
|
||||
);
|
||||
if (currentVideoIndex !== -1) {
|
||||
const nextVideoIndex = currentVideoIndex + 1;
|
||||
@ -490,8 +490,8 @@ export const PlaylistContent = () => {
|
||||
name={videoData?.user}
|
||||
service={videoData?.service}
|
||||
identifier={videoData?.id}
|
||||
onSuccess={(val) => {
|
||||
setSuperlikelist((prev) => [val, ...prev]);
|
||||
onSuccess={val => {
|
||||
setSuperlikelist(prev => [val, ...prev]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -554,16 +554,16 @@ export const PlaylistContent = () => {
|
||||
cursor: !descriptionHeight
|
||||
? "default"
|
||||
: isExpandedDescription
|
||||
? "default"
|
||||
: "pointer",
|
||||
? "default"
|
||||
: "pointer",
|
||||
position: "relative",
|
||||
}}
|
||||
className={
|
||||
!descriptionHeight
|
||||
? ""
|
||||
: isExpandedDescription
|
||||
? ""
|
||||
: "hover-click"
|
||||
? ""
|
||||
: "hover-click"
|
||||
}
|
||||
>
|
||||
{descriptionHeight && !isExpandedDescription && (
|
||||
@ -588,8 +588,8 @@ export const PlaylistContent = () => {
|
||||
height: !descriptionHeight
|
||||
? "auto"
|
||||
: isExpandedDescription
|
||||
? "auto"
|
||||
: "100px",
|
||||
? "auto"
|
||||
: "100px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
@ -610,7 +610,7 @@ export const PlaylistContent = () => {
|
||||
{descriptionHeight && (
|
||||
<Typography
|
||||
onClick={() => {
|
||||
setIsExpandedDescription((prev) => !prev);
|
||||
setIsExpandedDescription(prev => !prev);
|
||||
}}
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
|
@ -1,4 +1,10 @@
|
||||
import React, { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
||||
@ -32,8 +38,7 @@ import { CommentSection } from "../../components/common/Comments/CommentSection"
|
||||
import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../../components/UploadVideo/Upload-styles";
|
||||
import { FOR_SUPER_LIKE, QTUBE_VIDEO_BASE, SUPER_LIKE_BASE, minPriceSuperlike } from "../../constants";
|
||||
} from "../../components/PublishVideo/PublishVideo-styles.tsx";
|
||||
import { Playlists } from "../../components/Playlists/Playlists";
|
||||
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||
import FileElement from "../../components/common/FileElement";
|
||||
@ -42,6 +47,12 @@ import { CommentContainer } from "../../components/common/Comments/Comments-styl
|
||||
import { Comment } from "../../components/common/Comments/Comment";
|
||||
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
|
||||
import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes";
|
||||
import {
|
||||
FOR_SUPER_LIKE,
|
||||
QTUBE_VIDEO_BASE,
|
||||
SUPER_LIKE_BASE,
|
||||
} from "../../constants/Identifiers.ts";
|
||||
import { minPriceSuperlike } from "../../constants/Misc.ts";
|
||||
|
||||
export function isTimestampWithinRange(resTimestamp, resCreated) {
|
||||
// Calculate the absolute difference in milliseconds
|
||||
@ -57,25 +68,25 @@ export function isTimestampWithinRange(resTimestamp, resCreated) {
|
||||
export function extractSigValue(metadescription) {
|
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) {
|
||||
const match = str.match(/\*\*(.*?)\*\*/);
|
||||
return match ? match[1] : null;
|
||||
const match = str.match(/\*\*(.*?)\*\*/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) {
|
||||
const regex = /sig:(.*?)(;|$)/;
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] : null;
|
||||
const regex = /sig:(.*?)(;|$)/;
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription);
|
||||
|
||||
if (relevantSubstring) {
|
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring);
|
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring);
|
||||
} else {
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,63 +102,61 @@ export const getPaymentInfo = async (signature: string) => {
|
||||
// Coin payment info must be added to responseData so we can display it to the user
|
||||
const responseData = await response.json();
|
||||
if (responseData && !responseData.error) {
|
||||
return responseData;
|
||||
return responseData;
|
||||
} else {
|
||||
throw new Error('unable to get payment')
|
||||
throw new Error("unable to get payment");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('unable to get payment')
|
||||
throw new Error("unable to get payment");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const VideoContent = () => {
|
||||
const { name, id } = useParams();
|
||||
const [isExpandedDescription, setIsExpandedDescription] =
|
||||
useState<boolean>(false);
|
||||
const [superlikeList, setSuperlikelist] = useState<any[]>([])
|
||||
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false)
|
||||
const {addSuperlikeRawDataGetToList} = useFetchSuperLikes()
|
||||
const [superlikeList, setSuperlikelist] = useState<any[]>([]);
|
||||
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
|
||||
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
|
||||
|
||||
const calculateAmountSuperlike = useMemo(() => {
|
||||
const totalQort = superlikeList?.reduce((acc, curr) => {
|
||||
if (curr?.amount && !isNaN(parseFloat(curr.amount)))
|
||||
return acc + parseFloat(curr.amount);
|
||||
else return acc;
|
||||
}, 0);
|
||||
return totalQort?.toFixed(2);
|
||||
}, [superlikeList]);
|
||||
const numberOfSuperlikes = useMemo(() => {
|
||||
return superlikeList?.length ?? 0;
|
||||
}, [superlikeList]);
|
||||
|
||||
const [nameAddress, setNameAddress] = useState<string>("");
|
||||
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||
null
|
||||
);
|
||||
|
||||
const calculateAmountSuperlike = useMemo(()=> {
|
||||
const totalQort = superlikeList?.reduce((acc, curr)=> {
|
||||
if(curr?.amount && !isNaN(parseFloat(curr.amount))) return acc + parseFloat(curr.amount)
|
||||
else return acc
|
||||
}, 0)
|
||||
return totalQort?.toFixed(2)
|
||||
}, [superlikeList])
|
||||
const numberOfSuperlikes = useMemo(()=> {
|
||||
|
||||
return superlikeList?.length ?? 0
|
||||
}, [superlikeList])
|
||||
|
||||
const [nameAddress, setNameAddress] = useState<string>('')
|
||||
const [descriptionHeight, setDescriptionHeight] =
|
||||
useState<null | number>(null);
|
||||
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
const contentRef = useRef(null);
|
||||
|
||||
const getAddressName = async (name)=> {
|
||||
|
||||
const getAddressName = async name => {
|
||||
const response = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name
|
||||
name: name,
|
||||
});
|
||||
|
||||
if(response?.owner){
|
||||
setNameAddress(response.owner)
|
||||
if (response?.owner) {
|
||||
setNameAddress(response.owner);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if(name){
|
||||
|
||||
|
||||
getAddressName(name)
|
||||
useEffect(() => {
|
||||
if (name) {
|
||||
getAddressName(name);
|
||||
}
|
||||
}, [name])
|
||||
}, [name]);
|
||||
const avatarUrl = useMemo(() => {
|
||||
let url = "";
|
||||
if (name && userAvatarHash[name]) {
|
||||
@ -237,7 +246,6 @@ export const VideoContent = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (name && id) {
|
||||
const existingVideo = hashMapVideos[id];
|
||||
@ -250,84 +258,80 @@ export const VideoContent = () => {
|
||||
}
|
||||
}, [id, name]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
if (height > 100) { // Assuming 100px is your threshold
|
||||
setDescriptionHeight(100)
|
||||
if (height > 100) {
|
||||
// Assuming 100px is your threshold
|
||||
setDescriptionHeight(100);
|
||||
}
|
||||
}
|
||||
}, [videoData]);
|
||||
}, [videoData]);
|
||||
|
||||
|
||||
const getComments = useCallback(
|
||||
async (id, nameAddressParam) => {
|
||||
if(!id) return
|
||||
try {
|
||||
setLoadingSuperLikes(true);
|
||||
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice(
|
||||
0,39
|
||||
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
let comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (comment.identifier && comment.name && comment?.metadata?.description) {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const result = extractSigValue(comment?.metadata?.description)
|
||||
if(!result) continue
|
||||
const res = await getPaymentInfo(result);
|
||||
if(+res?.amount >= minPriceSuperlike && res.recipient === nameAddressParam && isTimestampWithinRange(res?.timestamp, comment.created)){
|
||||
addSuperlikeRawDataGetToList({name:comment.name, identifier:comment.identifier, content: comment})
|
||||
const getComments = useCallback(async (id, nameAddressParam) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setLoadingSuperLikes(true);
|
||||
|
||||
comments = [...comments, {
|
||||
...comment,
|
||||
message: "",
|
||||
amount: res.amount
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice(
|
||||
0,
|
||||
39
|
||||
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
let comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (
|
||||
comment.identifier &&
|
||||
comment.name &&
|
||||
comment?.metadata?.description
|
||||
) {
|
||||
try {
|
||||
const result = extractSigValue(comment?.metadata?.description);
|
||||
if (!result) continue;
|
||||
const res = await getPaymentInfo(result);
|
||||
if (
|
||||
+res?.amount >= minPriceSuperlike &&
|
||||
res.recipient === nameAddressParam &&
|
||||
isTimestampWithinRange(res?.timestamp, comment.created)
|
||||
) {
|
||||
addSuperlikeRawDataGetToList({
|
||||
name: comment.name,
|
||||
identifier: comment.identifier,
|
||||
content: comment,
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
comments = [
|
||||
...comments,
|
||||
{
|
||||
...comment,
|
||||
message: "",
|
||||
amount: res.amount,
|
||||
},
|
||||
];
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
setSuperlikelist(comments);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingSuperLikes(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
setSuperlikelist(comments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingSuperLikes(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if(!nameAddress || !id) return
|
||||
if (!nameAddress || !id) return;
|
||||
getComments(id, nameAddress);
|
||||
}, [getComments, id, nameAddress]);
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -354,59 +358,68 @@ export const VideoContent = () => {
|
||||
)}
|
||||
|
||||
<Spacer height="15px" />
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<FileAttachmentContainer>
|
||||
|
||||
<FileAttachmentFont>
|
||||
save to disk
|
||||
</FileAttachmentFont>
|
||||
<FileElement
|
||||
fileInfo={{...videoReference,
|
||||
filename: videoData?.filename || videoData?.title?.slice(0,20) + '.mp4',
|
||||
mimeType: videoData?.videoType || '"video/mp4',
|
||||
|
||||
}}
|
||||
title={videoData?.filename || videoData?.title?.slice(0,20)}
|
||||
customStyles={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</FileElement>
|
||||
</FileAttachmentContainer>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginTop: '20px',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<VideoTitle
|
||||
variant="h1"
|
||||
color="textPrimary"
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: "start",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
{videoData?.title}
|
||||
</VideoTitle>
|
||||
{videoData && (
|
||||
<SuperLike numberOfSuperlikes={numberOfSuperlikes} totalAmount={calculateAmountSuperlike} name={videoData?.user} service={videoData?.service} identifier={videoData?.id} onSuccess={(val)=> {
|
||||
setSuperlikelist((prev)=> [val, ...prev])
|
||||
}} />
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
<FileAttachmentContainer>
|
||||
<FileAttachmentFont>save to disk</FileAttachmentFont>
|
||||
<FileElement
|
||||
fileInfo={{
|
||||
...videoReference,
|
||||
filename:
|
||||
videoData?.filename ||
|
||||
videoData?.title?.slice(0, 20) + ".mp4",
|
||||
mimeType: videoData?.videoType || '"video/mp4',
|
||||
}}
|
||||
title={videoData?.filename || videoData?.title?.slice(0, 20)}
|
||||
customStyles={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</FileElement>
|
||||
</FileAttachmentContainer>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<VideoTitle
|
||||
variant="h1"
|
||||
color="textPrimary"
|
||||
sx={{
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{videoData?.title}
|
||||
</VideoTitle>
|
||||
{videoData && (
|
||||
<SuperLike
|
||||
numberOfSuperlikes={numberOfSuperlikes}
|
||||
totalAmount={calculateAmountSuperlike}
|
||||
name={videoData?.user}
|
||||
service={videoData?.service}
|
||||
identifier={videoData?.id}
|
||||
onSuccess={val => {
|
||||
setSuperlikelist(prev => [val, ...prev]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{videoData?.created && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
@ -461,10 +474,16 @@ export const VideoContent = () => {
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
width: "100%",
|
||||
cursor: !descriptionHeight ? "default" : isExpandedDescription ? "default" : "pointer",
|
||||
cursor: !descriptionHeight
|
||||
? "default"
|
||||
: isExpandedDescription
|
||||
? "default"
|
||||
: "pointer",
|
||||
position: "relative",
|
||||
}}
|
||||
className={!descriptionHeight ? "": isExpandedDescription ? "" : "hover-click"}
|
||||
className={
|
||||
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
|
||||
}
|
||||
>
|
||||
{descriptionHeight && !isExpandedDescription && (
|
||||
<Box
|
||||
@ -483,45 +502,56 @@ export const VideoContent = () => {
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
ref={contentRef}
|
||||
ref={contentRef}
|
||||
sx={{
|
||||
height: !descriptionHeight ? 'auto' : isExpandedDescription ? "auto" : "100px",
|
||||
height: !descriptionHeight
|
||||
? "auto"
|
||||
: isExpandedDescription
|
||||
? "auto"
|
||||
: "100px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{videoData?.htmlDescription ? (
|
||||
<DisplayHtml html={videoData?.htmlDescription} />
|
||||
) : (
|
||||
<VideoDescription variant="body1" color="textPrimary" sx={{
|
||||
cursor: 'default'
|
||||
}}>
|
||||
<VideoDescription
|
||||
variant="body1"
|
||||
color="textPrimary"
|
||||
sx={{
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{videoData?.fullDescription}
|
||||
</VideoDescription>
|
||||
)}
|
||||
</Box>
|
||||
{descriptionHeight && (
|
||||
<Typography
|
||||
onClick={() => {
|
||||
setIsExpandedDescription((prev) => !prev);
|
||||
}}
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "16px",
|
||||
cursor: "pointer",
|
||||
paddingLeft: "15px",
|
||||
paddingTop: "15px",
|
||||
}}
|
||||
>
|
||||
{isExpandedDescription ? "Show less" : "...more"}
|
||||
</Typography>
|
||||
<Typography
|
||||
onClick={() => {
|
||||
setIsExpandedDescription(prev => !prev);
|
||||
}}
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "16px",
|
||||
cursor: "pointer",
|
||||
paddingLeft: "15px",
|
||||
paddingTop: "15px",
|
||||
}}
|
||||
>
|
||||
{isExpandedDescription ? "Show less" : "...more"}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</VideoPlayerContainer>
|
||||
<SuperLikesSection getMore={()=> {
|
||||
<SuperLikesSection
|
||||
getMore={() => {}}
|
||||
loadingSuperLikes={loadingSuperLikes}
|
||||
superlikes={superlikeList}
|
||||
postId={id || ""}
|
||||
postName={name || ""}
|
||||
/>
|
||||
|
||||
}} loadingSuperLikes={loadingSuperLikes} superlikes={superlikeList} postId={id || ""} postName={name || ""} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
154
src/utils/BoundedNumericTextField.tsx
Normal file
154
src/utils/BoundedNumericTextField.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import {
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
} from "@mui/material";
|
||||
import React, { useRef, useState } from "react";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import RemoveIcon from "@mui/icons-material/Remove";
|
||||
import {
|
||||
removeTrailingZeros,
|
||||
setNumberWithinBounds,
|
||||
} from "./numberFunctions.ts";
|
||||
|
||||
type eventType = React.ChangeEvent<HTMLInputElement>;
|
||||
type BoundedNumericTextFieldProps = {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
addIconButtons?: boolean;
|
||||
allowDecimals?: boolean;
|
||||
allowNegatives?: boolean;
|
||||
afterChange?: (s: string) => void;
|
||||
initialValue?: string;
|
||||
maxSigDigits?: number;
|
||||
} & TextFieldProps;
|
||||
|
||||
export const BoundedNumericTextField = ({
|
||||
minValue,
|
||||
maxValue,
|
||||
addIconButtons = true,
|
||||
allowDecimals = true,
|
||||
allowNegatives = false,
|
||||
afterChange,
|
||||
initialValue,
|
||||
maxSigDigits = 6,
|
||||
...props
|
||||
}: BoundedNumericTextFieldProps) => {
|
||||
const [textFieldValue, setTextFieldValue] = useState<string>(
|
||||
initialValue || ""
|
||||
);
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const stringIsEmpty = (value: string) => {
|
||||
return value === "";
|
||||
};
|
||||
const isAllZerosNum = /^0*\.?0*$/;
|
||||
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/;
|
||||
const isIntegerNum = /^-?[0-9]+$/;
|
||||
const skipMinMaxCheck = (value: string) => {
|
||||
const lastIndexIsDecimal = value.charAt(value.length - 1) === ".";
|
||||
const isEmpty = stringIsEmpty(value);
|
||||
const isAllZeros = isAllZerosNum.test(value);
|
||||
const isInteger = isIntegerNum.test(value);
|
||||
// skipping minMax on all 0s allows values less than 1 to be entered
|
||||
|
||||
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger);
|
||||
};
|
||||
|
||||
const setMinMaxValue = (value: string): string => {
|
||||
if (skipMinMaxCheck(value)) return value;
|
||||
const valueNum = Number(value);
|
||||
|
||||
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue);
|
||||
|
||||
const numberInBounds = boundedNum === valueNum;
|
||||
return numberInBounds ? value : boundedNum.toString();
|
||||
};
|
||||
|
||||
const getSigDigits = (number: string) => {
|
||||
if (isIntegerNum.test(number)) return 0;
|
||||
const decimalSplit = number.split(".");
|
||||
return decimalSplit[decimalSplit.length - 1].length;
|
||||
};
|
||||
|
||||
const sigDigitsExceeded = (number: string, sigDigits: number) => {
|
||||
return getSigDigits(number) > sigDigits;
|
||||
};
|
||||
|
||||
const filterTypes = (value: string) => {
|
||||
if (allowDecimals === false) value = value.replace(".", "");
|
||||
if (allowNegatives === false) value = value.replace("-", "");
|
||||
if (sigDigitsExceeded(value, maxSigDigits)) {
|
||||
value = value.substring(0, value.length - 1);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const filterValue = (value: string) => {
|
||||
if (stringIsEmpty(value)) return "";
|
||||
value = filterTypes(value);
|
||||
if (isFloatNum.test(value)) {
|
||||
return setMinMaxValue(value);
|
||||
}
|
||||
return textFieldValue;
|
||||
};
|
||||
|
||||
const listeners = (e: eventType) => {
|
||||
// console.log("changeEvent:", e);
|
||||
const newValue = filterValue(e.target.value);
|
||||
setTextFieldValue(newValue);
|
||||
if (afterChange) afterChange(newValue);
|
||||
};
|
||||
|
||||
const changeValueWithIncDecButton = (changeAmount: number) => {
|
||||
const changedValue = (+textFieldValue + changeAmount).toString();
|
||||
const inBoundsValue = setMinMaxValue(changedValue);
|
||||
setTextFieldValue(inBoundsValue);
|
||||
if (afterChange) afterChange(inBoundsValue);
|
||||
};
|
||||
|
||||
const formatValueOnBlur = (e: eventType) => {
|
||||
let value = e.target.value;
|
||||
if (stringIsEmpty(value) || value === ".") {
|
||||
setTextFieldValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
value = setMinMaxValue(value);
|
||||
value = removeTrailingZeros(value);
|
||||
if (isAllZerosNum.test(value)) value = minValue.toString();
|
||||
|
||||
setTextFieldValue(value);
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { onChange, ...noChangeProps } = { ...props };
|
||||
return (
|
||||
<TextField
|
||||
{...noChangeProps}
|
||||
InputProps={{
|
||||
...props?.InputProps,
|
||||
endAdornment: addIconButtons ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => changeValueWithIncDecButton(1)}>
|
||||
<AddIcon />{" "}
|
||||
</IconButton>
|
||||
<IconButton onClick={() => changeValueWithIncDecButton(-1)}>
|
||||
<RemoveIcon />{" "}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
}}
|
||||
onChange={e => listeners(e as eventType)}
|
||||
onBlur={e => {
|
||||
formatValueOnBlur(e as eventType);
|
||||
}}
|
||||
autoComplete="off"
|
||||
value={textFieldValue}
|
||||
inputRef={ref}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoundedNumericTextField;
|
28
src/utils/numberFunctions.ts
Normal file
28
src/utils/numberFunctions.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import * as colorsys from "colorsys";
|
||||
|
||||
export const truncateNumber = (value: string | number, sigDigits: number) => {
|
||||
return Number(value).toFixed(sigDigits);
|
||||
};
|
||||
|
||||
export const changeLightness = (hexColor: string, amount: number) => {
|
||||
const hsl = colorsys.hex2Hsl(hexColor);
|
||||
hsl.l += amount;
|
||||
return colorsys.hsl2Hex(hsl);
|
||||
};
|
||||
export const removeTrailingZeros = (s: string) => {
|
||||
return Number(s).toString();
|
||||
};
|
||||
|
||||
export const setNumberWithinBounds = (
|
||||
num: number,
|
||||
minValue: number,
|
||||
maxValue: number
|
||||
) => {
|
||||
if (num > maxValue) return maxValue;
|
||||
if (num < minValue) return minValue;
|
||||
return num;
|
||||
};
|
||||
|
||||
export const numberToInt = (num: number) => {
|
||||
return Math.floor(num);
|
||||
};
|
48
src/utils/qortalRequestFunctions.ts
Normal file
48
src/utils/qortalRequestFunctions.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
AccountInfo,
|
||||
AccountName,
|
||||
GetRequestData,
|
||||
SearchTransactionResponse,
|
||||
TransactionSearchParams,
|
||||
} from "./qortalRequestTypes.ts";
|
||||
|
||||
export const getBalance = async (address: string) => {
|
||||
return (await qortalRequest({
|
||||
action: "GET_BALANCE",
|
||||
address,
|
||||
})) as number;
|
||||
};
|
||||
|
||||
export const getUserAccount = async () => {
|
||||
return (await qortalRequest({
|
||||
action: "GET_USER_ACCOUNT",
|
||||
})) as AccountInfo;
|
||||
};
|
||||
export const getUserBalance = async () => {
|
||||
const accountInfo = await getUserAccount();
|
||||
return (await getBalance(accountInfo.address)) as number;
|
||||
};
|
||||
export const getAccountNames = async (
|
||||
address: string,
|
||||
params?: GetRequestData
|
||||
) => {
|
||||
const names = (await qortalRequest({
|
||||
action: "GET_ACCOUNT_NAMES",
|
||||
address,
|
||||
...params,
|
||||
})) as AccountName[];
|
||||
|
||||
const namelessAddress = { name: "", owner: address };
|
||||
const emptyNamesFilled = names.map(({ name, owner }) => {
|
||||
return name ? { name, owner } : namelessAddress;
|
||||
});
|
||||
|
||||
return emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
|
||||
};
|
||||
|
||||
export const searchTransactions = async (params: TransactionSearchParams) => {
|
||||
return (await qortalRequest({
|
||||
action: "SEARCH_TRANSACTIONS",
|
||||
...params,
|
||||
})) as SearchTransactionResponse[];
|
||||
};
|
76
src/utils/qortalRequestTypes.ts
Normal file
76
src/utils/qortalRequestTypes.ts
Normal file
@ -0,0 +1,76 @@
|
||||
export type AccountInfo = { address: string; publicKey: string };
|
||||
export type AccountName = { name: string; owner: string };
|
||||
export type ConfirmationStatus = "CONFIRMED" | "UNCONFIRMED" | "BOTH";
|
||||
|
||||
export interface GetRequestData {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
reverse?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchTransactionResponse {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
reference: string;
|
||||
fee: string;
|
||||
signature: string;
|
||||
txGroupId: number;
|
||||
blockHeight: number;
|
||||
approvalStatus: string;
|
||||
creatorAddress: string;
|
||||
senderPublicKey: string;
|
||||
recipient: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
export type TransactionType =
|
||||
| "GENESIS"
|
||||
| "PAYMENT"
|
||||
| "REGISTER_NAME"
|
||||
| "UPDATE_NAME"
|
||||
| "SELL_NAME"
|
||||
| "CANCEL_SELL_NAME"
|
||||
| "BUY_NAME"
|
||||
| "CREATE_POLL"
|
||||
| "VOTE_ON_POLL"
|
||||
| "ARBITRARY"
|
||||
| "ISSUE_ASSET"
|
||||
| "TRANSFER_ASSET"
|
||||
| "CREATE_ASSET_ORDER"
|
||||
| "CANCEL_ASSET_ORDER"
|
||||
| "MULTI_PAYMENT"
|
||||
| "DEPLOY_AT"
|
||||
| "MESSAGE"
|
||||
| "CHAT"
|
||||
| "PUBLICIZE"
|
||||
| "AIRDROP"
|
||||
| "AT"
|
||||
| "CREATE_GROUP"
|
||||
| "UPDATE_GROUP"
|
||||
| "ADD_GROUP_ADMIN"
|
||||
| "REMOVE_GROUP_ADMIN"
|
||||
| "GROUP_BAN"
|
||||
| "CANCEL_GROUP_BAN"
|
||||
| "GROUP_KICK"
|
||||
| "GROUP_INVITE"
|
||||
| "CANCEL_GROUP_INVITE"
|
||||
| "JOIN_GROUP"
|
||||
| "LEAVE_GROUP"
|
||||
| "GROUP_APPROVAL"
|
||||
| "SET_GROUP"
|
||||
| "UPDATE_ASSET"
|
||||
| "ACCOUNT_FLAGS"
|
||||
| "ENABLE_FORGING"
|
||||
| "REWARD_SHARE"
|
||||
| "ACCOUNT_LEVEL"
|
||||
| "TRANSFER_PRIVS"
|
||||
| "PRESENCE";
|
||||
|
||||
export interface TransactionSearchParams extends GetRequestData {
|
||||
startBlock?: number;
|
||||
blockLimit?: number;
|
||||
txGroupId?: number;
|
||||
txType: TransactionType[];
|
||||
address: string;
|
||||
confirmationStatus: ConfirmationStatus;
|
||||
}
|
8
src/utils/stringFunctions.ts
Normal file
8
src/utils/stringFunctions.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const getFileExtensionIndex = (s: string) => {
|
||||
const lastIndex = s.lastIndexOf(".");
|
||||
return lastIndex > 0 ? lastIndex : s.length - 1;
|
||||
};
|
||||
|
||||
export const getFileName = (s: string) => {
|
||||
return s.substring(0, getFileExtensionIndex(s));
|
||||
};
|
@ -11,16 +11,24 @@ import { addUser } from "../state/features/authSlice";
|
||||
import NavBar from "../components/layout/Navbar/Navbar";
|
||||
import PageLoader from "../components/common/PageLoader";
|
||||
import { RootState } from "../state/store";
|
||||
import { setSuperlikesAll, setUserAvatarHash } from "../state/features/globalSlice";
|
||||
import {
|
||||
setSuperlikesAll,
|
||||
setUserAvatarHash,
|
||||
} from "../state/features/globalSlice";
|
||||
import { VideoPlayerGlobal } from "../components/common/VideoPlayerGlobal";
|
||||
import { Rnd } from "react-rnd";
|
||||
import { RequestQueue } from "../utils/queue";
|
||||
import { EditVideo } from "../components/EditVideo/EditVideo";
|
||||
import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist";
|
||||
import ConsentModal from "../components/common/ConsentModal";
|
||||
import { SUPER_LIKE_BASE, minPriceSuperlike } from "../constants";
|
||||
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from "../pages/VideoContent/VideoContent";
|
||||
import {
|
||||
extractSigValue,
|
||||
getPaymentInfo,
|
||||
isTimestampWithinRange,
|
||||
} from "../pages/VideoContent/VideoContent";
|
||||
import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes";
|
||||
import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts";
|
||||
import { minPriceSuperlike } from "../constants/Misc.ts";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
@ -32,14 +40,13 @@ let timer: number | null = null;
|
||||
export const queue = new RequestQueue();
|
||||
export const queueSuperlikes = new RequestQueue();
|
||||
|
||||
|
||||
const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isDragging = useRef(false);
|
||||
const [userAvatar, setUserAvatar] = useState<string>("");
|
||||
const user = useSelector((state: RootState) => state.auth.user);
|
||||
const {addSuperlikeRawDataGetToList} = useFetchSuperLikes()
|
||||
const interval = useRef<any>(null)
|
||||
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
|
||||
const interval = useRef<any>(null);
|
||||
|
||||
const videoPlaying = useSelector(
|
||||
(state: RootState) => state.global.videoPlaying
|
||||
@ -134,80 +141,71 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
|
||||
return isDragging.current;
|
||||
}, []);
|
||||
|
||||
const getSuperlikes = useCallback(async () => {
|
||||
try {
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
let comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (
|
||||
comment.identifier &&
|
||||
comment.name &&
|
||||
comment?.metadata?.description
|
||||
) {
|
||||
try {
|
||||
const result = extractSigValue(comment?.metadata?.description);
|
||||
if (!result) continue;
|
||||
const res = await getPaymentInfo(result);
|
||||
if (
|
||||
+res?.amount >= minPriceSuperlike &&
|
||||
isTimestampWithinRange(res?.timestamp, comment.created)
|
||||
) {
|
||||
addSuperlikeRawDataGetToList({
|
||||
name: comment.name,
|
||||
identifier: comment.identifier,
|
||||
content: comment,
|
||||
});
|
||||
|
||||
const getSuperlikes = useCallback(
|
||||
async () => {
|
||||
try {
|
||||
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
let comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (comment.identifier && comment.name && comment?.metadata?.description) {
|
||||
|
||||
|
||||
try {
|
||||
|
||||
const result = extractSigValue(comment?.metadata?.description)
|
||||
if(!result) continue
|
||||
const res = await getPaymentInfo(result);
|
||||
if(+res?.amount >= minPriceSuperlike && isTimestampWithinRange(res?.timestamp, comment.created)){
|
||||
addSuperlikeRawDataGetToList({name:comment.name, identifier:comment.identifier, content: comment})
|
||||
|
||||
comments = [...comments, {
|
||||
...comment,
|
||||
message: "",
|
||||
amount: res.amount
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
comments = [
|
||||
...comments,
|
||||
{
|
||||
...comment,
|
||||
message: "",
|
||||
amount: res.amount,
|
||||
},
|
||||
];
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
dispatch(setSuperlikesAll(comments));
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
dispatch(setSuperlikesAll(comments));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkSuperlikes = useCallback(
|
||||
() => {
|
||||
let isCalling = false
|
||||
interval.current = setInterval(async () => {
|
||||
if (isCalling) return
|
||||
isCalling = true
|
||||
const res = await getSuperlikes()
|
||||
isCalling = false
|
||||
}, 300000)
|
||||
getSuperlikes()
|
||||
},
|
||||
[getSuperlikes])
|
||||
const checkSuperlikes = useCallback(() => {
|
||||
let isCalling = false;
|
||||
interval.current = setInterval(async () => {
|
||||
if (isCalling) return;
|
||||
isCalling = true;
|
||||
const res = await getSuperlikes();
|
||||
isCalling = false;
|
||||
}, 300000);
|
||||
getSuperlikes();
|
||||
}, [getSuperlikes]);
|
||||
|
||||
useEffect(() => {
|
||||
checkSuperlikes();
|
||||
}, [checkSuperlikes]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoadingGlobal && <PageLoader />}
|
||||
|
Loading…
x
Reference in New Issue
Block a user