Browse Source

Merge pull request #23 from QortalSeth/main

Follow, Like, and Dislike buttons Added to  Video, Playlist, and Channel pages
pull/24/head^2
Qortal Dev 6 months ago committed by GitHub
parent
commit
b8463daef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      src/App.tsx
  2. 2
      src/components/Playlists/Playlists.tsx
  3. 24
      src/components/Publish/EditPlaylist/EditPlaylist.tsx
  4. 28
      src/components/Publish/EditPlaylist/Upload-styles.tsx
  5. 28
      src/components/Publish/EditVideo/EditVideo-styles.tsx
  6. 24
      src/components/Publish/EditVideo/EditVideo.tsx
  7. 4
      src/components/Publish/MultiplePublish/MultiplePublishAll.tsx
  8. 8
      src/components/Publish/PlaylistListEdit/PlaylistListEdit.tsx
  9. 2
      src/components/Publish/PublishVideo/PublishVideo-styles.tsx
  10. 32
      src/components/Publish/PublishVideo/PublishVideo.tsx
  11. 7
      src/components/StatsData.tsx
  12. 2
      src/components/common/Comments/CommentSection.tsx
  13. 159
      src/components/common/ContentButtons/FollowButton.tsx
  14. 66
      src/components/common/ContentButtons/LikeAndDislike-functions.ts
  15. 230
      src/components/common/ContentButtons/LikeAndDislike.tsx
  16. 48
      src/components/common/ContentButtons/SubscribeButton.tsx
  17. 97
      src/components/common/ContentButtons/SuperLike.tsx
  18. 2
      src/components/common/Notifications/Notifications.tsx
  19. 2
      src/components/common/SuperLikesList/SuperLikesSection.tsx
  20. 25
      src/components/common/VideoPlayer/VideoPlayer-styles.ts
  21. 1759
      src/components/common/VideoPlayer/VideoPlayer.tsx
  22. 4
      src/components/layout/Navbar/Navbar.tsx
  23. 11
      src/constants/Categories.ts
  24. 4
      src/constants/Identifiers.ts
  25. 5
      src/constants/Misc.ts
  26. 24
      src/hooks/useFetchVideos.tsx
  27. 45
      src/pages/ContentPages/IndividualProfile/IndividualProfile.tsx
  28. 0
      src/pages/ContentPages/IndividualProfile/Profile-styles.tsx
  29. 0
      src/pages/ContentPages/PlaylistContent/PlaylistContent-styles.tsx
  30. 139
      src/pages/ContentPages/PlaylistContent/PlaylistContent.tsx
  31. 0
      src/pages/ContentPages/VideoContent/VideoContent-styles.tsx
  32. 143
      src/pages/ContentPages/VideoContent/VideoContent.tsx
  33. 27
      src/pages/Home/Home.tsx
  34. 21
      src/state/features/persistSlice.ts
  35. 2
      src/state/features/videoSlice.ts
  36. 28
      src/utils/qortalRequestFunctions.ts
  37. 17
      src/utils/qortalRequestTypes.ts
  38. 22
      src/wrappers/GlobalWrapper.tsx

8
src/App.tsx

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

2
src/components/Playlists/Playlists.tsx

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

24
src/components/EditPlaylist/EditPlaylist.tsx → src/components/Publish/EditPlaylist/EditPlaylist.tsx

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

28
src/components/EditPlaylist/Upload-styles.tsx → src/components/Publish/EditPlaylist/Upload-styles.tsx

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

28
src/components/EditVideo/EditVideo-styles.tsx → src/components/Publish/EditVideo/EditVideo-styles.tsx

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

24
src/components/EditVideo/EditVideo.tsx → src/components/Publish/EditVideo/EditVideo.tsx

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

4
src/components/common/MultiplePublish/MultiplePublishAll.tsx → src/components/Publish/MultiplePublish/MultiplePublishAll.tsx

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

