mirror of https://github.com/Qortal/q-tube
Browse Source
The Subscribe and Follow buttons no longer appear when the user views their own videos Minimum Superlike amount lowered from 10 to 1 QORT Fixed bug that made searches in the Home page Subscriptions Tab return results from all videos. Fixed bug that prevented filtering by name on Subscriptions Tab Clicking in area around video gives it focus, allowing hotkeys to work, orange border around video when focused is removed Subscription tab doesn't say "You have no subscriptions" while loading videospull/23/head
Qortal Dev
6 months ago
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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue