Browse Source

Superlike Dialog allows optional donation to DevFund.

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

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

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

Title prefix TextField added that starts all video titles with the entered value.
pull/1/head
Qortal Dev 9 months ago
parent
commit
8d3549739c
  1. 10
      .prettierrc
  2. 150
      src/components/EditPlaylist/EditPlaylist.tsx
  3. 0
      src/components/EditVideo/EditVideo-styles.tsx
  4. 133
      src/components/EditVideo/EditVideo.tsx
  5. 66
      src/components/PlaylistListEdit/PlaylistListEdit.tsx
  6. 113
      src/components/Playlists/Playlists.tsx
  7. 0
      src/components/PublishVideo/PublishVideo-styles.tsx
  8. 288
      src/components/PublishVideo/PublishVideo.tsx
  9. 3
      src/components/common/Comments/CommentEditor.tsx
  10. 10
      src/components/common/Comments/CommentSection.tsx
  11. 55
      src/components/common/MultiplePublish/MultiplePublish.tsx
  12. 283
      src/components/common/Notifications/Notifications.tsx
  13. 151
      src/components/common/SuperLike/SuperLike.tsx
  14. 58
      src/components/common/SuperLikesList/CommentEditor.tsx
  15. 68
      src/components/common/SuperLikesList/SuperLikesSection.tsx
  16. 18
      src/components/layout/Navbar/Navbar.tsx
  17. 107
      src/constants/Categories.ts
  18. 15
      src/constants/Identifiers.ts
  19. 2
      src/constants/Misc.ts
  20. 121
      src/constants/index.ts
  21. 333
      src/hooks/useFetchVideos.tsx
  22. 68
      src/pages/Home/VideoList.tsx
  23. 4
      src/pages/Home/VideoListComponentLevel.tsx
  24. 24
      src/pages/PlaylistContent/PlaylistContent.tsx
  25. 250
      src/pages/VideoContent/VideoContent.tsx
  26. 154
      src/utils/BoundedNumericTextField.tsx
  27. 28
      src/utils/numberFunctions.ts
  28. 48
      src/utils/qortalRequestFunctions.ts
  29. 76
      src/utils/qortalRequestTypes.ts
  30. 8
      src/utils/stringFunctions.ts
  31. 96
      src/wrappers/GlobalWrapper.tsx

10
.prettierrc

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

150
src/components/EditPlaylist/EditPlaylist.tsx

@ -43,11 +43,15 @@ import {
setEditPlaylist,
} 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 = [];
@ -175,20 +179,18 @@ export const EditPlaylist = () => {
if (editVideoProperties) {
setTitle(editVideoProperties?.title || "");
if(editVideoProperties?.htmlDescription){
if (editVideoProperties?.htmlDescription) {
setDescription(editVideoProperties?.htmlDescription);
} else if(editVideoProperties?.description) {
const paragraph = `<p>${editVideoProperties?.description}</p>`
} else if (editVideoProperties?.description) {
const paragraph = `<p>${editVideoProperties?.description}</p>`;
setDescription(paragraph);
}
setCoverImage(editVideoProperties?.image || "");
setVideos(editVideoProperties?.videos || []);
if (editVideoProperties?.category) {
const selectedOption = categories.find(
(option) => option.id === +editVideoProperties.category
option => option.id === +editVideoProperties.category
);
setSelectedCategoryVideos(selectedOption || null);
}
@ -200,7 +202,7 @@ export const EditPlaylist = () => {
) {
const selectedOption = subCategories[
+editVideoProperties?.category
]?.find((option) => option.id === +editVideoProperties.subcategory);
]?.find(option => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null);
}
@ -211,24 +213,22 @@ export const EditPlaylist = () => {
}, [editVideoProperties]);
const onClose = () => {
setTitle("")
setDescription("")
setVideos([])
setPlaylistData(null)
setSelectedCategoryVideos(null)
setSelectedSubCategoryVideos(null)
setCoverImage("")
setTitle("");
setDescription("");
setVideos([]);
setPlaylistData(null);
setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null);
setCoverImage("");
dispatch(setEditPlaylist(null));
};
async function publishQDNResource() {
try {
if(!title) throw new Error('Please enter a title')
if(!description) throw new Error('Please enter a description')
if(!coverImage) throw new Error('Please select cover image')
if(!selectedCategoryVideos) throw new Error('Please select a category')
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!coverImage) throw new Error("Please select cover image");
if (!selectedCategoryVideos) throw new Error("Please select a category");
if (!editVideoProperties) return;
if (!userAddress) throw new Error("Unable to locate user address");
@ -258,7 +258,7 @@ export const EditPlaylist = () => {
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
const videoStructured = playlistData.videos.map((item) => {
const videoStructured = playlistData.videos.map(item => {
const descriptionVid = item?.metadata?.description;
if (!descriptionVid) throw new Error("cannot find video code");
@ -286,13 +286,12 @@ export const EditPlaylist = () => {
});
const id = uid();
let commentsId = editVideoProperties?.id
let commentsId = editVideoProperties?.id;
if(isNew){
commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}`
if (isNew) {
commentsId = `${QTUBE_PLAYLIST_BASE}_cm_${id}`;
}
const stringDescription = extractTextFromHTML(description)
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({
@ -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>
)}
</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) => (
{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

133
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 = []
let imagesExtracts = [];
for (const img of imgs){
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) => (
{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"

66
src/components/PlaylistListEdit/PlaylistListEdit.tsx

@ -3,42 +3,43 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles"
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../UploadVideo/Upload-styles";
} from "../PublishVideo/PublishVideo-styles.tsx";
import { Box, Button, Input, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { removeVideo } from "../../state/features/videoSlice";
import AddIcon from '@mui/icons-material/Add';
import { QTUBE_VIDEO_BASE } from "../../constants";
import AddIcon from "@mui/icons-material/Add";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
const theme = useTheme();
const navigate = useNavigate();
const username = useSelector((state: RootState) => state.auth?.user?.name);
const [searchResults, setSearchResults] = useState([])
const [filterSearch, setFilterSearch] = useState("")
const search = async()=> {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QTUBE_VIDEO_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`
const [searchResults, setSearchResults] = useState([]);
const [filterSearch, setFilterSearch] = useState("");
const search = async () => {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QTUBE_VIDEO_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`;
const response = await fetch(url, {
method: 'GET',
method: "GET",
headers: {
'Content-Type': 'application/json'
}
})
const responseDataSearchVid = await response.json()
setSearchResults(responseDataSearchVid)
}
"Content-Type": "application/json",
},
});
const responseDataSearchVid = await response.json();
setSearchResults(responseDataSearchVid);
};
return (
<Box sx={{
display: 'flex',
gap: '10px',
width: '100%',
justifyContent: 'center'
}}>
<Box
sx={{
display: "flex",
gap: "10px",
width: "100%",
justifyContent: "center",
}}
>
<Box
sx={{
display: "flex",
@ -55,7 +56,7 @@ const [filterSearch, setFilterSearch] = useState("")
sx={{
marginTop: "25px",
height: "450px",
overflow: 'auto'
overflow: "auto",
}}
>
{playlistData?.videos?.map((vid, index) => {
@ -82,7 +83,7 @@ const [filterSearch, setFilterSearch] = useState("")
<Typography
sx={{
fontSize: "18px",
wordBreak: 'break-word'
wordBreak: "break-word",
}}
>
{vid?.metadata?.title}
@ -109,7 +110,6 @@ const [filterSearch, setFilterSearch] = useState("")
width: "100%",
}}
>
<CrowdfundSubTitleRow>
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
@ -117,16 +117,18 @@ const [filterSearch, setFilterSearch] = useState("")
sx={{
marginTop: "25px",
height: "450px",
overflow: 'auto'
overflow: "auto",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
<Box sx={{
display: 'flex',
gap: '10px'
}}>
<Input
id="standard-adornment-name"
onChange={(e) => {
onChange={e => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
@ -155,7 +157,6 @@ const [filterSearch, setFilterSearch] = useState("")
onClick={() => {
search();
}}
variant="contained"
>
Search
@ -186,7 +187,7 @@ const [filterSearch, setFilterSearch] = useState("")
<Typography
sx={{
fontSize: "18px",
wordBreak: 'break-word'
wordBreak: "break-word",
}}
>
{vid?.metadata?.title}
@ -205,6 +206,5 @@ const [filterSearch, setFilterSearch] = useState("")
</CardContentContainerComment>
</Box>
</Box>
);
};

113
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'
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}) => {
export const Playlists = ({
playlistData,
currentVideoIdentifier,
onClick,
}) => {
const theme = useTheme();
const navigate = useNavigate()
const navigate = useNavigate();
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: '400px',
width: '100%'
}}>
<CrowdfundSubTitleRow >
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;
<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%',
<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'
alignItems: "center",
padding: "10px",
borderRadius: "5px",
cursor: isCurrentVidPlayling ? "default" : "pointer",
userSelect: "none",
}}
onClick={()=> {
if(isCurrentVidPlayling) return
onClick(vid.name, vid.identifier)
onClick={() => {
if (isCurrentVidPlayling) return;
onClick(vid.name, vid.identifier);
// navigate(`/video/${vid.name}/${vid.identifier}`)
}}
>
<Typography sx={{
fontSize: '14px'
}}>{index + 1}</Typography>
<Typography sx={{
fontSize: '18px',
wordBreak: 'break-word'
}}>{vid?.metadata?.title}</Typography>
<Typography
sx={{
fontSize: "14px",
}}
>
{index + 1}
</Typography>
<Typography
sx={{
fontSize: "18px",
wordBreak: "break-word",
}}
>
{vid?.metadata?.title}
</Typography>
</Box>
)
);
})}
</CardContentContainerComment>
</Box>
)
}
);
};

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

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

@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import 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 = []
let imagesExtracts = [];
for (const img of imgs){
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 (
<>
@ -670,6 +683,17 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
/>
</FiltersRow>
</FiltersSubContainer>
<CustomInputField
name="prefix"
label="Titles Prefix"
variant="filled"
value={titlesPrefix}
onChange={e =>
setTitlesPrefix(e.target.value.replace(titleFormatter, ""))
}
inputProps={{ maxLength: 180 }}
required
/>
<Box
{...getRootProps()}
sx={{
@ -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>
@ -746,9 +770,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
<>
{!coverImageForAll ? (
<ImageUploader
onPick={(img: string) =>
setCoverImageForAll(img)
}
onPick={(img: string) => setCoverImageForAll(img)}
>
<AddCoverImageButton variant="contained">
Add Cover Image
@ -765,9 +787,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
<CoverImagePreview src={coverImageForAll} alt="logo" />
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() =>
setCoverImageForAll(null)
}
onClickFunc={() => setCoverImageForAll(null)}
height={"32"}
width={"32"}
></TimesIcon>
@ -778,7 +798,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
{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 && (
<>
@ -800,7 +823,10 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
</ImageUploader>
) : (
<LogoPreviewRow>
<CoverImagePreview src={file?.coverImage} alt="logo" />
<CoverImagePreview
src={file?.coverImage}
alt="logo"
/>
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() =>
@ -819,7 +845,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
label="Title of video"
variant="filled"
value={file.title}
onChange={(e) =>
onChange={e =>
handleOnchange(index, "title", e.target.value)
}
inputProps={{ maxLength: 180 }}
@ -827,12 +853,19 @@ 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);
}}
/>
</>
)}
@ -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,12 +1226,16 @@ 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} />
)}
@ -1209,14 +1253,14 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
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({

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

55
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 }) => {
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 && (
{!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>
<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>
);

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

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

151
src/components/common/SuperLike/SuperLike.tsx

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
import {
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
<BoundedNumericTextField
minValue={10}
initialValue={minPriceSuperlike.toString()}
maxValue={numberToInt(+currentBalance)}
allowDecimals={false}
allowNegatives={false}
id="standard-adornment-amount"
type="number"
value={amount}
onChange={(e) => setAmount(+e.target.value)}
startAdornment={
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>
<div>Current QORT Balance is: {currentBalance}</div>
<Spacer height="25px" />
<Box>
<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);

58
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,24 +156,31 @@ 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]
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({
@ -183,7 +190,7 @@ export const CommentEditor = ({
data64: isSuperLike ? data64 : base64,
identifier: identifier,
description,
tag1
tag1,
});
dispatch(
setNotification({
@ -192,13 +199,14 @@ export const CommentEditor = ({
})
);
if(isSuperLike){
dispatch(addtoHashMapSuperlikes({
if (isSuperLike) {
dispatch(
addtoHashMapSuperlikes({
...superObj,
...comment,
message: value
}))
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_", "");
@ -254,8 +262,8 @@ export const CommentEditor = ({
}
await publishComment(identifier, idForNotification);
if(isSuperLike){
onSubmit({})
if (isSuperLike) {
onSubmit({});
} else {
onSubmit({
created: Date.now(),

68
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,8 +156,7 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
[postId]
);
const getComments = useCallback(
async (superlikes, postId) => {
const getComments = useCallback(async (superlikes, postId) => {
try {
setLoadingComments(true);
@ -160,19 +168,16 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
}
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,19 +196,21 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
}, []);
}, [listComments]);
return (
<>
<Panel>
<CrowdfundSubTitleRow >
<CrowdfundSubTitle sx={{
fontSize: '18px',
color: 'gold'
}}>Super Likes</CrowdfundSubTitle>
<CrowdfundSubTitleRow>
<CrowdfundSubTitle
sx={{
fontSize: "18px",
color: "gold",
}}
>
Super Likes
</CrowdfundSubTitle>
</CrowdfundSubTitleRow>
<CommentsContainer>
{(loadingComments || loadingSuperLikes) ? (
{loadingComments || loadingSuperLikes ? (
<NoCommentsRow>
<CircularProgress />
</NoCommentsRow>
@ -214,18 +221,19 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
) : (
<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"

18
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 />
<PublishVideo />
<StyledButton
color="primary"
startIcon={<AddBoxIcon />}
onClick={() => {
dispatch(setEditPlaylist({mode: 'new'}))
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"}
]
}

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

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

4
src/pages/Home/VideoListComponentLevel.tsx

@ -21,8 +21,8 @@ import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { 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 {

24
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]);
}}
/>
)}
@ -610,7 +610,7 @@ export const PlaylistContent = () => {
{descriptionHeight && (
<Typography
onClick={() => {
setIsExpandedDescription((prev) => !prev);
setIsExpandedDescription(prev => !prev);
}}
sx={{
fontWeight: "bold",

250
src/pages/VideoContent/VideoContent.tsx

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

154
src/utils/BoundedNumericTextField.tsx

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

28
src/utils/numberFunctions.ts

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

48
src/utils/qortalRequestFunctions.ts

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

76
src/utils/qortalRequestTypes.ts

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

8
src/utils/stringFunctions.ts

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

96
src/wrappers/GlobalWrapper.tsx

@ -11,16 +11,24 @@ import { addUser } from "../state/features/authSlice";
import NavBar from "../components/layout/Navbar/Navbar";
import 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,12 +141,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
return isDragging.current;
}, []);
const getSuperlikes = useCallback(
async () => {
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",
@ -150,64 +153,59 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const responseData = await response.json();
let comments: any[] = [];
for (const comment of responseData) {
if (comment.identifier && comment.name && comment?.metadata?.description) {
if (
comment.identifier &&
comment.name &&
comment?.metadata?.description
) {
try {
const result = extractSigValue(comment?.metadata?.description)
if(!result) continue
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})
if (
+res?.amount >= minPriceSuperlike &&
isTimestampWithinRange(res?.timestamp, comment.created)
) {
addSuperlikeRawDataGetToList({
name: comment.name,
identifier: comment.identifier,
content: comment,
});
comments = [...comments, {
comments = [
...comments,
{
...comment,
message: "",
amount: res.amount
}];
}
} catch (error) {
amount: res.amount,
},
];
}
} catch (error) {}
}
}
dispatch(setSuperlikesAll(comments));
} catch (error) {
console.error(error);
} finally {
}
},
[]
);
}, []);
const checkSuperlikes = useCallback(
() => {
let isCalling = false
const checkSuperlikes = useCallback(() => {
let isCalling = false;
interval.current = setInterval(async () => {
if (isCalling) return
isCalling = true
const res = await getSuperlikes()
isCalling = false
}, 300000)
getSuperlikes()
},
[getSuperlikes])
if (isCalling) return;
isCalling = true;
const res = await getSuperlikes();
isCalling = false;
}, 300000);
getSuperlikes();
}, [getSuperlikes]);
useEffect(() => {
checkSuperlikes();
}, [checkSuperlikes]);
return (
<>
{isLoadingGlobal && <PageLoader />}

Loading…
Cancel
Save