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. 150
      src/components/EditPlaylist/EditPlaylist.tsx
  3. 0
      src/components/EditVideo/EditVideo-styles.tsx
  4. 133
      src/components/EditVideo/EditVideo.tsx
  5. 66
      src/components/PlaylistListEdit/PlaylistListEdit.tsx
  6. 113
      src/components/Playlists/Playlists.tsx
  7. 0
      src/components/PublishVideo/PublishVideo-styles.tsx
  8. 288
      src/components/PublishVideo/PublishVideo.tsx
  9. 3
      src/components/common/Comments/CommentEditor.tsx
  10. 10
      src/components/common/Comments/CommentSection.tsx
  11. 55
      src/components/common/MultiplePublish/MultiplePublish.tsx
  12. 283
      src/components/common/Notifications/Notifications.tsx
  13. 151
      src/components/common/SuperLike/SuperLike.tsx
  14. 58
      src/components/common/SuperLikesList/CommentEditor.tsx
  15. 68
      src/components/common/SuperLikesList/SuperLikesSection.tsx
  16. 18
      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. 333
      src/hooks/useFetchVideos.tsx
  22. 68
      src/pages/Home/VideoList.tsx
  23. 4
      src/pages/Home/VideoListComponentLevel.tsx
  24. 24
      src/pages/PlaylistContent/PlaylistContent.tsx
  25. 250
      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. 96
      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
}

150
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

133
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"

66
src/components/PlaylistListEdit/PlaylistListEdit.tsx

@ -3,42 +3,43 @@ 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', sx={{
gap: '10px', display: "flex",
width: '100%', gap: "10px",
justifyContent: 'center' width: "100%",
}}> justifyContent: "center",
}}
>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -55,7 +56,7 @@ const [filterSearch, setFilterSearch] = useState("")
sx={{ sx={{
marginTop: "25px", marginTop: "25px",
height: "450px", height: "450px",
overflow: 'auto' overflow: "auto",
}} }}
> >
{playlistData?.videos?.map((vid, index) => { {playlistData?.videos?.map((vid, index) => {
@ -82,7 +83,7 @@ const [filterSearch, setFilterSearch] = useState("")
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: "18px",
wordBreak: 'break-word' wordBreak: "break-word",
}} }}
> >
{vid?.metadata?.title} {vid?.metadata?.title}
@ -109,7 +110,6 @@ const [filterSearch, setFilterSearch] = useState("")
width: "100%", width: "100%",
}} }}
> >
<CrowdfundSubTitleRow> <CrowdfundSubTitleRow>
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle> <CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow> </CrowdfundSubTitleRow>
@ -117,16 +117,18 @@ const [filterSearch, setFilterSearch] = useState("")
sx={{ sx={{
marginTop: "25px", marginTop: "25px",
height: "450px", height: "450px",
overflow: 'auto' overflow: "auto",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
}} }}
> >
<Box sx={{
display: 'flex',
gap: '10px'
}}>
<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}
@ -155,7 +157,6 @@ const [filterSearch, setFilterSearch] = useState("")
onClick={() => { onClick={() => {
search(); search();
}} }}
variant="contained" variant="contained"
> >
Search Search
@ -186,7 +187,7 @@ const [filterSearch, setFilterSearch] = useState("")
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: "18px",
wordBreak: 'break-word' wordBreak: "break-word",
}} }}
> >
{vid?.metadata?.title} {vid?.metadata?.title}
@ -205,6 +206,5 @@ const [filterSearch, setFilterSearch] = useState("")
</CardContentContainerComment> </CardContentContainerComment>
</Box> </Box>
</Box> </Box>
); );
}; };

113
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";
import { Box, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
export const Playlists = ({playlistData, currentVideoIdentifier, onClick}) => { export const Playlists = ({
playlistData,
currentVideoIdentifier,
onClick,
}) => {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate() 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",
gap: "10px",
width: "100%",
background: isCurrentVidPlayling && theme.palette.primary.main, background: isCurrentVidPlayling && theme.palette.primary.main,
alignItems: 'center', alignItems: "center",
padding: '10px', padding: "10px",
borderRadius: '5px', borderRadius: "5px",
cursor: isCurrentVidPlayling ? 'default' : 'pointer', cursor: isCurrentVidPlayling ? "default" : "pointer",
userSelect: 'none' userSelect: "none",
}} }}
onClick={()=> { onClick={() => {
if(isCurrentVidPlayling) return if (isCurrentVidPlayling) return;
onClick(vid.name, vid.identifier) onClick(vid.name, vid.identifier);
// navigate(`/video/${vid.name}/${vid.identifier}`) // navigate(`/video/${vid.name}/${vid.identifier}`)
}} }}
> >
<Typography sx={{ <Typography
fontSize: '14px' sx={{
}}>{index + 1}</Typography> fontSize: "14px",
<Typography sx={{ }}
fontSize: '18px', >
wordBreak: 'break-word' {index + 1}
}}>{vid?.metadata?.title}</Typography> </Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
}}
>
{vid?.metadata?.title}
</Typography>
</Box> </Box>
) );
})} })}
</CardContentContainerComment> </CardContentContainerComment>
</Box> </Box>
);
) };
}

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

