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

finished super likes

This commit is contained in:
PhilReact 2023-12-15 02:20:06 +02:00
parent d8e2ecb382
commit a0e1c65900
12 changed files with 774 additions and 466 deletions

View File

@ -366,7 +366,12 @@ export const EditPlaylist = () => {
);
}
dispatch(
setNotification({
msg: "Playlist published",
alertType: "success",
})
);
onClose();
} catch (error: any) {
@ -394,21 +399,7 @@ export const EditPlaylist = () => {
}
}
const handleOnchange = (index: number, type: string, value: string) => {
// setFiles((prev) => {
// let formattedValue = value
// console.log({type})
// if(type === 'title'){
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
// }
// const copyFiles = [...prev];
// copyFiles[index] = {
// ...copyFiles[index],
// [type]: formattedValue,
// };
// return copyFiles;
// });
};
const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string>

View File

@ -115,7 +115,7 @@ export const Notifications = () => {
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=true&excludeblocked=true&offset=0&name=${username}`;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`;
const response2 = await fetch(url, {
method: "GET",
headers: {

View File

@ -19,7 +19,13 @@ 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 {
FOR,
FOR_SUPER_LIKE,
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
minPriceSuperlike,
} from "../../../constants";
import { CommentInput } from "../Comments/Comments-styles";
import {
CrowdfundActionButton,
@ -33,7 +39,14 @@ import { RootState } from "../../../state/store";
const uid = new ShortUniqueId({ length: 4 });
export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, numberOfSuperlikes }) => {
export const SuperLike = ({
onSuccess,
name,
service,
identifier,
totalAmount,
numberOfSuperlikes,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [amount, setAmount] = useState<number>(10);
const [comment, setComment] = useState<string>("");
@ -56,8 +69,8 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
async function publishSuperLike() {
try {
if(!username) throw new Error("You need a name to publish")
if(!name) throw new Error("Could not retrieve content creator's name");
if (!username) throw new Error("You need a name to publish");
if (!name) throw new Error("Could not retrieve content creator's name");
let resName = await qortalRequest({
action: "GET_NAME_DATA",
@ -65,13 +78,15 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
});
const address = resName.owner;
if(!identifier) throw new Error("Could not retrieve id of video post");
if (comment.length > 200) throw new Error("Comment needs to be under 200 characters")
if (!identifier) throw new Error("Could not retrieve id of video post");
// if (comment.length > 200) throw new Error("Comment needs to be under 200 characters")
if (!address)
throw new Error("Could not retrieve content creator's address");
if (!amount || amount < minPriceSuperlike)
throw new Error(`The amount needs to be at least ${minPriceSuperlike} QORT`);
throw new Error(
`The amount needs to be at least ${minPriceSuperlike} QORT`
);
let listOfPublishes = [];
@ -82,9 +97,12 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
amount: amount,
});
let metadescription = `**sig:${res.signature};${FOR}:${name}_${FOR_SUPER_LIKE};nm:${name.slice(0,20)};id:${identifier.slice(-30)}**`;
let metadescription = `**sig:${
res.signature
};${FOR}:${name}_${FOR_SUPER_LIKE};nm:${name.slice(
0,
20
)};id:${identifier.slice(-30)}**`;
const id = uid();
const identifierSuperLike = `${SUPER_LIKE_BASE}${identifier.slice(
@ -92,14 +110,25 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
39
)}_${id}`;
const superLikeToBase64 = await objectToBase64({
comment,
transactionReference: res.signature,
notificationInformation: {
name,
identifier,
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",
});
// Description is obtained from raw data
const base64 = utf8ToBase64(comment);
// const base64 = utf8ToBase64(comment);
const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE",
name: username,
service: "BLOG_COMMENT",
data64: base64,
data64: superLikeToBase64,
title: "",
description: metadescription,
identifier: identifierSuperLike,
@ -107,7 +136,6 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
filename: `superlike_metadata.json`,
};
listOfPublishes.push(requestBodyJson);
setPublishes(listOfPublishes);
@ -140,47 +168,63 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
<>
<Box
onClick={() => {
if (username === name) {
dispatch(
setNotification({
msg: "You cannot send yourself a Super like",
alertType: "error",
})
);
return;
}
setIsOpen(true);
}}
sx={{
display: 'flex',
alignItems: 'center',
gap: '15px',
cursor: 'pointer',
flexShrink: 0
display: "flex",
alignItems: "center",
gap: "15px",
cursor: "pointer",
flexShrink: 0,
}}
>
{numberOfSuperlikes === 0 ? null : (
<p style={{
fontSize: '16px',
userSelect: 'none',
margin: '0px',
padding: '0px'
}}>{totalAmount} QORT from {numberOfSuperlikes} Super Likes</p>
)}
<Tooltip title="Super Like" placement="top">
<Box sx={{
padding: '5px',
borderRadius: '7px',
gap: '10px',
display: 'flex',
alignItems: 'center',
outline: '1px gold solid'
}}>
<ThumbUpIcon
style={{
color: "gold",
}}
/>
<p style={{
fontSize: '16px',
margin: '0px'
}}>Super Like</p>
</Box>
>
{numberOfSuperlikes === 0 ? null : (
<p
style={{
fontSize: "16px",
userSelect: "none",
margin: "0px",
padding: "0px",
}}
>
{totalAmount} QORT from {numberOfSuperlikes} Super Likes
</p>
)}
<Tooltip title="Super Like" placement="top">
<Box
sx={{
padding: "5px",
borderRadius: "7px",
gap: "10px",
display: "flex",
alignItems: "center",
outline: "1px gold solid",
}}
>
<ThumbUpIcon
style={{
color: "gold",
}}
/>
<p
style={{
fontSize: "16px",
margin: "0px",
}}
>
Super Like
</p>
</Box>
</Tooltip>
</Box>
<Modal
@ -190,12 +234,12 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
aria-describedby="alert-dialog-description"
>
<ModalBody>
<Box
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: '100%'
width: "100%",
}}
>
<NewCrowdfundTitle>Super Like</NewCrowdfundTitle>
@ -210,7 +254,7 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
>
<Box>
<InputLabel htmlFor="standard-adornment-amount">
Amount in QORT
Amount in QORT (min 10 QORT)
</InputLabel>
<Input
id="standard-adornment-amount"
@ -241,7 +285,7 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
variant="filled"
value={comment}
inputProps={{
maxLength: 200,
maxLength: 500,
}}
InputLabelProps={{ style: { fontSize: "18px" } }}
onChange={(e) => setComment(e.target.value)}
@ -284,12 +328,12 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
isOpen={isOpenMultiplePublish}
onSubmit={() => {
onSuccess({
name: username,
message: comment,
service,
identifier,
amount: +amount,
created: Date.now()
name: username,
message: comment,
service,
identifier,
amount: +amount,
created: Date.now(),
});
setIsOpenMultiplePublish(false);
onClose();
@ -300,7 +344,6 @@ export const SuperLike = ({ onSuccess, name, service, identifier, totalAmount, n
alertType: "success",
})
);
}}
publishes={publishes}
/>

View File

@ -37,13 +37,17 @@ interface CommentProps {
postName: string;
onSubmit: (obj?: any, isEdit?: boolean) => void;
amount?: null | number
isSuperLike?: boolean
hasHash?: boolean
}
export const Comment = ({
comment,
postId,
postName,
onSubmit,
amount
amount,
isSuperLike,
hasHash
}: CommentProps) => {
const [isReplying, setIsReplying] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
@ -90,6 +94,9 @@ export const Comment = ({
isEdit
commentId={currentEdit?.identifier}
commentMessage={currentEdit?.message}
isSuperLike={!!currentEdit?.transactionReference}
comment={comment}
hasHash={hasHash}
/>
</Box>
</DialogContent>
@ -137,7 +144,7 @@ export const Comment = ({
>
reply
</CommentActionButton>
{user?.name === comment?.name && (
{user?.name === comment?.name && hasHash && (
<CommentActionButton
size="small"
variant="contained"

View File

@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../state/store";
import ShortUniqueId from "short-unique-id";
import { setNotification } from "../../../state/features/notificationsSlice";
import { toBase64 } from "../../../utils/toBase64";
import { objectToBase64, toBase64 } from "../../../utils/toBase64";
import localforage from "localforage";
import {
CommentInput,
@ -12,6 +12,7 @@ import {
SubmitCommentButton,
} from "./Comments-styles";
import { COMMENT_BASE } from "../../../constants";
import { addtoHashMapSuperlikes } from "../../../state/features/videoSlice";
const uid = new ShortUniqueId();
const notification = localforage.createInstance({
@ -83,6 +84,9 @@ interface CommentEditorProps {
commentId?: string;
isEdit?: boolean;
commentMessage?: string;
isSuperLike?: boolean
comment?: any;
hasHash?: boolean
}
export function utf8ToBase64(inputString: string): string {
@ -104,7 +108,10 @@ export const CommentEditor = ({
isReply,
commentId,
isEdit,
commentMessage
commentMessage,
isSuperLike,
comment,
hasHash
}: CommentEditorProps) => {
const [value, setValue] = useState<string>("");
const dispatch = useDispatch();
@ -149,13 +156,34 @@ 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 = {
comment: value,
transactionReference: comment.transactionReference,
notificationInformation: comment.notificationInformation,
about: comment.about
}
const superLikeToBase64 = await objectToBase64(superObj);
data64 = superLikeToBase64
}
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",
data64: base64,
data64: isSuperLike ? data64 : base64,
identifier: identifier,
description,
tag1
});
dispatch(
setNotification({
@ -163,6 +191,15 @@ export const CommentEditor = ({
alertType: "success",
})
);
if(isSuperLike){
dispatch(addtoHashMapSuperlikes({
...superObj,
...comment,
message: value
}))
}
if (idForNotification) {
addItem({
id: idForNotification,
@ -217,13 +254,18 @@ export const CommentEditor = ({
}
await publishComment(identifier, idForNotification);
onSubmit({
created: Date.now(),
identifier,
message: value,
service,
name: user?.name,
});
if(isSuperLike){
onSubmit({})
} else {
onSubmit({
created: Date.now(),
identifier,
message: value,
service,
name: user?.name,
});
}
setValue("");
} catch (error) {
console.error(error);

View File

@ -57,6 +57,9 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
const { user } = useSelector((state: RootState) => state.auth);
const [newMessages, setNewMessages] = useState(0);
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[]) => {
@ -168,7 +171,7 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
);
useEffect(() => {
if(superlikes.length > 0 && postId){
if(postId){
getComments(superlikes, postId)
}
}, [getComments, superlikes, postId]);
@ -188,7 +191,6 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
}, []);
}, [listComments]);
console.log({loadingComments, listComments, superlikes})
return (
<>
@ -212,14 +214,24 @@ export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postN
) : (
<CommentContainer>
{structuredCommentList.map((comment: any) => {
let hasHash = false
let message = {...comment}
let hash = {}
if(hashMapSuperlikes[comment?.identifier]){
message.message = hashMapSuperlikes[comment?.identifier]?.comment || ""
hasHash = true
hash = hashMapSuperlikes[comment?.identifier]
}
return (
<Comment
key={comment?.identifier}
comment={comment}
comment={{...message, ...hash}}
onSubmit={onSubmit}
postId={postId}
postName={postName}
amount={comment?.amount || null}
isSuperLike
hasHash={hasHash}
/>
);
})}

View File

@ -1,4 +1,4 @@
const useTestIdentifiers = true;
const useTestIdentifiers = false;
export const QTUBE_VIDEO_BASE = useTestIdentifiers
? "MYTEST_vid_"

View File

@ -0,0 +1,114 @@
import React, { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
addtoHashMapSuperlikes
} from '../state/features/videoSlice'
import { RootState } from '../state/store'
import { queueSuperlikes } from '../wrappers/GlobalWrapper'
export const useFetchSuperLikes = () => {
const dispatch = useDispatch()
const hashMapSuperlikes = useSelector(
(state: RootState) => state.video.hashMapSuperlikes
)
const checkAndUpdateSuperlike= React.useCallback(
(superlike: any) => {
const existingVideo = hashMapSuperlikes[superlike.identifier]
if (!existingVideo) {
return true
} else if (
superlike?.updated &&
existingVideo?.updated &&
(!existingVideo?.updated || superlike?.updated) > existingVideo?.updated
) {
return true
} else {
return false
}
},
[hashMapSuperlikes]
)
const fetchSuperlike = async (data: any) => {
const getsuper = async () => {
const { user, videoId, content } = data
let obj: any = {
...content,
isValid: false
}
if (!user || !videoId) return obj
try {
const responseData = await qortalRequest({
action: 'FETCH_QDN_RESOURCE',
name: user,
service: content?.service || 'BLOG_COMMENT',
identifier: videoId
})
obj = {
...content,
...responseData,
isValid: true
}
return obj
} catch (error: any) {
throw new Error(error?.message || 'error')
}
}
const res = await getsuper()
return res
}
const getSuperLikes = async (user: string, videoId: string, content: any, retries: number = 0) => {
try {
const res = await fetchSuperlike({
user,
videoId,
content
})
dispatch(addtoHashMapSuperlikes(res))
} catch (error) {
retries= retries + 1
if (retries < 2) { // 3 is the maximum number of retries here, you can adjust it to your needs
queueSuperlikes.push(() => getSuperLikes(user, videoId, content, retries + 1));
} else {
console.error('Failed to get video after 3 attempts', error);
}
}
}
const addSuperlikeRawDataGetToList = useCallback(({name, identifier, content})=> {
if (name && identifier) {
const res = checkAndUpdateSuperlike(content)
if (res) {
queueSuperlikes.push(() => getSuperLikes(name, identifier, content));
}
}
}, [checkAndUpdateSuperlike])
return {
addSuperlikeRawDataGetToList
}
}

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";
@ -33,25 +39,70 @@ import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../components/UploadVideo/Upload-styles";
import { QTUBE_VIDEO_BASE } from "../../constants";
import {
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
minPriceSuperlike,
} from "../../constants";
import { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement";
import { SuperLike } from "../../components/common/SuperLike/SuperLike";
import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes";
import {
extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../VideoContent/VideoContent";
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
export const PlaylistContent = () => {
const { name, id } = useParams();
const [doAutoPlay, setDoAutoPlay] = useState(false)
const [doAutoPlay, setDoAutoPlay] = useState(false);
const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false);
const [descriptionHeight, setDescriptionHeight] =
useState<null | number>(null);
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
null
);
const [superlikeList, setSuperlikelist] = useState<any[]>([]);
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const calculateAmountSuperlike = useMemo(() => {
const totalQort = superlikeList?.reduce((acc, curr) => {
if (curr?.amount && !isNaN(parseFloat(curr.amount)))
return acc + parseFloat(curr.amount);
else return acc;
}, 0);
return totalQort?.toFixed(2);
}, [superlikeList]);
const numberOfSuperlikes = useMemo(() => {
return superlikeList?.length ?? 0;
}, [superlikeList]);
const [nameAddress, setNameAddress] = useState<string>("");
const getAddressName = async (name) => {
const response = await qortalRequest({
action: "GET_NAME_DATA",
name: name,
});
if (response?.owner) {
setNameAddress(response.owner);
}
};
useEffect(() => {
if (name) {
getAddressName(name);
}
}, [name]);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const contentRef = useRef(null);
const avatarUrl = useMemo(() => {
let url = "";
@ -144,182 +195,203 @@ export const PlaylistContent = () => {
}
}, []);
const checkforPlaylist = React.useCallback(async (name, id) => {
const checkforPlaylist = React.useCallback(
async (name, id) => {
try {
dispatch(setIsLoadingGlobal(true));
if (!name || !id) return;
const url = `/arbitrary/resources/search?mode=ALL&service=PLAYLIST&identifier=${id}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseDataSearch = await response.json();
if (responseDataSearch?.length > 0) {
let resourceData = responseDataSearch[0];
resourceData = {
title: resourceData?.metadata?.title,
category: resourceData?.metadata?.category,
categoryName: resourceData?.metadata?.categoryName,
tags: resourceData?.metadata?.tags || [],
description: resourceData?.metadata?.description,
created: resourceData?.created,
updated: resourceData?.updated,
name: resourceData.name,
videoImage: "",
identifier: resourceData.identifier,
service: resourceData.service,
};
const responseData = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resourceData.name,
service: resourceData.service,
identifier: resourceData.identifier,
});
if (responseData && !responseData.error) {
const combinedData = {
...resourceData,
...responseData,
};
const videos = [];
if (combinedData?.videos) {
for (const vid of combinedData.videos) {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseDataSearchVid = await response.json();
if (responseDataSearchVid?.length > 0) {
let resourceData2 = responseDataSearchVid[0];
videos.push(resourceData2);
}
}
}
combinedData.videos = videos;
setPlaylistData(combinedData);
if (combinedData?.videos?.length > 0) {
const vid = combinedData?.videos[0];
const existingVideo = hashMapVideos[vid?.identifier];
if (existingVideo) {
setVideoData(existingVideo);
} else {
getVideoData(vid?.name, vid?.identifier);
}
}
}
}
} catch (error) {
} finally {
dispatch(setIsLoadingGlobal(false));
}
},
[hashMapVideos]
);
React.useEffect(() => {
if (name && id) {
checkforPlaylist(name, id);
}
}, [id, name]);
useEffect(() => {
if (contentRef.current) {
const height = contentRef.current.offsetHeight;
if (height > 100) {
// Assuming 100px is your threshold
setDescriptionHeight(100);
}
}
}, [videoData]);
const nextVideo = useMemo(() => {
const currentVideoIndex = playlistData?.videos?.findIndex(
(item) => item?.identifier === videoData?.id
);
if (currentVideoIndex !== -1) {
const nextVideoIndex = currentVideoIndex + 1;
const findVideo = playlistData?.videos[nextVideoIndex] || null;
if (findVideo) {
const id = findVideo?.identifier?.replace("_metadata", "");
return {
...findVideo,
service: "VIDEO",
identifier: id,
jsonId: findVideo?.identifier,
};
}
}
return null;
}, [playlistData, videoData]);
const onEndVideo = useCallback(() => {
const currentVideoIndex = playlistData?.videos?.findIndex(
(item) => item?.identifier === videoData?.id
);
if (currentVideoIndex !== -1) {
const nextVideoIndex = currentVideoIndex + 1;
const findVideo = playlistData?.videos[nextVideoIndex] || null;
if (findVideo) {
getVideoData(findVideo?.name, findVideo?.identifier);
setDoAutoPlay(true);
}
}
}, [videoData, playlistData]);
const getComments = useCallback(async (id, nameAddressParam) => {
if (!id) return;
try {
dispatch(setIsLoadingGlobal(true));
setLoadingSuperLikes(true);
if (!name || !id) return;
const url = `/arbitrary/resources/search?mode=ALL&service=PLAYLIST&identifier=${id}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0`;
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 responseDataSearch = await response.json();
if (responseDataSearch?.length > 0) {
let resourceData = responseDataSearch[0];
resourceData = {
title: resourceData?.metadata?.title,
category: resourceData?.metadata?.category,
categoryName: resourceData?.metadata?.categoryName,
tags: resourceData?.metadata?.tags || [],
description: resourceData?.metadata?.description,
created: resourceData?.created,
updated: resourceData?.updated,
name: resourceData.name,
videoImage: "",
identifier: resourceData.identifier,
service: resourceData.service,
};
const responseData = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: resourceData.name,
service: resourceData.service,
identifier: resourceData.identifier,
});
if (responseData && !responseData.error) {
const combinedData = {
...resourceData,
...responseData,
};
const videos = [];
if (combinedData?.videos) {
for (const vid of combinedData.videos) {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
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 responseDataSearchVid = await response.json();
if (responseDataSearchVid?.length > 0) {
let resourceData2 = responseDataSearchVid[0];
videos.push(resourceData2);
}
comments = [
...comments,
{
...comment,
message: "",
amount: res.amount,
},
];
}
}
combinedData.videos = videos;
setPlaylistData(combinedData);
if(combinedData?.videos?.length > 0){
const vid = combinedData?.videos[0]
const existingVideo = hashMapVideos[vid?.identifier];
if (existingVideo) {
setVideoData(existingVideo);
} else {
getVideoData(vid?.name, vid?.identifier);
}
}
} catch (error) {}
}
}
setSuperlikelist(comments);
} catch (error) {
console.error(error);
} finally {
dispatch(setIsLoadingGlobal(false));
setLoadingSuperLikes(false);
}
}, [hashMapVideos]);
// React.useEffect(() => {
// if (name && id) {
// const existingVideo = hashMapVideos[id];
// if (existingVideo) {
// setVideoData(existingVideo);
// checkforPlaylist(name, id, existingVideo?.code);
// } else {
// getVideoData(name, id);
// }
// }
// }, [id, name]);
React.useEffect(() => {
if (name && id) {
checkforPlaylist(name, id);
}
}, [id, name]);
// const getAvatar = React.useCallback(async (author: string) => {
// try {
// let url = await qortalRequest({
// action: 'GET_QDN_RESOURCE_URL',
// name: author,
// service: 'THUMBNAIL',
// identifier: 'qortal_avatar'
// })
// setAvatarUrl(url)
// dispatch(setUserAvatarHash({
// name: author,
// url
// }))
// } catch (error) { }
// }, [])
// React.useEffect(() => {
// if (name && !avatarUrl) {
// const existingAvatar = userAvatarHash[name]
// if (existingAvatar) {
// setAvatarUrl(existingAvatar)
// } else {
// getAvatar(name)
// }
// }
// }, [name, userAvatarHash])
}, []);
useEffect(() => {
if (contentRef.current) {
const height = contentRef.current.offsetHeight;
if (height > 100) { // Assuming 100px is your threshold
setDescriptionHeight(100)
}
}
}, [videoData]);
const nextVideo = useMemo(()=> {
const currentVideoIndex = playlistData?.videos?.findIndex((item)=> item?.identifier === videoData?.id)
if(currentVideoIndex !== -1){
const nextVideoIndex = currentVideoIndex + 1
const findVideo = playlistData?.videos[nextVideoIndex] || null
if(findVideo){
const id = findVideo?.identifier?.replace("_metadata", "");
return {
...findVideo,
service: 'VIDEO',
identifier: id,
jsonId: findVideo?.identifier
}
}
}
return null
}, [playlistData, videoData])
const onEndVideo = useCallback(()=> {
const currentVideoIndex = playlistData?.videos?.findIndex((item)=> item?.identifier === videoData?.id)
if(currentVideoIndex !== -1){
const nextVideoIndex = currentVideoIndex + 1
const findVideo = playlistData?.videos[nextVideoIndex] || null
if(findVideo){
getVideoData(findVideo?.name, findVideo?.identifier)
setDoAutoPlay(true)
}
}
}, [videoData, playlistData])
if (!nameAddress || !videoData?.id) return;
getComments(videoData?.id, nameAddress);
}, [getComments, videoData?.id, nameAddress]);
return (
<Box
@ -337,182 +409,231 @@ export const PlaylistContent = () => {
>
{videoData && videoData?.videos?.length === 0 ? (
<>
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<Typography>This playlist is empty</Typography>
</Box>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<Typography>This playlist is empty</Typography>
</Box>
</>
) : (
<>
{videoReference && (
<VideoPlayer
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={name}
jsonId={id}
poster={videoCover || ""}
nextVideo={nextVideo}
onEnd={onEndVideo}
autoPlay={doAutoPlay}
/>
)}
<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>
<VideoTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "center",
}}
>
{videoData?.title}
</VideoTitle>
{videoData?.created && (
<Typography
variant="h6"
sx={{
fontSize: "12px",
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
</Typography>
)}
<Spacer height="15px" />
<Box
sx={{
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${name}`);
}}
>
<StyledCardHeaderComment
sx={{
"& .MuiCardHeader-content": {
overflow: "hidden",
},
}}
>
<Box>
<Avatar
src={`/arbitrary/THUMBNAIL/${name}/qortal_avatar`}
alt={`${name}'s avatar`}
<VideoPlayer
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={name}
jsonId={id}
poster={videoCover || ""}
nextVideo={nextVideo}
onEnd={onEndVideo}
autoPlay={doAutoPlay}
/>
</Box>
<StyledCardColComment>
<AuthorTextComment
color={
theme.palette.mode === "light"
? theme.palette.text.secondary
: "#d6e8ff"
}
>
{name}
</AuthorTextComment>
</StyledCardColComment>
</StyledCardHeaderComment>
</Box>
<Spacer height="15px" />
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "100%",
cursor: !descriptionHeight ? "default" : isExpandedDescription ? "default" : "pointer",
position: "relative",
}}
className={!descriptionHeight ? "": isExpandedDescription ? "" : "hover-click"}
>
{descriptionHeight && !isExpandedDescription && (
)}
<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"
sx={{
textAlign: "center",
}}
>
{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"
sx={{
fontSize: "12px",
}}
color={theme.palette.text.primary}
>
{formatDate(videoData.created)}
</Typography>
)}
<Spacer height="15px" />
<Box
sx={{
position: "absolute",
top: "0px",
right: "0px",
left: "0px",
bottom: "0px",
cursor: "pointer",
}}
onClick={() => {
if (isExpandedDescription) return;
setIsExpandedDescription(true);
navigate(`/channel/${name}`);
}}
/>
)}
<Box
ref={contentRef}
sx={{
height: !descriptionHeight ? 'auto' : isExpandedDescription ? "auto" : "100px",
overflow: "hidden",
}}
>
{videoData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} />
) : (
<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>
)}
</Box>
>
<StyledCardHeaderComment
sx={{
"& .MuiCardHeader-content": {
overflow: "hidden",
},
}}
>
<Box>
<Avatar
src={`/arbitrary/THUMBNAIL/${name}/qortal_avatar`}
alt={`${name}'s avatar`}
/>
</Box>
<StyledCardColComment>
<AuthorTextComment
color={
theme.palette.mode === "light"
? theme.palette.text.secondary
: "#d6e8ff"
}
>
{name}
</AuthorTextComment>
</StyledCardColComment>
</StyledCardHeaderComment>
</Box>
<Spacer height="15px" />
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "100%",
cursor: !descriptionHeight
? "default"
: isExpandedDescription
? "default"
: "pointer",
position: "relative",
}}
className={
!descriptionHeight
? ""
: isExpandedDescription
? ""
: "hover-click"
}
>
{descriptionHeight && !isExpandedDescription && (
<Box
sx={{
position: "absolute",
top: "0px",
right: "0px",
left: "0px",
bottom: "0px",
cursor: "pointer",
}}
onClick={() => {
if (isExpandedDescription) return;
setIsExpandedDescription(true);
}}
/>
)}
<Box
ref={contentRef}
sx={{
height: !descriptionHeight
? "auto"
: isExpandedDescription
? "auto"
: "100px",
overflow: "hidden",
}}
>
{videoData?.htmlDescription ? (
<DisplayHtml html={videoData?.htmlDescription} />
) : (
<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>
)}
</Box>
</>
)}
</VideoPlayerContainer>
<SuperLikesSection
getMore={() => {}}
loadingSuperLikes={loadingSuperLikes}
superlikes={superlikeList}
postId={videoData?.id || ""}
postName={videoData?.user || ""}
/>
<Box
sx={{
display: "flex",
@ -521,9 +642,13 @@ export const PlaylistContent = () => {
maxWidth: "1200px",
}}
>
<CommentSection postId={id || ""} postName={name || ""} />
<CommentSection postId={videoData?.id || ""} postName={name || ""} />
{playlistData && (
<Playlists playlistData={playlistData} currentVideoIdentifier={videoData?.id} onClick={getVideoData} />
<Playlists
playlistData={playlistData}
currentVideoIdentifier={videoData?.id}
onClick={getVideoData}
/>
)}
</Box>
</Box>

View File

@ -41,6 +41,7 @@ import { SuperLike } from "../../components/common/SuperLike/SuperLike";
import { CommentContainer } from "../../components/common/Comments/Comments-styles";
import { Comment } from "../../components/common/Comments/Comment";
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes";
export function isTimestampWithinRange(resTimestamp, resCreated) {
// Calculate the absolute difference in milliseconds
@ -106,7 +107,7 @@ export const VideoContent = () => {
useState<boolean>(false);
const [superlikeList, setSuperlikelist] = useState<any[]>([])
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false)
const {addSuperlikeRawDataGetToList} = useFetchSuperLikes()
const calculateAmountSuperlike = useMemo(()=> {
const totalQort = superlikeList?.reduce((acc, curr)=> {
@ -249,36 +250,7 @@ export const VideoContent = () => {
}
}, [id, name]);
// const getAvatar = React.useCallback(async (author: string) => {
// try {
// let url = await qortalRequest({
// action: 'GET_QDN_RESOURCE_URL',
// name: author,
// service: 'THUMBNAIL',
// identifier: 'qortal_avatar'
// })
// setAvatarUrl(url)
// dispatch(setUserAvatarHash({
// name: author,
// url
// }))
// } catch (error) { }
// }, [])
// React.useEffect(() => {
// if (name && !avatarUrl) {
// const existingAvatar = userAvatarHash[name]
// if (existingAvatar) {
// setAvatarUrl(existingAvatar)
// } else {
// getAvatar(name)
// }
// }
// }, [name, userAvatarHash])
useEffect(() => {
if (contentRef.current) {
@ -313,23 +285,16 @@ export const VideoContent = () => {
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 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();
comments = [...comments, {
...comment,
message: responseData2,
message: "",
amount: res.amount
}];

View File

@ -6,6 +6,7 @@ interface GlobalState {
videos: Video[]
filteredVideos: Video[]
hashMapVideos: Record<string, Video>
hashMapSuperlikes: Record<string, any>
countNewVideos: number
isFiltering: boolean
filterValue: string
@ -21,6 +22,7 @@ const initialState: GlobalState = {
videos: [],
filteredVideos: [],
hashMapVideos: {},
hashMapSuperlikes: {},
countNewVideos: 0,
isFiltering: false,
filterValue: '',
@ -113,6 +115,10 @@ export const videoSlice = createSlice({
const video = action.payload
state.hashMapVideos[video.id] = video
},
addtoHashMapSuperlikes: (state, action) => {
const superlike = action.payload
state.hashMapSuperlikes[superlike.identifier] = superlike
},
updateInHashMap: (state, action) => {
const { id } = action.payload
const video = action.payload
@ -197,7 +203,8 @@ export const {
changeSelectedSubCategoryVideos,
blockUser,
setEditVideo,
setEditPlaylist
setEditPlaylist,
addtoHashMapSuperlikes
} = videoSlice.actions
export default videoSlice.reducer

View File

@ -27,6 +27,8 @@ interface Props {
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();