8
src/components/PlaylistListEdit/PlaylistListEdit.tsx → src/components/Publish/PlaylistListEdit/PlaylistListEdit.tsx

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

2
src/components/PublishVideo/PublishVideo-styles.tsx → src/components/Publish/PublishVideo/PublishVideo-styles.tsx

@ -12,7 +12,7 @@ import {
Select, Select,
} from "@mui/material"; } from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; 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)` export const DoubleLine = styled(Typography)`
display: -webkit-box; display: -webkit-box;

32
src/components/PublishVideo/PublishVideo.tsx → src/components/Publish/PublishVideo/PublishVideo.tsx

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

7
src/components/StatsData.tsx

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

2
src/components/common/Comments/CommentSection.tsx

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

159
src/components/common/ContentButtons/FollowButton.tsx

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

66
src/components/common/ContentButtons/LikeAndDislike-functions.ts

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

230
src/components/common/ContentButtons/LikeAndDislike.tsx

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

48
src/components/common/SubscribeButton.tsx → src/components/common/ContentButtons/SubscribeButton.tsx

@ -1,10 +1,14 @@
import { Button, ButtonProps } from "@mui/material"; import { Button, ButtonProps, Tooltip } from "@mui/material";
import { MouseEvent, useEffect, useState } from "react"; import { MouseEvent, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts"; import { RootState } from "../../../state/store.ts";
import { subscribe, unSubscribe } from "../../state/features/persistSlice.ts"; import {
import { setFilteredSubscriptions } from "../../state/features/videoSlice.ts"; subscribe,
import { subscriptionListFilter } from "../../App.tsx"; 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 { interface SubscribeButtonProps extends ButtonProps {
subscriberName: string; subscriberName: string;
@ -89,15 +93,31 @@ export const SubscribeButton = ({
height: "45px", height: "45px",
...props.sx, ...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 ( return (
<Button <Tooltip title={tooltipTitle} placement={"top"} arrow>
{...props} <Button
variant={"contained"} {...props}
color="error" variant={"contained"}
sx={buttonStyle} color="error"
onClick={e => manageSubscription(e)} sx={buttonStyle}
> onClick={e => manageSubscription(e)}
{isSubscribed ? "Unsubscribe" : "Subscribe"} >
</Button> {isSubscribed ? "Unsubscribe" : "Subscribe"}
</Button>
</Tooltip>
); );
}; };

97
src/components/common/SuperLike/SuperLike.tsx → src/components/common/ContentButtons/SuperLike.tsx

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

2
src/components/common/Notifications/Notifications.tsx

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

2
src/components/common/SuperLikesList/SuperLikesSection.tsx

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

25
src/components/common/VideoPlayer/VideoPlayer-styles.ts

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

1759
src/components/common/VideoPlayer/VideoPlayer.tsx

File diff suppressed because it is too large Load Diff

4
src/components/layout/Navbar/Navbar.tsx

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

11
src/constants/Categories.ts

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

4
src/constants/Identifiers.ts

@ -8,8 +8,12 @@ export const QTUBE_PLAYLIST_BASE = useTestIdentifiers
export const SUPER_LIKE_BASE = useTestIdentifiers export const SUPER_LIKE_BASE = useTestIdentifiers
? "MYTEST_superlike_" ? "MYTEST_superlike_"
: "qtube_superlike_"; : "qtube_superlike_";
export const LIKE_BASE = useTestIdentifiers ? "MYTEST_like_" : "qtube_like_";
export const COMMENT_BASE = useTestIdentifiers export const COMMENT_BASE = useTestIdentifiers
? "qcomment_v1_MYTEST_" ? "qcomment_v1_MYTEST_"
: "qcomment_v1_qtube_"; : "qcomment_v1_qtube_";
export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962"; export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962";
export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`; export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`;
export const FOR_LIKE = useTestIdentifiers ? "MYTEST_like" : `qtube_like`;

5
src/constants/Misc.ts

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

24
src/hooks/useFetchVideos.tsx

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

45
src/pages/IndividualProfile/IndividualProfile.tsx → src/pages/ContentPages/IndividualProfile/IndividualProfile.tsx

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

0
src/pages/IndividualProfile/Profile-styles.tsx → src/pages/ContentPages/IndividualProfile/Profile-styles.tsx

0
src/pages/PlaylistContent/PlaylistContent-styles.tsx → src/pages/ContentPages/PlaylistContent/PlaylistContent-styles.tsx

139
src/pages/PlaylistContent/PlaylistContent.tsx → src/pages/ContentPages/PlaylistContent/PlaylistContent.tsx

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

0
src/pages/VideoContent/VideoContent-styles.tsx → src/pages/ContentPages/VideoContent/VideoContent-styles.tsx

143
src/pages/VideoContent/VideoContent.tsx → src/pages/ContentPages/VideoContent/VideoContent.tsx

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

27
src/pages/Home/Home.tsx

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

21
src/state/features/persistSlice.ts

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

2
src/state/features/videoSlice.ts

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

28
src/utils/qortalRequestFunctions.ts

@ -2,6 +2,7 @@ import {
AccountInfo, AccountInfo,
AccountName, AccountName,
GetRequestData, GetRequestData,
SearchResourcesResponse,
SearchTransactionResponse, SearchTransactionResponse,
TransactionSearchParams, TransactionSearchParams,
} from "./qortalRequestTypes.ts"; } from "./qortalRequestTypes.ts";
@ -46,3 +47,30 @@ export const searchTransactions = async (params: TransactionSearchParams) => {
...params, ...params,
})) as SearchTransactionResponse[]; })) 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[];
};