288
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 (
<> <>
@ -670,6 +683,17 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
/> />
</FiltersRow> </FiltersRow>
</FiltersSubContainer> </FiltersSubContainer>
<CustomInputField
name="prefix"
label="Titles Prefix"
variant="filled"
value={titlesPrefix}
onChange={e =>
setTitlesPrefix(e.target.value.replace(titleFormatter, ""))
}
inputProps={{ maxLength: 180 }}
required
/>
<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>
@ -746,9 +770,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
<> <>
{!coverImageForAll ? ( {!coverImageForAll ? (
<ImageUploader <ImageUploader
onPick={(img: string) => onPick={(img: string) => setCoverImageForAll(img)}
setCoverImageForAll(img)
}
> >
<AddCoverImageButton variant="contained"> <AddCoverImageButton variant="contained">
Add Cover Image Add Cover Image
@ -765,9 +787,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
<CoverImagePreview src={coverImageForAll} alt="logo" /> <CoverImagePreview src={coverImageForAll} alt="logo" />
<TimesIcon <TimesIcon
color={theme.palette.text.primary} color={theme.palette.text.primary}
onClickFunc={() => onClickFunc={() => setCoverImageForAll(null)}
setCoverImageForAll(null)
}
height={"32"} height={"32"}
width={"32"} width={"32"}
></TimesIcon> ></TimesIcon>
@ -778,7 +798,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
{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 && (
<> <>
@ -800,7 +823,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
</ImageUploader> </ImageUploader>
) : ( ) : (
<LogoPreviewRow> <LogoPreviewRow>
<CoverImagePreview src={file?.coverImage} alt="logo" /> <CoverImagePreview
src={file?.coverImage}
alt="logo"
/>
<TimesIcon <TimesIcon
color={theme.palette.text.primary} color={theme.palette.text.primary}
onClickFunc={() => onClickFunc={() =>
@ -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,12 +1226,16 @@ 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} />
)} )}
@ -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({

10
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,9 +221,8 @@ export const CommentSection = ({ postId, postName }: CommentSectionProps) => {
return ( return (
<> <>
<Panel> <Panel>
<CrowdfundSubTitleRow > <CrowdfundSubTitleRow>
<CrowdfundSubTitle>Comments</CrowdfundSubTitle> <CrowdfundSubTitle>Comments</CrowdfundSubTitle>
</CrowdfundSubTitleRow> </CrowdfundSubTitleRow>
<CommentsContainer> <CommentsContainer>

55
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={{ <Typography
marginTop: '20px', sx={{
fontSize: '16px' marginTop: "20px",
}}>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> fontSize: "16px",
<Button onClick={()=> { }}
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>
); );

283
src/components/common/Notifications/Notifications.tsx

@ -1,19 +1,43 @@
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) {
@ -37,49 +61,54 @@ export function extractIdValue(metadescription) {
} 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
>(null);
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const usernameAddress = useSelector((state: RootState) => state.auth?.user?.address); const usernameAddress = useSelector(
(state: RootState) => state.auth?.user?.address
);
const navigate = useNavigate(); const navigate = useNavigate();
const interval = useRef<any>(null) const interval = useRef<any>(null);
const getInitialTimestamp = async ()=> { const getInitialTimestamp = async () => {
const timestamp: undefined | number = await generalLocal.getItem("notification-timestamp"); const timestamp: undefined | number = await generalLocal.getItem(
if(timestamp){ "notification-timestamp"
setNotificationTimestamp(timestamp) );
} if (timestamp) {
setNotificationTimestamp(timestamp);
} }
};
useEffect(()=> { useEffect(() => {
getInitialTimestamp() 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 {
@ -91,7 +120,7 @@ export const Notifications = () => {
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, {
@ -101,20 +130,25 @@ export const Notifications = () => {
}, },
}); });
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 &&
comment?.metadata?.description
) {
try { try {
const result = extractSigValue(comment?.metadata?.description) const result = extractSigValue(comment?.metadata?.description);
if(!result) continue if (!result) continue;
const res = await getPaymentInfo(result); const res = await getPaymentInfo(result);
if(+res?.amount >= minPriceSuperlike && res.recipient === usernameAddress && isTimestampWithinRange(res?.timestamp, comment.created)){ if (
+res?.amount >= minPriceSuperlike &&
let urlReference = null res.recipient === usernameAddress &&
isTimestampWithinRange(res?.timestamp, comment.created)
) {
let urlReference = null;
try { try {
let idForUrl = extractIdValue(comment?.metadata?.description) 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 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 response2 = await fetch(url, {
method: "GET", method: "GET",
@ -123,13 +157,10 @@ export const Notifications = () => {
}, },
}); });
const responseSearch = await response2.json(); const responseSearch = await response2.json();
if(responseSearch.length > 0){ if (responseSearch.length > 0) {
urlReference = responseSearch[0] urlReference = responseSearch[0];
}
} catch (error) {
} }
} catch (error) {}
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`; // const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
// const response = await fetch(url, { // const response = await fetch(url, {
// method: "GET", // method: "GET",
@ -140,118 +171,109 @@ export const Notifications = () => {
// if(!response.ok) continue // if(!response.ok) continue
// const responseData2 = await response.text(); // const responseData2 = await response.text();
notifys = [...notifys, { notifys = [
...notifys,
{
...comment, ...comment,
amount: res.amount, amount: res.amount,
urlReference: urlReference || null urlReference: urlReference || null,
}]; },
];
}
} catch (error) {
} }
} catch (error) {}
} }
} }
setNotifications((prev) => { setNotifications(prev => {
const allNotifications = [...notifys, ...prev]; const allNotifications = [...notifys, ...prev];
const uniqueNotifications = Array.from(new Map(allNotifications.map(notif => [notif.identifier, notif])).values()); const uniqueNotifications = Array.from(
new Map(
allNotifications.map(notif => [notif.identifier, notif])
).values()
);
return uniqueNotifications.slice(0, 20); return uniqueNotifications.slice(0, 20);
}); });
} catch (error) { } catch (error) {
console.log({ error }) console.log({ error });
} }
}, []) }, []);
const checkNotificationsFunc = useCallback( const checkNotificationsFunc = useCallback(
(username: string) => { (username: string) => {
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 checkNotifications(username) const res = await checkNotifications(username);
isCalling = false isCalling = false;
}, 60000) }, 60000);
checkNotifications(username) checkNotifications(username);
}, },
[checkNotifications]) [checkNotifications]
);
useEffect(() => { useEffect(() => {
if (!username) return if (!username) return;
checkNotificationsFunc(username) checkNotificationsFunc(username);
return () => { return () => {
if (interval?.current) { if (interval?.current) {
clearInterval(interval.current) clearInterval(interval.current);
}
} }
}, [checkNotificationsFunc, username]) };
}, [checkNotificationsFunc, username]);
const openPopover = Boolean(anchorElNotification) const openPopover = Boolean(anchorElNotification);
return ( return (
<Box <Box
sx={{ sx={{
display: 'flex', display: "flex",
alignItems: 'center' alignItems: "center",
}} }}
> >
<Badge <Badge
badgeContent={notificationBadgeLength} badgeContent={notificationBadgeLength}
color="primary" color="primary"
sx={{ sx={{
margin: '0px 12px' margin: "0px 12px",
}} }}
> >
<Button <Button
onClick={(e) => { onClick={e => {
openNotificationPopover(e) openNotificationPopover(e);
generalLocal.setItem("notification-timestamp", Date.now()); generalLocal.setItem("notification-timestamp", Date.now());
setNotificationTimestamp(Date.now) setNotificationTimestamp(Date.now);
}} }}
sx={{ sx={{
margin: '0px', margin: "0px",
padding: '0px', padding: "0px",
height: 'auto', height: "auto",
width: 'auto', width: "auto",
minWidth: 'unset' minWidth: "unset",
}} }}
> >
<NotificationsIcon color="action" /> <NotificationsIcon color="action" />
</Button> </Button>
</Badge> </Badge>
<Popover <Popover
id={'simple-popover-notification'} id={"simple-popover-notification"}
open={openPopover} open={openPopover}
anchorEl={anchorElNotification} anchorEl={anchorElNotification}
onClose={closeNotificationPopover} onClose={closeNotificationPopover}
anchorOrigin={{ anchorOrigin={{
vertical: 'bottom', vertical: "bottom",
horizontal: 'left' horizontal: "left",
}} }}
> >
<Box> <Box>
<List <List
sx={{ sx={{
maxHeight: '300px', maxHeight: "300px",
overflow: 'auto' overflow: "auto",
}} }}
> >
{fullNotifications.length === 0 && ( {fullNotifications.length === 0 && (
<ListItem <ListItem>
<ListItemText primary="No new notifications"></ListItemText>
>
<ListItemText
primary="No new notifications">
</ListItemText>
</ListItem> </ListItem>
)} )}
{fullNotifications.map((notification: any, index: number) => ( {fullNotifications.map((notification: any, index: number) => (
@ -259,34 +281,35 @@ export const Notifications = () => {
key={index} key={index}
divider divider
sx={{ sx={{
cursor: notification?.urlReference ? 'pointer' : 'default' cursor: notification?.urlReference ? "pointer" : "default",
}} }}
onClick={async () => { onClick={async () => {
if(notification?.urlReference){ if (notification?.urlReference) {
navigate(`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`); navigate(
`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`
);
} }
}} }}
> >
<ListItemText <ListItemText
primary={ primary={
<Box sx={{ <Box
display: 'flex', sx={{
alignItems: 'center', display: "flex",
gap: '5px' alignItems: "center",
}}> gap: "5px",
}}
>
<Typography <Typography
component="span" component="span"
variant="body1" variant="body1"
color="textPrimary" color="textPrimary"
> >
Super Like Super Like
</Typography> </Typography>
<ThumbUpIcon <ThumbUpIcon
style={{ style={{
color: "gold", color: "gold",
}} }}
/> />
</Box> </Box>
@ -296,7 +319,7 @@ export const Notifications = () => {
<Typography <Typography
component="span" component="span"
sx={{ sx={{
fontSize: '16px' fontSize: "16px",
}} }}
color="textSecondary" color="textSecondary"
> >
@ -305,7 +328,7 @@ export const Notifications = () => {
<Typography <Typography
component="span" component="span"
sx={{ sx={{
fontSize: '16px' fontSize: "16px",
}} }}
color="textSecondary" color="textSecondary"
> >
@ -320,5 +343,5 @@ export const Notifications = () => {
</Box> </Box>
</Popover> </Popover>
</Box> </Box>
) );
} };

151
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
sx={{
width: "300px",
display: "flex",
justifyContent: "center",
}}
>
<Box> <Box>
<InputLabel htmlFor="standard-adornment-amount"> <InputLabel htmlFor="standard-adornment-amount">
Amount in QORT (min 10 QORT) Amount in QORT (min 10 QORT)
</InputLabel> </InputLabel>
<Input <BoundedNumericTextField
minValue={10}
initialValue={minPriceSuperlike.toString()}
maxValue={numberToInt(+currentBalance)}
allowDecimals={false}
allowNegatives={false}
id="standard-adornment-amount" id="standard-adornment-amount"
type="number" value={superlikeDonationAmount}
value={amount} afterChange={(e: string) => setSuperlikeDonationAmount(+e)}
onChange={(e) => setAmount(+e.target.value)} InputProps={{
startAdornment={ style: { fontSize: 30, width: textFieldWidth },
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> <div>Current QORT Balance is: {currentBalance}</div>
<Spacer height="25px" /> <Spacer height="25px" />
<Box>
<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);

58
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,24 +156,31 @@ 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] ||
!comment?.transactionReference ||
!comment?.notificationInformation ||
!comment?.about
)
throw new Error("unable to edit Super like");
description = comment?.metadata?.description;
tag1 = comment?.metadata?.tags[0];
superObj = { 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({
@ -183,7 +190,7 @@ export const CommentEditor = ({
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(
addtoHashMapSuperlikes({
...superObj, ...superObj,
...comment, ...comment,
message: value 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(),

68
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,8 +156,7 @@ 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);
@ -160,19 +168,16 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
} }
setListComments(comments); setListComments(comments);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
setLoadingComments(false); setLoadingComments(false);
} }
}, }, []);
[]
);
useEffect(() => { useEffect(() => {
if(postId){ if (postId) {
getComments(superlikes, postId) getComments(superlikes, postId);
} }
}, [getComments, superlikes, postId]); }, [getComments, superlikes, postId]);
@ -191,19 +196,21 @@ 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",
}}
>
Super Likes
</CrowdfundSubTitle>
</CrowdfundSubTitleRow> </CrowdfundSubTitleRow>
<CommentsContainer> <CommentsContainer>
{(loadingComments || loadingSuperLikes) ? ( {loadingComments || loadingSuperLikes ? (
<NoCommentsRow> <NoCommentsRow>
<CircularProgress /> <CircularProgress />
</NoCommentsRow> </NoCommentsRow>
@ -214,18 +221,19 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
) : ( ) : (
<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"

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

333
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, name: author,
service: 'THUMBNAIL', service: "THUMBNAIL",
identifier: 'qortal_avatar' identifier: "qortal_avatar",
}) });
dispatch(setUserAvatarHash({ dispatch(
setUserAvatarHash({
name: author, name: author,
url url,
})) })
} catch (error) { } );
}, []) } catch (error) {}
}, []);
const getVideo = async (user: string, videoId: string, content: any, retries: number = 0) => {
const getVideo = async (
user: string,
videoId: string,
content: any,
retries: number = 0
) => {
try { 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,59 +166,65 @@ 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(
async (
filters = {},
reset?: boolean,
resetFilers?: boolean,
limit?: number
) => {
try { try {
const {name = '', const {
category = '', name = "",
subcategory = '', category = "",
keywords = '', subcategory = "",
type = '' }: any = resetFilers ? {} : filters keywords = "",
let offset = videos.length type = "",
if(reset){ }: any = resetFilers ? {} : filters;
offset = 0 let offset = videos.length;
} if (reset) {
const videoLimit = limit || 20 offset = 0;
}
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}` const videoLimit = limit || 20;
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
if(name){
defaultUrl = defaultUrl + `&name=${name}` if (name) {
} defaultUrl = defaultUrl + `&name=${name}`;
if(category){ }
if(!subcategory){ if (category) {
defaultUrl = defaultUrl + `&description=category:${category}` if (!subcategory) {
defaultUrl = defaultUrl + `&description=category:${category}`;
} else { } else {
defaultUrl = defaultUrl + `&description=category:${category};subcategory:${subcategory}` defaultUrl =
defaultUrl +
`&description=category:${category};subcategory:${subcategory}`;
} }
} }
if(keywords){ if (keywords) {
defaultUrl = defaultUrl + `&query=${keywords}` defaultUrl = defaultUrl + `&query=${keywords}`;
} }
if(type === 'playlists'){ if (type === "playlists") {
defaultUrl = defaultUrl + `&service=PLAYLIST` defaultUrl = defaultUrl + `&service=PLAYLIST`;
defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}` defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`;
} else { } else {
defaultUrl = defaultUrl + `&service=DOCUMENT` defaultUrl = defaultUrl + `&service=DOCUMENT`;
defaultUrl = defaultUrl + `&identifier=${QTUBE_VIDEO_BASE}` defaultUrl = defaultUrl + `&identifier=${QTUBE_VIDEO_BASE}`;
} }
// const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}` // const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
const url = defaultUrl const url = defaultUrl;
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",
@ -239,47 +250,45 @@ 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(reset){ if (reset) {
dispatch(addVideos(structureData)) dispatch(addVideos(structureData));
} else { } else {
dispatch(upsertVideos(structureData)) dispatch(upsertVideos(structureData));
} }
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));
} }
} }
} }
} catch (error) { } catch (error) {
console.log({error}) console.log({ error });
} finally { } finally {
} }
}, [videos, hashMapVideos]) },
[videos, hashMapVideos]
);
const getVideosFiltered = React.useCallback(async (filterValue: string) => { const getVideosFiltered = React.useCallback(
async (filterValue: string) => {
try { try {
const offset = filteredVideos.length const offset = filteredVideos.length;
const replaceSpacesWithUnderscore = filterValue.replace(/ /g, '_'); 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 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 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",
@ -305,15 +314,15 @@ 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,
} };
}) });
dispatch(upsertFilteredVideos(structureData)) dispatch(upsertFilteredVideos(structureData));
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));
} }
@ -321,22 +330,21 @@ export const useFetchVideos = () => {
} }
} catch (error) { } catch (error) {
} finally { } 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,
} };
} };

