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. 164
      src/components/EditPlaylist/EditPlaylist.tsx
  3. 0
      src/components/EditVideo/EditVideo-styles.tsx
  4. 145
      src/components/EditVideo/EditVideo.tsx
  5. 328
      src/components/PlaylistListEdit/PlaylistListEdit.tsx
  6. 131
      src/components/Playlists/Playlists.tsx
  7. 0
      src/components/PublishVideo/PublishVideo-styles.tsx
  8. 470
      src/components/PublishVideo/PublishVideo.tsx
  9. 3
      src/components/common/Comments/CommentEditor.tsx
  10. 14
      src/components/common/Comments/CommentSection.tsx
  11. 65
      src/components/common/MultiplePublish/MultiplePublish.tsx
  12. 589
      src/components/common/Notifications/Notifications.tsx
  13. 165
      src/components/common/SuperLike/SuperLike.tsx
  14. 70
      src/components/common/SuperLikesList/CommentEditor.tsx
  15. 102
      src/components/common/SuperLikesList/SuperLikesSection.tsx
  16. 34
      src/components/layout/Navbar/Navbar.tsx
  17. 107
      src/constants/Categories.ts
  18. 15
      src/constants/Identifiers.ts
  19. 2
      src/constants/Misc.ts
  20. 121
      src/constants/index.ts
  21. 481
      src/hooks/useFetchVideos.tsx
  22. 364
      src/pages/Home/VideoList.tsx
  23. 4
      src/pages/Home/VideoListComponentLevel.tsx
  24. 36
      src/pages/PlaylistContent/PlaylistContent.tsx
  25. 408
      src/pages/VideoContent/VideoContent.tsx
  26. 154
      src/utils/BoundedNumericTextField.tsx
  27. 28
      src/utils/numberFunctions.ts
  28. 48
      src/utils/qortalRequestFunctions.ts
  29. 76
      src/utils/qortalRequestTypes.ts
  30. 8
      src/utils/stringFunctions.ts
  31. 142
      src/wrappers/GlobalWrapper.tsx

10
.prettierrc

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

164
src/components/EditPlaylist/EditPlaylist.tsx

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

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

145
src/components/EditVideo/EditVideo.tsx

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

328
src/components/PlaylistListEdit/PlaylistListEdit.tsx

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

131
src/components/Playlists/Playlists.tsx

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

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

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

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

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

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

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

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

65
src/components/common/MultiplePublish/MultiplePublish.tsx

