mirror of
https://github.com/Qortal/q-tube.git
synced 2025-02-11 17:55:51 +00:00
added super likes
This commit is contained in:
parent
3ab514e1cf
commit
d8e2ecb382
BIN
src/assets/img/qort.png
Normal file
BIN
src/assets/img/qort.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
@ -179,7 +179,7 @@ export const Comment = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CommentCard = ({
|
||||
export const CommentCard = ({
|
||||
message,
|
||||
created,
|
||||
name,
|
||||
|
324
src/components/common/Notifications/Notifications.tsx
Normal file
324
src/components/common/Notifications/Notifications.tsx
Normal file
@ -0,0 +1,324 @@
|
||||
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 ThumbUpIcon from "@mui/icons-material/ThumbUp";
|
||||
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from '../../../pages/VideoContent/VideoContent'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import localForage from "localforage";
|
||||
import moment from 'moment'
|
||||
|
||||
const generalLocal = localForage.createInstance({
|
||||
name: "q-tube-general",
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 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 getInitialTimestamp = async ()=> {
|
||||
const timestamp: undefined | number = await generalLocal.getItem("notification-timestamp");
|
||||
if(timestamp){
|
||||
setNotificationTimestamp(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=> {
|
||||
getInitialTimestamp()
|
||||
}, [])
|
||||
|
||||
|
||||
const openNotificationPopover = (event: any) => {
|
||||
const target = event.currentTarget as unknown as HTMLButtonElement | null
|
||||
setAnchorElNotification(target)
|
||||
}
|
||||
const closeNotificationPopover = () => {
|
||||
setAnchorElNotification(null)
|
||||
}
|
||||
const fullNotifications = useMemo(() => {
|
||||
return [...notifications].sort(
|
||||
(a, b) => b.created - a.created
|
||||
)
|
||||
}, [notifications])
|
||||
const notificationBadgeLength = useMemo(()=> {
|
||||
if(!notificationTimestamp) return fullNotifications.length
|
||||
return fullNotifications?.filter((item)=> item.created > notificationTimestamp).length
|
||||
}, [fullNotifications, notificationTimestamp])
|
||||
|
||||
const checkNotifications = useCallback(async (username: string) => {
|
||||
try {
|
||||
// let notificationComments: Item[] =
|
||||
// (await notification.getItem('comments')) || []
|
||||
// notificationComments = notificationComments
|
||||
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
||||
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
||||
|
||||
const timestamp = await generalLocal.getItem("notification-timestamp");
|
||||
|
||||
const after = timestamp || moment().subtract(5, 'days').valueOf();
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`;
|
||||
const response = await fetch(url, {
|
||||
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=true&excludeblocked=true&offset=0&name=${username}`;
|
||||
const response2 = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseSearch = await response2.json();
|
||||
if(responseSearch.length > 0){
|
||||
urlReference = responseSearch[0]
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||
// const response = await fetch(url, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// });
|
||||
// if(!response.ok) continue
|
||||
// const responseData2 = await response.text();
|
||||
|
||||
notifys = [...notifys, {
|
||||
...comment,
|
||||
amount: res.amount,
|
||||
urlReference: urlReference || null
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
setNotifications((prev) => {
|
||||
const allNotifications = [...notifys, ...prev];
|
||||
const uniqueNotifications = Array.from(new Map(allNotifications.map(notif => [notif.identifier, notif])).values());
|
||||
return uniqueNotifications.slice(0, 20);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log({ error })
|
||||
}
|
||||
}, [])
|
||||
|
||||
const checkNotificationsFunc = useCallback(
|
||||
(username: string) => {
|
||||
let isCalling = false
|
||||
interval.current = setInterval(async () => {
|
||||
if (isCalling) return
|
||||
isCalling = true
|
||||
const res = await checkNotifications(username)
|
||||
isCalling = false
|
||||
}, 60000)
|
||||
checkNotifications(username)
|
||||
},
|
||||
[checkNotifications])
|
||||
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
openNotificationPopover(e)
|
||||
generalLocal.setItem("notification-timestamp", Date.now());
|
||||
setNotificationTimestamp(Date.now)
|
||||
}}
|
||||
sx={{
|
||||
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
|
||||
|
||||
>
|
||||
<ListItemText
|
||||
primary="No new notifications">
|
||||
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)}
|
||||
{fullNotifications.map((notification: any, index: number) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
divider
|
||||
sx={{
|
||||
cursor: notification?.urlReference ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={async () => {
|
||||
if(notification?.urlReference){
|
||||
navigate(`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px'
|
||||
}}>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body1"
|
||||
color="textPrimary"
|
||||
>
|
||||
Super Like
|
||||
|
||||
</Typography>
|
||||
<ThumbUpIcon
|
||||
style={{
|
||||
color: "gold",
|
||||
|
||||
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<React.Fragment>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '16px'
|
||||
}}
|
||||
color="textSecondary"
|
||||
>
|
||||
{formatDate(notification.created)}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '16px'
|
||||
}}
|
||||
color="textSecondary"
|
||||
>
|
||||
{` from ${notification.name}`}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Box>
|
||||
)
|
||||
}
|
310
src/components/common/SuperLike/SuperLike.tsx
Normal file
310
src/components/common/SuperLike/SuperLike.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
import React, { useState } from "react";
|
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Input,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Modal,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import qortImg from "../../../assets/img/qort.png";
|
||||
import { MultiplePublish } from "../MultiplePublish/MultiplePublish";
|
||||
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 { CommentInput } from "../Comments/Comments-styles";
|
||||
import {
|
||||
CrowdfundActionButton,
|
||||
CrowdfundActionButtonRow,
|
||||
ModalBody,
|
||||
NewCrowdfundTitle,
|
||||
Spacer,
|
||||
} from "../../UploadVideo/Upload-styles";
|
||||
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor";
|
||||
import { RootState } from "../../../state/store";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 4 });
|
||||
|
||||
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>("");
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
|
||||
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||
const [publishes, setPublishes] = useState<any[]>([]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const resetValues = () => {
|
||||
setAmount(0);
|
||||
setComment("");
|
||||
setPublishes([]);
|
||||
};
|
||||
const onClose = () => {
|
||||
resetValues();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
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");
|
||||
|
||||
let resName = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name,
|
||||
});
|
||||
|
||||
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 (!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`);
|
||||
|
||||
let listOfPublishes = [];
|
||||
|
||||
const res = await qortalRequest({
|
||||
action: "SEND_COIN",
|
||||
coin: "QORT",
|
||||
destinationAddress: address,
|
||||
amount: amount,
|
||||
});
|
||||
|
||||
|
||||
|
||||
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(
|
||||
0,
|
||||
39
|
||||
)}_${id}`;
|
||||
|
||||
// Description is obtained from raw data
|
||||
const base64 = utf8ToBase64(comment);
|
||||
|
||||
const requestBodyJson: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: username,
|
||||
service: "BLOG_COMMENT",
|
||||
data64: base64,
|
||||
title: "",
|
||||
description: metadescription,
|
||||
identifier: identifierSuperLike,
|
||||
tag1: SUPER_LIKE_BASE,
|
||||
filename: `superlike_metadata.json`,
|
||||
};
|
||||
|
||||
|
||||
listOfPublishes.push(requestBodyJson);
|
||||
|
||||
setPublishes(listOfPublishes);
|
||||
setIsOpenMultiplePublish(true);
|
||||
} catch (error: any) {
|
||||
let notificationObj: any = null;
|
||||
if (typeof error === "string") {
|
||||
notificationObj = {
|
||||
msg: error || "Failed to publish Super Like",
|
||||
alertType: "error",
|
||||
};
|
||||
} else if (typeof error?.error === "string") {
|
||||
notificationObj = {
|
||||
msg: error?.error || "Failed to publish Super Like",
|
||||
alertType: "error",
|
||||
};
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || "Failed to publish Super Like",
|
||||
alertType: "error",
|
||||
};
|
||||
}
|
||||
if (!notificationObj) return;
|
||||
dispatch(setNotification(notificationObj));
|
||||
|
||||
throw new Error("Failed to publish Super Like");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
sx={{
|
||||
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>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Modal
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<ModalBody>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<NewCrowdfundTitle>Super Like</NewCrowdfundTitle>
|
||||
</Box>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: "300px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<InputLabel htmlFor="standard-adornment-amount">
|
||||
Amount in QORT
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="standard-adornment-amount"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(+e.target.value)}
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<img
|
||||
style={{
|
||||
height: "15px",
|
||||
width: "15px",
|
||||
}}
|
||||
src={qortImg}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="25px" />
|
||||
<Box>
|
||||
<CommentInput
|
||||
id="standard-multiline-flexible"
|
||||
label="Your comment"
|
||||
multiline
|
||||
maxRows={8}
|
||||
variant="filled"
|
||||
value={comment}
|
||||
inputProps={{
|
||||
maxLength: 200,
|
||||
}}
|
||||
InputLabelProps={{ style: { fontSize: "18px" } }}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<CrowdfundActionButtonRow>
|
||||
<CrowdfundActionButton
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
resetValues();
|
||||
onClose();
|
||||
}}
|
||||
variant="contained"
|
||||
color="error"
|
||||
>
|
||||
Cancel
|
||||
</CrowdfundActionButton>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<CrowdfundActionButton
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
publishSuperLike();
|
||||
}}
|
||||
>
|
||||
Publish Super Like
|
||||
</CrowdfundActionButton>
|
||||
</Box>
|
||||
</CrowdfundActionButtonRow>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
{isOpenMultiplePublish && (
|
||||
<MultiplePublish
|
||||
isOpen={isOpenMultiplePublish}
|
||||
onSubmit={() => {
|
||||
onSuccess({
|
||||
name: username,
|
||||
message: comment,
|
||||
service,
|
||||
identifier,
|
||||
amount: +amount,
|
||||
created: Date.now()
|
||||
});
|
||||
setIsOpenMultiplePublish(false);
|
||||
onClose();
|
||||
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Super like published",
|
||||
alertType: "success",
|
||||
})
|
||||
);
|
||||
|
||||
}}
|
||||
publishes={publishes}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
327
src/components/common/SuperLikesList/Comment.tsx
Normal file
327
src/components/common/SuperLikesList/Comment.tsx
Normal file
@ -0,0 +1,327 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp";
|
||||
|
||||
import { CommentEditor } from "./CommentEditor";
|
||||
import {
|
||||
CardContentContainerComment,
|
||||
CommentActionButtonRow,
|
||||
CommentDateText,
|
||||
EditReplyButton,
|
||||
StyledCardComment,
|
||||
} from "./Comments-styles";
|
||||
import { StyledCardHeaderComment } from "./Comments-styles";
|
||||
import { StyledCardColComment } from "./Comments-styles";
|
||||
import { AuthorTextComment } from "./Comments-styles";
|
||||
import {
|
||||
StyledCardContentComment,
|
||||
LoadMoreCommentsButton as CommentActionButton,
|
||||
} from "./Comments-styles";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../state/store";
|
||||
import Portal from "../Portal";
|
||||
import { formatDate } from "../../../utils/time";
|
||||
interface CommentProps {
|
||||
comment: any;
|
||||
postId: string;
|
||||
postName: string;
|
||||
onSubmit: (obj?: any, isEdit?: boolean) => void;
|
||||
amount?: null | number
|
||||
}
|
||||
export const Comment = ({
|
||||
comment,
|
||||
postId,
|
||||
postName,
|
||||
onSubmit,
|
||||
amount
|
||||
}: CommentProps) => {
|
||||
const [isReplying, setIsReplying] = useState<boolean>(false);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
const [currentEdit, setCurrentEdit] = useState<any>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleSubmit = useCallback((comment: any, isEdit?: boolean) => {
|
||||
onSubmit(comment, isEdit);
|
||||
setCurrentEdit(null);
|
||||
setIsReplying(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
id={comment?.identifier}
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{currentEdit && (
|
||||
<Portal>
|
||||
<Dialog
|
||||
open={!!currentEdit}
|
||||
onClose={() => setCurrentEdit(null)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title"></DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: "300px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CommentEditor
|
||||
onSubmit={obj => handleSubmit(obj, true)}
|
||||
postId={postId}
|
||||
postName={postName}
|
||||
isEdit
|
||||
commentId={currentEdit?.identifier}
|
||||
commentMessage={currentEdit?.message}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={() => setCurrentEdit(null)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
)}
|
||||
<CommentCard
|
||||
name={comment?.name}
|
||||
message={comment?.message}
|
||||
replies={comment?.replies || []}
|
||||
setCurrentEdit={setCurrentEdit}
|
||||
amount={amount}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
marginTop: "20px",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{comment?.created && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
marginLeft: "5px",
|
||||
}}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{formatDate(+comment?.created)}
|
||||
</Typography>
|
||||
)}
|
||||
<CommentActionButtonRow>
|
||||
<CommentActionButton
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setIsReplying(true)}
|
||||
>
|
||||
reply
|
||||
</CommentActionButton>
|
||||
{user?.name === comment?.name && (
|
||||
<CommentActionButton
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setCurrentEdit(comment)}
|
||||
>
|
||||
edit
|
||||
</CommentActionButton>
|
||||
)}
|
||||
{isReplying && (
|
||||
<CommentActionButton
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsReplying(false);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
close
|
||||
</CommentActionButton>
|
||||
)}
|
||||
</CommentActionButtonRow>
|
||||
</Box>
|
||||
</CommentCard>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{isReplying && (
|
||||
<CommentEditor
|
||||
onSubmit={handleSubmit}
|
||||
postId={postId}
|
||||
postName={postName}
|
||||
isReply
|
||||
commentId={comment.identifier}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommentCard = ({
|
||||
message,
|
||||
created,
|
||||
name,
|
||||
replies,
|
||||
children,
|
||||
setCurrentEdit,
|
||||
isReply,
|
||||
amount
|
||||
}: any) => {
|
||||
const [avatarUrl, setAvatarUrl] = React.useState<string>("");
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const getAvatar = React.useCallback(async (author: string) => {
|
||||
try {
|
||||
const url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
name: author,
|
||||
service: "THUMBNAIL",
|
||||
identifier: "qortal_avatar",
|
||||
});
|
||||
|
||||
setAvatarUrl(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getAvatar(name);
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
<CardContentContainerComment>
|
||||
<StyledCardHeaderComment
|
||||
sx={{
|
||||
"& .MuiCardHeader-content": {
|
||||
overflow: "hidden",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
alt={`${name}'s avatar`}
|
||||
sx={{ width: "35px", height: "35px" }}
|
||||
/>
|
||||
</Box>
|
||||
<StyledCardColComment>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<AuthorTextComment>{name}</AuthorTextComment>
|
||||
{!isReply && (
|
||||
<ThumbUpIcon
|
||||
style={{
|
||||
color: "gold",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{amount && (
|
||||
<Typography sx={{
|
||||
fontSize: '20px',
|
||||
color: 'gold'
|
||||
}}>
|
||||
{parseFloat(amount)?.toFixed(2)} QORT
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
</StyledCardColComment>
|
||||
</StyledCardHeaderComment>
|
||||
<StyledCardContentComment>
|
||||
<StyledCardComment>{message}</StyledCardComment>
|
||||
</StyledCardContentComment>
|
||||
<Box
|
||||
sx={{
|
||||
paddingLeft: "15px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{replies?.map((reply: any) => {
|
||||
return (
|
||||
<Box
|
||||
key={reply?.identifier}
|
||||
id={reply?.identifier}
|
||||
sx={{
|
||||
display: "flex",
|
||||
border: "1px solid grey",
|
||||
borderRadius: "10px",
|
||||
marginTop: "8px",
|
||||
}}
|
||||
>
|
||||
<CommentCard
|
||||
name={reply?.name}
|
||||
message={reply?.message}
|
||||
setCurrentEdit={setCurrentEdit}
|
||||
isReply
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{reply?.created && (
|
||||
<CommentDateText>
|
||||
{formatDate(+reply?.created)}
|
||||
</CommentDateText>
|
||||
)}
|
||||
{user?.name === reply?.name ? (
|
||||
<EditReplyButton
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setCurrentEdit(reply)}
|
||||
sx={{}}
|
||||
>
|
||||
edit
|
||||
</EditReplyButton>
|
||||
) : (
|
||||
<Box />
|
||||
)}
|
||||
</Box>
|
||||
</CommentCard>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{children}
|
||||
</CardContentContainerComment>
|
||||
);
|
||||
};
|
254
src/components/common/SuperLikesList/CommentEditor.tsx
Normal file
254
src/components/common/SuperLikesList/CommentEditor.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import { Box, Button, TextField } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
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 localforage from "localforage";
|
||||
import {
|
||||
CommentInput,
|
||||
CommentInputContainer,
|
||||
SubmitCommentButton,
|
||||
} from "./Comments-styles";
|
||||
import { COMMENT_BASE } from "../../../constants";
|
||||
const uid = new ShortUniqueId();
|
||||
|
||||
const notification = localforage.createInstance({
|
||||
name: "notification",
|
||||
});
|
||||
|
||||
const MAX_ITEMS = 10;
|
||||
|
||||
export interface Item {
|
||||
id: string;
|
||||
lastSeen: number;
|
||||
postId: string;
|
||||
postName: string;
|
||||
}
|
||||
|
||||
export async function addItem(item: Item): Promise<void> {
|
||||
// Get all items
|
||||
let notificationComments: Item[] =
|
||||
(await notification.getItem("comments")) || [];
|
||||
|
||||
// Find the item with the same id, if it exists
|
||||
let existingItemIndex = notificationComments.findIndex(i => i.id === item.id);
|
||||
|
||||
if (existingItemIndex !== -1) {
|
||||
// If the item exists, update its date
|
||||
notificationComments[existingItemIndex].lastSeen = item.lastSeen;
|
||||
} else {
|
||||
// If the item doesn't exist, add it
|
||||
notificationComments.push(item);
|
||||
|
||||
// If adding the item has caused us to exceed the max number of items, remove the oldest one
|
||||
if (notificationComments.length > MAX_ITEMS) {
|
||||
notificationComments.sort((a, b) => b.lastSeen - a.lastSeen); // sort items by date, newest first
|
||||
notificationComments.pop(); // remove the oldest item
|
||||
}
|
||||
}
|
||||
|
||||
// Store the items back into localForage
|
||||
await notification.setItem("comments", notificationComments);
|
||||
}
|
||||
export async function updateItemDate(item: any): Promise<void> {
|
||||
// Get all items
|
||||
let notificationComments: Item[] =
|
||||
(await notification.getItem("comments")) || [];
|
||||
|
||||
let notificationCreatorComment: any =
|
||||
(await notification.getItem("post-comments")) || {};
|
||||
const findPostId = notificationCreatorComment[item.postId];
|
||||
if (findPostId) {
|
||||
notificationCreatorComment[item.postId].lastSeen = item.lastSeen;
|
||||
}
|
||||
|
||||
// Find the item with the same id, if it exists
|
||||
notificationComments.forEach((nc, index) => {
|
||||
if (nc.postId === item.postId) {
|
||||
notificationComments[index].lastSeen = item.lastSeen;
|
||||
}
|
||||
});
|
||||
|
||||
// Store the items back into localForage
|
||||
await notification.setItem("comments", notificationComments);
|
||||
await notification.setItem("post-comments", notificationCreatorComment);
|
||||
}
|
||||
interface CommentEditorProps {
|
||||
postId: string;
|
||||
postName: string;
|
||||
onSubmit: (obj: any) => void;
|
||||
isReply?: boolean;
|
||||
commentId?: string;
|
||||
isEdit?: boolean;
|
||||
commentMessage?: string;
|
||||
}
|
||||
|
||||
export function utf8ToBase64(inputString: string): string {
|
||||
// Encode the string as UTF-8
|
||||
const utf8String = encodeURIComponent(inputString).replace(
|
||||
/%([0-9A-F]{2})/g,
|
||||
(match, p1) => String.fromCharCode(Number("0x" + p1))
|
||||
);
|
||||
|
||||
// Convert the UTF-8 encoded string to base64
|
||||
const base64String = btoa(utf8String);
|
||||
return base64String;
|
||||
}
|
||||
|
||||
export const CommentEditor = ({
|
||||
onSubmit,
|
||||
postId,
|
||||
postName,
|
||||
isReply,
|
||||
commentId,
|
||||
isEdit,
|
||||
commentMessage
|
||||
}: CommentEditorProps) => {
|
||||
const [value, setValue] = useState<string>("");
|
||||
const dispatch = useDispatch();
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && commentMessage) {
|
||||
setValue(commentMessage);
|
||||
}
|
||||
}, [isEdit, commentMessage]);
|
||||
|
||||
const publishComment = async (
|
||||
identifier: string,
|
||||
idForNotification?: string
|
||||
) => {
|
||||
let address;
|
||||
let name;
|
||||
let errorMsg = "";
|
||||
|
||||
address = user?.address;
|
||||
name = user?.name || "";
|
||||
|
||||
if (!address) {
|
||||
errorMsg = "Cannot post: your address isn't available";
|
||||
}
|
||||
if (!name) {
|
||||
errorMsg = "Cannot post without a name";
|
||||
}
|
||||
|
||||
if (value.length > 200) {
|
||||
errorMsg = "Comment needs to be under 200 characters";
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: errorMsg,
|
||||
alertType: "error",
|
||||
})
|
||||
);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = utf8ToBase64(value);
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: name,
|
||||
service: "BLOG_COMMENT",
|
||||
data64: base64,
|
||||
identifier: identifier,
|
||||
});
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Comment successfully published",
|
||||
alertType: "success",
|
||||
})
|
||||
);
|
||||
if (idForNotification) {
|
||||
addItem({
|
||||
id: idForNotification,
|
||||
lastSeen: Date.now(),
|
||||
postId,
|
||||
postName: postName,
|
||||
});
|
||||
}
|
||||
|
||||
return resourceResponse;
|
||||
} catch (error: any) {
|
||||
let notificationObj: any = null;
|
||||
if (typeof error === "string") {
|
||||
notificationObj = {
|
||||
msg: error || "Failed to publish comment",
|
||||
alertType: "error",
|
||||
};
|
||||
} else if (typeof error?.error === "string") {
|
||||
notificationObj = {
|
||||
msg: error?.error || "Failed to publish comment",
|
||||
alertType: "error",
|
||||
};
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || "Failed to publish comment",
|
||||
alertType: "error",
|
||||
};
|
||||
}
|
||||
if (!notificationObj) throw new Error("Failed to publish comment");
|
||||
|
||||
dispatch(setNotification(notificationObj));
|
||||
throw new Error("Failed to publish comment");
|
||||
}
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = uid();
|
||||
|
||||
let identifier = `${COMMENT_BASE}${postId.slice(-12)}_base_${id}`;
|
||||
let idForNotification = identifier;
|
||||
let service = 'BLOG_COMMENT'
|
||||
if (isReply && commentId) {
|
||||
const removeBaseCommentId = commentId;
|
||||
removeBaseCommentId.replace("_base_", "");
|
||||
identifier = `${COMMENT_BASE}${postId.slice(
|
||||
-12
|
||||
)}_reply_${removeBaseCommentId.slice(-6)}_${id}`;
|
||||
idForNotification = commentId;
|
||||
}
|
||||
if (isEdit && commentId) {
|
||||
identifier = commentId;
|
||||
}
|
||||
|
||||
await publishComment(identifier, idForNotification);
|
||||
onSubmit({
|
||||
created: Date.now(),
|
||||
identifier,
|
||||
message: value,
|
||||
service,
|
||||
name: user?.name,
|
||||
});
|
||||
setValue("");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommentInputContainer>
|
||||
<CommentInput
|
||||
id="standard-multiline-flexible"
|
||||
label="Your comment"
|
||||
multiline
|
||||
maxRows={4}
|
||||
variant="filled"
|
||||
value={value}
|
||||
inputProps={{
|
||||
maxLength: 200,
|
||||
}}
|
||||
InputLabelProps={{ style: { fontSize: "18px" } }}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<SubmitCommentButton variant="contained" onClick={handleSubmit}>
|
||||
{isReply ? "Submit reply" : isEdit ? "Edit" : "Submit comment"}
|
||||
</SubmitCommentButton>
|
||||
</CommentInputContainer>
|
||||
);
|
||||
};
|
281
src/components/common/SuperLikesList/Comments-styles.tsx
Normal file
281
src/components/common/SuperLikesList/Comments-styles.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Card, Box, Typography, Button, TextField } from "@mui/material";
|
||||
|
||||
export const StyledCard = styled(Card)(({ theme }) => ({
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.primary.dark,
|
||||
maxWidth: "600px",
|
||||
width: "100%",
|
||||
margin: "10px 0px",
|
||||
cursor: "pointer",
|
||||
"@media (max-width: 450px)": {
|
||||
width: "100%;",
|
||||
},
|
||||
}));
|
||||
|
||||
export const CardContentContainer = styled(Box)(({ theme }) => ({
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.primary.dark
|
||||
: theme.palette.primary.light,
|
||||
margin: "5px 10px",
|
||||
borderRadius: "15px",
|
||||
}));
|
||||
|
||||
export const CardContentContainerComment = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === "light" ? "#a9d9d038" : "#c3abe414",
|
||||
border: `1px solid gold`,
|
||||
margin: "0px",
|
||||
padding: "8px 15px",
|
||||
borderRadius: "8px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}));
|
||||
|
||||
export const StyledCardHeader = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
gap: "5px",
|
||||
padding: "7px",
|
||||
});
|
||||
|
||||
export const StyledCardHeaderComment = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
gap: "7px",
|
||||
padding: "9px 7px",
|
||||
});
|
||||
|
||||
export const StyledCardCol = styled(Box)({
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
alignItems: "flex-start",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const StyledCardColComment = styled(Box)({
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
alignItems: "flex-start",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const StyledCardContent = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
padding: "5px 10px",
|
||||
gap: "10px",
|
||||
});
|
||||
|
||||
export const StyledCardContentComment = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
padding: "5px 10px",
|
||||
gap: "10px",
|
||||
});
|
||||
|
||||
export const StyledCardComment = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Mulish",
|
||||
letterSpacing: 0,
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "19px",
|
||||
wordBreak: "break-word"
|
||||
}));
|
||||
|
||||
export const TitleText = styled(Typography)({
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
width: "100%",
|
||||
fontFamily: "Cairo, sans-serif",
|
||||
fontSize: "22px",
|
||||
lineHeight: "1.2",
|
||||
});
|
||||
|
||||
export const AuthorText = styled(Typography)({
|
||||
fontFamily: "Raleway, sans-serif",
|
||||
fontSize: "16px",
|
||||
lineHeight: "1.2",
|
||||
});
|
||||
|
||||
export const AuthorTextComment = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Montserrat, sans-serif",
|
||||
fontSize: "17px",
|
||||
letterSpacing: "0.3px",
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const IconsBox = styled(Box)({
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
position: "absolute",
|
||||
top: "12px",
|
||||
right: "5px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
});
|
||||
|
||||
export const BookmarkIconContainer = styled(Box)({
|
||||
display: "flex",
|
||||
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
|
||||
backgroundColor: "#fbfbfb",
|
||||
color: "#50e3c2",
|
||||
padding: "5px",
|
||||
borderRadius: "3px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
transform: "scale(1.1)",
|
||||
},
|
||||
});
|
||||
|
||||
export const BlockIconContainer = styled(Box)({
|
||||
display: "flex",
|
||||
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
|
||||
backgroundColor: "#fbfbfb",
|
||||
color: "#c25252",
|
||||
padding: "5px",
|
||||
borderRadius: "3px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
transform: "scale(1.1)",
|
||||
},
|
||||
});
|
||||
|
||||
export const CommentsContainer = styled(Box)({
|
||||
width: "90%",
|
||||
maxWidth: "1000px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1",
|
||||
overflow: "auto",
|
||||
});
|
||||
|
||||
export const CommentContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "25px 0px 50px 0px",
|
||||
maxWidth: "100%",
|
||||
width: "100%",
|
||||
gap: "10px",
|
||||
padding: "0px 5px",
|
||||
});
|
||||
|
||||
export const NoCommentsRow = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flex: "1",
|
||||
padding: "10px 0px",
|
||||
fontFamily: "Mulish",
|
||||
letterSpacing: 0,
|
||||
fontWeight: 400,
|
||||
fontSize: "18px",
|
||||
});
|
||||
|
||||
export const LoadMoreCommentsButtonRow = styled(Box)({
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
export const EditReplyButton = styled(Button)(({ theme }) => ({
|
||||
width: "30px",
|
||||
alignSelf: "flex-end",
|
||||
background: theme.palette.primary.light,
|
||||
color: "#ffffff",
|
||||
}));
|
||||
|
||||
export const LoadMoreCommentsButton = styled(Button)(({ theme }) => ({
|
||||
fontFamily: "Montserrat",
|
||||
fontWeight: 400,
|
||||
letterSpacing: "0.2px",
|
||||
fontSize: "15px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: "#ffffff",
|
||||
}));
|
||||
|
||||
export const CommentActionButtonRow = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
});
|
||||
|
||||
export const CommentEditorContainer = styled(Box)({
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
export const CommentDateText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Mulish",
|
||||
letterSpacing: 0,
|
||||
fontWeight: 400,
|
||||
fontSize: "13px",
|
||||
marginLeft: "5px",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const CommentInputContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: "15px",
|
||||
width: "90%",
|
||||
maxWidth: "1000px",
|
||||
borderRadius: "8px",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
marginBottom: "25px",
|
||||
});
|
||||
|
||||
export const CommentInput = styled(TextField)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === "light" ? "#a9d9d01d" : "#c3abe4a",
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
'& [class$="-MuiFilledInput-root"]': {
|
||||
fontFamily: "Mulish",
|
||||
letterSpacing: 0,
|
||||
fontWeight: 400,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "19px",
|
||||
minHeight: "100px",
|
||||
backgroundColor: "transparent",
|
||||
"&:before": {
|
||||
borderBottom: "none",
|
||||
"&:hover": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
"&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const SubmitCommentButton = styled(Button)(({ theme }) => ({
|
||||
fontFamily: "Montserrat",
|
||||
fontWeight: 400,
|
||||
letterSpacing: "0.2px",
|
||||
fontSize: "15px",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: "#ffffff",
|
||||
width: "75%",
|
||||
}));
|
245
src/components/common/SuperLikesList/SuperLikesSection.tsx
Normal file
245
src/components/common/SuperLikesList/SuperLikesSection.tsx
Normal file
@ -0,0 +1,245 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CommentEditor } from "./CommentEditor";
|
||||
import { Comment } from "./Comment";
|
||||
import { Box, Button, CircularProgress, useTheme } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../state/store";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
CommentContainer,
|
||||
CommentEditorContainer,
|
||||
CommentsContainer,
|
||||
LoadMoreCommentsButton,
|
||||
LoadMoreCommentsButtonRow,
|
||||
NoCommentsRow,
|
||||
} from "./Comments-styles";
|
||||
import { COMMENT_BASE } from "../../../constants";
|
||||
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from "../../UploadVideo/Upload-styles";
|
||||
|
||||
interface CommentSectionProps {
|
||||
postId: string;
|
||||
postName: string;
|
||||
superlikes: any[];
|
||||
getMore: ()=> void;
|
||||
loadingSuperLikes: boolean;
|
||||
}
|
||||
|
||||
const Panel = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
`;
|
||||
export const SuperLikesSection = ({ loadingSuperLikes, superlikes, postId, postName, getMore }: CommentSectionProps) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [listComments, setListComments] = useState<any[]>([]);
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const { user } = useSelector((state: RootState) => state.auth);
|
||||
const [newMessages, setNewMessages] = useState(0);
|
||||
const [loadingComments, setLoadingComments] = useState<boolean>(null);
|
||||
const onSubmit = (obj?: any, isEdit?: boolean) => {
|
||||
if (isEdit) {
|
||||
setListComments((prev: any[]) => {
|
||||
const findCommentIndex = prev.findIndex(
|
||||
item => item?.identifier === obj?.identifier
|
||||
);
|
||||
if (findCommentIndex === -1) return prev;
|
||||
|
||||
const newArray = [...prev];
|
||||
newArray[findCommentIndex] = obj;
|
||||
return newArray;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
setListComments(prev => [
|
||||
...prev,
|
||||
{
|
||||
...obj,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(location.search);
|
||||
let commentVar = query?.get("comment");
|
||||
if (commentVar) {
|
||||
if (commentVar && commentVar.endsWith("/")) {
|
||||
commentVar = commentVar.slice(0, -1);
|
||||
}
|
||||
setIsOpen(true);
|
||||
if (listComments.length > 0) {
|
||||
const el = document.getElementById(commentVar);
|
||||
if (el) {
|
||||
el.scrollIntoView();
|
||||
el.classList.add("glow");
|
||||
setTimeout(() => {
|
||||
el.classList.remove("glow");
|
||||
}, 2000);
|
||||
}
|
||||
navigate(location.pathname, { replace: true });
|
||||
}
|
||||
}
|
||||
}, [navigate, location, listComments]);
|
||||
|
||||
const getReplies = useCallback(
|
||||
async (commentId, postId) => {
|
||||
const offset = 0;
|
||||
|
||||
const removeBaseCommentId = commentId.replace("_base_", "");
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${COMMENT_BASE}${postId.slice(
|
||||
-12
|
||||
)}_reply_${removeBaseCommentId.slice(
|
||||
-6
|
||||
)}&limit=0&includemetadata=false&offset=${offset}&reverse=false&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
const comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (comment.identifier && comment.name) {
|
||||
const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const responseData2 = await response.text();
|
||||
if (responseData) {
|
||||
comments.push({
|
||||
message: responseData2,
|
||||
...comment,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return comments;
|
||||
},
|
||||
[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);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if(superlikes.length > 0 && postId){
|
||||
getComments(superlikes, postId)
|
||||
}
|
||||
}, [getComments, superlikes, postId]);
|
||||
|
||||
const structuredCommentList = useMemo(() => {
|
||||
return listComments.reduce((acc, curr, index, array) => {
|
||||
if (curr?.identifier?.includes("_reply_")) {
|
||||
return acc;
|
||||
}
|
||||
acc.push({
|
||||
...curr,
|
||||
replies: array.filter(comment =>
|
||||
comment.identifier.includes(`_reply_${curr.identifier.slice(-6)}`)
|
||||
),
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
}, [listComments]);
|
||||
|
||||
console.log({loadingComments, listComments, superlikes})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Panel>
|
||||
<CrowdfundSubTitleRow >
|
||||
<CrowdfundSubTitle sx={{
|
||||
fontSize: '18px',
|
||||
color: 'gold'
|
||||
}}>Super Likes</CrowdfundSubTitle>
|
||||
</CrowdfundSubTitleRow>
|
||||
<CommentsContainer>
|
||||
{(loadingComments || loadingSuperLikes) ? (
|
||||
<NoCommentsRow>
|
||||
<CircularProgress />
|
||||
</NoCommentsRow>
|
||||
) : listComments.length === 0 ? (
|
||||
<NoCommentsRow>
|
||||
There are no super likes yet. Be the first!
|
||||
</NoCommentsRow>
|
||||
) : (
|
||||
<CommentContainer>
|
||||
{structuredCommentList.map((comment: any) => {
|
||||
return (
|
||||
<Comment
|
||||
key={comment?.identifier}
|
||||
comment={comment}
|
||||
onSubmit={onSubmit}
|
||||
postId={postId}
|
||||
postName={postName}
|
||||
amount={comment?.amount || null}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CommentContainer>
|
||||
)}
|
||||
{listComments.length > 20 && (
|
||||
<LoadMoreCommentsButtonRow>
|
||||
<LoadMoreCommentsButton
|
||||
onClick={() => {
|
||||
getMore()
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Load More Comments
|
||||
</LoadMoreCommentsButton>
|
||||
</LoadMoreCommentsButtonRow>
|
||||
)}
|
||||
</CommentsContainer>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
};
|
@ -37,6 +37,7 @@ import { RootState } from "../../../state/store";
|
||||
import { useWindowSize } from "../../../hooks/useWindowSize";
|
||||
import { UploadVideo } from "../../UploadVideo/UploadVideo";
|
||||
import { StyledButton } from "../../UploadVideo/Upload-styles";
|
||||
import { Notifications } from "../../common/Notifications/Notifications";
|
||||
interface Props {
|
||||
isAuthenticated: boolean;
|
||||
userName: string | null;
|
||||
@ -354,6 +355,9 @@ const NavBar: React.FC<Props> = ({
|
||||
/>
|
||||
</Box>
|
||||
</Popover>
|
||||
{isAuthenticated && userName && (
|
||||
<Notifications />
|
||||
)}
|
||||
|
||||
<DownloadTaskManager />
|
||||
{isAuthenticated && userName && (
|
||||
|
@ -1,4 +1,4 @@
|
||||
const useTestIdentifiers = false;
|
||||
const useTestIdentifiers = true;
|
||||
|
||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers
|
||||
? "MYTEST_vid_"
|
||||
@ -8,10 +8,24 @@ export const QTUBE_VIDEO_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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo, useRef, useEffect } 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,15 +33,94 @@ import {
|
||||
CrowdfundSubTitle,
|
||||
CrowdfundSubTitleRow,
|
||||
} from "../../components/UploadVideo/Upload-styles";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants";
|
||||
import { FOR_SUPER_LIKE, 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 { CommentContainer } from "../../components/common/Comments/Comments-styles";
|
||||
import { Comment } from "../../components/common/Comments/Comment";
|
||||
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
|
||||
|
||||
export function isTimestampWithinRange(resTimestamp, resCreated) {
|
||||
// Calculate the absolute difference in milliseconds
|
||||
var difference = Math.abs(resTimestamp - resCreated);
|
||||
|
||||
// 2 minutes in milliseconds
|
||||
var twoMinutesInMilliseconds = 2 * 60 * 1000;
|
||||
|
||||
// Check if the difference is within 2 minutes
|
||||
return difference <= twoMinutesInMilliseconds;
|
||||
}
|
||||
|
||||
export function extractSigValue(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 = /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);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getPaymentInfo = async (signature: string) => {
|
||||
try {
|
||||
const url = `/transactions/signature/${signature}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
// 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;
|
||||
} else {
|
||||
throw new Error('unable to get payment')
|
||||
}
|
||||
} catch (error) {
|
||||
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 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);
|
||||
|
||||
@ -50,8 +129,24 @@ export const VideoContent = () => {
|
||||
);
|
||||
const contentRef = useRef(null);
|
||||
|
||||
|
||||
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 avatarUrl = useMemo(() => {
|
||||
let url = "";
|
||||
if (name && userAvatarHash[name]) {
|
||||
@ -194,6 +289,79 @@ export const VideoContent = () => {
|
||||
}
|
||||
}, [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)){
|
||||
|
||||
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,
|
||||
amount: res.amount
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setSuperlikelist(comments);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingSuperLikes(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if(!nameAddress || !id) return
|
||||
getComments(id, nameAddress);
|
||||
}, [getComments, id, nameAddress]);
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -248,15 +416,32 @@ export const VideoContent = () => {
|
||||
</FileElement>
|
||||
</FileAttachmentContainer>
|
||||
</Box>
|
||||
<VideoTitle
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginTop: '20px',
|
||||
gap: '10px'
|
||||
}}>
|
||||
<VideoTitle
|
||||
variant="h1"
|
||||
color="textPrimary"
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
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"
|
||||
@ -368,6 +553,10 @@ export const VideoContent = () => {
|
||||
|
||||
</Box>
|
||||
</VideoPlayerContainer>
|
||||
<SuperLikesSection getMore={()=> {
|
||||
|
||||
}} loadingSuperLikes={loadingSuperLikes} superlikes={superlikeList} postId={id || ""} postName={name || ""} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
Loading…
x
Reference in New Issue
Block a user