mirror of https://github.com/Qortal/q-tube
Browse Source
Follow, Like, and Dislike buttons Added to Video, Playlist, and Channel pagespull/24/head^2
38 changed files with 1839 additions and 1210 deletions
@ -0,0 +1,159 @@
|
||||
import { Box, Button, ButtonProps } from "@mui/material"; |
||||
import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip"; |
||||
import { MouseEvent, useEffect, useState } from "react"; |
||||
import { styled } from "@mui/material/styles"; |
||||
|
||||
interface FollowButtonProps extends ButtonProps { |
||||
followerName: string; |
||||
} |
||||
|
||||
export type FollowData = { |
||||
userName: string; |
||||
followerName: string; |
||||
}; |
||||
|
||||
export const FollowButton = ({ followerName, ...props }: FollowButtonProps) => { |
||||
const [followingList, setFollowingList] = useState<string[]>([]); |
||||
const [followingSize, setFollowingSize] = useState<string>(""); |
||||
const [followingItemCount, setFollowingItemCount] = useState<string>(""); |
||||
const isFollowingName = () => { |
||||
return followingList.includes(followerName); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
qortalRequest({ |
||||
action: "GET_LIST_ITEMS", |
||||
list_name: "followedNames", |
||||
}).then(followList => { |
||||
setFollowingList(followList); |
||||
}); |
||||
getFollowSize(); |
||||
}, []); |
||||
|
||||
const followName = () => { |
||||
if (followingList.includes(followerName) === false) { |
||||
qortalRequest({ |
||||
action: "ADD_LIST_ITEM", |
||||
list_name: "followedNames", |
||||
item: followerName, |
||||
}).then(response => { |
||||
if (response === false) console.log("followName failed"); |
||||
else { |
||||
setFollowingList([...followingList, followerName]); |
||||
console.log("following Name: ", followerName); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
const unfollowName = () => { |
||||
if (followingList.includes(followerName)) { |
||||
qortalRequest({ |
||||
action: "DELETE_LIST_ITEM", |
||||
list_name: "followedNames", |
||||
item: followerName, |
||||
}).then(response => { |
||||
if (response === false) console.log("unfollowName failed"); |
||||
else { |
||||
const listWithoutName = followingList.filter( |
||||
item => followerName !== item |
||||
); |
||||
setFollowingList(listWithoutName); |
||||
console.log("unfollowing Name: ", followerName); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
const manageFollow = (e: MouseEvent<HTMLButtonElement>) => { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
isFollowingName() ? unfollowName() : followName(); |
||||
}; |
||||
|
||||
const verticalPadding = "3px"; |
||||
const horizontalPadding = "8px"; |
||||
const buttonStyle = { |
||||
fontSize: "15px", |
||||
fontWeight: "700", |
||||
paddingTop: verticalPadding, |
||||
paddingBottom: verticalPadding, |
||||
paddingLeft: horizontalPadding, |
||||
paddingRight: horizontalPadding, |
||||
borderRadius: 28, |
||||
color: "white", |
||||
width: "96px", |
||||
height: "45px", |
||||
...props.sx, |
||||
}; |
||||
|
||||
const getFollowSize = () => { |
||||
qortalRequest({ |
||||
action: "LIST_QDN_RESOURCES", |
||||
name: followerName, |
||||
limit: 0, |
||||
includeMetadata: false, |
||||
}).then(publishesList => { |
||||
let totalSize = 0; |
||||
let itemsCount = 0; |
||||
publishesList.map(publish => { |
||||
totalSize += +publish.size; |
||||
itemsCount++; |
||||
}); |
||||
setFollowingSize(formatBytes(totalSize)); |
||||
setFollowingItemCount(itemsCount.toString()); |
||||
}); |
||||
}; |
||||
|
||||
function formatBytes(bytes: number, decimals = 2) { |
||||
if (!+bytes) return "0 Bytes"; |
||||
|
||||
const k = 1024; |
||||
const dm = decimals < 0 ? 0 : decimals; |
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; |
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; |
||||
} |
||||
|
||||
const TooltipLine = styled("div")(({ theme }) => ({ |
||||
fontSize: "18px", |
||||
})); |
||||
|
||||
const tooltipTitle = followingSize && ( |
||||
<> |
||||
<TooltipLine> |
||||
Following a name automatically downloads all of its content to your |
||||
node. The more followers a name has, the faster its content will |
||||
download for everyone. |
||||
</TooltipLine> |
||||
<br /> |
||||
<TooltipLine>{`${followerName}'s Current Download Size: ${followingSize}`}</TooltipLine> |
||||
<TooltipLine>{`Number of Files: ${followingItemCount}`}</TooltipLine> |
||||
</> |
||||
); |
||||
|
||||
const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => ( |
||||
<Tooltip {...props} classes={{ popper: className }} /> |
||||
))({ |
||||
[`& .${tooltipClasses.tooltip}`]: { |
||||
maxWidth: 600, |
||||
}, |
||||
}); |
||||
|
||||
return ( |
||||
<> |
||||
<CustomWidthTooltip title={tooltipTitle} placement={"top"} arrow> |
||||
<Button |
||||
{...props} |
||||
variant={"contained"} |
||||
color="success" |
||||
sx={buttonStyle} |
||||
onClick={e => manageFollow(e)} |
||||
> |
||||
{isFollowingName() ? "Unfollow" : "Follow"} |
||||
</Button> |
||||
</CustomWidthTooltip> |
||||
</> |
||||
); |
||||
}; |
@ -0,0 +1,66 @@
|
||||
import { fetchResourcesByIdentifier } from "../../../utils/qortalRequestFunctions.ts"; |
||||
import { DISLIKE, LIKE, LikeType, NEUTRAL } from "./LikeAndDislike.tsx"; |
||||
|
||||
export const getCurrentLikeType = async ( |
||||
username: string, |
||||
likeIdentifier: string |
||||
): Promise<LikeType> => { |
||||
try { |
||||
const response = await qortalRequest({ |
||||
action: "FETCH_QDN_RESOURCE", |
||||
name: username, |
||||
service: "CHAIN_COMMENT", |
||||
identifier: likeIdentifier, |
||||
}); |
||||
return response?.likeType; |
||||
} catch (e) { |
||||
console.log("liketype error: ", e); |
||||
return NEUTRAL; |
||||
} |
||||
}; |
||||
|
||||
type ResourceType = { likeType: LikeType }; |
||||
export type LikesAndDislikes = { likes: number; dislikes: number }; |
||||
const countLikesAndDislikes = (likesAndDislikes: ResourceType[]) => { |
||||
let totalLikeCount = 0; |
||||
let totalDislikeCount = 0; |
||||
likesAndDislikes.map(likeOrDislike => { |
||||
const likeType = likeOrDislike.likeType; |
||||
if (likeType === LIKE) totalLikeCount += 1; |
||||
if (likeType === DISLIKE) totalDislikeCount += 1; |
||||
}); |
||||
return { |
||||
likes: totalLikeCount, |
||||
dislikes: totalDislikeCount, |
||||
} as LikesAndDislikes; |
||||
}; |
||||
export const getCurrentLikesAndDislikesCount = async ( |
||||
likeIdentifier: string |
||||
) => { |
||||
try { |
||||
const likesAndDislikes = await fetchResourcesByIdentifier<ResourceType>( |
||||
"CHAIN_COMMENT", |
||||
likeIdentifier |
||||
); |
||||
return countLikesAndDislikes(likesAndDislikes); |
||||
} catch (e) { |
||||
console.log(e); |
||||
return undefined; |
||||
} |
||||
}; |
||||
|
||||
export function formatLikeCount(likeCount: number, decimals = 2) { |
||||
if (!+likeCount) return ""; |
||||
|
||||
const sigDigits = Math.floor(Math.log10(likeCount) / 3); |
||||
if (sigDigits < 1) return likeCount.toString(); |
||||
|
||||
const sigDigitSize = 1000; |
||||
const dm = decimals < 0 ? 0 : decimals; |
||||
const sizes = ["K", "M", "B"]; |
||||
|
||||
const sigDigitsToTheThousands = Math.pow(sigDigitSize, sigDigits); |
||||
const sigDigitLikeCount = (likeCount / sigDigitsToTheThousands).toFixed(dm); |
||||
|
||||
return `${sigDigitLikeCount}${sizes[sigDigits - 1] || ""}`; |
||||
} |
@ -0,0 +1,230 @@
|
||||
import React, { useEffect, useState } from "react"; |
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; |
||||
import ThumbDownIcon from "@mui/icons-material/ThumbDown"; |
||||
import ThumbUpOffAltOutlinedIcon from "@mui/icons-material/ThumbUpOffAltOutlined"; |
||||
import ThumbDownOffAltOutlinedIcon from "@mui/icons-material/ThumbDownOffAltOutlined"; |
||||
import { Box, Tooltip } from "@mui/material"; |
||||
import { useDispatch, useSelector } from "react-redux"; |
||||
import { setNotification } from "../../../state/features/notificationsSlice.ts"; |
||||
import ShortUniqueId from "short-unique-id"; |
||||
import { objectToBase64 } from "../../../utils/toBase64.ts"; |
||||
import { RootState } from "../../../state/store.ts"; |
||||
import { FOR, FOR_LIKE, LIKE_BASE } from "../../../constants/Identifiers.ts"; |
||||
import { |
||||
formatLikeCount, |
||||
getCurrentLikesAndDislikesCount, |
||||
getCurrentLikeType, |
||||
LikesAndDislikes, |
||||
} from "./LikeAndDislike-functions.ts"; |
||||
|
||||
interface LikeAndDislikeProps { |
||||
name: string; |
||||
identifier: string; |
||||
} |
||||
export enum LikeType { |
||||
Like = 1, |
||||
Neutral = 0, |
||||
Dislike = -1, |
||||
} |
||||
export const LIKE = LikeType.Like; |
||||
export const DISLIKE = LikeType.Dislike; |
||||
export const NEUTRAL = LikeType.Neutral; |
||||
export const LikeAndDislike = ({ name, identifier }: LikeAndDislikeProps) => { |
||||
const username = useSelector((state: RootState) => state.auth?.user?.name); |
||||
const dispatch = useDispatch(); |
||||
const [likeCount, setLikeCount] = useState<number>(0); |
||||
const [dislikeCount, setDislikeCount] = useState<number>(0); |
||||
const [currentLikeType, setCurrentLikeType] = useState<LikeType>(NEUTRAL); |
||||
const likeIdentifier = `${LIKE_BASE}${identifier.slice(0, 39)}`; |
||||
const [isLoading, setIsLoading] = useState<boolean>(true); |
||||
|
||||
useEffect(() => { |
||||
type PromiseReturn = [LikeType, LikesAndDislikes]; |
||||
|
||||
Promise.all([ |
||||
getCurrentLikeType(username, likeIdentifier), |
||||
getCurrentLikesAndDislikesCount(likeIdentifier), |
||||
]).then(([likeType, likesAndDislikes]: PromiseReturn) => { |
||||
setCurrentLikeType(likeType); |
||||
|
||||
setLikeCount(likesAndDislikes?.likes || 0); |
||||
setDislikeCount(likesAndDislikes?.dislikes || 0); |
||||
setIsLoading(false); |
||||
}); |
||||
}, []); |
||||
|
||||
const updateLikeDataState = (newLikeType: LikeType) => { |
||||
const setSuccessNotification = (msg: string) => |
||||
dispatch( |
||||
setNotification({ |
||||
msg, |
||||
alertType: "success", |
||||
}) |
||||
); |
||||
setCurrentLikeType(newLikeType); |
||||
switch (newLikeType) { |
||||
case NEUTRAL: |
||||
if (currentLikeType === LIKE) { |
||||
setLikeCount(count => count - 1); |
||||
setSuccessNotification("Like Removed"); |
||||
} else { |
||||
setDislikeCount(count => count - 1); |
||||
setSuccessNotification("Dislike Removed"); |
||||
} |
||||
|
||||
break; |
||||
case LIKE: |
||||
if (currentLikeType === DISLIKE) setDislikeCount(count => count - 1); |
||||
setLikeCount(count => count + 1); |
||||
setSuccessNotification("Like Successful"); |
||||
break; |
||||
case DISLIKE: |
||||
if (currentLikeType === LIKE) setLikeCount(count => count - 1); |
||||
setDislikeCount(count => count + 1); |
||||
setSuccessNotification("Dislike Successful"); |
||||
break; |
||||
} |
||||
}; |
||||
function publishLike(chosenLikeType: LikeType) { |
||||
if (isLoading) { |
||||
dispatch( |
||||
setNotification({ |
||||
msg: "Wait for Like Data to load first", |
||||
alertType: "error", |
||||
}) |
||||
); |
||||
return; |
||||
} |
||||
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 (!identifier) throw new Error("Could not retrieve id of video post"); |
||||
|
||||
if (username === name) { |
||||
dispatch( |
||||
setNotification({ |
||||
msg: "You cannot send yourself a like", |
||||
alertType: "error", |
||||
}) |
||||
); |
||||
return; |
||||
} |
||||
qortalRequest({ |
||||
action: "GET_NAME_DATA", |
||||
name: name, |
||||
}).then(resName => { |
||||
const address = resName.owner; |
||||
if (!address) |
||||
throw new Error("Could not retrieve content creator's address"); |
||||
}); |
||||
|
||||
objectToBase64({ |
||||
likeType: chosenLikeType, |
||||
}).then(likeToBase64 => { |
||||
qortalRequest({ |
||||
action: "PUBLISH_QDN_RESOURCE", |
||||
name: username, |
||||
service: "CHAIN_COMMENT", |
||||
data64: likeToBase64, |
||||
title: "", |
||||
identifier: likeIdentifier, |
||||
filename: `like_metadata.json`, |
||||
}).then(() => { |
||||
updateLikeDataState(chosenLikeType); |
||||
}); |
||||
}); |
||||
} catch (error: any) { |
||||
dispatch( |
||||
setNotification({ |
||||
msg: |
||||
error || |
||||
error?.error || |
||||
error?.message || |
||||
"Failed to publish Like or Dislike", |
||||
alertType: "error", |
||||
}) |
||||
); |
||||
throw new Error("Failed to publish Super Like"); |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<Box |
||||
sx={{ |
||||
display: "flex", |
||||
alignItems: "center", |
||||
gap: "15px", |
||||
cursor: "pointer", |
||||
flexShrink: 0, |
||||
}} |
||||
> |
||||
<Tooltip title="Like or Dislike Video" placement="top"> |
||||
<Box |
||||
sx={{ |
||||
padding: "5px", |
||||
borderRadius: "7px", |
||||
gap: "5px", |
||||
display: "flex", |
||||
alignItems: "center", |
||||
marginRight: "10px", |
||||
height: "53px", |
||||
}} |
||||
> |
||||
{currentLikeType === LIKE ? ( |
||||
<ThumbUpIcon onClick={() => publishLike(NEUTRAL)} /> |
||||
) : ( |
||||
<ThumbUpOffAltOutlinedIcon onClick={() => publishLike(LIKE)} /> |
||||
)} |
||||
{likeCount > 0 && ( |
||||
<div |
||||
style={{ |
||||
display: "flex", |
||||
alignItems: "center", |
||||
justifyContent: "center", |
||||
userSelect: "none", |
||||
}} |
||||
> |
||||
<span style={{ marginRight: "10px", paddingBottom: "4px" }}> |
||||
{formatLikeCount(likeCount)} |
||||
</span> |
||||
</div> |
||||
)} |
||||
|
||||
{currentLikeType === DISLIKE ? ( |
||||
<ThumbDownIcon |
||||
onClick={() => publishLike(NEUTRAL)} |
||||
color={"error"} |
||||
/> |
||||
) : ( |
||||
<ThumbDownOffAltOutlinedIcon |
||||
onClick={() => publishLike(DISLIKE)} |
||||
color={"error"} |
||||
/> |
||||
)} |
||||
{dislikeCount > 0 && ( |
||||
<div |
||||
style={{ |
||||
display: "flex", |
||||
alignItems: "center", |
||||
justifyContent: "center", |
||||
userSelect: "none", |
||||
}} |
||||
> |
||||
<span |
||||
style={{ |
||||
marginRight: "10px", |
||||
paddingBottom: "4px", |
||||
color: "red", |
||||
}} |
||||
> |
||||
{formatLikeCount(dislikeCount)} |
||||
</span> |
||||
</div> |
||||
)} |
||||
</Box> |
||||
</Tooltip> |
||||
</Box> |
||||
</> |
||||
); |
||||
}; |
Loading…
Reference in new issue