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

Follow, Like, and Dislike buttons Added to Video, Playlist, and Channel pages

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 videos
This commit is contained in:
Qortal Dev 2024-03-26 17:11:15 -06:00
parent 10974f68aa
commit 9cf2932d81
38 changed files with 1829 additions and 1200 deletions

View File

@ -8,14 +8,14 @@ import { Provider } from "react-redux";
import GlobalWrapper from "./wrappers/GlobalWrapper";
import Notification from "./components/common/Notification/Notification";
import { Home } from "./pages/Home/Home";
import { VideoContent } from "./pages/VideoContent/VideoContent";
import { VideoContent } from "./pages/ContentPages/VideoContent/VideoContent";
import DownloadWrapper from "./wrappers/DownloadWrapper";
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/PlaylistContent/PlaylistContent";
import { IndividualProfile } from "./pages/ContentPages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/ContentPages/PlaylistContent/PlaylistContent";
import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist";
import { setFilteredSubscriptions } from "./state/features/videoSlice.ts";
import { SubscriptionData } from "./components/common/SubscribeButton.tsx";
import { SubscriptionData } from "./components/common/ContentButtons/SubscribeButton.tsx";
export const getUserName = async () => {
const account = await qortalRequest({

View File

@ -3,7 +3,7 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles"
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../PublishVideo/PublishVideo-styles.tsx";
} from "../Publish/PublishVideo/PublishVideo-styles.tsx";
import { Box, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";

View File

@ -12,7 +12,7 @@ import {
NewCrowdfundTitle,
StyledButton,
TimesIcon,
} from "./Upload-styles";
} from "./Upload-styles.tsx";
import {
Box,
FormControl,
@ -30,9 +30,9 @@ import { useDispatch, useSelector } from "react-redux";
import AddBoxIcon from "@mui/icons-material/AddBox";
import { useDropzone } from "react-dropzone";
import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import { objectToBase64, uint8ArrayToBase64 } from "../../../utils/toBase64.ts";
import { RootState } from "../../../state/store.ts";
import {
upsertVideosBeginning,
addToHashMap,
@ -41,17 +41,17 @@ import {
updateVideo,
updateInHashMap,
setEditPlaylist,
} from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader";
import { categories, subCategories } from "../../constants/Categories.ts";
import { Playlists } from "../Playlists/Playlists";
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils";
} from "../../../state/features/videoSlice.ts";
import ImageUploader from "../../common/ImageUploader.tsx";
import { categories, subCategories } from "../../../constants/Categories.ts";
import { Playlists } from "../../Playlists/Playlists.tsx";
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit.tsx";
import { TextEditor } from "../../common/TextEditor/TextEditor.tsx";
import { extractTextFromHTML } from "../../common/TextEditor/utils.ts";
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../../constants/Identifiers.ts";
} from "../../../constants/Identifiers.ts";
const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 });

View File

