Browse Source

Superlike Dialog allows optional donation to DevFund.

Refactored constants/index.ts into Identifiers.ts, Categories.ts, and Misc.ts.

Regular expressions that titles allow all use new variable in Misc.ts for consistency and ease of editing it. New Characters are allowed in titles.

Categories sorted by name, "Other" is always at end of list. New Categories such as Qortal under Education have been added

Title prefix TextField added that starts all video titles with the entered value.
pull/1/head
Qortal Dev 9 months ago
parent
commit
8d3549739c
  1. 10
      .prettierrc
  2. 158
      src/components/EditPlaylist/EditPlaylist.tsx
  3. 0
      src/components/EditVideo/EditVideo-styles.tsx
  4. 143
      src/components/EditVideo/EditVideo.tsx
  5. 328
      src/components/PlaylistListEdit/PlaylistListEdit.tsx
  6. 127
      src/components/Playlists/Playlists.tsx
  7. 0
      src/components/PublishVideo/PublishVideo-styles.tsx
  8. 460
      src/components/PublishVideo/PublishVideo.tsx
  9. 3
      src/components/common/Comments/CommentEditor.tsx
  10. 14
      src/components/common/Comments/CommentSection.tsx
  11. 63
      src/components/common/MultiplePublish/MultiplePublish.tsx
  12. 605
      src/components/common/Notifications/Notifications.tsx
  13. 165
      src/components/common/SuperLike/SuperLike.tsx
  14. 66
      src/components/common/SuperLikesList/CommentEditor.tsx
  15. 100
      src/components/common/SuperLikesList/SuperLikesSection.tsx
  16. 34
      src/components/layout/Navbar/Navbar.tsx
  17. 107
      src/constants/Categories.ts
  18. 15
      src/constants/Identifiers.ts
  19. 2
      src/constants/Misc.ts
  20. 121
      src/constants/index.ts
  21. 491
      src/hooks/useFetchVideos.tsx
  22. 360
      src/pages/Home/VideoList.tsx
  23. 4
      src/pages/Home/VideoListComponentLevel.tsx
  24. 36
      src/pages/PlaylistContent/PlaylistContent.tsx
  25. 394
      src/pages/VideoContent/VideoContent.tsx
  26. 154
      src/utils/BoundedNumericTextField.tsx
  27. 28
      src/utils/numberFunctions.ts
  28. 48
      src/utils/qortalRequestFunctions.ts
  29. 76
      src/utils/qortalRequestTypes.ts
  30. 8
      src/utils/stringFunctions.ts
  31. 142
      src/wrappers/GlobalWrapper.tsx

10
.prettierrc

@ -0,0 +1,10 @@
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"tabWidth": 2,
"semi": true
}

158
src/components/EditPlaylist/EditPlaylist.tsx

