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

Fixed Bug that allowed publishing issue with non-unique title if the title consisted only of characters that are filtered out when sanitized.

More references to Q-Share removed. Issue page has its routing change from /share to /issue

Consent modal changed app name from Q-Share to Q-Support

User can no longer block themselves

Edit and Block buttons in IssueList.tsx no longer have tooltips. Instead, a description is in text visible when the button is loaded to make it easier to see both the button as a whole as well as what it does.
This commit is contained in:
Qortal Dev 2024-04-16 16:01:03 -06:00
parent 7d700ccc74
commit 92f5c236b6
15 changed files with 252 additions and 272 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Q-Share</title>
<title>Q-Support</title>
</head>
<body>
<div id="root"></div>

View File

@ -26,7 +26,7 @@ function App() {
<CssBaseline />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/share/:name/:id" element={<IssueContent />} />
<Route path="/issue/:name/:id" element={<IssueContent />} />
<Route path="/channel/:name" element={<IndividualProfile />} />
</Routes>
</GlobalWrapper>

View File

@ -144,7 +144,6 @@ export const EditIssue = () => {
async function publishQDNResource() {
try {
const categoryList = categoryListRef.current?.getSelectedCategories();
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!categoryList[0]) throw new Error("Please select a category");
if (!editFileProperties) return;
@ -173,10 +172,6 @@ export const EditIssue = () => {
);
return;
}
let fileReferences = [];
let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description);
const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "")
@ -185,6 +180,12 @@ export const EditIssue = () => {
.trim()
.toLowerCase();
if (!sanitizeTitle) throw new Error("Please enter a title");
let fileReferences = [];
let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description);
for (const publish of files) {
if (publish?.identifier) {
fileReferences.push(publish);

View File

@ -219,7 +219,6 @@ export const EditPlaylist = () => {
async function publishQDNResource() {
try {
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!coverImage) throw new Error("Please select cover image");
if (!selectedCategoryVideos) throw new Error("Please select a category");
@ -249,6 +248,16 @@ export const EditPlaylist = () => {
);
return;
}
const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim()
.toLowerCase();
if (!sanitizeTitle) throw new Error("Please enter a title");
const category = selectedCategoryVideos.id;
const subcategory = selectedSubCategoryVideos?.id || "";
@ -308,12 +317,7 @@ export const EditPlaylist = () => {
// Description is obtained from raw data
let identifier = editVideoProperties?.id;
const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim()
.toLowerCase();
if (isNew) {
identifier = `${QSUPPORT_PLAYLIST_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
}

View File

@ -128,8 +128,6 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
try {
if (!categoryListRef.current) throw new Error("No CategoryListRef found");
if (!userAddress) throw new Error("Unable to locate user address");
if (!title) throw new Error("Please enter a title");
if (!description) throw new Error("Please enter a description");
if (!categoryListRef.current?.getSelectedCategories()[0])
throw new Error("Please select a category");
@ -157,18 +155,19 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
return;
}
let fileReferences = [];
let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description);
const sanitizeTitle = title
.replace(/[^a-zA-Z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim()
.toLowerCase();
if (!sanitizeTitle) throw new Error("Please enter a title");
let fileReferences = [];
let listOfPublishes = [];
const fullDescription = extractTextFromHTML(description);
for (const publish of files) {
const file = publish.file;

View File

@ -1,25 +1,21 @@
import { styled } from '@mui/system';
import {
Box,
Modal,
Typography
} from '@mui/material';
import { styled } from "@mui/system";
import { Box, Modal, Typography } from "@mui/material";
export const StyledModal = styled(Modal)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}))
display: "flex",
alignItems: "center",
justifyContent: "center",
}));
export const ModalContent = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
backgroundColor: theme.palette.background.default,
padding: theme.spacing(4),
borderRadius: theme.spacing(1),
width: '40%',
'&:focus': {
outline: 'none'
}
}))
width: "40%",
"&:focus": {
outline: "none",
},
}));
export const ModalText = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",

View File

@ -1,18 +1,9 @@
import React, { useState } from "react";
import { Button, List, ListItem, Typography, useTheme } from "@mui/material";
import {
Box,
Button,
Modal,
Typography,
SelectChangeEvent,
ListItem,
List,
useTheme
} from "@mui/material";
import {
StyledModal,
ModalContent,
ModalText
ModalText,
StyledModal,
} from "./BlockedNamesModal-styles";
interface PostModalProps {
@ -22,7 +13,7 @@ interface PostModalProps {
export const BlockedNamesModal: React.FC<PostModalProps> = ({
open,
onClose
onClose,
}) => {
const [blockedNames, setBlockedNames] = useState<string[]>([]);
const theme = useTheme();
@ -31,7 +22,7 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
const listName = `blockedNames`;
const response = await qortalRequest({
action: "GET_LIST_ITEMS",
list_name: listName
list_name: listName,
});
setBlockedNames(response);
} catch (error) {
@ -48,11 +39,11 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
const response = await qortalRequest({
action: "DELETE_LIST_ITEM",
list_name: "blockedNames",
item: name
item: name,
});
if (response === true) {
setBlockedNames((prev) => prev.filter((n) => n !== name));
setBlockedNames(prev => prev.filter(n => n !== name));
}
} catch (error) {}
};
@ -67,22 +58,22 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
display: "flex",
flexDirection: "column",
flex: "1",
overflow: "auto"
overflow: "auto",
}}
>
{blockedNames.map((name, index) => (
<ListItem
key={name + index}
sx={{
display: "flex"
display: "flex",
}}
>
<Typography>{name}</Typography>
<Button
sx={{
backgroundColor: theme.palette.primary.light,
backgroundColor: theme.palette.primary.main,
color: theme.palette.text.primary,
fontFamily: "Raleway"
fontFamily: "Raleway",
}}
onClick={() => removeFromBlockList(name)}
>
@ -91,7 +82,15 @@ export const BlockedNamesModal: React.FC<PostModalProps> = ({
</ListItem>
))}
</List>
<Button variant="contained" color="primary" onClick={onClose}>
<Button
variant="contained"
onClick={onClose}
sx={{
backgroundColor: theme.palette.primary.light,
color: theme.palette.text.primary,
fontFamily: "Raleway",
}}
>
Close
</Button>
</ModalContent>

View File

@ -7,8 +7,9 @@ import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import localForage from "localforage";
import { useTheme } from "@mui/material";
const generalLocal = localForage.createInstance({
name: "q-share-general",
name: "q-support-general",
});
export default function ConsentModal() {
@ -44,13 +45,15 @@ export default function ConsentModal() {
<DialogTitle id="alert-dialog-title">Welcome</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Q-Share is currently in its first version and as such there could be
some bugs. The Qortal community, along with its development team and
the creators of this application, cannot be held accountable for any
content published or displayed. Also, they are not responsible for
any loss of coin due to either bad actors or bugs in the
Q-Support is currently in its first version and as such there could
be some bugs. The Qortal community, along with its development team
and the creators of this application, cannot be held accountable for
any content published or displayed. Also, they are not responsible
for any loss of coin due to either bad actors or bugs in the
application. Furthermore, they bear no responsibility for any data
loss that may occur as a result of using this application. Finally, they bear no responsibility for any of the content uploaded by users.
loss that may occur as a result of using this application. Finally,
they bear no responsibility for any of the content uploaded by
users.
</DialogContentText>
</DialogContent>
<DialogActions>

View File

@ -1,8 +1,5 @@
import React, { useState, useEffect } from 'react'
import React, { useEffect, useState } from "react";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
LinearProgress,
@ -11,30 +8,26 @@ import {
ListItemIcon,
Popover,
Typography,
useTheme
} from '@mui/material'
import { Movie } from '@mui/icons-material'
import { useSelector } from 'react-redux'
import { RootState } from '../../state/store'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import { useLocation, useNavigate } from 'react-router-dom'
import { DownloadingLight } from '../../assets/svgs/DownloadingLight'
import { DownloadedLight } from '../../assets/svgs/DownloadedLight'
useTheme,
} from "@mui/material";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { useLocation, useNavigate } from "react-router-dom";
import { DownloadingLight } from "../../assets/svgs/DownloadingLight";
import { DownloadedLight } from "../../assets/svgs/DownloadedLight";
import AttachFileIcon from "@mui/icons-material/AttachFile";
export const DownloadTaskManager: React.FC = () => {
const { downloads } = useSelector((state: RootState) => state.global)
const location = useLocation()
const theme = useTheme()
const [visible, setVisible] = useState(false)
const [hidden, setHidden] = useState(true)
const navigate = useNavigate()
const { downloads } = useSelector((state: RootState) => state.global);
const location = useLocation();
const theme = useTheme();
const [visible, setVisible] = useState(false);
const [hidden, setHidden] = useState(true);
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const [openDownload, setOpenDownload] = useState<boolean>(false);
const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => {
const target = event?.currentTarget as unknown as HTMLButtonElement | null;
setAnchorEl(target);
@ -50,155 +43,150 @@ export const DownloadTaskManager: React.FC = () => {
if (visible) {
setTimeout(() => {
setHidden(true)
setVisible(false)
}, 3000)
setHidden(true);
setVisible(false);
}, 3000);
}
}, [visible])
}, [visible]);
useEffect(() => {
if (Object.keys(downloads).length === 0) return
setVisible(true)
setHidden(false)
}, [downloads])
if (Object.keys(downloads).length === 0) return;
setVisible(true);
setHidden(false);
}, [downloads]);
if (!downloads || Object.keys(downloads).length === 0) return null;
let downloadInProgress = false;
if (
!downloads ||
Object.keys(downloads).length === 0
)
return null
let downloadInProgress = false
if(Object.keys(downloads).find((key)=> (downloads[key]?.status?.status !== 'READY' && downloads[key]?.status?.status !== 'DOWNLOADED'))){
downloadInProgress = true
Object.keys(downloads).find(
key =>
downloads[key]?.status?.status !== "READY" &&
downloads[key]?.status?.status !== "DOWNLOADED"
)
) {
downloadInProgress = true;
}
return (
<Box>
<Button onClick={(e: any) => {
handleClick(e);
setOpenDownload(true);
}}>
{downloadInProgress ? (
<DownloadingLight height='24px' width='24px' className='download-icon' />
) : (
<DownloadedLight height='24px' width='24px' />
)}
<Button
onClick={(e: any) => {
handleClick(e);
setOpenDownload(true);
}}
>
{downloadInProgress ? (
<DownloadingLight
height="24px"
width="24px"
className="download-icon"
/>
) : (
<DownloadedLight height="24px" width="24px" />
)}
</Button>
</Button>
<Popover
id={"download-popover"}
open={openDownload}
anchorEl={anchorEl}
onClose={handleCloseDownload}
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
<Popover
id={"download-popover"}
open={openDownload}
anchorEl={anchorEl}
onClose={handleCloseDownload}
anchorOrigin={{
vertical: "bottom",
horizontal: "left",
}}
>
<List
sx={{
maxHeight: "50vh",
overflow: "auto",
width: "250px",
gap: "5px",
display: "flex",
flexDirection: "column",
}}
>
<List
sx={{
maxHeight: '50vh',
overflow: 'auto',
width: '250px',
gap: '5px',
display: 'flex',
flexDirection: 'column',
{Object.keys(downloads).map((download: any) => {
const downloadObj = downloads[download];
const progress = downloads[download]?.status?.percentLoaded || 0;
const status = downloads[download]?.status?.status;
const service = downloads[download]?.service;
return (
<ListItem
key={downloadObj?.identifier}
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
justifyContent: "center",
background: theme.palette.primary.main,
color: theme.palette.text.primary,
cursor: "pointer",
padding: "2px",
}}
onClick={() => {
const id = downloadObj?.properties?.jsonId;
if (!id) return;
}}
>
{Object.keys(downloads)
.map((download: any) => {
const downloadObj = downloads[download]
const progress = downloads[download]?.status?.percentLoaded || 0
const status = downloads[download]?.status?.status
const service = downloads[download]?.service
return (
<ListItem
key={downloadObj?.identifier}
navigate(`/issue/${downloadObj?.properties?.name}/${id}`);
}}
>
<Box
sx={{
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<ListItemIcon>
<AttachFileIcon
sx={{ color: theme.palette.text.primary }}
/>
</ListItemIcon>
<Box sx={{ width: "100px", marginLeft: 1, marginRight: 1 }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
borderRadius: "5px",
color: theme.palette.secondary.main,
}}
/>
</Box>
<Typography
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center',
background: theme.palette.primary.main,
fontFamily: "Arial",
color: theme.palette.text.primary,
cursor: 'pointer',
padding: '2px',
}}
onClick={() => {
const id = downloadObj?.properties?.jsonId
if (!id) return
navigate(
`/share/${downloadObj?.properties?.name}/${id}`
)
}}
variant="caption"
>
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<ListItemIcon>
<AttachFileIcon sx={{ color: theme.palette.text.primary }} />
</ListItemIcon>
<Box
sx={{ width: '100px', marginLeft: 1, marginRight: 1 }}
>
<LinearProgress
variant="determinate"
value={progress}
sx={{
borderRadius: '5px',
color: theme.palette.secondary.main
}}
/>
</Box>
<Typography
sx={{
fontFamily: 'Arial',
color: theme.palette.text.primary
}}
variant="caption"
>
{`${progress?.toFixed(0)}%`}{' '}
{status && status === 'REFETCHING' && '- refetching'}
{status && status === 'DOWNLOADED' && '- building'}
</Typography>
</Box>
<Typography
sx={{
fontSize: '10px',
width: '100%',
textAlign: 'end',
fontFamily: 'Arial',
color: theme.palette.text.primary,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{downloadObj?.identifier}
</Typography>
</ListItem>
)
})}
</List>
</Popover>
{`${progress?.toFixed(0)}%`}{" "}
{status && status === "REFETCHING" && "- refetching"}
{status && status === "DOWNLOADED" && "- building"}
</Typography>
</Box>
<Typography
sx={{
fontSize: "10px",
width: "100%",
textAlign: "end",
fontFamily: "Arial",
color: theme.palette.text.primary,
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
}}
>
{downloadObj?.identifier}
</Typography>
</ListItem>
);
})}
</List>
</Popover>
</Box>
)
}
);
};

View File

@ -1,4 +1,4 @@
const useTestIdentifiers = false;
const useTestIdentifiers = true;
export const QSUPPORT_FILE_BASE = useTestIdentifiers
? "MYTEST_support_issue_"

View File

@ -1,21 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { Avatar, Box, Button, Typography, useTheme } from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import {
BottomParent,
NameContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
FileContainer,
VideoUploadDate,
} from "./FileList-styles.tsx";
import { Box, useTheme } from "@mui/material";
import { FileContainer } from "./IssueList-styles.tsx";
import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { ChannelCard, ChannelTitle } from "./Home-styles";
interface VideoListProps {

View File

@ -15,7 +15,7 @@ import {
VideoCardName,
VideoCardTitle,
VideoUploadDate,
} from "./FileList-styles.tsx";
} from "./IssueList-styles.tsx";
import { formatDate } from "../../utils/time";
import { Video } from "../../state/features/fileSlice.ts";
import { queue } from "../../wrappers/GlobalWrapper";
@ -149,7 +149,7 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
<>
<VideoCard
onClick={() => {
navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
navigate(`/issue/${fileObj?.user}/${fileObj?.id}`);
}}
sx={{
height: "100%",

View File

@ -2,11 +2,11 @@ import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { FileList } from "./FileList.tsx";
import { IssueList } from "./IssueList.tsx";
import { Box, Button, Grid, Input, useTheme } from "@mui/material";
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import { FiltersCol, FiltersContainer } from "./FileList-styles.tsx";
import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
import { SubtitleContainer } from "./Home-styles";
import {
changefilterName,
@ -315,7 +315,7 @@ export const Home = ({ mode }: HomeProps) => {
maxWidth: "1400px",
}}
></SubtitleContainer>
<FileList files={videos} />
<IssueList files={videos} />
<LazyLoad
onLoadMore={getFilesHandler}
isLoading={isLoading}

View File

@ -1,12 +1,12 @@
import { styled } from "@mui/system";
import {
Box,
Grid,
Typography,
Checkbox,
TextField,
InputLabel,
Autocomplete,
Box,
Checkbox,
Grid,
InputLabel,
TextField,
Typography,
} from "@mui/material";
export const FileContainer = styled(Box)(({ theme }) => ({
@ -283,6 +283,7 @@ export const BlockIconContainer = styled(Box)({
padding: "2px",
borderRadius: "3px",
transition: "all 0.3s ease-in-out",
fontSize: "18px",
"&:hover": {
cursor: "pointer",
transform: "scale(1.1)",

View File

@ -1,4 +1,4 @@
import { Avatar, Box, Skeleton, Tooltip } from "@mui/material";
import { Avatar, Box, Skeleton } from "@mui/material";
import {
BlockIconContainer,
BottomParent,
@ -9,7 +9,7 @@ import {
VideoCardName,
VideoCardTitle,
VideoUploadDate,
} from "./FileList-styles.tsx";
} from "./IssueList-styles.tsx";
import EditIcon from "@mui/icons-material/Edit";
import {
blockUser,
@ -29,7 +29,7 @@ import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions
interface FileListProps {
files: Video[];
}
export const FileList = ({ files }: FileListProps) => {
export const IssueList = ({ files }: FileListProps) => {
const hashMapFiles = useSelector(
(state: RootState) => state.file.hashMapFiles
);
@ -40,7 +40,7 @@ export const FileList = ({ files }: FileListProps) => {
const navigate = useNavigate();
const blockUserFunc = async (user: string) => {
if (user === "Q-Share") return;
if (user === "Q-Support") return;
try {
const response = await qortalRequest({
@ -88,30 +88,30 @@ export const FileList = ({ files }: FileListProps) => {
}}
>
{fileObj?.user === username && (
<Tooltip title="Edit Issue Properties" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditFile(fileObj));
}}
/>
</BlockIconContainer>
</Tooltip>
<BlockIconContainer
onClick={() => {
dispatch(setEditFile(fileObj));
}}
>
<EditIcon />
Edit Issue
</BlockIconContainer>
)}
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(fileObj?.user);
}}
/>
{fileObj?.user !== username && (
<BlockIconContainer
onClick={() => {
blockUserFunc(fileObj?.user);
}}
>
<BlockIcon />
Block User
</BlockIconContainer>
</Tooltip>
)}
</IconsBox>
<VideoCard
onClick={() => {
navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
navigate(`/issue/${fileObj?.user}/${fileObj?.id}`);
}}
sx={{
height: "100%",