68
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>
@ -537,11 +535,9 @@ export const VideoList = ({ mode }: VideoListProps) => {
width: "100%", width: "100%",
maxWidth: "1400px", maxWidth: "1400px",
}} }}
> ></SubtitleContainer>
</SubtitleContainer>
<VideoCardContainer > <VideoCardContainer>
{videos.map((video: any, index: number) => { {videos.map((video: any, index: number) => {
const existingVideo = hashMapVideos[video?.id]; const existingVideo = hashMapVideos[video?.id];
let hasHash = false; let hasHash = false;
@ -564,12 +560,10 @@ export const VideoList = ({ mode }: VideoListProps) => {
if (isPlaylist) { if (isPlaylist) {
return ( return (
<VideoCardCol <VideoCardCol
onMouseEnter={() => setShowIcons(videoObj.id)} onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)} onMouseLeave={() => setShowIcons(null)}
key={videoObj.id} key={videoObj.id}
> >
<IconsBox <IconsBox
sx={{ sx={{
opacity: showIcons === videoObj.id ? 1 : 0, opacity: showIcons === videoObj.id ? 1 : 0,
@ -600,10 +594,10 @@ export const VideoList = ({ mode }: VideoListProps) => {
</IconsBox> </IconsBox>
<VideoCard <VideoCard
sx={{ sx={{
cursor: !hasHash && 'default' cursor: !hasHash && "default",
}} }}
onClick={() => { onClick={() => {
if(!hasHash) return if (!hasHash) return;
navigate( navigate(
`/playlist/${videoObj?.user}/${videoObj?.id}` `/playlist/${videoObj?.user}/${videoObj?.id}`
); );
@ -614,13 +608,13 @@ export const VideoList = ({ mode }: VideoListProps) => {
width={266} width={266}
height={150} height={150}
style={{ style={{
maxHeight: '50%' maxHeight: "50%",
}} }}
/> />
<VideoCardTitle>{videoObj?.title}</VideoCardTitle> <VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${videoObj?.user}`); navigate(`/channel/${videoObj?.user}`);
}} }}
@ -668,7 +662,6 @@ export const VideoList = ({ mode }: VideoListProps) => {
return ( return (
<VideoCardCol <VideoCardCol
key={videoObj.id} key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)} onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)} onMouseLeave={() => setShowIcons(null)}
@ -689,7 +682,6 @@ export const VideoList = ({ mode }: VideoListProps) => {
/> />
</BlockIconContainer> </BlockIconContainer>
</Tooltip> </Tooltip>
)} )}
<Tooltip title="Block user content" placement="top"> <Tooltip title="Block user content" placement="top">
@ -707,8 +699,12 @@ export const VideoList = ({ mode }: VideoListProps) => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`); navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}} }}
> >
<VideoCardImageContainer width={266} <VideoCardImageContainer
height={150} videoImage={videoObj.videoImage} frameImages={videoObj?.extracts || []} /> width={266}
height={150}
videoImage={videoObj.videoImage}
frameImages={videoObj?.extracts || []}
/>
{/* <ResponsiveImage {/* <ResponsiveImage
src={videoObj.videoImage} src={videoObj.videoImage}
width={266} width={266}
@ -717,7 +713,7 @@ export const VideoList = ({ mode }: VideoListProps) => {
<VideoCardTitle>{videoObj.title}</VideoCardTitle> <VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={(e) => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${videoObj?.user}`); navigate(`/channel/${videoObj?.user}`);
}} }}

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 {

24
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]);
}} }}
/> />
)} )}
@ -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",

250
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
@ -93,61 +104,59 @@ export const getPaymentInfo = async (signature: string) => {
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,27 +258,24 @@ 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) => {
const getComments = useCallback( if (!id) return;
async (id, nameAddressParam) => {
if(!id) return
try { try {
setLoadingSuperLikes(true); setLoadingSuperLikes(true);
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice( const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice(
0,39 0,
39
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`; )}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
@ -281,53 +286,52 @@ export const VideoContent = () => {
const responseData = await response.json(); const responseData = await response.json();
let comments: any[] = []; let comments: any[] = [];
for (const comment of responseData) { for (const comment of responseData) {
if (comment.identifier && comment.name && comment?.metadata?.description) { if (
comment.identifier &&
comment.name &&
comment?.metadata?.description
) {
try { try {
const result = extractSigValue(comment?.metadata?.description);
const result = extractSigValue(comment?.metadata?.description) if (!result) continue;
if(!result) continue
const res = await getPaymentInfo(result); const res = await getPaymentInfo(result);
if(+res?.amount >= minPriceSuperlike && res.recipient === nameAddressParam && isTimestampWithinRange(res?.timestamp, comment.created)){ if (
addSuperlikeRawDataGetToList({name:comment.name, identifier:comment.identifier, content: comment}) +res?.amount >= minPriceSuperlike &&
res.recipient === nameAddressParam &&
isTimestampWithinRange(res?.timestamp, comment.created)
) {
addSuperlikeRawDataGetToList({
name: comment.name,
identifier: comment.identifier,
content: comment,
});
comments = [...comments, { comments = [
...comments,
{
...comment, ...comment,
message: "", message: "",
amount: res.amount amount: res.amount,
}]; },
];
}
} catch (error) {
} }
} catch (error) {}
} }
} }
setSuperlikelist(comments); setSuperlikelist(comments);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } finally {
setLoadingSuperLikes(false); 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,23 +358,24 @@ export const VideoContent = () => {
)} )}
<Spacer height="15px" /> <Spacer height="15px" />
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
justifyContent: 'flex-end' display: "flex",
}}> justifyContent: "flex-end",
}}
>
<FileAttachmentContainer> <FileAttachmentContainer>
<FileAttachmentFont>save to disk</FileAttachmentFont>
<FileAttachmentFont>
save to disk
</FileAttachmentFont>
<FileElement <FileElement
fileInfo={{...videoReference, fileInfo={{
filename: videoData?.filename || videoData?.title?.slice(0,20) + '.mp4', ...videoReference,
filename:
videoData?.filename ||
videoData?.title?.slice(0, 20) + ".mp4",
mimeType: videoData?.videoType || '"video/mp4', mimeType: videoData?.videoType || '"video/mp4',
}} }}
title={videoData?.filename || videoData?.title?.slice(0,20)} title={videoData?.filename || videoData?.title?.slice(0, 20)}
customStyles={{ customStyles={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -382,14 +387,16 @@ export const VideoContent = () => {
</FileAttachmentContainer> </FileAttachmentContainer>
</Box> </Box>
<Box sx={{ <Box
display: 'flex', sx={{
justifyContent: 'space-between', display: "flex",
alignItems: 'center', justifyContent: "space-between",
width: '100%', alignItems: "center",
marginTop: '20px', width: "100%",
gap: '10px' marginTop: "20px",
}}> gap: "10px",
}}
>
<VideoTitle <VideoTitle
variant="h1" variant="h1"
color="textPrimary" color="textPrimary"
@ -400,11 +407,17 @@ export const VideoContent = () => {
{videoData?.title} {videoData?.title}
</VideoTitle> </VideoTitle>
{videoData && ( {videoData && (
<SuperLike numberOfSuperlikes={numberOfSuperlikes} totalAmount={calculateAmountSuperlike} name={videoData?.user} service={videoData?.service} identifier={videoData?.id} onSuccess={(val)=> { <SuperLike
setSuperlikelist((prev)=> [val, ...prev]) numberOfSuperlikes={numberOfSuperlikes}
}} /> totalAmount={calculateAmountSuperlike}
name={videoData?.user}
service={videoData?.service}
identifier={videoData?.id}
onSuccess={val => {
setSuperlikelist(prev => [val, ...prev]);
}}
/>
)} )}
</Box> </Box>
{videoData?.created && ( {videoData?.created && (
@ -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
@ -485,16 +504,24 @@ 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>
)} )}
@ -502,7 +529,7 @@ export const VideoContent = () => {
{descriptionHeight && ( {descriptionHeight && (
<Typography <Typography
onClick={() => { onClick={() => {
setIsExpandedDescription((prev) => !prev); setIsExpandedDescription(prev => !prev);
}} }}
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",
@ -515,12 +542,15 @@ export const VideoContent = () => {
{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));
};

96
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,12 +141,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
return isDragging.current; return isDragging.current;
}, []); }, []);
const getSuperlikes = useCallback(async () => {
const getSuperlikes = useCallback(
async () => {
try { try {
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`; const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
@ -150,64 +153,59 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const responseData = await response.json(); const responseData = await response.json();
let comments: any[] = []; let comments: any[] = [];
for (const comment of responseData) { for (const comment of responseData) {
if (comment.identifier && comment.name && comment?.metadata?.description) { if (
comment.identifier &&
comment.name &&
comment?.metadata?.description
) {
try { try {
const result = extractSigValue(comment?.metadata?.description);
const result = extractSigValue(comment?.metadata?.description) if (!result) continue;
if(!result) continue
const res = await getPaymentInfo(result); const res = await getPaymentInfo(result);
if(+res?.amount >= minPriceSuperlike && isTimestampWithinRange(res?.timestamp, comment.created)){ if (
addSuperlikeRawDataGetToList({name:comment.name, identifier:comment.identifier, content: comment}) +res?.amount >= minPriceSuperlike &&
isTimestampWithinRange(res?.timestamp, comment.created)
) {
addSuperlikeRawDataGetToList({
name: comment.name,
identifier: comment.identifier,
content: comment,
});
comments = [...comments, { comments = [
...comments,
{
...comment, ...comment,
message: "", message: "",
amount: res.amount amount: res.amount,
}]; },
];
}
} catch (error) {
} }
} catch (error) {}
} }
} }
dispatch(setSuperlikesAll(comments)); dispatch(setSuperlikesAll(comments));
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally { } 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