@ -43,11 +43,15 @@ import {
setEditPlaylist, setEditPlaylist,
} from "../../state/features/videoSlice"; } from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader"; 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 { Playlists } from "../Playlists/Playlists";
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit"; import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../../constants/Identifiers.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -87,17 +91,17 @@ export const EditPlaylist = () => {
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] = const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
useState<any>(null); useState<any>(null);
const isNew = useMemo(()=> { const isNew = useMemo(() => {
return editVideoProperties?.mode === 'new' return editVideoProperties?.mode === "new";
}, [editVideoProperties]) }, [editVideoProperties]);
useEffect(()=> { useEffect(() => {
if(isNew){ if (isNew) {
setPlaylistData({ setPlaylistData({
videos: [] videos: [],
}) });
} }
}, [isNew]) }, [isNew]);
// useEffect(() => { // useEffect(() => {
// if (editVideoProperties) { // if (editVideoProperties) {
@ -145,7 +149,7 @@ export const EditPlaylist = () => {
// } // }
// }, [editVideoProperties]); // }, [editVideoProperties]);
const checkforPlaylist = React.useCallback(async (videoList) => { const checkforPlaylist = React.useCallback(async videoList => {
try { try {
const combinedData: any = {}; const combinedData: any = {};
const videos = []; const videos = [];
@ -175,20 +179,18 @@ export const EditPlaylist = () => {
if (editVideoProperties) { if (editVideoProperties) {
setTitle(editVideoProperties?.title || ""); setTitle(editVideoProperties?.title || "");
if(editVideoProperties?.htmlDescription){ if (editVideoProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription); setDescription(editVideoProperties?.htmlDescription);
} else if (editVideoProperties?.description) {
} else if(editVideoProperties?.description) { const paragraph = `<p>${editVideoProperties?.description}</p>`;
const paragraph = `<p>${editVideoProperties?.description}</p>`
setDescription(paragraph); setDescription(paragraph);
} }
setCoverImage(editVideoProperties?.image || ""); setCoverImage(editVideoProperties?.image || "");
setVideos(editVideoProperties?.videos || []); setVideos(editVideoProperties?.videos || []);
if (editVideoProperties?.category) { if (editVideoProperties?.category) {
const selectedOption = categories.find( const selectedOption = categories.find(
(option) => option.id === +editVideoProperties.category option => option.id === +editVideoProperties.category
); );
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
} }
@ -200,7 +202,7 @@ export const EditPlaylist = () => {
) { ) {
const selectedOption = subCategories[ const selectedOption = subCategories[
+editVideoProperties?.category +editVideoProperties?.category
]?.find((option) => option.id === +editVideoProperties.subcategory); ]?.find(option => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
} }
@ -211,24 +213,22 @@ export const EditPlaylist = () => {
}, [editVideoProperties]); }, [editVideoProperties]);
const onClose = () => { const onClose = () => {
setTitle("") setTitle("");
setDescription("") setDescription("");
setVideos([]) setVideos([]);
setPlaylistData(null) setPlaylistData(null);
setSelectedCategoryVideos(null) setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null) setSelectedSubCategoryVideos(null);
setCoverImage("") setCoverImage("");
dispatch(setEditPlaylist(null)); dispatch(setEditPlaylist(null));
}; };
async function publishQDNResource() { async function publishQDNResource() {
try { try {
if (!title) throw new Error("Please enter a title");
if(!title) throw new Error('Please enter a title') if (!description) throw new Error("Please enter a description");
if(!description) throw new Error('Please enter a description') if (!coverImage) throw new Error("Please select cover image");
if(!coverImage) throw new Error('Please select cover image') if (!selectedCategoryVideos) throw new Error("Please select a category");
if(!selectedCategoryVideos) throw new Error('Please select a category')
if (!editVideoProperties) return; if (!editVideoProperties) return;
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
@ -258,7 +258,7 @@ export const EditPlaylist = () => {
const category = selectedCategoryVideos.id; const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || ""; const subcategory = selectedSubCategoryVideos?.id || "";
const videoStructured = playlistData.videos.map((item) => { const videoStructured = playlistData.videos.map(item => {
const descriptionVid = item?.metadata?.description; const descriptionVid = item?.metadata?.description;
if (!descriptionVid) throw new Error("cannot find video code"); if (!descriptionVid) throw new Error("cannot find video code");
@ -286,13 +286,12 @@ export const EditPlaylist = () => {
}); });
const id = uid(); const id = uid();
let commentsId = editVideoProperties?.id let commentsId = editVideoProperties?.id;
if(isNew){ if (isNew) {
commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}` commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}`;
} }
const stringDescription = extractTextFromHTML(description) const stringDescription = extractTextFromHTML(description);
const playlistObject: any = { const playlistObject: any = {
title, title,
@ -303,10 +302,13 @@ export const EditPlaylist = () => {
videos: videoStructured, videos: videoStructured,
commentsId: commentsId, commentsId: commentsId,
category, 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 = let metadescription =
`**category:${category};subcategory:${subcategory};${codes}**` + `**category:${category};subcategory:${subcategory};${codes}**` +
stringDescription.slice(0, 120); stringDescription.slice(0, 120);
@ -314,15 +316,18 @@ export const EditPlaylist = () => {
const crowdfundObjectToBase64 = await objectToBase64(playlistObject); const crowdfundObjectToBase64 = await objectToBase64(playlistObject);
// Description is obtained from raw data // Description is obtained from raw data
let identifier = editVideoProperties?.id let identifier = editVideoProperties?.id;
const sanitizeTitle = title const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "") .replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-") .replace(/\s+/g, "-")
.replace(/-+/g, "-") .replace(/-+/g, "-")
.trim() .trim()
.toLowerCase(); .toLowerCase();
if(isNew){ if (isNew) {
identifier = `${QTUBE_PLAYLIST_BASE}${sanitizeTitle.slice(0, 30)}_${id}`; identifier = `${QTUBE_PLAYLIST_BASE}${sanitizeTitle.slice(
0,
30
)}_${id}`;
} }
const requestBodyJson: any = { const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -336,21 +341,17 @@ export const EditPlaylist = () => {
}; };
await qortalRequest(requestBodyJson); await qortalRequest(requestBodyJson);
if(isNew){ if (isNew) {
const objectToStore = { const objectToStore = {
title: title.slice(0, 50), title: title.slice(0, 50),
description: metadescription, description: metadescription,
id: identifier, id: identifier,
service: "PLAYLIST", service: "PLAYLIST",
user: username, user: username,
...playlistObject ...playlistObject,
} };
dispatch( dispatch(updateVideo(objectToStore));
updateVideo(objectToStore) dispatch(updateInHashMap(objectToStore));
);
dispatch(
updateInHashMap(objectToStore)
);
} else { } else {
dispatch( dispatch(
updateVideo({ updateVideo({
@ -399,13 +400,11 @@ export const EditPlaylist = () => {
} }
} }
const handleOptionCategoryChangeVideos = ( const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = categories.find((option) => option.id === +optionId); const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
}; };
const handleOptionSubCategoryChangeVideos = ( const handleOptionSubCategoryChangeVideos = (
@ -414,19 +413,18 @@ export const EditPlaylist = () => {
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = subcategories.find( const selectedOption = subcategories.find(
(option) => option.id === +optionId option => option.id === +optionId
); );
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
}; };
const removeVideo = index => {
const removeVideo = (index) => {
const copyData = structuredClone(playlistData); const copyData = structuredClone(playlistData);
copyData.videos.splice(index, 1); copyData.videos.splice(index, 1);
setPlaylistData(copyData); setPlaylistData(copyData);
}; };
const addVideo = (data) => { const addVideo = data => {
const copyData = structuredClone(playlistData); const copyData = structuredClone(playlistData);
copyData.videos = [...copyData.videos, { ...data }]; copyData.videos = [...copyData.videos, { ...data }];
setPlaylistData(copyData); setPlaylistData(copyData);
@ -449,10 +447,8 @@ export const EditPlaylist = () => {
> >
{isNew ? ( {isNew ? (
<NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle> <NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle>
) : ( ) : (
<NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle> <NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle>
)} )}
</Box> </Box>
<> <>
@ -471,7 +467,7 @@ export const EditPlaylist = () => {
value={selectedCategoryVideos?.id || ""} value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos} onChange={handleOptionCategoryChangeVideos}
> >
{categories.map((option) => ( {categories.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -486,20 +482,18 @@ export const EditPlaylist = () => {
labelId="Sub-Category" labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />} input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""} value={selectedSubCategoryVideos?.id || ""}
onChange={(e) => onChange={e =>
handleOptionSubCategoryChangeVideos( handleOptionSubCategoryChangeVideos(
e, e,
subCategories[selectedCategoryVideos?.id] subCategories[selectedCategoryVideos?.id]
) )
} }
> >
{subCategories[selectedCategoryVideos.id].map( {subCategories[selectedCategoryVideos.id].map(option => (
(option) => ( <MenuItem key={option.id} value={option.id}>
<MenuItem key={option.id} value={option.id}> {option.name}
{option.name} </MenuItem>
</MenuItem> ))}
)
)}
</Select> </Select>
</FormControl> </FormControl>
)} )}
@ -533,9 +527,12 @@ export const EditPlaylist = () => {
label="Title of playlist" label="Title of playlist"
variant="filled" variant="filled"
value={title} value={title}
onChange={(e) => { onChange={e => {
const value = e.target.value; 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); setTitle(formattedValue);
}} }}
inputProps={{ maxLength: 180 }} inputProps={{ maxLength: 180 }}
@ -552,12 +549,19 @@ export const EditPlaylist = () => {
maxRows={3} maxRows={3}
required required
/> */} /> */}
<Typography sx={{ <Typography
fontSize: '18px' sx={{
}}>Description of playlist</Typography> fontSize: "18px",
<TextEditor inlineContent={description} setInlineContent={(value)=> { }}
setDescription(value) >
}} /> Description of playlist
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setDescription(value);
}}
/>
</React.Fragment> </React.Fragment>
<PlaylistListEdit <PlaylistListEdit

0
src/components/EditVideo/Upload-styles.tsx → src/components/EditVideo/EditVideo-styles.tsx

143
src/components/EditVideo/EditVideo.tsx

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Compressor from 'compressorjs' import Compressor from "compressorjs";
import { import {
AddCoverImageButton, AddCoverImageButton,
@ -14,7 +14,7 @@ import {
NewCrowdfundTitle, NewCrowdfundTitle,
StyledButton, StyledButton,
TimesIcon, TimesIcon,
} from "./Upload-styles"; } from "./EditVideo-styles.tsx";
import { CircularProgress } from "@mui/material"; import { CircularProgress } from "@mui/material";
import { import {
@ -46,12 +46,14 @@ import {
updateInHashMap, updateInHashMap,
} from "../../state/features/videoSlice"; } from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader"; 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 { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import { toBase64 } from "../UploadVideo/UploadVideo"; import { toBase64 } from "../PublishVideo/PublishVideo.tsx";
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor"; 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 uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -94,8 +96,7 @@ export const EditVideo = () => {
useState<any>(null); useState<any>(null);
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] = const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
useState<any>(null); useState<any>(null);
const [imageExtracts, setImageExtracts] = useState<any>([]) const [imageExtracts, setImageExtracts] = useState<any>([]);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
@ -111,7 +112,7 @@ export const EditVideo = () => {
let errorString = null; let errorString = null;
rejectedFiles.forEach(({ file, errors }) => { rejectedFiles.forEach(({ file, errors }) => {
errors.forEach((error) => { errors.forEach(error => {
if (error.code === "file-too-large") { if (error.code === "file-too-large") {
errorString = "File must be under 400mb"; errorString = "File must be under 400mb";
} }
@ -178,19 +179,17 @@ export const EditVideo = () => {
useEffect(() => { useEffect(() => {
if (editVideoProperties) { if (editVideoProperties) {
setTitle(editVideoProperties?.title || ""); setTitle(editVideoProperties?.title || "");
if(editVideoProperties?.htmlDescription){ if (editVideoProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription); setDescription(editVideoProperties?.htmlDescription);
} else if (editVideoProperties?.fullDescription) {
} else if(editVideoProperties?.fullDescription) { const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`;
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>`
setDescription(paragraph); setDescription(paragraph);
} }
setCoverImage(editVideoProperties?.videoImage || ""); setCoverImage(editVideoProperties?.videoImage || "");
if (editVideoProperties?.category) { if (editVideoProperties?.category) {
const selectedOption = categories.find( const selectedOption = categories.find(
(option) => option.id === +editVideoProperties.category option => option.id === +editVideoProperties.category
); );
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
} }
@ -202,7 +201,7 @@ export const EditVideo = () => {
) { ) {
const selectedOption = subCategories[ const selectedOption = subCategories[
+editVideoProperties?.category +editVideoProperties?.category
]?.find((option) => option.id === +editVideoProperties.subcategory); ]?.find(option => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
} }
} }
@ -213,7 +212,7 @@ export const EditVideo = () => {
setVideoPropertiesToSetToRedux(null); setVideoPropertiesToSetToRedux(null);
setFile(null); setFile(null);
setTitle(""); setTitle("");
setImageExtracts([]) setImageExtracts([]);
setDescription(""); setDescription("");
setCoverImage(""); setCoverImage("");
}; };
@ -253,7 +252,7 @@ export const EditVideo = () => {
const category = selectedCategoryVideos.id; const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || ""; const subcategory = selectedSubCategoryVideos?.id || "";
const fullDescription = extractTextFromHTML(description) const fullDescription = extractTextFromHTML(description);
let fileExtension = "mp4"; let fileExtension = "mp4";
const fileExtensionSplit = file?.name?.split("."); const fileExtensionSplit = file?.name?.split(".");
if (fileExtensionSplit?.length > 1) { if (fileExtensionSplit?.length > 1) {
@ -285,15 +284,13 @@ export const EditVideo = () => {
subcategory, subcategory,
code: editVideoProperties.code, code: editVideoProperties.code,
videoType: file?.type || "video/mp4", videoType: file?.type || "video/mp4",
filename: `${alphanumericString.trim()}.${fileExtension}` filename: `${alphanumericString.trim()}.${fileExtension}`,
}; };
let metadescription = let metadescription =
`**category:${category};subcategory:${subcategory};code:${editVideoProperties.code}**` + `**category:${category};subcategory:${subcategory};code:${editVideoProperties.code}**` +
description.slice(0, 150); description.slice(0, 150);
const crowdfundObjectToBase64 = await objectToBase64(videoObject); const crowdfundObjectToBase64 = await objectToBase64(videoObject);
// Description is obtained from raw data // Description is obtained from raw data
const requestBodyJson: any = { const requestBodyJson: any = {
@ -319,7 +316,7 @@ export const EditVideo = () => {
description: metadescription, description: metadescription,
identifier: editVideoProperties.videoReference?.identifier, identifier: editVideoProperties.videoReference?.identifier,
tag1: QTUBE_VIDEO_BASE, tag1: QTUBE_VIDEO_BASE,
filename: `${alphanumericString.trim()}.${fileExtension}` filename: `${alphanumericString.trim()}.${fileExtension}`,
}; };
listOfPublishes.push(requestBodyVideo); listOfPublishes.push(requestBodyVideo);
@ -356,13 +353,11 @@ export const EditVideo = () => {
} }
} }
const handleOptionCategoryChangeVideos = ( const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = categories.find((option) => option.id === +optionId); const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
}; };
const handleOptionSubCategoryChangeVideos = ( const handleOptionSubCategoryChangeVideos = (
@ -371,48 +366,45 @@ export const EditVideo = () => {
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = subcategories.find( const selectedOption = subcategories.find(
(option) => option.id === +optionId option => option.id === +optionId
); );
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
}; };
const onFramesExtracted = async (imgs)=> { const onFramesExtracted = async imgs => {
try { try {
let imagesExtracts = [] let imagesExtracts = [];
for (const img of imgs){ for (const img of imgs) {
try { try {
let compressedFile let compressedFile;
const image = img const image = img;
await new Promise<void>((resolve) => { await new Promise<void>(resolve => {
new Compressor(image, { new Compressor(image, {
quality: .8, quality: 0.8,
maxWidth: 750, maxWidth: 750,
mimeType: 'image/webp', mimeType: "image/webp",
success(result) { success(result) {
const file = new File([result], 'name', { const file = new File([result], "name", {
type: 'image/webp' type: "image/webp",
}) });
compressedFile = file compressedFile = file;
resolve() resolve();
}, },
error(err) {} error(err) {},
}) });
}) });
if (!compressedFile) continue if (!compressedFile) continue;
const base64Img = await toBase64(compressedFile) const base64Img = await toBase64(compressedFile);
imagesExtracts.push(base64Img) imagesExtracts.push(base64Img);
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
} }
setImageExtracts(imagesExtracts) setImageExtracts(imagesExtracts);
} catch (error) { } catch (error) {}
};
}
}
return ( return (
<> <>
@ -467,7 +459,7 @@ export const EditVideo = () => {
value={selectedCategoryVideos?.id || ""} value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos} onChange={handleOptionCategoryChangeVideos}
> >
{categories.map((option) => ( {categories.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -482,26 +474,27 @@ export const EditVideo = () => {
labelId="Sub-Category" labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />} input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""} value={selectedSubCategoryVideos?.id || ""}
onChange={(e) => onChange={e =>
handleOptionSubCategoryChangeVideos( handleOptionSubCategoryChangeVideos(
e, e,
subCategories[selectedCategoryVideos?.id] subCategories[selectedCategoryVideos?.id]
) )
} }
> >
{subCategories[selectedCategoryVideos.id].map( {subCategories[selectedCategoryVideos.id].map(option => (
(option) => ( <MenuItem key={option.id} value={option.id}>
<MenuItem key={option.id} value={option.id}> {option.name}
{option.name} </MenuItem>
</MenuItem> ))}
)
)}
</Select> </Select>
</FormControl> </FormControl>
)} )}
</Box> </Box>
{file && ( {file && (
<FrameExtractor videoFile={file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs)}/> <FrameExtractor
videoFile={file}
onFramesExtracted={imgs => onFramesExtracted(imgs)}
/>
)} )}
<React.Fragment> <React.Fragment>
{!coverImage ? ( {!coverImage ? (
@ -532,23 +525,27 @@ export const EditVideo = () => {
label="Title of video" label="Title of video"
variant="filled" variant="filled"
value={title} value={title}
onChange={(e) => { onChange={e => {
const value = e.target.value; const value = e.target.value;
const formattedValue = value.replace( const formattedValue = value.replace(titleFormatter, "");
/[^a-zA-Z0-9\s-_!?]/g,
""
);
setTitle(formattedValue); setTitle(formattedValue);
}} }}
inputProps={{ maxLength: 180 }} inputProps={{ maxLength: 180 }}
required required
/> />
<Typography sx={{ <Typography
fontSize: '18px' sx={{
}}>Description of video</Typography> fontSize: "18px",
<TextEditor inlineContent={description} setInlineContent={(value)=> { }}
setDescription(value) >
}} /> Description of video
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setDescription(value);
}}
/>
{/* <CustomInputField {/* <CustomInputField
name="description" name="description"
label="Describe your video in a few words" label="Describe your video in a few words"
@ -588,8 +585,8 @@ export const EditVideo = () => {
disabled={file && imageExtracts.length === 0} disabled={file && imageExtracts.length === 0}
> >
{file && imageExtracts.length === 0 && ( {file && imageExtracts.length === 0 && (
<CircularProgress color="secondary" size={14} /> <CircularProgress color="secondary" size={14} />
)} )}
Publish Publish
</CrowdfundActionButton> </CrowdfundActionButton>
</Box> </Box>

328
src/components/PlaylistListEdit/PlaylistListEdit.tsx

@ -3,208 +3,208 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles"
import { import {
CrowdfundSubTitle, CrowdfundSubTitle,
CrowdfundSubTitleRow, CrowdfundSubTitleRow,
} from "../UploadVideo/Upload-styles"; } from "../PublishVideo/PublishVideo-styles.tsx";
import { Box, Button, Input, Typography, useTheme } from "@mui/material"; import { Box, Button, Input, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { removeVideo } from "../../state/features/videoSlice"; import { removeVideo } from "../../state/features/videoSlice";
import AddIcon from '@mui/icons-material/Add'; import AddIcon from "@mui/icons-material/Add";
import { QTUBE_VIDEO_BASE } from "../../constants";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => { export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const [searchResults, setSearchResults] = useState([]) const [searchResults, setSearchResults] = useState([]);
const [filterSearch, setFilterSearch] = useState("") const [filterSearch, setFilterSearch] = useState("");
const search = async()=> { 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 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, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseDataSearchVid = await response.json() const responseDataSearchVid = await response.json();
setSearchResults(responseDataSearchVid) setSearchResults(responseDataSearchVid);
} };
return ( return (
<Box sx={{ <Box
display: 'flex',
gap: '10px',
width: '100%',
justifyContent: 'center'
}}>
<Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", gap: "10px",
maxWidth: "300px",
width: "100%", width: "100%",
justifyContent: "center",
}} }}
> >
<CrowdfundSubTitleRow> <Box
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CardContentContainerComment
sx={{ sx={{
marginTop: "25px", display: "flex",
height: "450px", flexDirection: "column",
overflow: 'auto'
maxWidth: "300px",
width: "100%",
}} }}
> >
{playlistData?.videos?.map((vid, index) => { <CrowdfundSubTitleRow>
return ( <CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
<Box </CrowdfundSubTitleRow>
key={vid?.identifier} <CardContentContainerComment
sx={{ sx={{
display: "flex", marginTop: "25px",
gap: "10px", height: "450px",
width: "100%", overflow: "auto",
alignItems: "center", }}
padding: "10px", >
borderRadius: "5px", {playlistData?.videos?.map((vid, index) => {
userSelect: "none", return (
}} <Box
> key={vid?.identifier}
<Typography
sx={{
fontSize: "14px",
}}
>
{index + 1}
</Typography>
<Typography
sx={{ sx={{
fontSize: "18px", display: "flex",
wordBreak: 'break-word' gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
}} }}
> >
{vid?.metadata?.title} <Typography
</Typography> sx={{
<DeleteOutlineIcon fontSize: "14px",
onClick={() => { }}
removeVideo(index); >
}} {index + 1}
sx={{ </Typography>
cursor: "pointer", <Typography
}} sx={{
/> fontSize: "18px",
</Box> wordBreak: "break-word",
); }}
})} >
</CardContentContainerComment> {vid?.metadata?.title}
</Box> </Typography>
<Box <DeleteOutlineIcon
sx={{ onClick={() => {
display: "flex", removeVideo(index);
flexDirection: "column", }}
sx={{
maxWidth: "300px", cursor: "pointer",
width: "100%", }}
}} />
> </Box>
);
<CrowdfundSubTitleRow> })}
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle> </CardContentContainerComment>
</CrowdfundSubTitleRow> </Box>
<CardContentContainerComment <Box
sx={{ sx={{
marginTop: "25px", display: "flex",
height: "450px", flexDirection: "column",
overflow: 'auto'
maxWidth: "300px",
width: "100%",
}} }}
> >
<Box sx={{ <CrowdfundSubTitleRow>
display: 'flex', <CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
gap: '10px' </CrowdfundSubTitleRow>
}}> <CardContentContainerComment
<Input sx={{
id="standard-adornment-name" marginTop: "25px",
onChange={(e) => { height: "450px",
setFilterSearch(e.target.value); overflow: "auto",
}} }}
value={filterSearch} >
placeholder="Search by title" <Box
sx={{ sx={{
borderBottom: "1px solid white", display: "flex",
"&&:before": { gap: "10px",
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"
> >
Search <Input
</Button> id="standard-adornment-name"
</Box> onChange={e => {
setFilterSearch(e.target.value);
{searchResults?.map((vid, index) => { }}
return ( value={filterSearch}
<Box placeholder="Search by title"
key={vid?.identifier}
sx={{ sx={{
display: "flex", borderBottom: "1px solid white",
gap: "10px", "&&:before": {
width: "100%", borderBottom: "none",
alignItems: "center", },
padding: "10px", "&&:after": {
borderRadius: "5px", borderBottom: "none",
userSelect: "none", },
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "18px",
}} }}
/>
<Button
onClick={() => {
search();
}}
variant="contained"
> >
<Typography Search
sx={{ </Button>
fontSize: "14px", </Box>
}}
> {searchResults?.map((vid, index) => {
{index + 1} return (
</Typography> <Box
<Typography key={vid?.identifier}
sx={{ sx={{
fontSize: "18px", display: "flex",
wordBreak: 'break-word' gap: "10px",
width: "100%",
alignItems: "center",
padding: "10px",
borderRadius: "5px",
userSelect: "none",
}} }}
> >
{vid?.metadata?.title} <Typography
</Typography> sx={{
<AddIcon fontSize: "14px",
onClick={() => { }}
addVideo(vid); >
}} {index + 1}
sx={{ </Typography>
cursor: "pointer", <Typography
}} sx={{
/> fontSize: "18px",
</Box> wordBreak: "break-word",
); }}
})} >
</CardContentContainerComment> {vid?.metadata?.title}
</Typography>
<AddIcon
onClick={() => {
addVideo(vid);
}}
sx={{
cursor: "pointer",
}}
/>
</Box>
);
})}
</CardContentContainerComment>
</Box>
</Box> </Box>
</Box>
); );
}; };

127
src/components/Playlists/Playlists.tsx

@ -1,66 +1,83 @@
import React from 'react' import React from "react";
import { CardContentContainerComment } from '../common/Comments/Comments-styles' import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from '../UploadVideo/Upload-styles' import {
import { Box, Typography, useTheme } from '@mui/material' CrowdfundSubTitle,
import { useNavigate } from 'react-router-dom' CrowdfundSubTitleRow,
} from "../PublishVideo/PublishVideo-styles.tsx";
export const Playlists = ({playlistData, currentVideoIdentifier, onClick}) => { import { Box, Typography, useTheme } from "@mui/material";
const theme = useTheme(); import { useNavigate } from "react-router-dom";
const navigate = useNavigate()
export const Playlists = ({
playlistData,
currentVideoIdentifier,
onClick,
}) => {
const theme = useTheme();
const navigate = useNavigate();
return ( return (
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
flexDirection: "column",
maxWidth: '400px', maxWidth: "400px",
width: '100%' width: "100%",
}}> }}
<CrowdfundSubTitleRow > >
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle> <CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow> </CrowdfundSubTitleRow>
<CardContentContainerComment sx={{ <CardContentContainerComment
marginTop: '25px', sx={{
height: '450px', marginTop: "25px",
overflow: 'auto' height: "450px",
}}> overflow: "auto",
{playlistData?.videos?.map((vid, index)=> { }}
const isCurrentVidPlayling = vid?.identifier === currentVideoIdentifier; >
{playlistData?.videos?.map((vid, index) => {
const isCurrentVidPlayling =
vid?.identifier === currentVideoIdentifier;
return ( return (
<Box key={vid?.identifier} sx={{ <Box
display: 'flex', key={vid?.identifier}
gap: '10px', sx={{
width: '100%', display: "flex",
background: isCurrentVidPlayling && theme.palette.primary.main, gap: "10px",
alignItems: 'center', width: "100%",
padding: '10px', background: isCurrentVidPlayling && theme.palette.primary.main,
borderRadius: '5px', alignItems: "center",
cursor: isCurrentVidPlayling ? 'default' : 'pointer', padding: "10px",
userSelect: 'none' 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 {index + 1}
onClick(vid.name, vid.identifier) </Typography>
// navigate(`/video/${vid.name}/${vid.identifier}`) <Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
}} }}
> >
<Typography sx={{ {vid?.metadata?.title}
fontSize: '14px' </Typography>
}}>{index + 1}</Typography> </Box>
<Typography sx={{ );
fontSize: '18px',
wordBreak: 'break-word'
}}>{vid?.metadata?.title}</Typography>
</Box>
)
})} })}
</CardContentContainerComment> </CardContentContainerComment>
</Box> </Box>
);
) };
}

0
src/components/UploadVideo/Upload-styles.tsx → src/components/PublishVideo/PublishVideo-styles.tsx

460
src/components/UploadVideo/UploadVideo.tsx → src/components/PublishVideo/PublishVideo.tsx

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Compressor from 'compressorjs' import Compressor from "compressorjs";
import { import {
AddCoverImageButton, AddCoverImageButton,
AddLogoIcon, AddLogoIcon,
@ -13,7 +13,7 @@ import {
NewCrowdfundTitle, NewCrowdfundTitle,
StyledButton, StyledButton,
TimesIcon, TimesIcon,
} from "./Upload-styles"; } from "./PublishVideo-styles.tsx";
import { CircularProgress } from "@mui/material"; import { CircularProgress } from "@mui/material";
import { import {
@ -45,12 +45,7 @@ import {
upsertVideos, upsertVideos,
} from "../../state/features/videoSlice"; } from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader"; import ImageUploader from "../common/ImageUploader";
import { import { categories, subCategories } from "../../constants/Categories.ts";
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
categories,
subCategories,
} from "../../constants";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
import { import {
CrowdfundSubTitle, CrowdfundSubTitle,
@ -59,18 +54,28 @@ import {
import { CardContentContainerComment } from "../common/Comments/Comments-styles"; import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; 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 { 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> => export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader();
reader.readAsDataURL(file) reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result) reader.onload = () => resolve(reader.result);
reader.onerror = (error) => { reader.onerror = error => {
reject(error) reject(error);
} };
}) });
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -90,7 +95,7 @@ interface VideoFile {
description: string; description: string;
coverImage?: string; coverImage?: string;
} }
export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => { export const PublishVideo = ({ editId, editContent }: NewCrowdfundProps) => {
const theme = useTheme(); const theme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
@ -113,6 +118,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
useState<any>(null); useState<any>(null);
const [searchResults, setSearchResults] = useState([]); const [searchResults, setSearchResults] = useState([]);
const [filterSearch, setFilterSearch] = useState(""); const [filterSearch, setFilterSearch] = useState("");
const [titlesPrefix, setTitlesPrefix] = useState("");
const [playlistTitle, setPlaylistTitle] = useState<string>(""); const [playlistTitle, setPlaylistTitle] = useState<string>("");
const [playlistDescription, setPlaylistDescription] = useState<string>(""); const [playlistDescription, setPlaylistDescription] = useState<string>("");
const [selectedCategory, setSelectedCategory] = useState<any>(null); const [selectedCategory, setSelectedCategory] = useState<any>(null);
@ -125,39 +131,37 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null); const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
const [publishes, setPublishes] = useState<any[]>([]); const [publishes, setPublishes] = useState<any[]>([]);
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false) const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(true);
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false) const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(true);
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false) const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] =
const [imageExtracts, setImageExtracts] = useState<any>({}) useState(false);
const [imageExtracts, setImageExtracts] = useState<any>({});
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
"video/*": [], "video/*": [],
}, },
maxSize: 419430400, // 400 MB in bytes maxSize: 419430400, // 400 MB in bytes
onDrop: (acceptedFiles, rejectedFiles) => { onDrop: (acceptedFiles, rejectedFiles) => {
const formatArray = acceptedFiles.map((item) => { const formatArray = acceptedFiles.map(item => {
let filteredTitle = "";
let formatTitle = '' if (isCheckTitleByFile) {
if(isCheckTitleByFile && item?.name){ const fileName = getFileName(item?.name || "");
const fileExtensionSplit = item?.name?.split("."); filteredTitle = (titlesPrefix + fileName).replace(titleFormatter, "");
if (fileExtensionSplit?.length > 1) {
formatTitle = fileExtensionSplit[0]
}
formatTitle = (formatTitle || "").replace(/[^a-zA-Z0-9\s-_!?]/g, "");
} }
return { return {
file: item, file: item,
title: formatTitle, title: filteredTitle || "",
description: "", description: "",
coverImage: "", coverImage: "",
}; };
}); });
setFiles((prev) => [...prev, ...formatArray]); setFiles(prev => [...prev, ...formatArray]);
let errorString = null; let errorString = null;
rejectedFiles.forEach(({ file, errors }) => { rejectedFiles.forEach(({ file, errors }) => {
errors.forEach((error) => { errors.forEach(error => {
if (error.code === "file-too-large") { if (error.code === "file-too-large") {
errorString = "File must be under 400mb"; 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 (!playlistCoverImage) throw new Error("Please select cover image");
if (!selectedCategory) throw new Error("Please select a category"); if (!selectedCategory) throw new Error("Please select a category");
} }
if(files?.length === 0) throw new Error("Please select at least one file"); if (files?.length === 0)
if(isCheckSameCoverImage && !coverImageForAll) throw new Error("Please select cover image"); 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"); if (!userAddress) throw new Error("Unable to locate user address");
let errorMsg = ""; let errorMsg = "";
let name = ""; let name = "";
@ -234,12 +240,16 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
let listOfPublishes = []; let listOfPublishes = [];
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const publish = files[i] const publish = files[i];
const title = publish.title; const title = publish.title;
const description = isCheckDescriptionIsTitle ? publish.title : publish.description; const description = isCheckDescriptionIsTitle
? publish.title
: publish.description;
const category = selectedCategoryVideos.id; const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || ""; const subcategory = selectedSubCategoryVideos?.id || "";
const coverImage = isCheckSameCoverImage ? coverImageForAll : publish.coverImage; const coverImage = isCheckSameCoverImage
? coverImageForAll
: publish.coverImage;
const file = publish.file; const file = publish.file;
const sanitizeTitle = title const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "") .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}`; : `${QTUBE_VIDEO_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
const code = shortuid(); const code = shortuid();
const fullDescription = extractTextFromHTML(description) const fullDescription = extractTextFromHTML(description);
let fileExtension = "mp4"; let fileExtension = "mp4";
const fileExtensionSplit = file?.name?.split("."); const fileExtensionSplit = file?.name?.split(".");
@ -292,15 +302,13 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
subcategory, subcategory,
code, code,
videoType: file?.type || "video/mp4", videoType: file?.type || "video/mp4",
filename: `${alphanumericString.trim()}.${fileExtension}` filename: `${alphanumericString.trim()}.${fileExtension}`,
}; };
let metadescription = let metadescription =
`**category:${category};subcategory:${subcategory};code:${code}**` + `**category:${category};subcategory:${subcategory};code:${code}**` +
fullDescription.slice(0, 150); fullDescription.slice(0, 150);
const crowdfundObjectToBase64 = await objectToBase64(videoObject); const crowdfundObjectToBase64 = await objectToBase64(videoObject);
// Description is obtained from raw data // Description is obtained from raw data
const requestBodyJson: any = { const requestBodyJson: any = {
@ -335,7 +343,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
if (isNewPlaylist) { if (isNewPlaylist) {
const title = playlistTitle; const title = playlistTitle;
const description = playlistDescription; const description = playlistDescription;
const stringDescription = extractTextFromHTML(description) const stringDescription = extractTextFromHTML(description);
const category = selectedCategory.id; const category = selectedCategory.id;
const subcategory = selectedSubCategory?.id || ""; const subcategory = selectedSubCategory?.id || "";
const coverImage = playlistCoverImage; const coverImage = playlistCoverImage;
@ -354,10 +362,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
const videos = listOfPublishes const videos = listOfPublishes
.filter( .filter(
(item) => item =>
item.service === "DOCUMENT" && item.tag1 === QTUBE_VIDEO_BASE item.service === "DOCUMENT" && item.tag1 === QTUBE_VIDEO_BASE
) )
.map((vid) => { .map(vid => {
return { return {
identifier: vid.identifier, identifier: vid.identifier,
service: vid.service, service: vid.service,
@ -378,7 +386,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
subcategory, 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 = let metadescription =
`**category:${category};subcategory:${subcategory};${codes}**` + `**category:${category};subcategory:${subcategory};${codes}**` +
@ -413,10 +424,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
if (responseData && !responseData.error) { if (responseData && !responseData.error) {
const videos = listOfPublishes const videos = listOfPublishes
.filter( .filter(
(item) => item =>
item.service === "DOCUMENT" && item.tag1 === QTUBE_VIDEO_BASE item.service === "DOCUMENT" && item.tag1 === QTUBE_VIDEO_BASE
) )
.map((vid) => { .map(vid => {
return { return {
identifier: vid.identifier, identifier: vid.identifier,
service: vid.service, service: vid.service,
@ -431,7 +442,8 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
videos: videosInPlaylist, videos: videosInPlaylist,
}; };
const codes = videosInPlaylist const codes = videosInPlaylist
.map((item) => `c:${item.code};`).slice(0,10) .map(item => `c:${item.code};`)
.slice(0, 10)
.join(""); .join("");
let metadescription = let metadescription =
@ -485,10 +497,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
} }
const handleOnchange = (index: number, type: string, value: string) => { const handleOnchange = (index: number, type: string, value: string) => {
setFiles((prev) => { setFiles(prev => {
let formattedValue = value; let formattedValue = value;
if (type === "title") { if (type === "title") {
formattedValue = value.replace(/[^a-zA-Z0-9\s-_!?]/g, ""); formattedValue = value.replace(titleFormatter, "");
} }
const copyFiles = [...prev]; const copyFiles = [...prev];
copyFiles[index] = { copyFiles[index] = {
@ -501,7 +513,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
const handleOptionCategoryChange = (event: SelectChangeEvent<string>) => { const handleOptionCategoryChange = (event: SelectChangeEvent<string>) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = categories.find((option) => option.id === +optionId); const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategory(selectedOption || null); setSelectedCategory(selectedOption || null);
}; };
const handleOptionSubCategoryChange = ( const handleOptionSubCategoryChange = (
@ -510,7 +522,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = subcategories.find( const selectedOption = subcategories.find(
(option) => option.id === +optionId option => option.id === +optionId
); );
setSelectedSubCategory(selectedOption || null); setSelectedSubCategory(selectedOption || null);
}; };
@ -519,7 +531,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = categories.find((option) => option.id === +optionId); const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
}; };
const handleOptionSubCategoryChangeVideos = ( const handleOptionSubCategoryChangeVideos = (
@ -528,20 +540,24 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = subcategories.find( const selectedOption = subcategories.find(
(option) => option.id === +optionId option => option.id === +optionId
); );
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
}; };
const next = () => { const next = () => {
try { try {
if(isCheckSameCoverImage && !coverImageForAll) throw new Error("Please select cover image"); if (isCheckSameCoverImage && !coverImageForAll)
if(files?.length === 0) throw new Error("Please select at least one file"); 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"); 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 (!file.title) throw new Error("Please enter a title");
if (!isCheckTitleByFile && !file.description) throw new Error("Please enter a description"); if (!isCheckTitleByFile && !file.description)
if (!isCheckSameCoverImage && !file.coverImage) throw new Error("Please select cover image"); throw new Error("Please enter a description");
if (!isCheckSameCoverImage && !file.coverImage)
throw new Error("Please select cover image");
}); });
setStep("playlist"); setStep("playlist");
@ -555,48 +571,45 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
} }
}; };
const onFramesExtracted = async (imgs, index)=> { const onFramesExtracted = async (imgs, index) => {
try { try {
let imagesExtracts = [] let imagesExtracts = [];
for (const img of imgs){ for (const img of imgs) {
try { try {
let compressedFile let compressedFile;
const image = img const image = img;
await new Promise<void>((resolve) => { await new Promise<void>(resolve => {
new Compressor(image, { new Compressor(image, {
quality: .8, quality: 0.8,
maxWidth: 750, maxWidth: 750,
mimeType: 'image/webp', mimeType: "image/webp",
success(result) { success(result) {
const file = new File([result], 'name', { const file = new File([result], "name", {
type: 'image/webp' type: "image/webp",
}) });
compressedFile = file compressedFile = file;
resolve() resolve();
}, },
error(err) {} error(err) {},
}) });
}) });
if (!compressedFile) continue if (!compressedFile) continue;
const base64Img = await toBase64(compressedFile) const base64Img = await toBase64(compressedFile);
imagesExtracts.push(base64Img) imagesExtracts.push(base64Img);
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
} }
setImageExtracts((prev)=> { setImageExtracts(prev => {
return { return {
...prev, ...prev,
[index]: imagesExtracts [index]: imagesExtracts,
} };
}) });
} catch (error) { } catch (error) {}
};
}
}
return ( return (
<> <>
@ -638,38 +651,49 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
{step === "videos" && ( {step === "videos" && (
<> <>
<FiltersSubContainer> <FiltersSubContainer>
<FiltersRow> <FiltersRow>
Populate Titles by filename (when the files are picked) Populate Titles by filename (when the files are picked)
<FiltersCheckbox <FiltersCheckbox
checked={isCheckTitleByFile} checked={isCheckTitleByFile}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckTitleByFile(e.target.checked); setIsCheckTitleByFile(e.target.checked);
}} }}
inputProps={{ "aria-label": "controlled" }} inputProps={{ "aria-label": "controlled" }}
/> />
</FiltersRow> </FiltersRow>
<FiltersRow> <FiltersRow>
All videos use the same Cover Image All videos use the same Cover Image
<FiltersCheckbox <FiltersCheckbox
checked={isCheckSameCoverImage} checked={isCheckSameCoverImage}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckSameCoverImage(e.target.checked); setIsCheckSameCoverImage(e.target.checked);
}} }}
inputProps={{ "aria-label": "controlled" }} inputProps={{ "aria-label": "controlled" }}
/> />
</FiltersRow> </FiltersRow>
<FiltersRow> <FiltersRow>
Populate all descriptions by Title Populate all descriptions by Title
<FiltersCheckbox <FiltersCheckbox
checked={isCheckDescriptionIsTitle} checked={isCheckDescriptionIsTitle}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckDescriptionIsTitle(e.target.checked); setIsCheckDescriptionIsTitle(e.target.checked);
}} }}
inputProps={{ "aria-label": "controlled" }} 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>
</FiltersSubContainer>
<Box <Box
{...getRootProps()} {...getRootProps()}
sx={{ sx={{
@ -703,7 +727,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
value={selectedCategoryVideos?.id || ""} value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos} onChange={handleOptionCategoryChangeVideos}
> >
{categories.map((option) => ( {categories.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -722,7 +746,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
<OutlinedInput label="Select a Sub-Category" /> <OutlinedInput label="Select a Sub-Category" />
} }
value={selectedSubCategoryVideos?.id || ""} value={selectedSubCategoryVideos?.id || ""}
onChange={(e) => onChange={e =>
handleOptionSubCategoryChangeVideos( handleOptionSubCategoryChangeVideos(
e, e,
subCategories[selectedCategoryVideos?.id] subCategories[selectedCategoryVideos?.id]
@ -730,7 +754,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
} }
> >
{subCategories[selectedCategoryVideos.id].map( {subCategories[selectedCategoryVideos.id].map(
(option) => ( option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -743,74 +767,76 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
)} )}
</Box> </Box>
{files?.length > 0 && isCheckSameCoverImage && ( {files?.length > 0 && isCheckSameCoverImage && (
<> <>
{!coverImageForAll ? ( {!coverImageForAll ? (
<ImageUploader <ImageUploader
onPick={(img: string) => onPick={(img: string) => setCoverImageForAll(img)}
setCoverImageForAll(img) >
} <AddCoverImageButton variant="contained">
> Add Cover Image
<AddCoverImageButton variant="contained"> <AddLogoIcon
Add Cover Image sx={{
<AddLogoIcon height: "25px",
sx={{ width: "auto",
height: "25px", }}
width: "auto", ></AddLogoIcon>
}} </AddCoverImageButton>
></AddLogoIcon> </ImageUploader>
</AddCoverImageButton> ) : (
</ImageUploader> <LogoPreviewRow>
) : ( <CoverImagePreview src={coverImageForAll} alt="logo" />
<LogoPreviewRow> <TimesIcon
<CoverImagePreview src={coverImageForAll} alt="logo" /> color={theme.palette.text.primary}
<TimesIcon onClickFunc={() => setCoverImageForAll(null)}
color={theme.palette.text.primary} height={"32"}
onClickFunc={() => width={"32"}
setCoverImageForAll(null) ></TimesIcon>
} </LogoPreviewRow>
height={"32"} )}
width={"32"} </>
></TimesIcon> )}
</LogoPreviewRow>
)}
</>
)}
{files.map((file, index) => { {files.map((file, index) => {
return ( return (
<React.Fragment key={index}> <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> <Typography>{file?.file?.name}</Typography>
{!isCheckSameCoverImage && ( {!isCheckSameCoverImage && (
<> <>
{!file?.coverImage ? ( {!file?.coverImage ? (
<ImageUploader <ImageUploader
onPick={(img: string) => onPick={(img: string) =>
handleOnchange(index, "coverImage", img) handleOnchange(index, "coverImage", img)
} }
> >
<AddCoverImageButton variant="contained"> <AddCoverImageButton variant="contained">
Add Cover Image Add Cover Image
<AddLogoIcon <AddLogoIcon
sx={{ sx={{
height: "25px", height: "25px",
width: "auto", width: "auto",
}} }}
></AddLogoIcon> ></AddLogoIcon>
</AddCoverImageButton> </AddCoverImageButton>
</ImageUploader> </ImageUploader>
) : ( ) : (
<LogoPreviewRow> <LogoPreviewRow>
<CoverImagePreview src={file?.coverImage} alt="logo" /> <CoverImagePreview
<TimesIcon src={file?.coverImage}
color={theme.palette.text.primary} alt="logo"
onClickFunc={() => />
handleOnchange(index, "coverImage", "") <TimesIcon
} color={theme.palette.text.primary}
height={"32"} onClickFunc={() =>
width={"32"} handleOnchange(index, "coverImage", "")
></TimesIcon> }
</LogoPreviewRow> height={"32"}
)} width={"32"}
></TimesIcon>
</LogoPreviewRow>
)}
</> </>
)} )}
@ -819,7 +845,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
label="Title of video" label="Title of video"
variant="filled" variant="filled"
value={file.title} value={file.title}
onChange={(e) => onChange={e =>
handleOnchange(index, "title", e.target.value) handleOnchange(index, "title", e.target.value)
} }
inputProps={{ maxLength: 180 }} inputProps={{ maxLength: 180 }}
@ -827,12 +853,19 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
/> />
{!isCheckDescriptionIsTitle && ( {!isCheckDescriptionIsTitle && (
<> <>
<Typography sx={{ <Typography
fontSize: '18px' sx={{
}}>Description of video</Typography> fontSize: "18px",
<TextEditor inlineContent={file?.description} setInlineContent={(value)=> { }}
handleOnchange(index, "description", value) >
}} /> Description of video
</Typography>
<TextEditor
inlineContent={file?.description}
setInlineContent={value => {
handleOnchange(index, "description", value);
}}
/>
</> </>
)} )}
@ -962,7 +995,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
> >
<Input <Input
id="standard-adornment-name" id="standard-adornment-name"
onChange={(e) => { onChange={e => {
setFilterSearch(e.target.value); setFilterSearch(e.target.value);
}} }}
value={filterSearch} value={filterSearch}
@ -1072,11 +1105,11 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
label="Title of playlist" label="Title of playlist"
variant="filled" variant="filled"
value={playlistTitle} value={playlistTitle}
onChange={(e) => { onChange={e => {
const value = e.target.value; const value = e.target.value;
let formattedValue: string = value; let formattedValue: string = value;
formattedValue = value.replace(/[^a-zA-Z0-9\s-_!?]/g, ""); formattedValue = value.replace(titleFormatter, "");
setPlaylistTitle(formattedValue); setPlaylistTitle(formattedValue);
}} }}
@ -1095,12 +1128,19 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
required required
/> */} /> */}
<Typography sx={{ <Typography
fontSize: '18px' sx={{
}}>Description of playlist</Typography> fontSize: "18px",
<TextEditor inlineContent={playlistDescription} setInlineContent={(value)=> { }}
setPlaylistDescription(value) >
}} /> Description of playlist
</Typography>
<TextEditor
inlineContent={playlistDescription}
setInlineContent={value => {
setPlaylistDescription(value);
}}
/>
<FormControl fullWidth sx={{ marginBottom: 2, marginTop: 2 }}> <FormControl fullWidth sx={{ marginBottom: 2, marginTop: 2 }}>
<InputLabel id="Category">Select a Category</InputLabel> <InputLabel id="Category">Select a Category</InputLabel>
<Select <Select
@ -1109,7 +1149,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
value={selectedCategory?.id || ""} value={selectedCategory?.id || ""}
onChange={handleOptionCategoryChange} onChange={handleOptionCategoryChange}
> >
{categories.map((option) => ( {categories.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -1125,14 +1165,14 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
labelId="Sub-Category" labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />} input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategory?.id || ""} value={selectedSubCategory?.id || ""}
onChange={(e) => onChange={e =>
handleOptionSubCategoryChange( handleOptionSubCategoryChange(
e, e,
subCategories[selectedCategory?.id] subCategories[selectedCategory?.id]
) )
} }
> >
{subCategories[selectedCategory.id].map((option) => ( {subCategories[selectedCategory.id].map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -1186,16 +1226,20 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
) : ( ) : (
<CrowdfundActionButton <CrowdfundActionButton
variant="contained" variant="contained"
disabled={files?.length !== Object.keys(imageExtracts)?.length} disabled={
files?.length !== Object.keys(imageExtracts)?.length
}
onClick={() => { onClick={() => {
next(); 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 && ( {files?.length !== Object.keys(imageExtracts)?.length && (
<CircularProgress color="secondary" size={14} /> <CircularProgress color="secondary" size={14} />
)} )}
Next Next
</CrowdfundActionButton> </CrowdfundActionButton>
)} )}
</Box> </Box>
@ -1209,14 +1253,14 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
onSubmit={() => { onSubmit={() => {
setIsOpenMultiplePublish(false); setIsOpenMultiplePublish(false);
setIsOpen(false); setIsOpen(false);
setImageExtracts({}) setImageExtracts({});
setFiles([]); setFiles([]);
setStep("videos"); setStep("videos");
setPlaylistCoverImage(null); setPlaylistCoverImage(null);
setPlaylistTitle(""); setPlaylistTitle("");
setPlaylistDescription(""); setPlaylistDescription("");
setSelectedCategory(null); setSelectedCategory(null);
setCoverImageForAll(null) setCoverImageForAll(null);
setSelectedSubCategory(null); setSelectedSubCategory(null);
setSelectedCategoryVideos(null); setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null); setSelectedSubCategoryVideos(null);

3
src/components/common/Comments/CommentEditor.tsx

@ -11,7 +11,8 @@ import {
CommentInputContainer, CommentInputContainer,
SubmitCommentButton, SubmitCommentButton,
} from "./Comments-styles"; } from "./Comments-styles";
import { COMMENT_BASE } from "../../../constants";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const notification = localforage.createInstance({ const notification = localforage.createInstance({

14
src/components/common/Comments/CommentSection.tsx

@ -14,8 +14,11 @@ import {
LoadMoreCommentsButtonRow, LoadMoreCommentsButtonRow,
NoCommentsRow, NoCommentsRow,
} from "./Comments-styles"; } from "./Comments-styles";
import { COMMENT_BASE } from "../../../constants"; import {
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from "../../UploadVideo/Upload-styles"; CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../PublishVideo/PublishVideo-styles.tsx";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
interface CommentSectionProps { interface CommentSectionProps {
postId: string; postId: string;
@ -218,11 +221,10 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
return ( return (
<> <>
<Panel> <Panel>
<CrowdfundSubTitleRow > <CrowdfundSubTitleRow>
<CrowdfundSubTitle>Comments</CrowdfundSubTitle> <CrowdfundSubTitle>Comments</CrowdfundSubTitle>
</CrowdfundSubTitleRow> </CrowdfundSubTitleRow>
<CommentsContainer> <CommentsContainer>
{loadingComments ? ( {loadingComments ? (
<NoCommentsRow> <NoCommentsRow>

63
src/components/common/MultiplePublish/MultiplePublish.tsx

@ -8,13 +8,13 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react"; 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 { CircleSVG } from "../../../assets/svgs/CircleSVG";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG"; import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => { export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
const theme = useTheme(); const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([]) const listOfSuccessfulPublishesRef = useRef([]);
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState< const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[] any[]
>([]); >([]);
@ -23,7 +23,7 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
const publish = useCallback(async (pub: any) => { const publish = useCallback(async (pub: any) => {
await qortalRequest(pub); await qortalRequest(pub);
}, []); }, []);
const [isPublishing, setIsPublishing] = useState(true) const [isPublishing, setIsPublishing] = useState(true);
const handlePublish = useCallback( const handlePublish = useCallback(
async (pub: any) => { async (pub: any) => {
@ -33,10 +33,13 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
await publish(pub); await publish(pub);
setListOfSuccessfulPublishes((prev: any) => [...prev, pub?.identifier]); setListOfSuccessfulPublishes((prev: any) => [...prev, pub?.identifier]);
listOfSuccessfulPublishesRef.current = [...listOfSuccessfulPublishesRef.current, pub?.identifier] listOfSuccessfulPublishesRef.current = [
...listOfSuccessfulPublishesRef.current,
pub?.identifier,
];
} catch (error) { } catch (error) {
console.log({ error }); console.log({ error });
await new Promise<void>((res) => { await new Promise<void>(res => {
setTimeout(() => { setTimeout(() => {
res(); res();
}, 5000); }, 5000);
@ -49,17 +52,18 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
const startPublish = useCallback( const startPublish = useCallback(
async (pubs: any) => { async (pubs: any) => {
setIsPublishing(true) setIsPublishing(true);
const filterPubs = pubs.filter((pub)=> !listOfSuccessfulPublishesRef.current.includes(pub.identifier)) const filterPubs = pubs.filter(
pub => !listOfSuccessfulPublishesRef.current.includes(pub.identifier)
);
for (const pub of filterPubs) { for (const pub of filterPubs) {
await handlePublish(pub); await handlePublish(pub);
} }
if(listOfSuccessfulPublishesRef.current.length === pubs.length){ if (listOfSuccessfulPublishesRef.current.length === pubs.length) {
onSubmit() onSubmit();
} }
setIsPublishing(false) setIsPublishing(false);
}, },
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes] [handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
); );
@ -71,7 +75,6 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
} }
}, [startPublish, publishes, listOfSuccessfulPublishes]); }, [startPublish, publishes, listOfSuccessfulPublishes]);
return ( return (
<Modal <Modal
open={isOpen} open={isOpen}
@ -118,18 +121,28 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
</Box> </Box>
); );
})} })}
{!isPublishing && listOfSuccessfulPublishes.length !== publishes.length && ( {!isPublishing &&
<> listOfSuccessfulPublishes.length !== publishes.length && (
<Typography sx={{ <>
marginTop: '20px', <Typography
fontSize: '16px' sx={{
}}>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> marginTop: "20px",
<Button onClick={()=> { fontSize: "16px",
startPublish(publishes) }}
}}>Try again</Button> >
</> 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> </ModalBody>
</Modal> </Modal>
); );

605
src/components/common/Notifications/Notifications.tsx

@ -1,324 +1,347 @@
import { Badge, Box, Button, List, ListItem, ListItemText, Popover, Typography } from '@mui/material' import {
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' Badge,
import { useDispatch, useSelector } from 'react-redux' Box,
import { RootState } from '../../../state/store' Button,
import { FOR, FOR_SUPER_LIKE, SUPER_LIKE_BASE, minPriceSuperlike } from '../../../constants' List,
import NotificationsIcon from '@mui/icons-material/Notifications' ListItem,
import { formatDate } from '../../../utils/time' 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 ThumbUpIcon from "@mui/icons-material/ThumbUp";
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from '../../../pages/VideoContent/VideoContent' import {
import { useNavigate } from 'react-router-dom' extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../../../pages/VideoContent/VideoContent";
import { useNavigate } from "react-router-dom";
import localForage from "localforage"; 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({ const generalLocal = localForage.createInstance({
name: "q-tube-general", name: "q-tube-general",
}); });
export function extractIdValue(metadescription) { export function extractIdValue(metadescription) {
// Function to extract the substring within double asterisks // Function to extract the substring within double asterisks
function extractSubstring(str) { function extractSubstring(str) {
const match = str.match(/\*\*(.*?)\*\*/); const match = str.match(/\*\*(.*?)\*\*/);
return match ? match[1] : null; return match ? match[1] : null;
} }
// Function to extract the 'sig' value // Function to extract the 'sig' value
function extractSig(str) { function extractSig(str) {
const regex = /id:(.*?)(;|$)/; const regex = /id:(.*?)(;|$)/;
const match = str.match(regex); const match = str.match(regex);
return match ? match[1] : null; return match ? match[1] : null;
} }
// Extracting the relevant substring // Extracting the relevant substring
const relevantSubstring = extractSubstring(metadescription); const relevantSubstring = extractSubstring(metadescription);
if (relevantSubstring) { if (relevantSubstring) {
// Extracting the 'sig' value // Extracting the 'sig' value
return extractSig(relevantSubstring); return extractSig(relevantSubstring);
} else { } else {
return null; return null;
}
} }
}
export const Notifications = () => { export const Notifications = () => {
const dispatch = useDispatch() const dispatch = useDispatch();
const [anchorElNotification, setAnchorElNotification] = useState<HTMLButtonElement | null>(null) const [anchorElNotification, setAnchorElNotification] =
const [notifications, setNotifications] = useState<any[]>([]) useState<HTMLButtonElement | null>(null);
const [notificationTimestamp, setNotificationTimestamp] = useState<null | number>(null) const [notifications, setNotifications] = useState<any[]>([]);
const [notificationTimestamp, setNotificationTimestamp] = useState<
null | number
const username = useSelector((state: RootState) => state.auth?.user?.name); >(null);
const usernameAddress = useSelector((state: RootState) => state.auth?.user?.address);
const navigate = useNavigate(); const username = useSelector((state: RootState) => state.auth?.user?.name);
const usernameAddress = useSelector(
const interval = useRef<any>(null) (state: RootState) => state.auth?.user?.address
);
const getInitialTimestamp = async ()=> { const navigate = useNavigate();
const timestamp: undefined | number = await generalLocal.getItem("notification-timestamp");
if(timestamp){ const interval = useRef<any>(null);
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 openNotificationPopover = (event: any) => {
const target = event.currentTarget as unknown as HTMLButtonElement | null const target = event.currentTarget as unknown as HTMLButtonElement | null;
setAnchorElNotification(target) setAnchorElNotification(target);
} };
const closeNotificationPopover = () => { const closeNotificationPopover = () => {
setAnchorElNotification(null) setAnchorElNotification(null);
} };
const fullNotifications = useMemo(() => { const fullNotifications = useMemo(() => {
return [...notifications].sort( return [...notifications].sort((a, b) => b.created - a.created);
(a, b) => b.created - a.created }, [notifications]);
) const notificationBadgeLength = useMemo(() => {
}, [notifications]) if (!notificationTimestamp) return fullNotifications.length;
const notificationBadgeLength = useMemo(()=> { return fullNotifications?.filter(
if(!notificationTimestamp) return fullNotifications.length item => item.created > notificationTimestamp
return fullNotifications?.filter((item)=> item.created > notificationTimestamp).length ).length;
}, [fullNotifications, notificationTimestamp]) }, [fullNotifications, notificationTimestamp]);
const checkNotifications = useCallback(async (username: string) => { const checkNotifications = useCallback(async (username: string) => {
try { try {
// let notificationComments: Item[] = // let notificationComments: Item[] =
// (await notification.getItem('comments')) || [] // (await notification.getItem('comments')) || []
// notificationComments = notificationComments // notificationComments = notificationComments
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen) // .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
// .sort((a, b) => b.lastSeen - a.lastSeen) // .sort((a, b) => b.lastSeen - a.lastSeen)
const timestamp = await generalLocal.getItem("notification-timestamp"); const timestamp = await generalLocal.getItem("notification-timestamp");
const after = timestamp || moment().subtract(5, 'days').valueOf(); const after = timestamp || moment().subtract(5, "days").valueOf();
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 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, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); });
const responseDataSearch = await response.json(); const responseDataSearch = await response.json();
let notifys = [] let notifys = [];
for (const comment of responseDataSearch) { for (const comment of responseDataSearch) {
if (comment.identifier && comment.name && comment?.metadata?.description) { if (
comment.identifier &&
comment.name &&
try { comment?.metadata?.description
const result = extractSigValue(comment?.metadata?.description) ) {
if(!result) continue try {
const res = await getPaymentInfo(result); const result = extractSigValue(comment?.metadata?.description);
if(+res?.amount >= minPriceSuperlike && res.recipient === usernameAddress && isTimestampWithinRange(res?.timestamp, comment.created)){ if (!result) continue;
const res = await getPaymentInfo(result);
let urlReference = null if (
try { +res?.amount >= minPriceSuperlike &&
let idForUrl = extractIdValue(comment?.metadata?.description) res.recipient === usernameAddress &&
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`; isTimestampWithinRange(res?.timestamp, comment.created)
const response2 = await fetch(url, { ) {
method: "GET", let urlReference = null;
headers: { try {
"Content-Type": "application/json", 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, {
const responseSearch = await response2.json(); method: "GET",
if(responseSearch.length > 0){ headers: {
urlReference = responseSearch[0] "Content-Type": "application/json",
} },
});
} catch (error) { const responseSearch = await response2.json();
if (responseSearch.length > 0) {
} urlReference = responseSearch[0];
// 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) {
} }
} 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 })
} }
}, []) }
setNotifications(prev => {
const checkNotificationsFunc = useCallback( const allNotifications = [...notifys, ...prev];
(username: string) => { const uniqueNotifications = Array.from(
let isCalling = false new Map(
interval.current = setInterval(async () => { allNotifications.map(notif => [notif.identifier, notif])
if (isCalling) return ).values()
isCalling = true );
const res = await checkNotifications(username) return uniqueNotifications.slice(0, 20);
isCalling = false });
}, 60000) } catch (error) {
checkNotifications(username) console.log({ error });
}, }
[checkNotifications]) }, []);
useEffect(() => { const checkNotificationsFunc = useCallback(
if (!username) return (username: string) => {
checkNotificationsFunc(username) let isCalling = false;
interval.current = setInterval(async () => {
if (isCalling) return;
isCalling = true;
return () => { const res = await checkNotifications(username);
if (interval?.current) { isCalling = false;
clearInterval(interval.current) }, 60000);
} checkNotifications(username);
} },
}, [checkNotificationsFunc, username]) [checkNotifications]
);
useEffect(() => {
if (!username) return;
checkNotificationsFunc(username);
return () => {
if (interval?.current) {
clearInterval(interval.current);
}
};
}, [checkNotificationsFunc, username]);
const openPopover = Boolean(anchorElNotification) const openPopover = Boolean(anchorElNotification);
return ( return (
<Box <Box
sx={{
display: 'flex',
alignItems: 'center'
}}
>
<Badge
badgeContent={notificationBadgeLength}
color="primary"
sx={{ sx={{
margin: '0px 12px' display: "flex",
alignItems: "center",
}} }}
> >
<Button <Badge
onClick={(e) => { badgeContent={notificationBadgeLength}
openNotificationPopover(e) color="primary"
generalLocal.setItem("notification-timestamp", Date.now());
setNotificationTimestamp(Date.now)
}}
sx={{ sx={{
margin: '0px', margin: "0px 12px",
padding: '0px',
height: 'auto',
width: 'auto',
minWidth: 'unset'
}} }}
> >
<NotificationsIcon color="action" /> <Button
</Button> onClick={e => {
</Badge> openNotificationPopover(e);
<Popover generalLocal.setItem("notification-timestamp", Date.now());
id={'simple-popover-notification'} setNotificationTimestamp(Date.now);
open={openPopover} }}
anchorEl={anchorElNotification}
onClose={closeNotificationPopover}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left'
}}
>
<Box>
<List
sx={{ sx={{
maxHeight: '300px', margin: "0px",
overflow: 'auto' 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 && ( {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 <ListItemText
primary="No new notifications"> primary={
<Box
</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"
>
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={{ sx={{
fontSize: '16px' display: "flex",
alignItems: "center",
gap: "5px",
}} }}
color="textSecondary"
> >
{` from ${notification.name}`} <Typography
</Typography> component="span"
</React.Fragment> variant="body1"
} color="textPrimary"
/> >
</ListItem> Super Like
))} </Typography>
</List> <ThumbUpIcon
</Box> style={{
</Popover> color: "gold",
</Box> }}
) />
} </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>
);
};

165
src/components/common/SuperLike/SuperLike.tsx

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; import ThumbUpIcon from "@mui/icons-material/ThumbUp";
import { import {
Box, Box,
@ -7,10 +7,13 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
FormControl,
Input, Input,
InputAdornment, InputAdornment,
InputLabel, InputLabel,
MenuItem,
Modal, Modal,
Select,
Tooltip, Tooltip,
} from "@mui/material"; } from "@mui/material";
import qortImg from "../../../assets/img/qort.png"; import qortImg from "../../../assets/img/qort.png";
@ -19,13 +22,7 @@ import { useDispatch, useSelector } from "react-redux";
import { setNotification } from "../../../state/features/notificationsSlice"; import { setNotification } from "../../../state/features/notificationsSlice";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { objectToBase64 } from "../../../utils/toBase64"; import { objectToBase64 } from "../../../utils/toBase64";
import { import { minPriceSuperlike } from "../../../constants/Misc.ts";
FOR,
FOR_SUPER_LIKE,
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
minPriceSuperlike,
} from "../../../constants";
import { CommentInput } from "../Comments/Comments-styles"; import { CommentInput } from "../Comments/Comments-styles";
import { import {
CrowdfundActionButton, CrowdfundActionButton,
@ -33,9 +30,18 @@ import {
ModalBody, ModalBody,
NewCrowdfundTitle, NewCrowdfundTitle,
Spacer, Spacer,
} from "../../UploadVideo/Upload-styles"; } from "../../PublishVideo/PublishVideo-styles.tsx";
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor"; import { utf8ToBase64 } from "../SuperLikesList/CommentEditor";
import { RootState } from "../../../state/store"; 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 }); const uid = new ShortUniqueId({ length: 4 });
@ -48,17 +54,21 @@ export const SuperLike = ({
numberOfSuperlikes, numberOfSuperlikes,
}) => { }) => {
const [isOpen, setIsOpen] = useState<boolean>(false); 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 [comment, setComment] = useState<string>("");
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [publishes, setPublishes] = useState<any[]>([]); const [publishes, setPublishes] = useState<any[]>([]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const resetValues = () => { const resetValues = () => {
setAmount(0); setSuperlikeDonationAmount(0);
setComment(""); setComment("");
setPublishes([]); setPublishes([]);
}; };
@ -71,6 +81,15 @@ export const SuperLike = ({
try { try {
if (!username) throw new Error("You need a name to publish"); if (!username) throw new Error("You need a name to publish");
if (!name) throw new Error("Could not retrieve content creator's name"); 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({ let resName = await qortalRequest({
action: "GET_NAME_DATA", action: "GET_NAME_DATA",
@ -83,7 +102,10 @@ export const SuperLike = ({
if (!address) if (!address)
throw new Error("Could not retrieve content creator's address"); throw new Error("Could not retrieve content creator's address");
if (!amount || amount < minPriceSuperlike) if (
!superlikeDonationAmount ||
superlikeDonationAmount < minPriceSuperlike
)
throw new Error( throw new Error(
`The amount needs to be at least ${minPriceSuperlike} QORT` `The amount needs to be at least ${minPriceSuperlike} QORT`
); );
@ -94,9 +116,26 @@ export const SuperLike = ({
action: "SEND_COIN", action: "SEND_COIN",
coin: "QORT", coin: "QORT",
destinationAddress: address, 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:${ let metadescription = `**sig:${
res.signature res.signature
};${FOR}:${name}_${FOR_SUPER_LIKE};nm:${name.slice( };${FOR}:${name}_${FOR_SUPER_LIKE};nm:${name.slice(
@ -119,7 +158,7 @@ export const SuperLike = ({
for: `${name}_${FOR_SUPER_LIKE}`, for: `${name}_${FOR_SUPER_LIKE}`,
}, },
about: 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 // Description is obtained from raw data
// const base64 = utf8ToBase64(comment); // const base64 = utf8ToBase64(comment);
@ -164,6 +203,14 @@ export const SuperLike = ({
throw new Error("Failed to publish Super Like"); throw new Error("Failed to publish Super Like");
} }
} }
useEffect(() => {
getUserBalance().then(foundBalance => {
setCurrentBalance(truncateNumber(foundBalance, 2));
});
}, []);
const textFieldWidth = "350px";
return ( return (
<> <>
<Box <Box
@ -245,42 +292,44 @@ export const SuperLike = ({
<NewCrowdfundTitle>Super Like</NewCrowdfundTitle> <NewCrowdfundTitle>Super Like</NewCrowdfundTitle>
</Box> </Box>
<DialogContent> <DialogContent>
<Box <Box>
sx={{ <InputLabel htmlFor="standard-adornment-amount">
width: "300px", Amount in QORT (min 10 QORT)
display: "flex", </InputLabel>
justifyContent: "center", <BoundedNumericTextField
}} minValue={10}
> initialValue={minPriceSuperlike.toString()}
<Box> maxValue={numberToInt(+currentBalance)}
<InputLabel htmlFor="standard-adornment-amount"> allowDecimals={false}
Amount in QORT (min 10 QORT) allowNegatives={false}
</InputLabel> id="standard-adornment-amount"
<Input value={superlikeDonationAmount}
id="standard-adornment-amount" afterChange={(e: string) => setSuperlikeDonationAmount(+e)}
type="number" InputProps={{
value={amount} style: { fontSize: 30, width: textFieldWidth },
onChange={(e) => setAmount(+e.target.value)} startAdornment: (
startAdornment={
<InputAdornment position="start"> <InputAdornment position="start">
<img <img
style={{ style={{
height: "15px", height: "40px",
width: "15px", width: "40px",
}} }}
src={qortImg} src={qortImg}
alt={"Qort Icon"}
/> />
</InputAdornment> </InputAdornment>
} ),
/> }}
</Box> />
</Box>
<Spacer height="25px" /> <div>Current QORT Balance is: {currentBalance}</div>
<Box> <Spacer height="25px" />
<CommentInput <CommentInput
id="standard-multiline-flexible" id="standard-multiline-flexible"
label="Your comment" label="Your comment"
multiline multiline
minRows={8}
maxRows={8} maxRows={8}
variant="filled" variant="filled"
value={comment} value={comment}
@ -288,7 +337,37 @@ export const SuperLike = ({
maxLength: 500, maxLength: 500,
}} }}
InputLabelProps={{ style: { fontSize: "18px" } }} 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> </Box>
</DialogContent> </DialogContent>
@ -332,7 +411,7 @@ export const SuperLike = ({
message: comment, message: comment,
service, service,
identifier, identifier,
amount: +amount, amount: +superlikeDonationAmount,
created: Date.now(), created: Date.now(),
}); });
setIsOpenMultiplePublish(false); setIsOpenMultiplePublish(false);

66
src/components/common/SuperLikesList/CommentEditor.tsx

@ -11,8 +11,8 @@ import {
CommentInputContainer, CommentInputContainer,
SubmitCommentButton, SubmitCommentButton,
} from "./Comments-styles"; } from "./Comments-styles";
import { COMMENT_BASE } from "../../../constants";
import { addtoHashMapSuperlikes } from "../../../state/features/videoSlice"; import { addtoHashMapSuperlikes } from "../../../state/features/videoSlice";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const notification = localforage.createInstance({ const notification = localforage.createInstance({
@ -84,9 +84,9 @@ interface CommentEditorProps {
commentId?: string; commentId?: string;
isEdit?: boolean; isEdit?: boolean;
commentMessage?: string; commentMessage?: string;
isSuperLike?: boolean isSuperLike?: boolean;
comment?: any; comment?: any;
hasHash?: boolean hasHash?: boolean;
} }
export function utf8ToBase64(inputString: string): string { export function utf8ToBase64(inputString: string): string {
@ -111,7 +111,7 @@ export const CommentEditor = ({
commentMessage, commentMessage,
isSuperLike, isSuperLike,
comment, comment,
hasHash hasHash,
}: CommentEditorProps) => { }: CommentEditorProps) => {
const [value, setValue] = useState<string>(""); const [value, setValue] = useState<string>("");
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -156,34 +156,41 @@ export const CommentEditor = ({
} }
try { try {
let data64 = null let data64 = null;
let description = "" let description = "";
let tag1 = "" let tag1 = "";
let superObj = {} let superObj = {};
if(isSuperLike){ if (isSuperLike) {
if(!comment?.metadata?.description || !comment?.metadata?.tags[0] || !comment?.transactionReference || !comment?.notificationInformation || !comment?.about) throw new Error('unable to edit Super like') if (
description = comment?.metadata?.description !comment?.metadata?.description ||
tag1 = comment?.metadata?.tags[0] !comment?.metadata?.tags[0] ||
superObj = { !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, comment: value,
transactionReference: comment.transactionReference, transactionReference: comment.transactionReference,
notificationInformation: comment.notificationInformation, notificationInformation: comment.notificationInformation,
about: comment.about about: comment.about,
} };
const superLikeToBase64 = await objectToBase64(superObj); 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 base64 = utf8ToBase64(value);
const resourceResponse = await qortalRequest({ const resourceResponse = await qortalRequest({
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
name: name, name: name,
service: "BLOG_COMMENT", service: "BLOG_COMMENT",
data64: isSuperLike ? data64 : base64, data64: isSuperLike ? data64 : base64,
identifier: identifier, identifier: identifier,
description, description,
tag1 tag1,
}); });
dispatch( dispatch(
setNotification({ setNotification({
@ -192,13 +199,14 @@ export const CommentEditor = ({
}) })
); );
if(isSuperLike){ if (isSuperLike) {
dispatch(addtoHashMapSuperlikes({ dispatch(
...superObj, addtoHashMapSuperlikes({
...comment, ...superObj,
message: value ...comment,
})) message: value,
})
);
} }
if (idForNotification) { if (idForNotification) {
addItem({ addItem({
@ -240,7 +248,7 @@ export const CommentEditor = ({
let identifier = `${COMMENT_BASE}${postId.slice(-12)}_base_${id}`; let identifier = `${COMMENT_BASE}${postId.slice(-12)}_base_${id}`;
let idForNotification = identifier; let idForNotification = identifier;
let service = 'BLOG_COMMENT' let service = "BLOG_COMMENT";
if (isReply && commentId) { if (isReply && commentId) {
const removeBaseCommentId = commentId; const removeBaseCommentId = commentId;
removeBaseCommentId.replace("_base_", ""); removeBaseCommentId.replace("_base_", "");
@ -254,8 +262,8 @@ export const CommentEditor = ({
} }
await publishComment(identifier, idForNotification); await publishComment(identifier, idForNotification);
if(isSuperLike){ if (isSuperLike) {
onSubmit({}) onSubmit({});
} else { } else {
onSubmit({ onSubmit({
created: Date.now(), created: Date.now(),

100
src/components/common/SuperLikesList/SuperLikesSection.tsx

@ -14,14 +14,17 @@ import {
LoadMoreCommentsButtonRow, LoadMoreCommentsButtonRow,
NoCommentsRow, NoCommentsRow,
} from "./Comments-styles"; } from "./Comments-styles";
import { COMMENT_BASE } from "../../../constants"; import {
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from "../../UploadVideo/Upload-styles"; CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../PublishVideo/PublishVideo-styles.tsx";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
interface CommentSectionProps { interface CommentSectionProps {
postId: string; postId: string;
postName: string; postName: string;
superlikes: any[]; superlikes: any[];
getMore: ()=> void; getMore: () => void;
loadingSuperLikes: boolean; loadingSuperLikes: boolean;
} }
@ -49,7 +52,13 @@ const Panel = styled("div")`
background-color: #555; 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 navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [listComments, setListComments] = useState<any[]>([]); const [listComments, setListComments] = useState<any[]>([]);
@ -59,7 +68,7 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
const [loadingComments, setLoadingComments] = useState<boolean>(null); const [loadingComments, setLoadingComments] = useState<boolean>(null);
const hashMapSuperlikes = useSelector( const hashMapSuperlikes = useSelector(
(state: RootState) => state.video.hashMapSuperlikes (state: RootState) => state.video.hashMapSuperlikes
) );
const onSubmit = (obj?: any, isEdit?: boolean) => { const onSubmit = (obj?: any, isEdit?: boolean) => {
if (isEdit) { if (isEdit) {
setListComments((prev: any[]) => { setListComments((prev: any[]) => {
@ -147,32 +156,28 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
[postId] [postId]
); );
const getComments = useCallback( const getComments = useCallback(async (superlikes, postId) => {
async (superlikes, postId) => { try {
try { setLoadingComments(true);
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) { let comments: any[] = [];
console.error(error); for (const comment of superlikes) {
} finally { comments.push(comment);
setLoadingComments(false); const res = await getReplies(comment.identifier, postId);
comments = [...comments, ...res];
} }
},
[] setListComments(comments);
); } catch (error) {
console.error(error);
} finally {
setLoadingComments(false);
}
}, []);
useEffect(() => { useEffect(() => {
if(postId){ if (postId) {
getComments(superlikes, postId) getComments(superlikes, postId);
} }
}, [getComments, superlikes, postId]); }, [getComments, superlikes, postId]);
@ -191,41 +196,44 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
}, []); }, []);
}, [listComments]); }, [listComments]);
return ( return (
<> <>
<Panel> <Panel>
<CrowdfundSubTitleRow > <CrowdfundSubTitleRow>
<CrowdfundSubTitle sx={{ <CrowdfundSubTitle
fontSize: '18px', sx={{
color: 'gold' fontSize: "18px",
}}>Super Likes</CrowdfundSubTitle> color: "gold",
</CrowdfundSubTitleRow> }}
>
Super Likes
</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CommentsContainer> <CommentsContainer>
{(loadingComments || loadingSuperLikes) ? ( {loadingComments || loadingSuperLikes ? (
<NoCommentsRow> <NoCommentsRow>
<CircularProgress /> <CircularProgress />
</NoCommentsRow> </NoCommentsRow>
) : listComments.length === 0 ? ( ) : listComments.length === 0 ? (
<NoCommentsRow> <NoCommentsRow>
There are no super likes yet. Be the first! There are no super likes yet. Be the first!
</NoCommentsRow> </NoCommentsRow>
) : ( ) : (
<CommentContainer> <CommentContainer>
{structuredCommentList.map((comment: any) => { {structuredCommentList.map((comment: any) => {
let hasHash = false let hasHash = false;
let message = {...comment} let message = { ...comment };
let hash = {} let hash = {};
if(hashMapSuperlikes[comment?.identifier]){ if (hashMapSuperlikes[comment?.identifier]) {
message.message = hashMapSuperlikes[comment?.identifier]?.comment || "" message.message =
hasHash = true hashMapSuperlikes[comment?.identifier]?.comment || "";
hash = hashMapSuperlikes[comment?.identifier] hasHash = true;
hash = hashMapSuperlikes[comment?.identifier];
} }
return ( return (
<Comment <Comment
key={comment?.identifier} key={comment?.identifier}
comment={{...message, ...hash}} comment={{ ...message, ...hash }}
onSubmit={onSubmit} onSubmit={onSubmit}
postId={postId} postId={postId}
postName={postName} postName={postName}
@ -241,7 +249,7 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
<LoadMoreCommentsButtonRow> <LoadMoreCommentsButtonRow>
<LoadMoreCommentsButton <LoadMoreCommentsButton
onClick={() => { onClick={() => {
getMore() getMore();
}} }}
variant="contained" variant="contained"
size="small" size="small"

34
src/components/layout/Navbar/Navbar.tsx

@ -35,8 +35,8 @@ import {
} from "../../../state/features/videoSlice"; } from "../../../state/features/videoSlice";
import { RootState } from "../../../state/store"; import { RootState } from "../../../state/store";
import { useWindowSize } from "../../../hooks/useWindowSize"; import { useWindowSize } from "../../../hooks/useWindowSize";
import { UploadVideo } from "../../UploadVideo/UploadVideo"; import { PublishVideo } from "../../PublishVideo/PublishVideo.tsx";
import { StyledButton } from "../../UploadVideo/Upload-styles"; import { StyledButton } from "../../PublishVideo/PublishVideo-styles.tsx";
import { Notifications } from "../../common/Notifications/Notifications"; import { Notifications } from "../../common/Notifications/Notifications";
interface Props { interface Props {
isAuthenticated: boolean; isAuthenticated: boolean;
@ -279,10 +279,10 @@ const NavBar: React.FC<Props> = ({
<Input <Input
id="standard-adornment-name" id="standard-adornment-name"
inputRef={inputRef} inputRef={inputRef}
onChange={(e) => { onChange={e => {
searchValRef.current = e.target.value; searchValRef.current = e.target.value;
}} }}
onKeyDown={(event) => { onKeyDown={event => {
if (event.key === "Enter" || event.keyCode === 13) { if (event.key === "Enter" || event.keyCode === 13) {
if (!searchValRef.current) { if (!searchValRef.current) {
dispatch(setIsFiltering(false)); dispatch(setIsFiltering(false));
@ -355,9 +355,7 @@ const NavBar: React.FC<Props> = ({
/> />
</Box> </Box>
</Popover> </Popover>
{isAuthenticated && userName && ( {isAuthenticated && userName && <Notifications />}
<Notifications />
)}
<DownloadTaskManager /> <DownloadTaskManager />
{isAuthenticated && userName && ( {isAuthenticated && userName && (
@ -393,20 +391,18 @@ const NavBar: React.FC<Props> = ({
<AvatarContainer> <AvatarContainer>
{isAuthenticated && userName && ( {isAuthenticated && userName && (
<> <>
<UploadVideo /> <PublishVideo />
<StyledButton <StyledButton
color="primary" color="primary"
startIcon={<AddBoxIcon />} startIcon={<AddBoxIcon />}
onClick={() => { onClick={() => {
dispatch(setEditPlaylist({mode: 'new'})) dispatch(setEditPlaylist({ mode: "new" }));
}} }}
> >
create playlist create playlist
</StyledButton> </StyledButton>
</> </>
)} )}
</AvatarContainer> </AvatarContainer>
<Popover <Popover

107
src/constants/Categories.ts

@ -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

@ -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

@ -0,0 +1,2 @@
export const minPriceSuperlike = 10;
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^&*+=]/g;

121
src/constants/index.ts

@ -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"}
]
}

491
src/hooks/useFetchVideos.tsx

@ -1,5 +1,5 @@
import React from 'react' import React from "react";
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from "react-redux";
import { import {
addVideos, addVideos,
addToHashMap, addToHashMap,
@ -7,102 +7,107 @@ import {
upsertVideos, upsertVideos,
upsertVideosBeginning, upsertVideosBeginning,
Video, Video,
upsertFilteredVideos upsertFilteredVideos,
} from '../state/features/videoSlice' } from "../state/features/videoSlice";
import { import {
setIsLoadingGlobal, setUserAvatarHash setIsLoadingGlobal,
} from '../state/features/globalSlice' setUserAvatarHash,
import { RootState } from '../state/store' } from "../state/features/globalSlice";
import { fetchAndEvaluateVideos } from '../utils/fetchVideos' import { RootState } from "../state/store";
import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE } from '../constants' import { fetchAndEvaluateVideos } from "../utils/fetchVideos";
import { RequestQueue } from '../utils/queue' import { RequestQueue } from "../utils/queue";
import { queue } from '../wrappers/GlobalWrapper' import { queue } from "../wrappers/GlobalWrapper";
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../constants/Identifiers.ts";
export const useFetchVideos = () => { export const useFetchVideos = () => {
const dispatch = useDispatch() const dispatch = useDispatch();
const hashMapVideos = useSelector( const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos (state: RootState) => state.video.hashMapVideos
) );
const videos = useSelector((state: RootState) => state.video.videos) const videos = useSelector((state: RootState) => state.video.videos);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
const filteredVideos = useSelector( const filteredVideos = useSelector(
(state: RootState) => state.video.filteredVideos (state: RootState) => state.video.filteredVideos
) );
const checkAndUpdateVideo = React.useCallback( const checkAndUpdateVideo = React.useCallback(
(video: Video) => { (video: Video) => {
const existingVideo = hashMapVideos[video.id] const existingVideo = hashMapVideos[video.id];
if (!existingVideo) { if (!existingVideo) {
return true return true;
} else if ( } else if (
video?.updated && video?.updated &&
existingVideo?.updated && existingVideo?.updated &&
(!existingVideo?.updated || video?.updated) > existingVideo?.updated (!existingVideo?.updated || video?.updated) > existingVideo?.updated
) { ) {
return true return true;
} else { } else {
return false return false;
} }
}, },
[hashMapVideos] [hashMapVideos]
) );
const getAvatar = React.useCallback(async (author: string) => { const getAvatar = React.useCallback(async (author: string) => {
try { try {
let url = await qortalRequest({ let url = await qortalRequest({
action: 'GET_QDN_RESOURCE_URL', action: "GET_QDN_RESOURCE_URL",
name: author,
service: 'THUMBNAIL',
identifier: 'qortal_avatar'
})
dispatch(setUserAvatarHash({
name: author, name: author,
url service: "THUMBNAIL",
})) identifier: "qortal_avatar",
} catch (error) { } });
}, [])
dispatch(
const getVideo = async (user: string, videoId: string, content: any, retries: number = 0) => { setUserAvatarHash({
name: author,
url,
})
);
} catch (error) {}
}, []);
const getVideo = async (
user: string,
videoId: string,
content: any,
retries: number = 0
) => {
try { try {
const res = await fetchAndEvaluateVideos({ const res = await fetchAndEvaluateVideos({
user, user,
videoId, videoId,
content content,
}) });
dispatch(addToHashMap(res)) dispatch(addToHashMap(res));
} catch (error) { } catch (error) {
retries= retries + 1 retries = retries + 1;
if (retries < 2) { // 3 is the maximum number of retries here, you can adjust it to your needs 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)); queue.push(() => getVideo(user, videoId, content, retries + 1));
} else { } 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 () => { const getNewVideos = React.useCallback(async () => {
try { 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, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
// const responseData = await qortalRequest({ // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES", // action: "SEARCH_QDN_RESOURCES",
@ -116,16 +121,16 @@ export const useFetchVideos = () => {
// exactMatchNames: true, // exactMatchNames: true,
// name: names // name: names
// }) // })
const latestVideo = videos[0] const latestVideo = videos[0];
if (!latestVideo) return if (!latestVideo) return;
const findVideo = responseData?.findIndex( const findVideo = responseData?.findIndex(
(item: any) => item?.identifier === latestVideo?.id (item: any) => item?.identifier === latestVideo?.id
) );
let fetchAll = responseData let fetchAll = responseData;
let willFetchAll = true let willFetchAll = true;
if (findVideo !== -1) { if (findVideo !== -1) {
willFetchAll = false willFetchAll = false;
fetchAll = responseData.slice(0, findVideo) fetchAll = responseData.slice(0, findVideo);
} }
const structureData = fetchAll.map((video: any): Video => { const structureData = fetchAll.map((video: any): Video => {
@ -138,22 +143,22 @@ export const useFetchVideos = () => {
created: video?.created, created: video?.created,
updated: video?.updated, updated: video?.updated,
user: video.name, user: video.name,
videoImage: '', videoImage: "",
id: video.identifier id: video.identifier,
} };
}) });
if (!willFetchAll) { if (!willFetchAll) {
dispatch(upsertVideosBeginning(structureData)) dispatch(upsertVideosBeginning(structureData));
} }
if (willFetchAll) { if (willFetchAll) {
dispatch(addVideos(structureData)) dispatch(addVideos(structureData));
} }
setTimeout(()=> { setTimeout(() => {
dispatch(setCountNewVideos(0)) dispatch(setCountNewVideos(0));
}, 1000) }, 1000);
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateVideo(content) const res = checkAndUpdateVideo(content);
if (res) { if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getVideo(content.user, content.id, content));
} }
@ -161,182 +166,185 @@ export const useFetchVideos = () => {
} }
} catch (error) { } catch (error) {
} finally { } finally {
dispatch(setIsLoadingGlobal(false)) dispatch(setIsLoadingGlobal(false));
} }
}, [videos, hashMapVideos]) }, [videos, hashMapVideos]);
const getVideos = React.useCallback(async (filters = {}, reset?:boolean, resetFilers?: boolean,limit?: number) => { const getVideos = React.useCallback(
try { async (
const {name = '', filters = {},
category = '', reset?: boolean,
subcategory = '', resetFilers?: boolean,
keywords = '', limit?: number
type = '' }: any = resetFilers ? {} : filters ) => {
let offset = videos.length try {
if(reset){ const {
offset = 0 name = "",
} category = "",
const videoLimit = limit || 20 subcategory = "",
keywords = "",
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}` type = "",
}: any = resetFilers ? {} : filters;
let offset = videos.length;
if(name){ if (reset) {
defaultUrl = defaultUrl + `&name=${name}` offset = 0;
}
if(category){
if(!subcategory){
defaultUrl = defaultUrl + `&description=category:${category}`
} else {
defaultUrl = defaultUrl + `&description=category:${category};subcategory:${subcategory}`
} }
} const videoLimit = limit || 20;
if(keywords){
defaultUrl = defaultUrl + `&query=${keywords}`
}
if(type === 'playlists'){
defaultUrl = defaultUrl + `&service=PLAYLIST`
defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`
} else { let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
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}` if (name) {
const url = defaultUrl defaultUrl = defaultUrl + `&name=${name}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
} }
}) if (category) {
const responseData = await response.json() if (!subcategory) {
defaultUrl = defaultUrl + `&description=category:${category}`;
} else {
// const responseData = await qortalRequest({ defaultUrl =
// action: "SEARCH_QDN_RESOURCES", defaultUrl +
// mode: "ALL", `&description=category:${category};subcategory:${subcategory}`;
// service: "DOCUMENT", }
// query: "${QTUBE_VIDEO_BASE}", }
// limit: 20, if (keywords) {
// includeMetadata: true, defaultUrl = defaultUrl + `&query=${keywords}`;
// offset: offset, }
// reverse: true, if (type === "playlists") {
// excludeBlocked: true, defaultUrl = defaultUrl + `&service=PLAYLIST`;
// exactMatchNames: true, defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`;
// name: names } else {
// }) defaultUrl = defaultUrl + `&service=DOCUMENT`;
const structureData = responseData.map((video: any): Video => { defaultUrl = defaultUrl + `&identifier=${QTUBE_VIDEO_BASE}`;
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 url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
for (const content of structureData) { const url = defaultUrl;
if (content.user && content.id) { const response = await fetch(url, {
const res = checkAndUpdateVideo(content) method: "GET",
if (res) { 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));
}
for (const content of structureData) {
if (content.user && content.id) {
const res = checkAndUpdateVideo(content);
if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getVideo(content.user, content.id, content));
}
} }
} }
} catch (error) {
console.log({ error });
} finally {
} }
} catch (error) { },
console.log({error}) [videos, hashMapVideos]
} finally { );
} const getVideosFiltered = React.useCallback(
}, [videos, hashMapVideos]) async (filterValue: string) => {
try {
const getVideosFiltered = React.useCallback(async (filterValue: string) => { const offset = filteredVideos.length;
try { const replaceSpacesWithUnderscore = filterValue.replace(/ /g, "_");
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, {
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}` method: "GET",
const response = await fetch(url, { headers: {
method: 'GET', "Content-Type": "application/json",
headers: { },
'Content-Type': 'application/json' });
} const responseData = await response.json();
})
const responseData = await response.json() // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// const responseData = await qortalRequest({ // mode: "ALL",
// action: "SEARCH_QDN_RESOURCES", // service: "DOCUMENT",
// mode: "ALL", // query: replaceSpacesWithUnderscore,
// service: "DOCUMENT", // identifier: "${QTUBE_VIDEO_BASE}",
// query: replaceSpacesWithUnderscore, // limit: 20,
// identifier: "${QTUBE_VIDEO_BASE}", // includeMetadata: true,
// limit: 20, // offset: offset,
// includeMetadata: true, // reverse: true,
// offset: offset, // excludeBlocked: true,
// reverse: true, // exactMatchNames: true,
// excludeBlocked: true, // name: names
// exactMatchNames: true, // })
// name: names const structureData = responseData.map((video: any): Video => {
// }) return {
const structureData = responseData.map((video: any): Video => { title: video?.metadata?.title,
return { category: video?.metadata?.category,
title: video?.metadata?.title, categoryName: video?.metadata?.categoryName,
category: video?.metadata?.category, tags: video?.metadata?.tags || [],
categoryName: video?.metadata?.categoryName, description: video?.metadata?.description,
tags: video?.metadata?.tags || [], created: video?.created,
description: video?.metadata?.description, updated: video?.updated,
created: video?.created, user: video.name,
updated: video?.updated, videoImage: "",
user: video.name, id: video.identifier,
videoImage: '', };
id: video.identifier });
} dispatch(upsertFilteredVideos(structureData));
})
dispatch(upsertFilteredVideos(structureData)) for (const content of structureData) {
if (content.user && content.id) {
for (const content of structureData) { const res = checkAndUpdateVideo(content);
if (content.user && content.id) { if (res) {
const res = checkAndUpdateVideo(content) queue.push(() => getVideo(content.user, content.id, 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 () => { const checkNewVideos = React.useCallback(async () => {
try { 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, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
// const responseData = await qortalRequest({ // const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES", // action: "SEARCH_QDN_RESOURCES",
// mode: "ALL", // mode: "ALL",
@ -349,21 +357,20 @@ export const useFetchVideos = () => {
// exactMatchNames: true, // exactMatchNames: true,
// name: names // name: names
// }) // })
const latestVideo = videos[0] const latestVideo = videos[0];
if (!latestVideo) return if (!latestVideo) return;
const findVideo = responseData?.findIndex( const findVideo = responseData?.findIndex(
(item: any) => item?.identifier === latestVideo?.id (item: any) => item?.identifier === latestVideo?.id
) );
if (findVideo === -1) { if (findVideo === -1) {
dispatch(setCountNewVideos(responseData.length)) dispatch(setCountNewVideos(responseData.length));
return return;
} }
const newArray = responseData.slice(0, findVideo) const newArray = responseData.slice(0, findVideo);
dispatch(setCountNewVideos(newArray.length)) dispatch(setCountNewVideos(newArray.length));
return return;
} catch (error) {} } catch (error) {}
}, [videos]) }, [videos]);
return { return {
getVideos, getVideos,
@ -372,6 +379,6 @@ export const useFetchVideos = () => {
hashMapVideos, hashMapVideos,
getNewVideos, getNewVideos,
checkNewVideos, checkNewVideos,
getVideosFiltered getVideosFiltered,
} };
} };

360
src/pages/Home/VideoList.tsx

@ -58,11 +58,11 @@ import {
setEditPlaylist, setEditPlaylist,
setEditVideo, setEditVideo,
} from "../../state/features/videoSlice"; } from "../../state/features/videoSlice";
import { categories, subCategories } from "../../constants"; import { categories, subCategories } from "../../constants/Categories.ts";
import { Playlists } from "../../components/Playlists/Playlists"; import { Playlists } from "../../components/Playlists/Playlists";
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG"; import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
import BlockIcon from "@mui/icons-material/Block"; 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 { LiskSuperLikeContainer } from "../../components/common/ListSuperLikes/LiskSuperLikeContainer";
import { VideoCardImageContainer } from "./VideoCardImageContainer"; import { VideoCardImageContainer } from "./VideoCardImageContainer";
@ -83,19 +83,19 @@ export const VideoList = ({ mode }: VideoListProps) => {
const filterType = useSelector((state: RootState) => state.video.filterType); const filterType = useSelector((state: RootState) => state.video.filterType);
const setFilterType = (payload) => { const setFilterType = payload => {
dispatch(changeFilterType(payload)); dispatch(changeFilterType(payload));
}; };
const filterSearch = useSelector( const filterSearch = useSelector(
(state: RootState) => state.video.filterSearch (state: RootState) => state.video.filterSearch
); );
const setFilterSearch = (payload) => { const setFilterSearch = payload => {
dispatch(changefilterSearch(payload)); dispatch(changefilterSearch(payload));
}; };
const filterName = useSelector((state: RootState) => state.video.filterName); const filterName = useSelector((state: RootState) => state.video.filterName);
const setFilterName = (payload) => { const setFilterName = payload => {
dispatch(changefilterName(payload)); dispatch(changefilterName(payload));
}; };
@ -103,14 +103,14 @@ export const VideoList = ({ mode }: VideoListProps) => {
(state: RootState) => state.video.selectedCategoryVideos (state: RootState) => state.video.selectedCategoryVideos
); );
const setSelectedCategoryVideos = (payload) => { const setSelectedCategoryVideos = payload => {
dispatch(changeSelectedCategoryVideos(payload)); dispatch(changeSelectedCategoryVideos(payload));
}; };
const selectedSubCategoryVideos = useSelector( const selectedSubCategoryVideos = useSelector(
(state: RootState) => state.video.selectedSubCategoryVideos (state: RootState) => state.video.selectedSubCategoryVideos
); );
const setSelectedSubCategoryVideos = (payload) => { const setSelectedSubCategoryVideos = payload => {
dispatch(changeSelectedSubCategoryVideos(payload)); dispatch(changeSelectedSubCategoryVideos(payload));
}; };
@ -144,8 +144,6 @@ export const VideoList = ({ mode }: VideoListProps) => {
const getVideosHandler = React.useCallback( const getVideosHandler = React.useCallback(
async (reset?: boolean, resetFilers?: boolean) => { async (reset?: boolean, resetFilers?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return; if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return; if (isFetching.current) return;
isFetching.current = true; isFetching.current = true;
@ -259,7 +257,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = categories.find((option) => option.id === +optionId); const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
}; };
const handleOptionSubCategoryChangeVideos = ( const handleOptionSubCategoryChangeVideos = (
@ -268,7 +266,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = subcategories.find( const selectedOption = subcategories.find(
(option) => option.id === +optionId option => option.id === +optionId
); );
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
}; };
@ -284,24 +282,24 @@ export const VideoList = ({ mode }: VideoListProps) => {
}); });
if (response === true) { if (response === true) {
dispatch(blockUser(user)) dispatch(blockUser(user));
} }
} catch (error) {} } catch (error) {}
}; };
const handleInputKeyDown = (event: any) => { const handleInputKeyDown = (event: any) => {
if (event.key === 'Enter') { if (event.key === "Enter") {
getVideosHandler(true); getVideosHandler(true);
} }
} };
return ( return (
<Grid container sx={{ width: "100%" }}> <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> <FiltersContainer>
<Input <Input
id="standard-adornment-name" id="standard-adornment-name"
onChange={(e) => { onChange={e => {
setFilterSearch(e.target.value); setFilterSearch(e.target.value);
}} }}
value={filterSearch} value={filterSearch}
@ -329,11 +327,11 @@ export const VideoList = ({ mode }: VideoListProps) => {
/> />
<Input <Input
id="standard-adornment-name" id="standard-adornment-name"
onChange={(e) => { onChange={e => {
setFilterName(e.target.value); setFilterName(e.target.value);
}} }}
value={filterName} value={filterName}
placeholder="User's name" placeholder="User's Name (Exact)"
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
sx={{ sx={{
marginTop: "20px", marginTop: "20px",
@ -406,7 +404,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
}, },
}} }}
> >
{categories.map((option) => ( {categories.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -428,7 +426,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
labelId="Sub-Category" labelId="Sub-Category"
input={<OutlinedInput label="Sub-Category" />} input={<OutlinedInput label="Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""} value={selectedSubCategoryVideos?.id || ""}
onChange={(e) => onChange={e =>
handleOptionSubCategoryChangeVideos( handleOptionSubCategoryChangeVideos(
e, e,
subCategories[selectedCategoryVideos?.id] subCategories[selectedCategoryVideos?.id]
@ -453,7 +451,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
}} }}
> >
{subCategories[selectedCategoryVideos.id].map( {subCategories[selectedCategoryVideos.id].map(
(option) => ( option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -521,67 +519,165 @@ export const VideoList = ({ mode }: VideoListProps) => {
</FiltersCol> </FiltersCol>
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}> <Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
<ProductManagerRow> <ProductManagerRow>
<Box <Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}}
>
<SubtitleContainer
sx={{ sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%", width: "100%",
maxWidth: "1400px", display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}} }}
> >
<SubtitleContainer
sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%",
maxWidth: "1400px",
}}
></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;
}
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>
);
}
</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;
}
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 ( return (
<VideoCardCol <VideoCardCol
key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)} onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)} onMouseLeave={() => setShowIcons(null)}
key={videoObj.id}
> >
<IconsBox <IconsBox
sx={{ sx={{
opacity: showIcons === videoObj.id ? 1 : 0, opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2, zIndex: 2,
}} }}
> >
{videoObj?.user === username && ( {videoObj?.user === username && (
<Tooltip title="Edit playlist" placement="top"> <Tooltip title="Edit video properties" placement="top">
<BlockIconContainer> <BlockIconContainer>
<EditIcon <EditIcon
onClick={() => { onClick={() => {
dispatch(setEditPlaylist(videoObj)); dispatch(setEditVideo(videoObj));
}} }}
/> />
</BlockIconContainer> </BlockIconContainer>
@ -599,28 +695,25 @@ export const VideoList = ({ mode }: VideoListProps) => {
</Tooltip> </Tooltip>
</IconsBox> </IconsBox>
<VideoCard <VideoCard
sx={{
cursor: !hasHash && 'default'
}}
onClick={() => { onClick={() => {
if(!hasHash) return navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
navigate(
`/playlist/${videoObj?.user}/${videoObj?.id}`
);
}} }}
> >
<ResponsiveImage <VideoCardImageContainer
src={videoObj?.image}
width={266} width={266}
height={150} height={150}
style={{ videoImage={videoObj.videoImage}
maxHeight: '50%' frameImages={videoObj?.extracts || []}
}}
/> />
<VideoCardTitle>{videoObj?.title}</VideoCardTitle> {/* <ResponsiveImage
src={videoObj.videoImage}
width={266}
height={150}
/> */}
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${videoObj?.user}`); navigate(`/channel/${videoObj?.user}`);
}} }}
@ -646,115 +739,18 @@ export const VideoList = ({ mode }: VideoListProps) => {
{formatDate(videoObj.created)} {formatDate(videoObj.created)}
</VideoUploadDate> </VideoUploadDate>
)} )}
<Box
sx={{
display: "flex",
position: "absolute",
bottom: "5px",
right: "5px",
}}
>
<PlaylistSVG
color={theme.palette.text.primary}
height="36px"
width="36px"
/>
</Box>
</BottomParent> </BottomParent>
</VideoCard> </VideoCard>
</VideoCardCol> </VideoCardCol>
); );
} })}
</VideoCardContainer>
return (
<VideoCardCol <LazyLoad
onLoadMore={getVideosHandler}
key={videoObj.id} isLoading={isLoading}
onMouseEnter={() => setShowIcons(videoObj.id)} ></LazyLoad>
onMouseLeave={() => setShowIcons(null)} </Box>
>
<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>
</ProductManagerRow> </ProductManagerRow>
</Grid> </Grid>
<FiltersCol item xs={0} lg={3} xl={2}> <FiltersCol item xs={0} lg={3} xl={2}>

4
src/pages/Home/VideoListComponentLevel.tsx

@ -21,8 +21,8 @@ import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time"; import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { Video } from "../../state/features/videoSlice"; import { Video } from "../../state/features/videoSlice";
import { queue } from "../../wrappers/GlobalWrapper"; import { queue } from "../../wrappers/GlobalWrapper";
import { QTUBE_VIDEO_BASE } from "../../constants";
import { VideoCardImageContainer } from "./VideoCardImageContainer"; import { VideoCardImageContainer } from "./VideoCardImageContainer";
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
interface VideoListProps { interface VideoListProps {
mode?: string; mode?: string;
@ -80,7 +80,7 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
const copiedVideos: Video[] = [...videos]; const copiedVideos: Video[] = [...videos];
structureData.forEach((video: Video) => { 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) { if (index !== -1) {
copiedVideos[index] = video; copiedVideos[index] = video;
} else { } else {

36
src/pages/PlaylistContent/PlaylistContent.tsx

@ -38,12 +38,7 @@ import { CommentSection } from "../../components/common/Comments/CommentSection"
import { import {
CrowdfundSubTitle, CrowdfundSubTitle,
CrowdfundSubTitleRow, CrowdfundSubTitleRow,
} from "../../components/UploadVideo/Upload-styles"; } from "../../components/PublishVideo/PublishVideo-styles.tsx";
import {
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
minPriceSuperlike,
} from "../../constants";
import { Playlists } from "../../components/Playlists/Playlists"; import { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement"; import FileElement from "../../components/common/FileElement";
@ -55,6 +50,11 @@ import {
isTimestampWithinRange, isTimestampWithinRange,
} from "../VideoContent/VideoContent"; } from "../VideoContent/VideoContent";
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection"; 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 = () => { export const PlaylistContent = () => {
const { name, id } = useParams(); const { name, id } = useParams();
@ -82,7 +82,7 @@ export const PlaylistContent = () => {
const [nameAddress, setNameAddress] = useState<string>(""); const [nameAddress, setNameAddress] = useState<string>("");
const getAddressName = async (name) => { const getAddressName = async name => {
const response = await qortalRequest({ const response = await qortalRequest({
action: "GET_NAME_DATA", action: "GET_NAME_DATA",
name: name, name: name,
@ -297,7 +297,7 @@ export const PlaylistContent = () => {
const nextVideo = useMemo(() => { const nextVideo = useMemo(() => {
const currentVideoIndex = playlistData?.videos?.findIndex( const currentVideoIndex = playlistData?.videos?.findIndex(
(item) => item?.identifier === videoData?.id item => item?.identifier === videoData?.id
); );
if (currentVideoIndex !== -1) { if (currentVideoIndex !== -1) {
const nextVideoIndex = currentVideoIndex + 1; const nextVideoIndex = currentVideoIndex + 1;
@ -318,7 +318,7 @@ export const PlaylistContent = () => {
const onEndVideo = useCallback(() => { const onEndVideo = useCallback(() => {
const currentVideoIndex = playlistData?.videos?.findIndex( const currentVideoIndex = playlistData?.videos?.findIndex(
(item) => item?.identifier === videoData?.id item => item?.identifier === videoData?.id
); );
if (currentVideoIndex !== -1) { if (currentVideoIndex !== -1) {
const nextVideoIndex = currentVideoIndex + 1; const nextVideoIndex = currentVideoIndex + 1;
@ -490,8 +490,8 @@ export const PlaylistContent = () => {
name={videoData?.user} name={videoData?.user}
service={videoData?.service} service={videoData?.service}
identifier={videoData?.id} identifier={videoData?.id}
onSuccess={(val) => { onSuccess={val => {
setSuperlikelist((prev) => [val, ...prev]); setSuperlikelist(prev => [val, ...prev]);
}} }}
/> />
)} )}
@ -554,16 +554,16 @@ export const PlaylistContent = () => {
cursor: !descriptionHeight cursor: !descriptionHeight
? "default" ? "default"
: isExpandedDescription : isExpandedDescription
? "default" ? "default"
: "pointer", : "pointer",
position: "relative", position: "relative",
}} }}
className={ className={
!descriptionHeight !descriptionHeight
? "" ? ""
: isExpandedDescription : isExpandedDescription
? "" ? ""
: "hover-click" : "hover-click"
} }
> >
{descriptionHeight && !isExpandedDescription && ( {descriptionHeight && !isExpandedDescription && (
@ -588,8 +588,8 @@ export const PlaylistContent = () => {
height: !descriptionHeight height: !descriptionHeight
? "auto" ? "auto"
: isExpandedDescription : isExpandedDescription
? "auto" ? "auto"
: "100px", : "100px",
overflow: "hidden", overflow: "hidden",
}} }}
> >
@ -610,7 +610,7 @@ export const PlaylistContent = () => {
{descriptionHeight && ( {descriptionHeight && (
<Typography <Typography
onClick={() => { onClick={() => {
setIsExpandedDescription((prev) => !prev); setIsExpandedDescription(prev => !prev);
}} }}
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",

394
src/pages/VideoContent/VideoContent.tsx

@ -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 { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice"; import { setIsLoadingGlobal } from "../../state/features/globalSlice";
@ -32,8 +38,7 @@ import { CommentSection } from "../../components/common/Comments/CommentSection"
import { import {
CrowdfundSubTitle, CrowdfundSubTitle,
CrowdfundSubTitleRow, CrowdfundSubTitleRow,
} from "../../components/UploadVideo/Upload-styles"; } from "../../components/PublishVideo/PublishVideo-styles.tsx";
import { FOR_SUPER_LIKE, QTUBE_VIDEO_BASE, SUPER_LIKE_BASE, minPriceSuperlike } from "../../constants";
import { Playlists } from "../../components/Playlists/Playlists"; import { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement"; 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 { Comment } from "../../components/common/Comments/Comment";
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection"; import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes"; 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) { export function isTimestampWithinRange(resTimestamp, resCreated) {
// Calculate the absolute difference in milliseconds // Calculate the absolute difference in milliseconds
@ -57,25 +68,25 @@ export function isTimestampWithinRange(resTimestamp, resCreated) {
export function extractSigValue(metadescription) { export function extractSigValue(metadescription) {
// Function to extract the substring within double asterisks // Function to extract the substring within double asterisks
function extractSubstring(str) { function extractSubstring(str) {
const match = str.match(/\*\*(.*?)\*\*/); const match = str.match(/\*\*(.*?)\*\*/);
return match ? match[1] : null; return match ? match[1] : null;
} }
// Function to extract the 'sig' value // Function to extract the 'sig' value
function extractSig(str) { function extractSig(str) {
const regex = /sig:(.*?)(;|$)/; const regex = /sig:(.*?)(;|$)/;
const match = str.match(regex); const match = str.match(regex);
return match ? match[1] : null; return match ? match[1] : null;
} }
// Extracting the relevant substring // Extracting the relevant substring
const relevantSubstring = extractSubstring(metadescription); const relevantSubstring = extractSubstring(metadescription);
if (relevantSubstring) { if (relevantSubstring) {
// Extracting the 'sig' value // Extracting the 'sig' value
return extractSig(relevantSubstring); return extractSig(relevantSubstring);
} else { } 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 // Coin payment info must be added to responseData so we can display it to the user
const responseData = await response.json(); const responseData = await response.json();
if (responseData && !responseData.error) { if (responseData && !responseData.error) {
return responseData; return responseData;
} else { } else {
throw new Error('unable to get payment') throw new Error("unable to get payment");
} }
} catch (error) { } catch (error) {
throw new Error('unable to get payment') throw new Error("unable to get payment");
} }
}; };
export const VideoContent = () => { export const VideoContent = () => {
const { name, id } = useParams(); const { name, id } = useParams();
const [isExpandedDescription, setIsExpandedDescription] = const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false); useState<boolean>(false);
const [superlikeList, setSuperlikelist] = useState<any[]>([]) const [superlikeList, setSuperlikelist] = useState<any[]>([]);
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false) const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
const {addSuperlikeRawDataGetToList} = useFetchSuperLikes() const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const calculateAmountSuperlike = useMemo(()=> { const calculateAmountSuperlike = useMemo(() => {
const totalQort = superlikeList?.reduce((acc, curr)=> { const totalQort = superlikeList?.reduce((acc, curr) => {
if(curr?.amount && !isNaN(parseFloat(curr.amount))) return acc + parseFloat(curr.amount) if (curr?.amount && !isNaN(parseFloat(curr.amount)))
else return acc return acc + parseFloat(curr.amount);
}, 0) else return acc;
return totalQort?.toFixed(2) }, 0);
}, [superlikeList]) return totalQort?.toFixed(2);
const numberOfSuperlikes = useMemo(()=> { }, [superlikeList]);
const numberOfSuperlikes = useMemo(() => {
return superlikeList?.length ?? 0 return superlikeList?.length ?? 0;
}, [superlikeList]) }, [superlikeList]);
const [nameAddress, setNameAddress] = useState<string>('') const [nameAddress, setNameAddress] = useState<string>("");
const [descriptionHeight, setDescriptionHeight] = const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
useState<null | number>(null); null
);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
); );
const contentRef = useRef(null); const contentRef = useRef(null);
const getAddressName = async (name)=> { const getAddressName = async name => {
const response = await qortalRequest({ const response = await qortalRequest({
action: "GET_NAME_DATA", action: "GET_NAME_DATA",
name: name name: name,
}); });
if(response?.owner){ if (response?.owner) {
setNameAddress(response.owner) setNameAddress(response.owner);
} }
} };
useEffect(()=> { useEffect(() => {
if(name){ if (name) {
getAddressName(name);
getAddressName(name)
} }
}, [name]) }, [name]);
const avatarUrl = useMemo(() => { const avatarUrl = useMemo(() => {
let url = ""; let url = "";
if (name && userAvatarHash[name]) { if (name && userAvatarHash[name]) {
@ -237,7 +246,6 @@ export const VideoContent = () => {
} }
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
if (name && id) { if (name && id) {
const existingVideo = hashMapVideos[id]; const existingVideo = hashMapVideos[id];
@ -250,84 +258,80 @@ export const VideoContent = () => {
} }
}, [id, name]); }, [id, name]);
useEffect(() => { useEffect(() => {
if (contentRef.current) { if (contentRef.current) {
const height = contentRef.current.offsetHeight; const height = contentRef.current.offsetHeight;
if (height > 100) { // Assuming 100px is your threshold if (height > 100) {
setDescriptionHeight(100) // Assuming 100px is your threshold
setDescriptionHeight(100);
} }
} }
}, [videoData]); }, [videoData]);
const getComments = useCallback(async (id, nameAddressParam) => {
if (!id) return;
try {
setLoadingSuperLikes(true);
const getComments = useCallback( const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice(
async (id, nameAddressParam) => { 0,
if(!id) return 39
try { )}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
setLoadingSuperLikes(true); const response = await fetch(url, {
method: "GET",
headers: {
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice( "Content-Type": "application/json",
0,39 },
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`; });
const response = await fetch(url, { const responseData = await response.json();
method: "GET", let comments: any[] = [];
headers: { for (const comment of responseData) {
"Content-Type": "application/json", if (
}, comment.identifier &&
}); comment.name &&
const responseData = await response.json(); comment?.metadata?.description
let comments: any[] = []; ) {
for (const comment of responseData) { try {
if (comment.identifier && comment.name && comment?.metadata?.description) { const result = extractSigValue(comment?.metadata?.description);
if (!result) continue;
const res = await getPaymentInfo(result);
try { if (
+res?.amount >= minPriceSuperlike &&
const result = extractSigValue(comment?.metadata?.description) res.recipient === nameAddressParam &&
if(!result) continue isTimestampWithinRange(res?.timestamp, comment.created)
const res = await getPaymentInfo(result); ) {
if(+res?.amount >= minPriceSuperlike && res.recipient === nameAddressParam && isTimestampWithinRange(res?.timestamp, comment.created)){ addSuperlikeRawDataGetToList({
addSuperlikeRawDataGetToList({name:comment.name, identifier:comment.identifier, content: comment}) name: comment.name,
identifier: comment.identifier,
comments = [...comments, { content: comment,
...comment, });
message: "",
amount: res.amount comments = [
}]; ...comments,
{
} ...comment,
message: "",
} catch (error) { 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(() => { useEffect(() => {
if(!nameAddress || !id) return if (!nameAddress || !id) return;
getComments(id, nameAddress); getComments(id, nameAddress);
}, [getComments, id, nameAddress]); }, [getComments, id, nameAddress]);
return ( return (
<Box <Box
sx={{ sx={{
@ -354,58 +358,67 @@ export const VideoContent = () => {
)} )}
<Spacer height="15px" /> <Spacer height="15px" />
<Box sx={{ <Box
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"
sx={{ sx={{
textAlign: "start", width: "100%",
display: "flex",
justifyContent: "flex-end",
}} }}
> >
{videoData?.title} <FileAttachmentContainer>
</VideoTitle> <FileAttachmentFont>save to disk</FileAttachmentFont>
{videoData && ( <FileElement
<SuperLike numberOfSuperlikes={numberOfSuperlikes} totalAmount={calculateAmountSuperlike} name={videoData?.user} service={videoData?.service} identifier={videoData?.id} onSuccess={(val)=> { fileInfo={{
setSuperlikelist((prev)=> [val, ...prev]) ...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> <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 && ( {videoData?.created && (
<Typography <Typography
@ -461,10 +474,16 @@ export const VideoContent = () => {
borderRadius: "5px", borderRadius: "5px",
padding: "5px", padding: "5px",
width: "100%", width: "100%",
cursor: !descriptionHeight ? "default" : isExpandedDescription ? "default" : "pointer", cursor: !descriptionHeight
? "default"
: isExpandedDescription
? "default"
: "pointer",
position: "relative", position: "relative",
}} }}
className={!descriptionHeight ? "": isExpandedDescription ? "" : "hover-click"} className={
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
}
> >
{descriptionHeight && !isExpandedDescription && ( {descriptionHeight && !isExpandedDescription && (
<Box <Box
@ -483,44 +502,55 @@ export const VideoContent = () => {
/> />
)} )}
<Box <Box
ref={contentRef} ref={contentRef}
sx={{ sx={{
height: !descriptionHeight ? 'auto' : isExpandedDescription ? "auto" : "100px", height: !descriptionHeight
? "auto"
: isExpandedDescription
? "auto"
: "100px",
overflow: "hidden", overflow: "hidden",
}} }}
> >
{videoData?.htmlDescription ? ( {videoData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} /> <DisplayHtml html={videoData?.htmlDescription} />
) : ( ) : (
<VideoDescription variant="body1" color="textPrimary" sx={{ <VideoDescription
cursor: 'default' variant="body1"
}}> color="textPrimary"
sx={{
cursor: "default",
}}
>
{videoData?.fullDescription} {videoData?.fullDescription}
</VideoDescription> </VideoDescription>
)} )}
</Box> </Box>
{descriptionHeight && ( {descriptionHeight && (
<Typography <Typography
onClick={() => { onClick={() => {
setIsExpandedDescription((prev) => !prev); setIsExpandedDescription(prev => !prev);
}} }}
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",
fontSize: "16px", fontSize: "16px",
cursor: "pointer", cursor: "pointer",
paddingLeft: "15px", paddingLeft: "15px",
paddingTop: "15px", paddingTop: "15px",
}} }}
> >
{isExpandedDescription ? "Show less" : "...more"} {isExpandedDescription ? "Show less" : "...more"}
</Typography> </Typography>
)} )}
</Box> </Box>
</VideoPlayerContainer> </VideoPlayerContainer>
<SuperLikesSection getMore={()=> { <SuperLikesSection
getMore={() => {}}
}} loadingSuperLikes={loadingSuperLikes} superlikes={superlikeList} postId={id || ""} postName={name || ""} /> loadingSuperLikes={loadingSuperLikes}
superlikes={superlikeList}
postId={id || ""}
postName={name || ""}
/>
<Box <Box
sx={{ sx={{

154
src/utils/BoundedNumericTextField.tsx

@ -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

@ -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

@ -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

@ -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

@ -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));
};

142
src/wrappers/GlobalWrapper.tsx

@ -11,16 +11,24 @@ import { addUser } from "../state/features/authSlice";
import NavBar from "../components/layout/Navbar/Navbar"; import NavBar from "../components/layout/Navbar/Navbar";
import PageLoader from "../components/common/PageLoader"; import PageLoader from "../components/common/PageLoader";
import { RootState } from "../state/store"; 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 { VideoPlayerGlobal } from "../components/common/VideoPlayerGlobal";
import { Rnd } from "react-rnd"; import { Rnd } from "react-rnd";
import { RequestQueue } from "../utils/queue"; import { RequestQueue } from "../utils/queue";
import { EditVideo } from "../components/EditVideo/EditVideo"; import { EditVideo } from "../components/EditVideo/EditVideo";
import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist"; import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist";
import ConsentModal from "../components/common/ConsentModal"; import ConsentModal from "../components/common/ConsentModal";
import { SUPER_LIKE_BASE, minPriceSuperlike } from "../constants"; import {
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from "../pages/VideoContent/VideoContent"; extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../pages/VideoContent/VideoContent";
import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes"; import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes";
import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts";
import { minPriceSuperlike } from "../constants/Misc.ts";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@ -32,14 +40,13 @@ let timer: number | null = null;
export const queue = new RequestQueue(); export const queue = new RequestQueue();
export const queueSuperlikes = new RequestQueue(); export const queueSuperlikes = new RequestQueue();
const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => { const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isDragging = useRef(false); const isDragging = useRef(false);
const [userAvatar, setUserAvatar] = useState<string>(""); const [userAvatar, setUserAvatar] = useState<string>("");
const user = useSelector((state: RootState) => state.auth.user); const user = useSelector((state: RootState) => state.auth.user);
const {addSuperlikeRawDataGetToList} = useFetchSuperLikes() const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const interval = useRef<any>(null) const interval = useRef<any>(null);
const videoPlaying = useSelector( const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying (state: RootState) => state.global.videoPlaying
@ -134,80 +141,71 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
return isDragging.current; return isDragging.current;
}, []); }, []);
const getSuperlikes = useCallback(async () => {
const getSuperlikes = useCallback( try {
async () => { const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`;
try { const response = await fetch(url, {
method: "GET",
headers: {
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`; "Content-Type": "application/json",
const response = await fetch(url, { },
method: "GET", });
headers: { const responseData = await response.json();
"Content-Type": "application/json", let comments: any[] = [];
}, for (const comment of responseData) {
}); if (
const responseData = await response.json(); comment.identifier &&
let comments: any[] = []; comment.name &&
for (const comment of responseData) { comment?.metadata?.description
if (comment.identifier && comment.name && comment?.metadata?.description) { ) {
try {
const result = extractSigValue(comment?.metadata?.description);
try { if (!result) continue;
const res = await getPaymentInfo(result);
const result = extractSigValue(comment?.metadata?.description) if (
if(!result) continue +res?.amount >= minPriceSuperlike &&
const res = await getPaymentInfo(result); isTimestampWithinRange(res?.timestamp, comment.created)
if(+res?.amount >= minPriceSuperlike && isTimestampWithinRange(res?.timestamp, comment.created)){ ) {
addSuperlikeRawDataGetToList({name:comment.name, identifier:comment.identifier, content: comment}) addSuperlikeRawDataGetToList({
name: comment.name,
comments = [...comments, { identifier: comment.identifier,
...comment, content: comment,
message: "", });
amount: res.amount
}]; comments = [
...comments,
} {
...comment,
} catch (error) { 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( const checkSuperlikes = useCallback(() => {
() => { let isCalling = false;
let isCalling = false interval.current = setInterval(async () => {
interval.current = setInterval(async () => { if (isCalling) return;
if (isCalling) return isCalling = true;
isCalling = true const res = await getSuperlikes();
const res = await getSuperlikes() isCalling = false;
isCalling = false }, 300000);
}, 300000) getSuperlikes();
getSuperlikes() }, [getSuperlikes]);
},
[getSuperlikes])
useEffect(() => { useEffect(() => {
checkSuperlikes(); checkSuperlikes();
}, [checkSuperlikes]); }, [checkSuperlikes]);
return ( return (
<> <>
{isLoadingGlobal && <PageLoader />} {isLoadingGlobal && <PageLoader />}

Loading…
Cancel
Save