@ -8,13 +8,13 @@ import {
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { ModalBody } from "../../UploadVideo/Upload-styles";
import { ModalBody } from "../../PublishVideo/PublishVideo-styles.tsx";
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([])
const listOfSuccessfulPublishesRef = useRef([]);
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
@ -23,7 +23,7 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
const publish = useCallback(async (pub: any) => {
await qortalRequest(pub);
}, []);
const [isPublishing, setIsPublishing] = useState(true)
const [isPublishing, setIsPublishing] = useState(true);
const handlePublish = useCallback(
async (pub: any) => {
@ -33,10 +33,13 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
await publish(pub);
setListOfSuccessfulPublishes((prev: any) => [...prev, pub?.identifier]);
listOfSuccessfulPublishesRef.current = [...listOfSuccessfulPublishesRef.current, pub?.identifier]
listOfSuccessfulPublishesRef.current = [
...listOfSuccessfulPublishesRef.current,
pub?.identifier,
];
} catch (error) {
console.log({ error });
await new Promise<void>((res) => {
await new Promise<void>(res => {
setTimeout(() => {
res();
}, 5000);
@ -49,17 +52,18 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
const startPublish = useCallback(
async (pubs: any) => {
setIsPublishing(true)
const filterPubs = pubs.filter((pub)=> !listOfSuccessfulPublishesRef.current.includes(pub.identifier))
setIsPublishing(true);
const filterPubs = pubs.filter(
pub => !listOfSuccessfulPublishesRef.current.includes(pub.identifier)
);
for (const pub of filterPubs) {
await handlePublish(pub);
}
if(listOfSuccessfulPublishesRef.current.length === pubs.length){
onSubmit()
if (listOfSuccessfulPublishesRef.current.length === pubs.length) {
onSubmit();
}
setIsPublishing(false)
setIsPublishing(false);
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
@ -71,7 +75,6 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
return (
<Modal
open={isOpen}
@ -118,18 +121,28 @@ export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => {
</Box>
);
})}
{!isPublishing && listOfSuccessfulPublishes.length !== publishes.length && (
<>
<Typography sx={{
marginTop: '20px',
fontSize: '16px'
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography>
<Button onClick={()=> {
startPublish(publishes)
}}>Try again</Button>
</>
)}
{!isPublishing &&
listOfSuccessfulPublishes.length !== publishes.length && (
<>
<Typography
sx={{
marginTop: "20px",
fontSize: "16px",
}}
>
Some files were not published. Please try again. It's important
that all the files get published. Maybe wait a couple minutes if
the error keeps occurring
</Typography>
<Button
onClick={() => {
startPublish(publishes);
}}
>
Try again
</Button>
</>
)}
</ModalBody>
</Modal>
);

589
src/components/common/Notifications/Notifications.tsx

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

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

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

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

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

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

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

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

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

107
src/constants/Categories.ts

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

481
src/hooks/useFetchVideos.tsx

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

364
src/pages/Home/VideoList.tsx

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

4
src/pages/Home/VideoListComponentLevel.tsx

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

36
src/pages/PlaylistContent/PlaylistContent.tsx

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

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

154
src/utils/BoundedNumericTextField.tsx

@ -0,0 +1,154 @@
import {
IconButton,
InputAdornment,
TextField,
TextFieldProps,
} from "@mui/material";
import React, { useRef, useState } from "react";
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import {
removeTrailingZeros,
setNumberWithinBounds,
} from "./numberFunctions.ts";
type eventType = React.ChangeEvent<HTMLInputElement>;
type BoundedNumericTextFieldProps = {
minValue: number;
maxValue: number;
addIconButtons?: boolean;
allowDecimals?: boolean;
allowNegatives?: boolean;
afterChange?: (s: string) => void;
initialValue?: string;
maxSigDigits?: number;
} & TextFieldProps;
export const BoundedNumericTextField = ({
minValue,
maxValue,
addIconButtons = true,
allowDecimals = true,
allowNegatives = false,
afterChange,
initialValue,
maxSigDigits = 6,
...props
}: BoundedNumericTextFieldProps) => {
const [textFieldValue, setTextFieldValue] = useState<string>(
initialValue || ""
);
const ref = useRef<HTMLInputElement | null>(null);
const stringIsEmpty = (value: string) => {
return value === "";
};
const isAllZerosNum = /^0*\.?0*$/;
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/;
const isIntegerNum = /^-?[0-9]+$/;
const skipMinMaxCheck = (value: string) => {
const lastIndexIsDecimal = value.charAt(value.length - 1) === ".";
const isEmpty = stringIsEmpty(value);
const isAllZeros = isAllZerosNum.test(value);
const isInteger = isIntegerNum.test(value);
// skipping minMax on all 0s allows values less than 1 to be entered
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger);
};
const setMinMaxValue = (value: string): string => {
if (skipMinMaxCheck(value)) return value;
const valueNum = Number(value);
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue);
const numberInBounds = boundedNum === valueNum;
return numberInBounds ? value : boundedNum.toString();
};
const getSigDigits = (number: string) => {
if (isIntegerNum.test(number)) return 0;
const decimalSplit = number.split(".");
return decimalSplit[decimalSplit.length - 1].length;
};
const sigDigitsExceeded = (number: string, sigDigits: number) => {
return getSigDigits(number) > sigDigits;
};
const filterTypes = (value: string) => {
if (allowDecimals === false) value = value.replace(".", "");
if (allowNegatives === false) value = value.replace("-", "");
if (sigDigitsExceeded(value, maxSigDigits)) {
value = value.substring(0, value.length - 1);
}
return value;
};
const filterValue = (value: string) => {
if (stringIsEmpty(value)) return "";
value = filterTypes(value);
if (isFloatNum.test(value)) {
return setMinMaxValue(value);
}
return textFieldValue;
};
const listeners = (e: eventType) => {
// console.log("changeEvent:", e);
const newValue = filterValue(e.target.value);
setTextFieldValue(newValue);
if (afterChange) afterChange(newValue);
};
const changeValueWithIncDecButton = (changeAmount: number) => {
const changedValue = (+textFieldValue + changeAmount).toString();
const inBoundsValue = setMinMaxValue(changedValue);
setTextFieldValue(inBoundsValue);
if (afterChange) afterChange(inBoundsValue);
};
const formatValueOnBlur = (e: eventType) => {
let value = e.target.value;
if (stringIsEmpty(value) || value === ".") {
setTextFieldValue("");
return;
}
value = setMinMaxValue(value);
value = removeTrailingZeros(value);
if (isAllZerosNum.test(value)) value = minValue.toString();
setTextFieldValue(value);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { onChange, ...noChangeProps } = { ...props };
return (
<TextField
{...noChangeProps}
InputProps={{
...props?.InputProps,
endAdornment: addIconButtons ? (
<InputAdornment position="end">
<IconButton onClick={() => changeValueWithIncDecButton(1)}>
<AddIcon />{" "}
</IconButton>
<IconButton onClick={() => changeValueWithIncDecButton(-1)}>
<RemoveIcon />{" "}
</IconButton>
</InputAdornment>
) : (
<></>
),
}}
onChange={e => listeners(e as eventType)}
onBlur={e => {
formatValueOnBlur(e as eventType);
}}
autoComplete="off"
value={textFieldValue}
inputRef={ref}
/>
);
};
export default BoundedNumericTextField;

28
src/utils/numberFunctions.ts

@ -0,0 +1,28 @@
import * as colorsys from "colorsys";
export const truncateNumber = (value: string | number, sigDigits: number) => {
return Number(value).toFixed(sigDigits);
};
export const changeLightness = (hexColor: string, amount: number) => {
const hsl = colorsys.hex2Hsl(hexColor);
hsl.l += amount;
return colorsys.hsl2Hex(hsl);
};
export const removeTrailingZeros = (s: string) => {
return Number(s).toString();
};
export const setNumberWithinBounds = (
num: number,
minValue: number,
maxValue: number
) => {
if (num > maxValue) return maxValue;
if (num < minValue) return minValue;
return num;
};
export const numberToInt = (num: number) => {
return Math.floor(num);
};

48
src/utils/qortalRequestFunctions.ts

@ -0,0 +1,48 @@
import {
AccountInfo,
AccountName,
GetRequestData,
SearchTransactionResponse,
TransactionSearchParams,
} from "./qortalRequestTypes.ts";
export const getBalance = async (address: string) => {
return (await qortalRequest({
action: "GET_BALANCE",
address,
})) as number;
};
export const getUserAccount = async () => {
return (await qortalRequest({
action: "GET_USER_ACCOUNT",
})) as AccountInfo;
};
export const getUserBalance = async () => {
const accountInfo = await getUserAccount();
return (await getBalance(accountInfo.address)) as number;
};
export const getAccountNames = async (
address: string,
params?: GetRequestData
) => {
const names = (await qortalRequest({
action: "GET_ACCOUNT_NAMES",
address,
...params,
})) as AccountName[];
const namelessAddress = { name: "", owner: address };
const emptyNamesFilled = names.map(({ name, owner }) => {
return name ? { name, owner } : namelessAddress;
});
return emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
};
export const searchTransactions = async (params: TransactionSearchParams) => {
return (await qortalRequest({
action: "SEARCH_TRANSACTIONS",
...params,
})) as SearchTransactionResponse[];
};

76
src/utils/qortalRequestTypes.ts

@ -0,0 +1,76 @@
export type AccountInfo = { address: string; publicKey: string };
export type AccountName = { name: string; owner: string };
export type ConfirmationStatus = "CONFIRMED" | "UNCONFIRMED" | "BOTH";
export interface GetRequestData {
limit?: number;
offset?: number;
reverse?: boolean;
}
export interface SearchTransactionResponse {
type: string;
timestamp: number;
reference: string;
fee: string;
signature: string;
txGroupId: number;
blockHeight: number;
approvalStatus: string;
creatorAddress: string;
senderPublicKey: string;
recipient: string;
amount: string;
}
export type TransactionType =
| "GENESIS"
| "PAYMENT"
| "REGISTER_NAME"
| "UPDATE_NAME"
| "SELL_NAME"
| "CANCEL_SELL_NAME"
| "BUY_NAME"
| "CREATE_POLL"
| "VOTE_ON_POLL"
| "ARBITRARY"
| "ISSUE_ASSET"
| "TRANSFER_ASSET"
| "CREATE_ASSET_ORDER"
| "CANCEL_ASSET_ORDER"
| "MULTI_PAYMENT"
| "DEPLOY_AT"
| "MESSAGE"
| "CHAT"
| "PUBLICIZE"
| "AIRDROP"
| "AT"
| "CREATE_GROUP"
| "UPDATE_GROUP"
| "ADD_GROUP_ADMIN"
| "REMOVE_GROUP_ADMIN"
| "GROUP_BAN"
| "CANCEL_GROUP_BAN"
| "GROUP_KICK"
| "GROUP_INVITE"
| "CANCEL_GROUP_INVITE"
| "JOIN_GROUP"
| "LEAVE_GROUP"
| "GROUP_APPROVAL"
| "SET_GROUP"
| "UPDATE_ASSET"
| "ACCOUNT_FLAGS"
| "ENABLE_FORGING"
| "REWARD_SHARE"
| "ACCOUNT_LEVEL"
| "TRANSFER_PRIVS"
| "PRESENCE";
export interface TransactionSearchParams extends GetRequestData {
startBlock?: number;
blockLimit?: number;
txGroupId?: number;
txType: TransactionType[];
address: string;
confirmationStatus: ConfirmationStatus;
}

8
src/utils/stringFunctions.ts

@ -0,0 +1,8 @@
export const getFileExtensionIndex = (s: string) => {
const lastIndex = s.lastIndexOf(".");
return lastIndex > 0 ? lastIndex : s.length - 1;
};
export const getFileName = (s: string) => {
return s.substring(0, getFileExtensionIndex(s));
};

142
src/wrappers/GlobalWrapper.tsx

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

Loading…
Cancel
Save