17
src/utils/qortalRequestTypes.ts

@ -23,6 +23,23 @@ export interface SearchTransactionResponse {
amount: string; 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 = export type TransactionType =
| "GENESIS" | "GENESIS"
| "PAYMENT" | "PAYMENT"

22
src/wrappers/GlobalWrapper.tsx

@ -18,14 +18,14 @@ import {
import { VideoPlayerGlobal } from "../components/common/VideoPlayer/VideoPlayerGlobal.tsx"; import { VideoPlayerGlobal } from "../components/common/VideoPlayer/VideoPlayerGlobal.tsx";
import { Rnd } from "react-rnd"; import { Rnd } from "react-rnd";
import { RequestQueue } from "../utils/queue"; import { RequestQueue } from "../utils/queue";
import { EditVideo } from "../components/EditVideo/EditVideo"; import { EditVideo } from "../components/Publish/EditVideo/EditVideo";
import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist"; import { EditPlaylist } from "../components/Publish/EditPlaylist/EditPlaylist";
import ConsentModal from "../components/common/ConsentModal"; import ConsentModal from "../components/common/ConsentModal";
import { import {
extractSigValue, extractSigValue,
getPaymentInfo, getPaymentInfo,
isTimestampWithinRange, isTimestampWithinRange,
} from "../pages/VideoContent/VideoContent"; } from "../pages/ContentPages/VideoContent/VideoContent";
import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes"; import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes";
import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts"; import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts";
import { minPriceSuperlike } from "../constants/Misc.ts"; import { minPriceSuperlike } from "../constants/Misc.ts";
@ -143,8 +143,8 @@ const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => {
const getSuperlikes = useCallback(async () => { const getSuperlikes = useCallback(async () => {
try { try {
let totalCount = 0 let totalCount = 0;
let validCount = 0 let validCount = 0;
let comments: any[] = []; let comments: any[] = [];
while (validCount < 20 && totalCount < 100) { 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`; 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 = [
...comments, ...comments,
{ {
...comment, ...comment,
message: "", message: "",
amount: res.amount, amount: res.amount,
}, },
]; ];
validCount++; validCount++;
} }
} catch (error) {} } catch (error) {}

Loading…
Cancel
Save