Browse Source

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
pull/23/head
Qortal Dev 6 months ago
parent
commit
9cf2932d81
  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