@ -9,10 +9,10 @@ import {
Rating,
TextField,
Typography,
Select
Select,
} from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../assets/svgs/TimesSVG";
import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx";
export const DoubleLine = styled(Typography)`
display: -webkit-box;
@ -159,8 +159,6 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({
},
}));
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
@ -539,8 +537,8 @@ export const NoReviewsFont = styled(Typography)(({ theme }) => ({
export const StyledButton = styled(Button)(({ theme }) => ({
fontWeight: 600,
color: theme.palette.text.primary
}))
color: theme.palette.text.primary,
}));
export const CustomSelect = styled(Select)(({ theme }) => ({
fontFamily: "Mulish",
@ -549,34 +547,34 @@ export const CustomSelect = styled(Select)(({ theme }) => ({
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
'& .MuiSelect-select': {
padding: '12px',
"& .MuiSelect-select": {
padding: "12px",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontWeight: 400,
borderRadius: theme.shape.borderRadius, // Match border radius
},
'&:before': {
"&:before": {
// Underline style
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
},
'&:after': {
"&:after": {
// Underline style when focused
borderBottomColor: theme.palette.secondary.main,
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
},
'&:hover fieldset': {
"&:hover fieldset": {
borderColor: "#B2BAC2",
},
'&.Mui-focused fieldset': {
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
},
},
'& .MuiInputBase-root': {
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",

View File

@ -9,10 +9,10 @@ import {
Rating,
TextField,
Typography,
Select
Select,
} from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../assets/svgs/TimesSVG";
import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx";
export const DoubleLine = styled(Typography)`
display: -webkit-box;
@ -159,8 +159,6 @@ export const CustomInputField = styled(TextField)(({ theme }) => ({
},
}));
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Copse",
letterSpacing: "1px",
@ -539,8 +537,8 @@ export const NoReviewsFont = styled(Typography)(({ theme }) => ({
export const StyledButton = styled(Button)(({ theme }) => ({
fontWeight: 600,
color: theme.palette.text.primary
}))
color: theme.palette.text.primary,
}));
export const CustomSelect = styled(Select)(({ theme }) => ({
fontFamily: "Mulish",
@ -549,34 +547,34 @@ export const CustomSelect = styled(Select)(({ theme }) => ({
fontWeight: 400,
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.default,
'& .MuiSelect-select': {
padding: '12px',
"& .MuiSelect-select": {
padding: "12px",
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",
fontWeight: 400,
borderRadius: theme.shape.borderRadius, // Match border radius
},
'&:before': {
"&:before": {
// Underline style
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
},
'&:after': {
"&:after": {
// Underline style when focused
borderBottomColor: theme.palette.secondary.main,
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "#E0E3E7",
},
'&:hover fieldset': {
"&:hover fieldset": {
borderColor: "#B2BAC2",
},
'&.Mui-focused fieldset': {
"&.Mui-focused fieldset": {
borderColor: "#6F7E8C",
},
},
'& .MuiInputBase-root': {
"& .MuiInputBase-root": {
fontFamily: "Mulish",
fontSize: "19px",
letterSpacing: "0px",

View File

@ -34,9 +34,9 @@ import { useDispatch, useSelector } from "react-redux";
import AddBoxIcon from "@mui/icons-material/AddBox";
import { useDropzone } from "react-dropzone";
import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import { objectToBase64, uint8ArrayToBase64 } from "../../../utils/toBase64.ts";
import { RootState } from "../../../state/store.ts";
import {
upsertVideosBeginning,
addToHashMap,
@ -44,16 +44,16 @@ import {
setEditVideo,
updateVideo,
updateInHashMap,
} from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader";
import { categories, subCategories } from "../../constants/Categories.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils";
} from "../../../state/features/videoSlice.ts";
import ImageUploader from "../../common/ImageUploader.tsx";
import { categories, subCategories } from "../../../constants/Categories.ts";
import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll.tsx";
import { TextEditor } from "../../common/TextEditor/TextEditor.tsx";
import { extractTextFromHTML } from "../../common/TextEditor/utils.ts";
import { toBase64 } from "../PublishVideo/PublishVideo.tsx";
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
import { titleFormatter } from "../../constants/Misc.ts";
import { FrameExtractor } from "../../common/FrameExtractor/FrameExtractor.tsx";
import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts";
import { titleFormatter } from "../../../constants/Misc.ts";
const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 });

View File

@ -8,8 +8,8 @@ import {
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
import { CircleSVG } from "../../../assets/svgs/CircleSVG.tsx";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG.tsx";
import { styled } from "@mui/system";
interface Publish {

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
@ -7,11 +7,11 @@ import {
import { Box, Button, Input, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { removeVideo } from "../../state/features/videoSlice";
import { removeVideo } from "../../../state/features/videoSlice.ts";
import AddIcon from "@mui/icons-material/Add";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
import { RootState } from "../../../state/store.ts";
import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts";
export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
const theme = useTheme();
const navigate = useNavigate();

View File

@ -12,7 +12,7 @@ import {
Select,
} from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../assets/svgs/TimesSVG";
import { TimesSVG } from "../../../assets/svgs/TimesSVG.tsx";
export const DoubleLine = styled(Typography)`
display: -webkit-box;

View File

@ -37,36 +37,36 @@ import AddBoxIcon from "@mui/icons-material/AddBox";
import { useDropzone } from "react-dropzone";
import AddIcon from "@mui/icons-material/Add";
import { setNotification } from "../../state/features/notificationsSlice";
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import { objectToBase64, uint8ArrayToBase64 } from "../../../utils/toBase64.ts";
import { RootState } from "../../../state/store.ts";
import {
upsertVideosBeginning,
addToHashMap,
upsertVideos,
} from "../../state/features/videoSlice";
import ImageUploader from "../common/ImageUploader";
import { categories, subCategories } from "../../constants/Categories.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
} from "../../../state/features/videoSlice.ts";
import ImageUploader from "../../common/ImageUploader.tsx";
import { categories, subCategories } from "../../../constants/Categories.ts";
import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll.tsx";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../EditPlaylist/Upload-styles";
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils";
} from "../EditPlaylist/Upload-styles.tsx";
import { CardContentContainerComment } from "../../common/Comments/Comments-styles.tsx";
import { TextEditor } from "../../common/TextEditor/TextEditor.tsx";
import { extractTextFromHTML } from "../../common/TextEditor/utils.ts";
import {
FiltersCheckbox,
FiltersRow,
FiltersSubContainer,
} from "../../pages/Home/VideoList-styles";
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
} from "../../../pages/Home/VideoList-styles.tsx";
import { FrameExtractor } from "../../common/FrameExtractor/FrameExtractor.tsx";
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../../constants/Identifiers.ts";
import { titleFormatter } from "../../constants/Misc.ts";
import { getFileName } from "../../utils/stringFunctions.ts";
} from "../../../constants/Identifiers.ts";
import { titleFormatter } from "../../../constants/Misc.ts";
import { getFileName } from "../../../utils/stringFunctions.ts";
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => {

View File

@ -12,8 +12,6 @@ export const StatsData = () => {
width: "100%",
padding: "20px 0px",
backgroundColor: theme.palette.background.default,
borderTop: `1px solid ${theme.palette.background.paper}`,
borderRight: `1px solid ${theme.palette.background.paper}`,
}));
const {
@ -51,7 +49,10 @@ export const StatsData = () => {
</div>
<div>
Average:{" "}
<span style={{ fontWeight: "bold" }}>{videosPerNamePublished}</span>
<span style={{ fontWeight: "bold" }}>
{videosPerNamePublished > 0 &&
Number(videosPerNamePublished).toFixed(0)}
</span>
</div>
</StatsCol>
);

View File

@ -17,7 +17,7 @@ import {
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../PublishVideo/PublishVideo-styles.tsx";
} from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
interface CommentSectionProps {

View File

@ -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>
</>
);
};

View File

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

View File

@ -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>
</>
);
};

View File

@ -1,10 +1,14 @@
import { Button, ButtonProps } from "@mui/material";
import { Button, ButtonProps, Tooltip } from "@mui/material";
import { MouseEvent, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts";
import { subscribe, unSubscribe } from "../../state/features/persistSlice.ts";
import { setFilteredSubscriptions } from "../../state/features/videoSlice.ts";
import { subscriptionListFilter } from "../../App.tsx";
import { RootState } from "../../../state/store.ts";
import {
subscribe,
unSubscribe,
} from "../../../state/features/persistSlice.ts";
import { setFilteredSubscriptions } from "../../../state/features/videoSlice.ts";
import { subscriptionListFilter } from "../../../App.tsx";
import { styled } from "@mui/material/styles";
interface SubscribeButtonProps extends ButtonProps {
subscriberName: string;
@ -89,15 +93,31 @@ export const SubscribeButton = ({
height: "45px",
...props.sx,
};
const TooltipLine = styled("div")(({ theme }) => ({
fontSize: "18px",
}));
const tooltipTitle = (
<>
<TooltipLine>
Subscribing to a name lets you see their content on the Subscriptions
tab of the Home Page. This does NOT download any data to your node.
</TooltipLine>
</>
);
return (
<Button
{...props}
variant={"contained"}
color="error"
sx={buttonStyle}
onClick={e => manageSubscription(e)}
>
{isSubscribed ? "Unsubscribe" : "Subscribe"}
</Button>
<Tooltip title={tooltipTitle} placement={"top"} arrow>
<Button
{...props}
variant={"contained"}
color="error"
sx={buttonStyle}
onClick={e => manageSubscription(e)}
>
{isSubscribed ? "Unsubscribe" : "Subscribe"}
</Button>
</Tooltip>
);
};

View File

@ -17,26 +17,25 @@ import {
Tooltip,
} from "@mui/material";
import qortImg from "../../../assets/img/qort.png";
import { MultiplePublish } from "../MultiplePublish/MultiplePublishAll";
import { MultiplePublish } from "../../Publish/MultiplePublish/MultiplePublishAll.tsx";
import { useDispatch, useSelector } from "react-redux";
import { setNotification } from "../../../state/features/notificationsSlice";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import ShortUniqueId from "short-unique-id";
import { objectToBase64 } from "../../../utils/toBase64";
import { objectToBase64 } from "../../../utils/toBase64.ts";
import { minPriceSuperlike } from "../../../constants/Misc.ts";
import { CommentInput } from "../Comments/Comments-styles";
import { CommentInput } from "../Comments/Comments-styles.tsx";
import {
CrowdfundActionButton,
CrowdfundActionButtonRow,
ModalBody,
NewCrowdfundTitle,
Spacer,
} from "../../PublishVideo/PublishVideo-styles.tsx";
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor";
import { RootState } from "../../../state/store";
} from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { utf8ToBase64 } from "../SuperLikesList/CommentEditor.tsx";
import { RootState } from "../../../state/store.ts";
import {
FOR,
FOR_SUPER_LIKE,
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
} from "../../../constants/Identifiers.ts";
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx";
@ -158,7 +157,7 @@ export const SuperLike = ({
for: `${name}_${FOR_SUPER_LIKE}`,
},
about:
"Super likes are a way to suppert your favorite content creators. Attach a message to the Super like and have your message seen before normal comments. There is a minimum superLikeAmount for a Super like. Each Super like is verified before displaying to make there aren't any non-paid Super likes",
"Super likes are a way to support your favorite content creators. Attach a message to the Super like and have your message seen before normal comments. There is a minimum superLikeAmount for a Super like. Each Super like is verified before displaying to make there aren't any non-paid Super likes",
});
// Description is obtained from raw data
// const base64 = utf8ToBase64(comment);
@ -185,26 +184,16 @@ export const SuperLike = ({
setIsOpenMultiplePublish(true);
} catch (error: any) {
let notificationObj: any = null;
if (typeof error === "string") {
notificationObj = {
msg: error || "Failed to publish Super Like",
dispatch(
setNotification({
msg:
error ||
error?.error ||
error?.message ||
"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");
}
}
@ -239,8 +228,6 @@ export const SuperLike = ({
flexShrink: 0,
}}
>
<Tooltip title="Super Like" placement="top">
<Box
sx={{
@ -250,8 +237,8 @@ export const SuperLike = ({
display: "flex",
alignItems: "center",
outline: "1px gold solid",
marginRight:'10px',
height: '53px',
marginRight: "10px",
height: "53px",
}}
>
<ThumbUpIcon
@ -260,24 +247,30 @@ export const SuperLike = ({
}}
/>
{numberOfSuperlikes === 0 ? null : (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center', userSelect: "none"}}>
<span style={{marginRight:'10px', paddingBottom:'4px'}}>{numberOfSuperlikes}</span>
<img
style={{
height: "25px",
width: "25px",
marginRight:'5px',
}}
src={qortImg}
alt={"Qort Icon"}
/>
{truncateNumber(totalAmount,0)}
</div>
)}
{numberOfSuperlikes === 0 ? null : (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
userSelect: "none",
}}
>
<span style={{ marginRight: "10px", paddingBottom: "4px" }}>
{numberOfSuperlikes}
</span>
<img
style={{
height: "25px",
width: "25px",
marginRight: "5px",
}}
src={qortImg}
alt={"Qort Icon"}
/>
{truncateNumber(totalAmount, 0)}
</div>
)}
</Box>
</Tooltip>
</Box>
@ -301,10 +294,10 @@ export const SuperLike = ({
<DialogContent>
<Box>
<InputLabel htmlFor="standard-adornment-amount">
Amount in QORT (min 10 QORT)
Amount in QORT (min 1 QORT)
</InputLabel>
<BoundedNumericTextField
minValue={10}
minValue={+minPriceSuperlike}
initialValue={minPriceSuperlike.toString()}
maxValue={numberToInt(+currentBalance)}
allowDecimals={false}

View File

@ -24,7 +24,7 @@ import {
extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../../../pages/VideoContent/VideoContent";
} from "../../../pages/ContentPages/VideoContent/VideoContent";
import { useNavigate } from "react-router-dom";
import localForage from "localforage";
import moment from "moment";

View File

@ -17,7 +17,7 @@ import {
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../PublishVideo/PublishVideo-styles.tsx";
} from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { COMMENT_BASE } from "../../../constants/Identifiers.ts";
interface CommentSectionProps {

View File

@ -1,18 +1,19 @@
import { styled } from "@mui/system";
import { Box } from "@mui/material";
export const VideoContainer = styled(Box)`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
max-height: 70vh;
`;
export const VideoContainer = styled(Box)(({ theme }) => ({
position: "relative",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
margin: 0,
padding: 0,
maxHeight: "70vh",
"&:focus": { outline: "none" },
}));
export const VideoElement = styled("video")`
width: 100%;

File diff suppressed because it is too large Load Diff

View File

@ -35,8 +35,8 @@ import {
} from "../../../state/features/videoSlice";
import { RootState } from "../../../state/store";
import { useWindowSize } from "../../../hooks/useWindowSize";
import { PublishVideo } from "../../PublishVideo/PublishVideo.tsx";
import { StyledButton } from "../../PublishVideo/PublishVideo-styles.tsx";
import { PublishVideo } from "../../Publish/PublishVideo/PublishVideo.tsx";
import { StyledButton } from "../../Publish/PublishVideo/PublishVideo-styles.tsx";
import { Notifications } from "../../common/Notifications/Notifications";
interface Props {
isAuthenticated: boolean;

View File

@ -39,6 +39,7 @@ export const categories = [
{ id: 24, name: "Anime" },
{ id: 25, name: "Cartoons" },
{ id: 26, name: "Qortal" },
{ id: 99, name: "Other" },
].sort(sortCategory);
export const subCategories: CategoryMap = {
@ -59,7 +60,7 @@ export const subCategories: CategoryMap = {
{ id: 113, name: "Indie Films" },
{ id: 114, name: "International Films" },
{ id: 115, name: "Biographies & True Stories" },
{ id: 116, name: "Other" },
{ id: 199, name: "Other" },
].sort(sortCategory),
2: [
// Series
@ -78,14 +79,14 @@ export const subCategories: CategoryMap = {
{ id: 213, name: "Anthologies" },
{ id: 214, name: "International Series" },
{ id: 215, name: "Miniseries" },
{ id: 216, name: "Other" },
{ id: 299, name: "Other" },
].sort(sortCategory),
4: [
// Education
{ id: 400, name: "Tutorial" },
{ id: 401, name: "Documentary" },
{ id: 401, name: "Qortal" },
{ id: 402, name: "Other" },
{ id: 402, name: "Documentary" },
{ id: 499, name: "Other" },
].sort(sortCategory),
24: [
@ -102,6 +103,6 @@ export const subCategories: CategoryMap = {
{ id: 2411, name: "Harem" },
{ id: 2412, name: "Ecchi" },
{ id: 2413, name: "Idol" },
{ id: 2414, name: "Other" },
{ id: 2499, name: "Other" },
].sort(sortCategory),
};

View File

@ -8,8 +8,12 @@ export const QTUBE_PLAYLIST_BASE = useTestIdentifiers
export const SUPER_LIKE_BASE = useTestIdentifiers
? "MYTEST_superlike_"
: "qtube_superlike_";
export const LIKE_BASE = useTestIdentifiers ? "MYTEST_like_" : "qtube_like_";
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 FOR_LIKE = useTestIdentifiers ? "MYTEST_like" : `qtube_like`;

View File

@ -1,6 +1,3 @@
export const minPriceSuperlike = 10;
export const minPriceSuperlike = 1;
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g;
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;
export const allTabValue = "all";
export const subscriptionTabValue = "subscriptions";

View File

@ -24,9 +24,9 @@ import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../constants/Identifiers.ts";
import { allTabValue, subscriptionTabValue } from "../constants/Misc.ts";
import { persistReducer } from "redux-persist";
import { subscriptionListFilter } from "../App.tsx";
import { ContentType, VideoListType } from "../state/features/persistSlice.ts";
export const useFetchVideos = () => {
const dispatch = useDispatch();
@ -194,15 +194,15 @@ export const useFetchVideos = () => {
category?: string;
subcategory?: string;
keywords?: string;
type?: string;
contentType?: ContentType;
};
const emptyFilters = {
const emptyFilters: FilterType = {
name: "",
category: "",
subcategory: "",
keywords: "",
type: "",
contentType: "videos",
};
const getVideos = React.useCallback(
async (
@ -210,16 +210,16 @@ export const useFetchVideos = () => {
reset?: boolean,
resetFilters?: boolean,
limit?: number,
listType = allTabValue
videoListType: VideoListType = "all"
) => {
emptyFilters.type = filters.type;
emptyFilters.contentType = filters.contentType;
try {
const {
name = "",
category = "",
subcategory = "",
keywords = "",
type = filters.type,
contentType = filters.contentType,
}: FilterType = resetFilters ? emptyFilters : filters;
let offset = videos.length;
if (reset) {
@ -231,9 +231,7 @@ export const useFetchVideos = () => {
if (name) {
defaultUrl = defaultUrl + `&name=${name}`;
}
if (listType === subscriptionTabValue) {
} else if (videoListType === "subscriptions") {
const filteredSubscribeList = await subscriptionListFilter(false);
filteredSubscribeList.map(sub => {
defaultUrl += `&name=${sub.subscriberName}`;
@ -252,7 +250,7 @@ export const useFetchVideos = () => {
if (keywords) {
defaultUrl = defaultUrl + `&query=${keywords}`;
}
if (type === "playlists") {
if (contentType === "playlists") {
defaultUrl = defaultUrl + `&service=PLAYLIST`;
defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}`;
} else {
@ -431,9 +429,7 @@ export const useFetchVideos = () => {
const totalVideosPublished = responseData.length;
const uniqueNames = new Set(responseData.map(video => video.name));
const totalNamesPublished = uniqueNames.size;
const videosPerNamePublished = (
totalVideosPublished / totalNamesPublished
).toFixed(0);
const videosPerNamePublished = totalVideosPublished / totalNamesPublished;
dispatch(setTotalVideosPublished(totalVideosPublished));
dispatch(setTotalNamesPublished(totalNamesPublished));

View File

@ -1,34 +1,37 @@
import React, { useMemo } from "react";
import { VideoListComponentLevel } from "../Home/VideoListComponentLevel";
import { HeaderContainer, ProfileContainer } from "./Profile-styles";
import { VideoListComponentLevel } from "../../Home/VideoListComponentLevel.tsx";
import { HeaderContainer, ProfileContainer } from "./Profile-styles.tsx";
import {
AuthorTextComment,
StyledCardColComment,
StyledCardHeaderComment,
} from "../VideoContent/VideoContent-styles";
} from "../VideoContent/VideoContent-styles.tsx";
import { Avatar, Box, useTheme } from "@mui/material";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { setUserAvatarHash } from "../../state/features/globalSlice";
import { RootState } from "../../state/store";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
import { setUserAvatarHash } from "../../../state/features/globalSlice.ts";
import { RootState } from "../../../state/store.ts";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
export const IndividualProfile = () => {
const { name: paramName } = useParams();
const { name: channelName } = useParams();
const userName = useSelector((state: RootState) => state.auth.user?.name);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const theme = useTheme();
const avatarUrl = useMemo(() => {
let url = "";
if (paramName && userAvatarHash[paramName]) {
url = userAvatarHash[paramName];
if (channelName && userAvatarHash[channelName]) {
url = userAvatarHash[channelName];
}
return url;
}, [userAvatarHash, paramName]);
}, [userAvatarHash, channelName]);
return (
<ProfileContainer>
<HeaderContainer>
@ -46,8 +49,8 @@ export const IndividualProfile = () => {
>
<Box>
<Avatar
src={`/arbitrary/THUMBNAIL/${paramName}/qortal_avatar`}
alt={`${paramName}'s avatar`}
src={`/arbitrary/THUMBNAIL/${channelName}/qortal_avatar`}
alt={`${channelName}'s avatar`}
/>
</Box>
<StyledCardColComment>
@ -58,13 +61,21 @@ export const IndividualProfile = () => {
: "#d6e8ff"
}
>
{paramName}
{channelName}
</AuthorTextComment>
</StyledCardColComment>
<SubscribeButton
subscriberName={paramName}
sx={{ marginLeft: "10px" }}
/>
{channelName !== userName && (
<>
<SubscribeButton
subscriberName={channelName}
sx={{ marginLeft: "10px" }}
/>
<FollowButton
followerName={channelName}
sx={{ marginLeft: "20px" }}
/>
</>
)}
</StyledCardHeaderComment>
</Box>
</HeaderContainer>

View File

@ -7,15 +7,18 @@ import React, {
} from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts";
import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { VideoPlayer } from "../../components/common/VideoPlayer/VideoPlayer.tsx";
import { RootState } from "../../state/store";
import { addToHashMap } from "../../state/features/videoSlice";
import {
refType,
VideoPlayer,
} from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { RootState } from "../../../state/store.ts";
import { addToHashMap } from "../../../state/features/videoSlice.ts";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import DownloadIcon from "@mui/icons-material/Download";
import mockImg from "../../test/mockimg.jpg";
import mockImg from "../../../test/mockimg.jpg";
import {
AuthorTextComment,
FileAttachmentContainer,
@ -26,39 +29,43 @@ import {
VideoDescription,
VideoPlayerContainer,
VideoTitle,
} from "./PlaylistContent-styles";
import { setUserAvatarHash } from "../../state/features/globalSlice";
} from "./PlaylistContent-styles.tsx";
import { setUserAvatarHash } from "../../../state/features/globalSlice.ts";
import {
formatDate,
formatDateSeconds,
formatTimestampSeconds,
} from "../../utils/time";
import { NavbarName } from "../../components/layout/Navbar/Navbar-styles";
import { CommentSection } from "../../components/common/Comments/CommentSection";
} from "../../../utils/time.ts";
import { NavbarName } from "../../../components/layout/Navbar/Navbar-styles.tsx";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../components/PublishVideo/PublishVideo-styles.tsx";
import { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement";
import { SuperLike } from "../../components/common/SuperLike/SuperLike";
import { useFetchSuperLikes } from "../../hooks/useFetchSuperLikes";
} from "../../../components/Publish/PublishVideo/PublishVideo-styles.tsx";
import { Playlists } from "../../../components/Playlists/Playlists.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import FileElement from "../../../components/common/FileElement.tsx";
import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx";
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
import {
extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../VideoContent/VideoContent";
import { SuperLikesSection } from "../../components/common/SuperLikesList/SuperLikesSection";
} from "../VideoContent/VideoContent.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import {
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
} from "../../constants/Identifiers.ts";
import { minPriceSuperlike } from "../../constants/Misc.ts";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
} from "../../../constants/Identifiers.ts";
import { minPriceSuperlike } from "../../../constants/Misc.ts";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAndDislike.tsx";
export const PlaylistContent = () => {
const { name, id } = useParams();
const { name: channelName, id } = useParams();
const userName = useSelector((state: RootState) => state.auth.user?.name);
const [doAutoPlay, setDoAutoPlay] = useState(false);
const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false);
@ -68,6 +75,7 @@ export const PlaylistContent = () => {
const [superlikeList, setSuperlikelist] = useState<any[]>([]);
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const containerRef = useRef<refType>(null);
const calculateAmountSuperlike = useMemo(() => {
const totalQort = superlikeList?.reduce((acc, curr) => {
@ -95,10 +103,10 @@ export const PlaylistContent = () => {
};
useEffect(() => {
if (name) {
getAddressName(name);
if (channelName) {
getAddressName(channelName);
}
}, [name]);
}, [channelName]);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
@ -107,12 +115,12 @@ export const PlaylistContent = () => {
const avatarUrl = useMemo(() => {
let url = "";
if (name && userAvatarHash[name]) {
url = userAvatarHash[name];
if (channelName && userAvatarHash[channelName]) {
url = userAvatarHash[channelName];
}
return url;
}, [userAvatarHash, name]);
}, [userAvatarHash, channelName]);
const navigate = useNavigate();
const theme = useTheme();
@ -282,10 +290,10 @@ export const PlaylistContent = () => {
);
React.useEffect(() => {
if (name && id) {
checkforPlaylist(name, id);
if (channelName && id) {
checkforPlaylist(channelName, id);
}
}, [id, name]);
}, [id, channelName]);
useEffect(() => {
if (contentRef.current) {
@ -395,6 +403,14 @@ export const PlaylistContent = () => {
getComments(videoData?.id, nameAddress);
}, [getComments, videoData?.id, nameAddress]);
const focusVideo = (e: React.MouseEvent<HTMLDivElement>) => {
const focusRef = containerRef.current?.getContainerRef()?.current;
const isCorrectTarget = e.currentTarget == e.target;
if (focusRef && isCorrectTarget) {
focusRef.focus({ preventScroll: true });
}
};
return (
<Box
sx={{
@ -403,6 +419,7 @@ export const PlaylistContent = () => {
flexDirection: "column",
padding: "20px 10px",
}}
onClick={focusVideo}
>
<VideoPlayerContainer
sx={{
@ -434,13 +451,14 @@ export const PlaylistContent = () => {
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={name}
user={channelName}
jsonId={id}
poster={videoCover || ""}
nextVideo={nextVideo}
onEnd={onEndVideo}
autoPlay={doAutoPlay}
customStyle={{ aspectRatio: "16/9" }}
ref={containerRef}
/>
)}
{playlistData && (
@ -473,12 +491,12 @@ export const PlaylistContent = () => {
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${name}`);
navigate(`/channel/${channelName}`);
}}
>
<Avatar
src={`/arbitrary/THUMBNAIL/${name}/qortal_avatar`}
alt={`${name}'s avatar`}
src={`/arbitrary/THUMBNAIL/${channelName}/qortal_avatar`}
alt={`${channelName}'s avatar`}
/>
</Box>
<StyledCardColComment>
@ -492,14 +510,22 @@ export const PlaylistContent = () => {
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${name}`);
navigate(`/channel/${channelName}`);
}}
>
{name}
<SubscribeButton
subscriberName={name}
sx={{ marginLeft: "20px" }}
/>
{channelName}
{channelName !== userName && (
<>
<SubscribeButton
subscriberName={channelName}
sx={{ marginLeft: "20px" }}
/>
<FollowButton
followerName={channelName}
sx={{ marginLeft: "20px" }}
/>
</>
)}
</AuthorTextComment>
</StyledCardColComment>
</StyledCardHeaderComment>
@ -511,16 +537,22 @@ export const PlaylistContent = () => {
}}
>
{videoData && (
<SuperLike
numberOfSuperlikes={numberOfSuperlikes}
totalAmount={calculateAmountSuperlike}
name={videoData?.user}
service={videoData?.service}
identifier={videoData?.id}
onSuccess={val => {
setSuperlikelist(prev => [val, ...prev]);
}}
/>
<>
<LikeAndDislike
name={videoData?.user}
identifier={videoData?.id}
/>
<SuperLike
numberOfSuperlikes={numberOfSuperlikes}
totalAmount={calculateAmountSuperlike}
name={videoData?.user}
service={videoData?.service}
identifier={videoData?.id}
onSuccess={val => {
setSuperlikelist(prev => [val, ...prev]);
}}
/>
</>
)}
<FileAttachmentContainer>
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
@ -677,7 +709,10 @@ export const PlaylistContent = () => {
maxWidth: "1200px",
}}
>
<CommentSection postId={videoData?.id || ""} postName={name || ""} />
<CommentSection
postId={videoData?.id || ""}
postName={channelName || ""}
/>
</Box>
</Box>
);

View File

@ -7,15 +7,18 @@ import React, {
} from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts";
import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { VideoPlayer } from "../../components/common/VideoPlayer/VideoPlayer.tsx";
import { RootState } from "../../state/store";
import { addToHashMap } from "../../state/features/videoSlice";
import {
refType,
VideoPlayer,
} from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { RootState } from "../../../state/store.ts";
import { addToHashMap } from "../../../state/features/videoSlice.ts";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import DownloadIcon from "@mui/icons-material/Download";
import mockImg from "../../test/mockimg.jpg";
import mockImg from "../../../test/mockimg.jpg";
import {
AuthorTextComment,
FileAttachmentContainer,
@ -26,37 +29,39 @@ import {
VideoDescription,
VideoPlayerContainer,
VideoTitle,
} from "./VideoContent-styles";
import { setUserAvatarHash } from "../../state/features/globalSlice";
} from "./VideoContent-styles.tsx";
import { setUserAvatarHash } from "../../../state/features/globalSlice.ts";
import {
formatDate,
formatDateSeconds,
formatTimestampSeconds,
} from "../../utils/time";
import { NavbarName } from "../../components/layout/Navbar/Navbar-styles";
import { CommentSection } from "../../components/common/Comments/CommentSection";
} from "../../../utils/time.ts";
import { NavbarName } from "../../../components/layout/Navbar/Navbar-styles.tsx";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import {
CrowdfundSubTitle,
CrowdfundSubTitleRow,
} from "../../components/PublishVideo/PublishVideo-styles.tsx";
import { Playlists } from "../../components/Playlists/Playlists";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement";
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";
} from "../../../components/Publish/PublishVideo/PublishVideo-styles.tsx";
import { Playlists } from "../../../components/Playlists/Playlists.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import FileElement from "../../../components/common/FileElement.tsx";
import { SuperLike } from "../../../components/common/ContentButtons/SuperLike.tsx";
import { CommentContainer } from "../../../components/common/Comments/Comments-styles.tsx";
import { Comment } from "../../../components/common/Comments/Comment.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
import {
FOR_SUPER_LIKE,
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
} from "../../constants/Identifiers.ts";
} from "../../../constants/Identifiers.ts";
import {
minPriceSuperlike,
titleFormatterOnSave,
} from "../../constants/Misc.ts";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
} from "../../../constants/Misc.ts";
import { SubscribeButton } from "../../../components/common/ContentButtons/SubscribeButton.tsx";
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
import { LikeAndDislike } from "../../../components/common/ContentButtons/LikeAndDislike.tsx";
export function isTimestampWithinRange(resTimestamp, resCreated) {
// Calculate the absolute difference in milliseconds
@ -116,12 +121,15 @@ export const getPaymentInfo = async (signature: string) => {
};
export const VideoContent = () => {
const { name, id } = useParams();
const { name: channelName, id } = useParams();
const userName = useSelector((state: RootState) => state.auth.user?.name);
const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false);
const [superlikeList, setSuperlikelist] = useState<any[]>([]);
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const containerRef = useRef<refType>(null);
const calculateAmountSuperlike = useMemo(() => {
const totalQort = superlikeList?.reduce((acc, curr) => {
@ -143,6 +151,7 @@ export const VideoContent = () => {
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const contentRef = useRef(null);
const getAddressName = async name => {
@ -157,18 +166,18 @@ export const VideoContent = () => {
};
useEffect(() => {
if (name) {
getAddressName(name);
if (channelName) {
getAddressName(channelName);
}
}, [name]);
}, [channelName]);
const avatarUrl = useMemo(() => {
let url = "";
if (name && userAvatarHash[name]) {
url = userAvatarHash[name];
if (channelName && userAvatarHash[channelName]) {
url = userAvatarHash[channelName];
}
return url;
}, [userAvatarHash, name]);
}, [userAvatarHash, channelName]);
const navigate = useNavigate();
const theme = useTheme();
@ -279,16 +288,16 @@ export const VideoContent = () => {
}, []);
React.useEffect(() => {
if (name && id) {
const existingVideo = hashMapVideos[id + "-" + name];
if (channelName && id) {
const existingVideo = hashMapVideos[id + "-" + channelName];
if (existingVideo) {
setVideoData(existingVideo);
} else {
getVideoData(name, id);
getVideoData(channelName, id);
}
}
}, [id, name]);
}, [id, channelName]);
useEffect(() => {
if (contentRef.current) {
@ -367,6 +376,14 @@ export const VideoContent = () => {
(state: RootState) => state.persist.subscriptionList
);
const focusVideo = (e: React.MouseEvent<HTMLDivElement>) => {
const focusRef = containerRef.current?.getContainerRef()?.current;
const isCorrectTarget = e.currentTarget == e.target;
if (focusRef && isCorrectTarget) {
focusRef.focus({ preventScroll: true });
}
};
return (
<Box
sx={{
@ -375,6 +392,7 @@ export const VideoContent = () => {
flexDirection: "column",
padding: "20px 10px",
}}
onClick={focusVideo}
>
<VideoPlayerContainer
sx={{
@ -387,10 +405,11 @@ export const VideoContent = () => {
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={name}
user={channelName}
jsonId={id}
poster={videoCover || ""}
customStyle={{ aspectRatio: "16/9" }}
ref={containerRef}
/>
)}
<Box
@ -414,12 +433,12 @@ export const VideoContent = () => {
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${name}`);
navigate(`/channel/${channelName}`);
}}
>
<Avatar
src={`/arbitrary/THUMBNAIL/${name}/qortal_avatar`}
alt={`${name}'s avatar`}
src={`/arbitrary/THUMBNAIL/${channelName}/qortal_avatar`}
alt={`${channelName}'s avatar`}
/>
</Box>
<StyledCardColComment>
@ -433,14 +452,22 @@ export const VideoContent = () => {
cursor: "pointer",
}}
onClick={() => {
navigate(`/channel/${name}`);
navigate(`/channel/${channelName}`);
}}
>
{name}
<SubscribeButton
subscriberName={name}
sx={{ marginLeft: "20px" }}
/>
{channelName}
{channelName !== userName && (
<>
<SubscribeButton
subscriberName={channelName}
sx={{ marginLeft: "20px" }}
/>
<FollowButton
followerName={channelName}
sx={{ marginLeft: "20px" }}
/>
</>
)}
</AuthorTextComment>
</StyledCardColComment>
</StyledCardHeaderComment>
@ -452,16 +479,22 @@ export const VideoContent = () => {
}}
>
{videoData && (
<SuperLike
numberOfSuperlikes={numberOfSuperlikes}
totalAmount={calculateAmountSuperlike}
name={videoData?.user}
service={videoData?.service}
identifier={videoData?.id}
onSuccess={val => {
setSuperlikelist(prev => [val, ...prev]);
}}
/>
<>
<LikeAndDislike
name={videoData?.user}
identifier={videoData?.id}
/>
<SuperLike
numberOfSuperlikes={numberOfSuperlikes}
totalAmount={calculateAmountSuperlike}
name={videoData?.user}
service={videoData?.service}
identifier={videoData?.id}
onSuccess={val => {
setSuperlikelist(prev => [val, ...prev]);
}}
/>
</>
)}
<FileAttachmentContainer>
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
@ -598,7 +631,7 @@ export const VideoContent = () => {
loadingSuperLikes={loadingSuperLikes}
superlikes={superlikeList}
postId={id || ""}
postName={name || ""}
postName={channelName || ""}
/>
<Box
@ -609,7 +642,7 @@ export const VideoContent = () => {
maxWidth: "1200px",
}}
>
<CommentSection postId={id || ""} postName={name || ""} />
<CommentSection postId={id || ""} postName={channelName || ""} />
</Box>
</Box>
);

View File

@ -37,12 +37,12 @@ import {
import {
changeFilterType,
resetSubscriptions,
VideoListType,
} from "../../state/features/persistSlice.ts";
import { categories, subCategories } from "../../constants/Categories.ts";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { TabContext, TabList, TabPanel } from "@mui/lab";
import VideoList from "./VideoList.tsx";
import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts";
import { setHomePageSelectedTab } from "../../state/features/persistSlice.ts";
import { StatsData } from "../../components/StatsData.tsx";
@ -74,7 +74,9 @@ export const Home = ({ mode }: HomeProps) => {
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>(persistReducer.selectedTab);
const [tabValue, setTabValue] = useState<VideoListType>(
persistReducer.selectedTab
);
const tabFontSize = "20px";
@ -123,14 +125,14 @@ export const Home = ({ mode }: HomeProps) => {
if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return;
isFetching.current = true;
console.log("in getvideoshandler");
await getVideos(
{
name: filterName,
category: selectedCategoryVideos?.id,
subcategory: selectedSubCategoryVideos?.id,
keywords: filterSearch,
type: filterType,
contentType: filterType,
},
reset,
resetFilters,
@ -171,7 +173,7 @@ export const Home = ({ mode }: HomeProps) => {
category: "",
subcategory: "",
keywords: "",
type: filterType,
contentType: filterType,
},
null,
null,
@ -269,11 +271,10 @@ export const Home = ({ mode }: HomeProps) => {
};
useEffect(() => {
console.log("useeffect 5");
getVideosHandler(true);
}, [tabValue]);
const changeTab = (e: React.SyntheticEvent, newValue: string) => {
const changeTab = (e: React.SyntheticEvent, newValue: VideoListType) => {
setTabValue(newValue);
dispatch(setHomePageSelectedTab(newValue));
};
@ -516,23 +517,23 @@ export const Home = ({ mode }: HomeProps) => {
>
<Tab
label="All Videos"
value={allTabValue}
value={"all"}
sx={{ fontSize: tabFontSize }}
/>
<Tab
label="Subscriptions"
value={subscriptionTabValue}
value={"subscriptions"}
sx={{ fontSize: tabFontSize }}
/>
</TabList>
<TabPanel value={allTabValue} sx={{ width: "100%" }}>
<TabPanel value={"all"} sx={{ width: "100%" }}>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</TabPanel>
<TabPanel value={subscriptionTabValue} sx={{ width: "100%" }}>
<TabPanel value={"subscriptions"} sx={{ width: "100%" }}>
{filteredSubscriptionList.length > 0 ? (
<>
<VideoList videos={videos} />
@ -541,10 +542,12 @@ export const Home = ({ mode }: HomeProps) => {
isLoading={isLoading}
></LazyLoad>
</>
) : (
) : !isLoading ? (
<div style={{ textAlign: "center" }}>
You have no subscriptions
</div>
) : (
<></>
)}
</TabPanel>
</TabContext>

View File

@ -1,14 +1,19 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts";
import { SubscriptionData } from "../../components/common/SubscribeButton.tsx";
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
type SubscriptionListFilterType = "ALL" | "currentNameOnly";
import { SubscriptionData } from "../../components/common/ContentButtons/SubscribeButton.tsx";
export type StretchVideoType =
| "contain"
| "fill"
| "cover"
| "none"
| "scale-down";
export type SubscriptionListFilterType = "ALL" | "currentNameOnly";
export type ContentType = "videos" | "playlists";
export type VideoListType = "all" | "subscriptions";
interface settingsState {
selectedTab: string;
selectedTab: VideoListType;
stretchVideoSetting: StretchVideoType;
filterType: string;
filterType: ContentType;
subscriptionList: SubscriptionData[];
playbackRate: number;
subscriptionListFilter: SubscriptionListFilterType;
@ -16,7 +21,7 @@ interface settingsState {
}
const initialState: settingsState = {
selectedTab: allTabValue,
selectedTab: "all",
stretchVideoSetting: "contain",
filterType: "videos",
subscriptionList: [],

View File

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SubscriptionData } from "../../components/common/SubscribeButton";
import { SubscriptionData } from "../../components/common/ContentButtons/SubscribeButton.tsx";
interface GlobalState {
videos: Video[];

View File

@ -2,6 +2,7 @@ import {
AccountInfo,
AccountName,
GetRequestData,
SearchResourcesResponse,
SearchTransactionResponse,
TransactionSearchParams,
} from "./qortalRequestTypes.ts";
@ -46,3 +47,30 @@ export const searchTransactions = async (params: TransactionSearchParams) => {
...params,
})) as SearchTransactionResponse[];
};
export const fetchResourcesByIdentifier = async <T>(
service: string,
identifier: string
) => {
const names: SearchResourcesResponse[] = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service,
identifier,
includeMetadata: false,
});
const distinctNames = names.filter(
(searchResponse, index) => names.indexOf(searchResponse) === index
);
const promises: Promise<T>[] = [];
distinctNames.map(response => {
const resource: Promise<T> = qortalRequest({
action: "FETCH_QDN_RESOURCE",
name: response.name,
service,
identifier,
});
promises.push(resource);
});
return (await Promise.all(promises)) as T[];
};

View File

@ -23,6 +23,23 @@ export interface SearchTransactionResponse {
amount: string;
}
export interface MetaData {
title: string;
description: string;
tags: string[];
mimeType: string;
}
export interface SearchResourcesResponse {
name: string;
service: string;
identifier: string;
metadata?: MetaData;
size: number;
created: number;
updated: number;
}
export type TransactionType =
| "GENESIS"
| "PAYMENT"

View File

@ -18,14 +18,14 @@ import {
import { VideoPlayerGlobal } from "../components/common/VideoPlayer/VideoPlayerGlobal.tsx";
import { Rnd } from "react-rnd";
import { RequestQueue } from "../utils/queue";
import { EditVideo } from "../components/EditVideo/EditVideo";
import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist";
import { EditVideo } from "../components/Publish/EditVideo/EditVideo";
import { EditPlaylist } from "../components/Publish/EditPlaylist/EditPlaylist";
import ConsentModal from "../components/common/ConsentModal";
import {
extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../pages/VideoContent/VideoContent";
} from "../pages/ContentPages/VideoContent/VideoContent";
import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes";
import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts";
import { minPriceSuperlike } from "../constants/Misc.ts";
@ -143,8 +143,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const getSuperlikes = useCallback(async () => {
try {
let totalCount = 0
let validCount = 0
let totalCount = 0;
let validCount = 0;
let comments: any[] = [];
while (validCount < 20 && totalCount < 100) {
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=1&offset=${totalCount}&includemetadata=true&reverse=true&excludeblocked=true`;
@ -177,12 +177,12 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
});
comments = [
...comments,
{
...comment,
message: "",
amount: res.amount,
},
];
{
...comment,
message: "",
amount: res.amount,
},
];
validCount++;
}
} catch (error) {}