3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-11 17:55:51 +00:00

Merge branch 'Qortal:main' into main

This commit is contained in:
crowetic 2024-01-19 13:37:37 -08:00 committed by GitHub
commit b6a64de857
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 3690 additions and 2606 deletions

View File

@ -10,7 +10,6 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
'@typescript-eslint/no-explicit-any': "off"
'@typescript-eslint/no-explicit-any': "off",
},
}

10
.prettierrc Normal file
View File

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

1494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,11 +37,11 @@
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.0.2",
"vite": "^4.3.2"
"vite": "^5.0.5"
}
}

View File

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

View File

@ -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 { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
import { categories, subCategories } from "../../constants/Categories.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
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 });
@ -81,7 +83,7 @@ export const EditVideo = () => {
const editVideoProperties = useSelector(
(state: RootState) => state.video.editVideoProperties
);
const [publishes, setPublishes] = useState<any[]>([]);
const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
useState(null);
@ -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,13 +316,17 @@ export const EditVideo = () => {
description: metadescription,
identifier: editVideoProperties.videoReference?.identifier,
tag1: QTUBE_VIDEO_BASE,
filename: `${alphanumericString.trim()}.${fileExtension}`
filename: `${alphanumericString.trim()}.${fileExtension}`,
};
listOfPublishes.push(requestBodyVideo);
}
setPublishes(listOfPublishes);
const multiplePublish = {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [...listOfPublishes],
};
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
setVideoPropertiesToSetToRedux({
...editVideoProperties,
@ -356,13 +357,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 +370,45 @@ export const EditVideo = () => {
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
(option) => option.id === +optionId
option => option.id === +optionId
);
setSelectedSubCategoryVideos(selectedOption || null);
};
const onFramesExtracted = async (imgs)=> {
const onFramesExtracted = async imgs => {
try {
let imagesExtracts = []
for (const img of imgs){
let imagesExtracts = [];
for (const img of imgs) {
try {
let compressedFile
const image = img
await new Promise<void>((resolve) => {
let compressedFile;
const image = img;
await new Promise<void>(resolve => {
new Compressor(image, {
quality: .8,
quality: 0.8,
maxWidth: 750,
mimeType: 'image/webp',
mimeType: "image/webp",
success(result) {
const file = new File([result], 'name', {
type: 'image/webp'
})
compressedFile = file
resolve()
const file = new File([result], "name", {
type: "image/webp",
});
compressedFile = file;
resolve();
},
error(err) {}
})
})
if (!compressedFile) continue
const base64Img = await toBase64(compressedFile)
imagesExtracts.push(base64Img)
error(err) {},
});
});
if (!compressedFile) continue;
const base64Img = await toBase64(compressedFile);
imagesExtracts.push(base64Img);
} catch (error) {
console.error(error)
console.error(error);
}
}
setImageExtracts(imagesExtracts)
} catch (error) {
}
}
setImageExtracts(imagesExtracts);
} catch (error) {}
};
return (
<>
@ -467,7 +463,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 +478,27 @@ export const EditVideo = () => {
labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={(e) =>
onChange={e =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
>
{subCategories[selectedCategoryVideos.id].map(
(option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
{subCategories[selectedCategoryVideos.id].map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
)}
</Box>
{file && (
<FrameExtractor videoFile={file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs)}/>
<FrameExtractor
videoFile={file}
onFramesExtracted={imgs => onFramesExtracted(imgs)}
/>
)}
<React.Fragment>
{!coverImage ? (
@ -532,23 +529,27 @@ export const EditVideo = () => {
label="Title of video"
variant="filled"
value={title}
onChange={(e) => {
onChange={e => {
const value = e.target.value;
const formattedValue = value.replace(
/[^a-zA-Z0-9\s-_!?]/g,
""
);
const formattedValue = value.replace(titleFormatter, "");
setTitle(formattedValue);
}}
inputProps={{ maxLength: 180 }}
required
/>
<Typography sx={{
fontSize: '18px'
}}>Description of video</Typography>
<TextEditor inlineContent={description} setInlineContent={(value)=> {
setDescription(value)
}} />
<Typography
sx={{
fontSize: "18px",
}}
>
Description of video
</Typography>
<TextEditor
inlineContent={description}
setInlineContent={value => {
setDescription(value);
}}
/>
{/* <CustomInputField
name="description"
label="Describe your video in a few words"
@ -588,8 +589,8 @@ export const EditVideo = () => {
disabled={file && imageExtracts.length === 0}
>
{file && imageExtracts.length === 0 && (
<CircularProgress color="secondary" size={14} />
)}
<CircularProgress color="secondary" size={14} />
)}
Publish
</CrowdfundActionButton>
</Box>
@ -599,6 +600,18 @@ export const EditVideo = () => {
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onError={messageNotification => {
setIsOpenMultiplePublish(false);
setPublishes(null);
if (messageNotification) {
dispatch(
setNotification({
msg: messageNotification,
alertType: "error",
})
);
}
}}
onSubmit={() => {
setIsOpenMultiplePublish(false);
const clonedCopy = structuredClone(videoPropertiesToSetToRedux);

View File

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

View File

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

View File

@ -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,13 +45,8 @@ import {
upsertVideos,
} from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader";
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
categories,
subCategories,
} from "../../constants";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
import { categories, subCategories } from "../../constants/Categories.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
@ -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);
@ -124,40 +130,38 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
useState<any>(null);
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 [publishes, setPublishes] = useState<any>(null);
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 =
@ -457,7 +469,11 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
}
}
setPublishes(listOfPublishes);
const multiplePublish = {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [...listOfPublishes],
};
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
} catch (error: any) {
let notificationObj: any = null;
@ -485,10 +501,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 +517,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 +526,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 +535,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 +544,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 +575,45 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
}
};
const onFramesExtracted = async (imgs, index)=> {
const onFramesExtracted = async (imgs, index) => {
try {
let imagesExtracts = []
for (const img of imgs){
let imagesExtracts = [];
for (const img of imgs) {
try {
let compressedFile
const image = img
await new Promise<void>((resolve) => {
let compressedFile;
const image = img;
await new Promise<void>(resolve => {
new Compressor(image, {
quality: .8,
quality: 0.8,
maxWidth: 750,
mimeType: 'image/webp',
mimeType: "image/webp",
success(result) {
const file = new File([result], 'name', {
type: 'image/webp'
})
compressedFile = file
resolve()
const file = new File([result], "name", {
type: "image/webp",
});
compressedFile = file;
resolve();
},
error(err) {}
})
})
if (!compressedFile) continue
const base64Img = await toBase64(compressedFile)
imagesExtracts.push(base64Img)
error(err) {},
});
});
if (!compressedFile) continue;
const base64Img = await toBase64(compressedFile);
imagesExtracts.push(base64Img);
} catch (error) {
console.error(error)
console.error(error);
}
}
setImageExtracts((prev)=> {
setImageExtracts(prev => {
return {
...prev,
[index]: imagesExtracts
}
})
} catch (error) {
}
}
[index]: imagesExtracts,
};
});
} catch (error) {}
};
return (
<>
@ -638,38 +655,48 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
{step === "videos" && (
<>
<FiltersSubContainer>
<FiltersRow>
Populate Titles by filename (when the files are picked)
<FiltersCheckbox
checked={isCheckTitleByFile}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckTitleByFile(e.target.checked);
}}
inputProps={{ "aria-label": "controlled" }}
<FiltersSubContainer>
<FiltersRow>
Populate Titles by filename (when the files are picked)
<FiltersCheckbox
checked={isCheckTitleByFile}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckTitleByFile(e.target.checked);
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
<FiltersRow>
All videos use the same Cover Image
<FiltersCheckbox
checked={isCheckSameCoverImage}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckSameCoverImage(e.target.checked);
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
<FiltersRow>
Populate all descriptions by Title
<FiltersCheckbox
checked={isCheckDescriptionIsTitle}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckDescriptionIsTitle(e.target.checked);
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
</FiltersSubContainer>
<CustomInputField
name="prefix"
label="Titles Prefix"
variant="filled"
value={titlesPrefix}
onChange={e =>
setTitlesPrefix(e.target.value.replace(titleFormatter, ""))
}
inputProps={{ maxLength: 180 }}
/>
</FiltersRow>
<FiltersRow>
All videos use the same Cover Image
<FiltersCheckbox
checked={isCheckSameCoverImage}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckSameCoverImage(e.target.checked);
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
<FiltersRow>
Populate all descriptions by Title
<FiltersCheckbox
checked={isCheckDescriptionIsTitle}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIsCheckDescriptionIsTitle(e.target.checked);
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
</FiltersSubContainer>
<Box
{...getRootProps()}
sx={{
@ -685,7 +712,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
Drag and drop a video files here or click to select files
</Typography>
</Box>
<Box
sx={{
display: "flex",
@ -703,7 +730,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 +749,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 +757,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
}
>
{subCategories[selectedCategoryVideos.id].map(
(option) => (
option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
@ -743,83 +770,85 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
)}
</Box>
{files?.length > 0 && isCheckSameCoverImage && (
<>
{!coverImageForAll ? (
<ImageUploader
onPick={(img: string) =>
setCoverImageForAll(img)
}
>
<AddCoverImageButton variant="contained">
Add Cover Image
<AddLogoIcon
sx={{
height: "25px",
width: "auto",
}}
></AddLogoIcon>
</AddCoverImageButton>
</ImageUploader>
) : (
<LogoPreviewRow>
<CoverImagePreview src={coverImageForAll} alt="logo" />
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() =>
setCoverImageForAll(null)
}
height={"32"}
width={"32"}
></TimesIcon>
</LogoPreviewRow>
)}
</>
)}
<>
{!coverImageForAll ? (
<ImageUploader
onPick={(img: string) => setCoverImageForAll(img)}
>
<AddCoverImageButton variant="contained">
Add Cover Image
<AddLogoIcon
sx={{
height: "25px",
width: "auto",
}}
></AddLogoIcon>
</AddCoverImageButton>
</ImageUploader>
) : (
<LogoPreviewRow>
<CoverImagePreview src={coverImageForAll} alt="logo" />
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() => setCoverImageForAll(null)}
height={"32"}
width={"32"}
></TimesIcon>
</LogoPreviewRow>
)}
</>
)}
{files.map((file, index) => {
return (
<React.Fragment key={index}>
<FrameExtractor videoFile={file.file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs, index)}/>
<FrameExtractor
videoFile={file.file}
onFramesExtracted={imgs => onFramesExtracted(imgs, index)}
/>
<Typography>{file?.file?.name}</Typography>
{!isCheckSameCoverImage && (
<>
{!file?.coverImage ? (
<ImageUploader
onPick={(img: string) =>
handleOnchange(index, "coverImage", img)
}
>
<AddCoverImageButton variant="contained">
Add Cover Image
<AddLogoIcon
sx={{
height: "25px",
width: "auto",
}}
></AddLogoIcon>
</AddCoverImageButton>
</ImageUploader>
) : (
<LogoPreviewRow>
<CoverImagePreview src={file?.coverImage} alt="logo" />
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() =>
handleOnchange(index, "coverImage", "")
}
height={"32"}
width={"32"}
></TimesIcon>
</LogoPreviewRow>
)}
{!file?.coverImage ? (
<ImageUploader
onPick={(img: string) =>
handleOnchange(index, "coverImage", img)
}
>
<AddCoverImageButton variant="contained">
Add Cover Image
<AddLogoIcon
sx={{
height: "25px",
width: "auto",
}}
></AddLogoIcon>
</AddCoverImageButton>
</ImageUploader>
) : (
<LogoPreviewRow>
<CoverImagePreview
src={file?.coverImage}
alt="logo"
/>
<TimesIcon
color={theme.palette.text.primary}
onClickFunc={() =>
handleOnchange(index, "coverImage", "")
}
height={"32"}
width={"32"}
></TimesIcon>
</LogoPreviewRow>
)}
</>
)}
<CustomInputField
name="title"
label="Title of video"
variant="filled"
value={file.title}
onChange={(e) =>
onChange={e =>
handleOnchange(index, "title", e.target.value)
}
inputProps={{ maxLength: 180 }}
@ -827,15 +856,22 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
/>
{!isCheckDescriptionIsTitle && (
<>
<Typography sx={{
fontSize: '18px'
}}>Description of video</Typography>
<TextEditor inlineContent={file?.description} setInlineContent={(value)=> {
handleOnchange(index, "description", value)
}} />
<Typography
sx={{
fontSize: "18px",
}}
>
Description of video
</Typography>
<TextEditor
inlineContent={file?.description}
setInlineContent={value => {
handleOnchange(index, "description", value);
}}
/>
</>
)}
{/* <CustomInputField
name="description"
label="Describe your video in a few words"
@ -962,7 +998,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
>
<Input
id="standard-adornment-name"
onChange={(e) => {
onChange={e => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
@ -1072,11 +1108,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 +1131,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 +1152,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 +1168,14 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
labelId="Sub-Category"
input={<OutlinedInput label="Select a Sub-Category" />}
value={selectedSubCategory?.id || ""}
onChange={(e) =>
onChange={e =>
handleOptionSubCategoryChange(
e,
subCategories[selectedCategory?.id]
)
}
>
{subCategories[selectedCategory.id].map((option) => (
{subCategories[selectedCategory.id].map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
@ -1186,37 +1229,53 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
) : (
<CrowdfundActionButton
variant="contained"
disabled={files?.length !== Object.keys(imageExtracts)?.length}
disabled={
files?.length !== Object.keys(imageExtracts)?.length
}
onClick={() => {
next();
}}
>
{files?.length !== Object.keys(imageExtracts)?.length ? 'Generating image extracts' : ''}
{files?.length !== Object.keys(imageExtracts)?.length
? "Generating image extracts"
: ""}
{files?.length !== Object.keys(imageExtracts)?.length && (
<CircularProgress color="secondary" size={14} />
)}
Next
Next
</CrowdfundActionButton>
)}
</Box>
</CrowdfundActionButtonRow>
</ModalBody>
</Modal>
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onError={messageNotification => {
setIsOpenMultiplePublish(false);
setPublishes(null);
if (messageNotification) {
dispatch(
setNotification({
msg: messageNotification,
alertType: "error",
})
);
}
}}
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);

View File

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

View File

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

View File

@ -11,8 +11,8 @@ import { Box } from "@mui/material";
import { useSelector } from "react-redux";
import { RootState } from "../../../state/store";
import { useNavigate } from "react-router-dom";
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
const truncateMessage = (message) => {
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents";
const truncateMessage = message => {
return message.length > 40 ? message.slice(0, 40) + "..." : message;
};
@ -28,18 +28,16 @@ export default function ListSuperLikes({ superlikes }) {
// let hasHash = false
let message = "";
let url = "";
let forName = ""
let forName = "";
// let hash = {}
if (hashMapSuperlikes[superlike?.identifier]) {
message = hashMapSuperlikes[superlike?.identifier]?.comment || "";
if (
hashMapSuperlikes[superlike?.identifier]?.notificationInformation
) {
const info =
hashMapSuperlikes[superlike?.identifier]?.notificationInformation;
forName = info?.name
forName = info?.name;
url = `/video/${info?.name}/${info?.identifier}`;
}
@ -57,7 +55,7 @@ export default function ListSuperLikes({ superlikes }) {
alignItems="flex-start"
sx={{
cursor: url ? "pointer" : "default",
minHeight: '130px'
minHeight: "130px",
}}
onClick={async () => {
if (url) {
@ -65,79 +63,86 @@ export default function ListSuperLikes({ superlikes }) {
}
}}
>
<Box sx={{
width: '100%'
}}>
<ListItem
sx={{
padding: '0px'
}}
alignItems="flex-start"
>
<ListItemAvatar>
<Avatar
alt="Remy Sharp"
src={`/arbitrary/THUMBNAIL/${superlike?.name}/qortal_avatar`}
/>
</ListItemAvatar>
<ListItemText
primary={
<Box
sx={{
width: "100%",
}}
>
<ListItem
sx={{
padding: "0px",
}}
alignItems="flex-start"
>
<ListItemAvatar>
<Avatar
alt="Remy Sharp"
src={`/arbitrary/THUMBNAIL/${superlike?.name}/qortal_avatar`}
/>
</ListItemAvatar>
<ListItemText
primary={
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
fontSize: "16px",
}}
>
<ThumbUpIcon
style={{
color: "gold",
}}
/>
<Typography
sx={{
fontSize: "18px",
}}
>
{amount ? amount : ""} QORT
</Typography>
</Box>
}
secondary={
<Box
sx={{
fontSize: "15px",
}}
>
<Typography
sx={{
display: "inline",
wordBreak: "break-word",
fontSize: "16px",
}}
component="span"
variant="body2"
color="text.primary"
>
{superlike?.name}
</Typography>
{` - ${truncateMessage(message)}`}
</Box>
}
/>
</ListItem>
{forName && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
fontSize: "16px",
fontSize: "17px",
gap: "10px",
justifyContent: "flex-end",
}}
>
<ThumbUpIcon
style={{
color: "gold",
}}
/>
<Typography
sx={{
fontSize: "18px",
}}
>
{amount ? amount : ""} QORT
</Typography>
<EmojiEventsIcon />
{forName}
</Box>
}
secondary={
<Box sx={{
fontSize: '15px'
}}>
<Typography
sx={{ display: "inline", wordBreak: "break-word", fontSize: '16px'}}
component="span"
variant="body2"
color="text.primary"
>
{superlike?.name}
</Typography>
{` - ${truncateMessage(message)}`}
</Box>
}
/>
</ListItem>
{forName && (
<Box sx={{
display: 'flex',
alignItems: 'center',
fontSize: '17px',
gap: '10px',
justifyContent: 'flex-end'
}}>
<EmojiEventsIcon />
{forName}
</Box>
)}
</Box>
)}
</Box>
</ListItem>
<Box
sx={{

View File

@ -1,136 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Box,
Button,
CircularProgress,
Modal,
Typography,
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { ModalBody } from "../../UploadVideo/Upload-styles";
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 [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
await qortalRequest(pub);
}, []);
const [isPublishing, setIsPublishing] = useState(true)
const handlePublish = useCallback(
async (pub: any) => {
try {
setCurrentlyInPublish(pub?.identifier);
await publish(pub);
setListOfSuccessfulPublishes((prev: any) => [...prev, pub?.identifier]);
listOfSuccessfulPublishesRef.current = [...listOfSuccessfulPublishesRef.current, pub?.identifier]
} catch (error) {
console.log({ error });
await new Promise<void>((res) => {
setTimeout(() => {
res();
}, 5000);
});
// await handlePublish(pub);
}
},
[publish]
);
const startPublish = useCallback(
async (pubs: any) => {
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()
}
setIsPublishing(false)
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
useEffect(() => {
if (publishes && !hasStarted.current) {
hasStarted.current = true;
startPublish(publishes);
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
return (
<Modal
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody
sx={{
minHeight: "50vh",
}}
>
{publishes.map((publish: any) => {
return (
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>{publish?.title}</Typography>
{publish?.identifier === currentlyInPublish ? (
<CircularProgress
size={20}
thickness={2}
sx={{
color: theme.palette.secondary.main,
}}
/>
) : listOfSuccessfulPublishes.includes(publish.identifier) ? (
<CircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
) : (
<EmptyCircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
)}
</Box>
);
})}
{!isPublishing && listOfSuccessfulPublishes.length !== publishes.length && (
<>
<Typography sx={{
marginTop: '20px',
fontSize: '16px'
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography>
<Button onClick={()=> {
startPublish(publishes)
}}>Try again</Button>
</>
)}
</ModalBody>
</Modal>
);
};

View File

@ -0,0 +1,224 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Box,
Button,
CircularProgress,
Modal,
Typography,
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
import { styled } from "@mui/system";
interface Publish {
resources: any[];
action: string;
}
interface MultiplePublishProps {
publishes: Publish;
isOpen: boolean;
onSubmit: () => void;
onError: (message?: string) => void;
}
export const MultiplePublish = ({
publishes,
isOpen,
onSubmit,
onError,
}: MultiplePublishProps) => {
const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([]);
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] =
useState<any[]>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length;
const lengthOfTimeout = lengthOfResources * 30000;
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []);
const [isPublishing, setIsPublishing] = useState(true);
const handlePublish = useCallback(
async (pub: any) => {
try {
setCurrentlyInPublish(pub?.identifier);
setIsPublishing(true);
const res = await publish(pub);
onSubmit();
setListOfUnSuccessfulPublishes([]);
} catch (error: any) {
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || [];
if (error?.error === "User declined request") {
onError();
return;
}
if (error?.error === "The request timed out") {
onError("The request timed out");
return;
}
if (unsuccessfulPublishes?.length > 0) {
setListOfUnSuccessfulPublishes(unsuccessfulPublishes);
}
} finally {
setIsPublishing(false);
}
},
[publish]
);
const retry = () => {
let newlistOfMultiplePublishes: any[] = [];
listOfUnsuccessfulPublishes?.forEach(item => {
const findPub = publishes?.resources.find(
(res: any) => res?.identifier === item.identifier
);
if (findPub) {
newlistOfMultiplePublishes.push(findPub);
}
});
const multiplePublish = {
...publishes,
resources: newlistOfMultiplePublishes,
};
handlePublish(multiplePublish);
};
const startPublish = useCallback(
async (pubs: any) => {
await handlePublish(pubs);
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
useEffect(() => {
if (publishes && !hasStarted.current) {
hasStarted.current = true;
startPublish(publishes);
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
return (
<Modal
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody
sx={{
minHeight: "50vh",
}}
>
{publishes?.resources?.map((publish: any) => {
const unpublished = listOfUnsuccessfulPublishes.map(
item => item?.identifier
);
return (
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>{publish?.identifier}</Typography>
{!isPublishing && hasStarted.current ? (
<>
{!unpublished.includes(publish.identifier) ? (
<CircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
) : (
<EmptyCircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
)}
</>
) : (
<CircularProgress size={16} color="secondary" />
)}
</Box>
);
})}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
<>
<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
variant="contained"
onClick={() => {
retry();
}}
>
Try again
</Button>
</>
)}
</ModalBody>
</Modal>
);
};
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
},
}));

View File

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

View File

@ -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,25 +7,22 @@ import {
DialogActions,
DialogContent,
DialogTitle,
FormControl,
Input,
InputAdornment,
InputLabel,
MenuItem,
Modal,
Select,
Tooltip,
} from "@mui/material";
import qortImg from "../../../assets/img/qort.png";
import { MultiplePublish } from "../MultiplePublish/MultiplePublish";
import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll";
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,19 +54,23 @@ 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 [publishes, setPublishes] = useState<any>(null);
const dispatch = useDispatch();
const resetValues = () => {
setAmount(0);
setSuperlikeDonationAmount(0);
setComment("");
setPublishes([]);
setPublishes(null);
};
const onClose = () => {
resetValues();
@ -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);
@ -138,7 +177,12 @@ export const SuperLike = ({
listOfPublishes.push(requestBodyJson);
setPublishes(listOfPublishes);
const multiplePublish = {
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
resources: [...listOfPublishes],
};
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
} catch (error: any) {
let notificationObj: any = null;
@ -164,6 +208,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 +297,44 @@ export const SuperLike = ({
<NewCrowdfundTitle>Super Like</NewCrowdfundTitle>
</Box>
<DialogContent>
<Box
sx={{
width: "300px",
display: "flex",
justifyContent: "center",
}}
>
<Box>
<InputLabel htmlFor="standard-adornment-amount">
Amount in QORT (min 10 QORT)
</InputLabel>
<Input
id="standard-adornment-amount"
type="number"
value={amount}
onChange={(e) => setAmount(+e.target.value)}
startAdornment={
<Box>
<InputLabel htmlFor="standard-adornment-amount">
Amount in QORT (min 10 QORT)
</InputLabel>
<BoundedNumericTextField
minValue={10}
initialValue={minPriceSuperlike.toString()}
maxValue={numberToInt(+currentBalance)}
allowDecimals={false}
allowNegatives={false}
id="standard-adornment-amount"
value={superlikeDonationAmount}
afterChange={(e: string) => setSuperlikeDonationAmount(+e)}
InputProps={{
style: { fontSize: 30, width: textFieldWidth },
startAdornment: (
<InputAdornment position="start">
<img
style={{
height: "15px",
width: "15px",
height: "40px",
width: "40px",
}}
src={qortImg}
alt={"Qort Icon"}
/>
</InputAdornment>
}
/>
</Box>
</Box>
<Spacer height="25px" />
<Box>
),
}}
/>
<div>Current QORT Balance is: {currentBalance}</div>
<Spacer height="25px" />
<CommentInput
id="standard-multiline-flexible"
label="Your comment"
multiline
minRows={8}
maxRows={8}
variant="filled"
value={comment}
@ -288,7 +342,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>
@ -326,13 +410,25 @@ export const SuperLike = ({
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onError={messageNotification => {
setIsOpenMultiplePublish(false);
setPublishes(null);
if (messageNotification) {
dispatch(
setNotification({
msg: messageNotification,
alertType: "error",
})
);
}
}}
onSubmit={() => {
onSuccess({
name: username,
message: comment,
service,
identifier,
amount: +amount,
amount: +superlikeDonationAmount,
created: Date.now(),
});
setIsOpenMultiplePublish(false);

View File

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

View File

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

View File

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

107
src/constants/Categories.ts Normal file
View File

@ -0,0 +1,107 @@
interface SubCategory {
id: number;
name: string;
}
interface CategoryMap {
[key: number]: SubCategory[];
}
const sortCategory = (a: SubCategory, b: SubCategory) => {
if (a.name === "Other") return 1;
else if (b.name === "Other") return -1;
else return a.name.localeCompare(b.name);
};
export const categories = [
{ id: 1, name: "Movies" },
{ id: 2, name: "Series" },
{ id: 3, name: "Music" },
{ id: 4, name: "Education" },
{ id: 5, name: "Lifestyle" },
{ id: 6, name: "Gaming" },
{ id: 7, name: "Technology" },
{ id: 8, name: "Sports" },
{ id: 9, name: "News & Politics" },
{ id: 10, name: "Cooking & Food" },
{ id: 11, name: "Animation" },
{ id: 12, name: "Science" },
{ id: 13, name: "Health & Wellness" },
{ id: 14, name: "DIY & Crafts" },
{ id: 15, name: "Kids & Family" },
{ id: 16, name: "Comedy" },
{ id: 17, name: "Travel & Adventure" },
{ id: 18, name: "Art & Design" },
{ id: 19, name: "Nature & Environment" },
{ id: 20, name: "Business & Finance" },
{ id: 21, name: "Personal Development" },
{ id: 22, name: "Other" },
{ id: 23, name: "History" },
{ id: 24, name: "Anime" },
{ id: 25, name: "Cartoons" },
{ id: 26, name: "Qortal" },
].sort(sortCategory);
export const subCategories: CategoryMap = {
1: [
// Movies
{ id: 101, name: "Action & Adventure" },
{ id: 102, name: "Comedy" },
{ id: 103, name: "Drama" },
{ id: 104, name: "Fantasy & Science Fiction" },
{ id: 105, name: "Horror & Thriller" },
{ id: 106, name: "Documentaries" },
{ id: 107, name: "Animated" },
{ id: 108, name: "Family & Kids" },
{ id: 109, name: "Romance" },
{ id: 110, name: "Mystery & Crime" },
{ id: 111, name: "Historical & War" },
{ id: 112, name: "Musicals & Music Films" },
{ id: 113, name: "Indie Films" },
{ id: 114, name: "International Films" },
{ id: 115, name: "Biographies & True Stories" },
{ id: 116, name: "Other" },
].sort(sortCategory),
2: [
// Series
{ id: 201, name: "Dramas" },
{ id: 202, name: "Comedies" },
{ id: 203, name: "Reality & Competition" },
{ id: 204, name: "Documentaries & Docuseries" },
{ id: 205, name: "Sci-Fi & Fantasy" },
{ id: 206, name: "Crime & Mystery" },
{ id: 207, name: "Animated Series" },
{ id: 208, name: "Kids & Family" },
{ id: 209, name: "Historical & Period Pieces" },
{ id: 210, name: "Action & Adventure" },
{ id: 211, name: "Horror & Thriller" },
{ id: 212, name: "Romance" },
{ id: 213, name: "Anthologies" },
{ id: 214, name: "International Series" },
{ id: 215, name: "Miniseries" },
{ id: 216, name: "Other" },
].sort(sortCategory),
4: [
// Education
{ id: 400, name: "Tutorial" },
{ id: 401, name: "Documentary" },
{ id: 401, name: "Qortal" },
{ id: 402, name: "Other" },
].sort(sortCategory),
24: [
{ id: 2401, name: "Kodomomuke" },
{ id: 2402, name: "Shonen" },
{ id: 2403, name: "Shoujo" },
{ id: 2404, name: "Seinen" },
{ id: 2405, name: "Josei" },
{ id: 2406, name: "Mecha" },
{ id: 2407, name: "Mahou Shoujo" },
{ id: 2408, name: "Isekai" },
{ id: 2409, name: "Yaoi" },
{ id: 2410, name: "Yuri" },
{ id: 2411, name: "Harem" },
{ id: 2412, name: "Ecchi" },
{ id: 2413, name: "Idol" },
{ id: 2414, name: "Other" },
].sort(sortCategory),
};

View File

@ -0,0 +1,15 @@
const useTestIdentifiers = false;
export const QTUBE_VIDEO_BASE = useTestIdentifiers
? "MYTEST_vid_"
: "qtube_vid_";
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers
? "MYTEST_playlist_"
: "qtube_playlist_";
export const SUPER_LIKE_BASE = useTestIdentifiers
? "MYTEST_superlike_"
: "qtube_superlike_";
export const COMMENT_BASE = useTestIdentifiers
? "qcomment_v1_MYTEST_"
: "qcomment_v1_qtube_";
export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962";
export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`;

2
src/constants/Misc.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
export const truncateNumber = (value: string | number, sigDigits: number) => {
return Number(value).toFixed(sigDigits);
};
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);
};

View File

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

View File

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

View File

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

View File

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