mirror of https://github.com/Qortal/q-tube
Browse Source
Refactored constants/index.ts into Identifiers.ts, Categories.ts, and Misc.ts. Regular expressions that titles allow all use new variable in Misc.ts for consistency and ease of editing it. New Characters are allowed in titles. Categories sorted by name, "Other" is always at end of list. New Categories such as Qortal under Education have been added Title prefix TextField added that starts all video titles with the entered value.pull/1/head
Qortal Dev
9 months ago
31 changed files with 2417 additions and 1867 deletions
@ -0,0 +1,10 @@
|
||||
{ |
||||
"printWidth": 80, |
||||
"singleQuote": false, |
||||
"trailingComma": "es5", |
||||
"bracketSpacing": true, |
||||
"jsxBracketSameLine": false, |
||||
"arrowParens": "avoid", |
||||
"tabWidth": 2, |
||||
"semi": true |
||||
} |
@ -1,66 +1,83 @@
|
||||
import React from 'react' |
||||
import { CardContentContainerComment } from '../common/Comments/Comments-styles' |
||||
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from '../UploadVideo/Upload-styles' |
||||
import { Box, Typography, useTheme } from '@mui/material' |
||||
import { useNavigate } from 'react-router-dom' |
||||
|
||||
export const Playlists = ({playlistData, currentVideoIdentifier, onClick}) => { |
||||
const theme = useTheme(); |
||||
const navigate = useNavigate() |
||||
import React from "react"; |
||||
import { CardContentContainerComment } from "../common/Comments/Comments-styles"; |
||||
import { |
||||
CrowdfundSubTitle, |
||||
CrowdfundSubTitleRow, |
||||
} from "../PublishVideo/PublishVideo-styles.tsx"; |
||||
import { Box, Typography, useTheme } from "@mui/material"; |
||||
import { useNavigate } from "react-router-dom"; |
||||
|
||||
export const Playlists = ({ |
||||
playlistData, |
||||
currentVideoIdentifier, |
||||
onClick, |
||||
}) => { |
||||
const theme = useTheme(); |
||||
const navigate = useNavigate(); |
||||
|
||||
return ( |
||||
<Box sx={{ |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
<Box |
||||
sx={{ |
||||
display: "flex", |
||||
flexDirection: "column", |
||||
|
||||
maxWidth: '400px', |
||||
width: '100%' |
||||
}}> |
||||
<CrowdfundSubTitleRow > |
||||
maxWidth: "400px", |
||||
width: "100%", |
||||
}} |
||||
> |
||||
<CrowdfundSubTitleRow> |
||||
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle> |
||||
</CrowdfundSubTitleRow> |
||||
<CardContentContainerComment sx={{ |
||||
marginTop: '25px', |
||||
height: '450px', |
||||
overflow: 'auto' |
||||
}}> |
||||
{playlistData?.videos?.map((vid, index)=> { |
||||
const isCurrentVidPlayling = vid?.identifier === currentVideoIdentifier; |
||||
|
||||
|
||||
<CardContentContainerComment |
||||
sx={{ |
||||
marginTop: "25px", |
||||
height: "450px", |
||||
overflow: "auto", |
||||
}} |
||||
> |
||||
{playlistData?.videos?.map((vid, index) => { |
||||
const isCurrentVidPlayling = |
||||
vid?.identifier === currentVideoIdentifier; |
||||
|
||||
return ( |
||||
<Box key={vid?.identifier} sx={{ |
||||
display: 'flex', |
||||
gap: '10px', |
||||
width: '100%', |
||||
background: isCurrentVidPlayling && theme.palette.primary.main, |
||||
alignItems: 'center', |
||||
padding: '10px', |
||||
borderRadius: '5px', |
||||
cursor: isCurrentVidPlayling ? 'default' : 'pointer', |
||||
userSelect: 'none' |
||||
return ( |
||||
<Box |
||||
key={vid?.identifier} |
||||
sx={{ |
||||
display: "flex", |
||||
gap: "10px", |
||||
width: "100%", |
||||
background: isCurrentVidPlayling && theme.palette.primary.main, |
||||
alignItems: "center", |
||||
padding: "10px", |
||||
borderRadius: "5px", |
||||
cursor: isCurrentVidPlayling ? "default" : "pointer", |
||||
userSelect: "none", |
||||
}} |
||||
onClick={() => { |
||||
if (isCurrentVidPlayling) return; |
||||
onClick(vid.name, vid.identifier); |
||||
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||
}} |
||||
> |
||||
<Typography |
||||
sx={{ |
||||
fontSize: "14px", |
||||
}} |
||||
onClick={()=> { |
||||
if(isCurrentVidPlayling) return |
||||
onClick(vid.name, vid.identifier) |
||||
// navigate(`/video/${vid.name}/${vid.identifier}`)
|
||||
> |
||||
{index + 1} |
||||
</Typography> |
||||
<Typography |
||||
sx={{ |
||||
fontSize: "18px", |
||||
wordBreak: "break-word", |
||||
}} |
||||
> |
||||
<Typography sx={{ |
||||
fontSize: '14px' |
||||
}}>{index + 1}</Typography> |
||||
<Typography sx={{ |
||||
fontSize: '18px', |
||||
wordBreak: 'break-word' |
||||
}}>{vid?.metadata?.title}</Typography> |
||||
|
||||
</Box> |
||||
) |
||||
> |
||||
{vid?.metadata?.title} |
||||
</Typography> |
||||
</Box> |
||||
); |
||||
})} |
||||
</CardContentContainerComment> |
||||
</CardContentContainerComment> |
||||
</Box> |
||||
|
||||
) |
||||
} |
||||
); |
||||
}; |
||||
|
@ -1,324 +1,347 @@
|
||||
import { Badge, Box, Button, List, ListItem, ListItemText, Popover, Typography } from '@mui/material' |
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' |
||||
import { useDispatch, useSelector } from 'react-redux' |
||||
import { RootState } from '../../../state/store' |
||||
import { FOR, FOR_SUPER_LIKE, SUPER_LIKE_BASE, minPriceSuperlike } from '../../../constants' |
||||
import NotificationsIcon from '@mui/icons-material/Notifications' |
||||
import { formatDate } from '../../../utils/time' |
||||
import { |
||||
Badge, |
||||
Box, |
||||
Button, |
||||
List, |
||||
ListItem, |
||||
ListItemText, |
||||
Popover, |
||||
Typography, |
||||
} from "@mui/material"; |
||||
import React, { |
||||
useCallback, |
||||
useEffect, |
||||
useMemo, |
||||
useRef, |
||||
useState, |
||||
} from "react"; |
||||
import { useDispatch, useSelector } from "react-redux"; |
||||
import { RootState } from "../../../state/store"; |
||||
import NotificationsIcon from "@mui/icons-material/Notifications"; |
||||
import { formatDate } from "../../../utils/time"; |
||||
import ThumbUpIcon from "@mui/icons-material/ThumbUp"; |
||||
import { extractSigValue, getPaymentInfo, isTimestampWithinRange } from '../../../pages/VideoContent/VideoContent' |
||||
import { useNavigate } from 'react-router-dom' |
||||
import { |
||||
extractSigValue, |
||||
getPaymentInfo, |
||||
isTimestampWithinRange, |
||||
} from "../../../pages/VideoContent/VideoContent"; |
||||
import { useNavigate } from "react-router-dom"; |
||||
import localForage from "localforage"; |
||||
import moment from 'moment' |
||||
import moment from "moment"; |
||||
import { |
||||
FOR, |
||||
FOR_SUPER_LIKE, |
||||
SUPER_LIKE_BASE, |
||||
} from "../../../constants/Identifiers.ts"; |
||||
import { minPriceSuperlike } from "../../../constants/Misc.ts"; |
||||
|
||||
const generalLocal = localForage.createInstance({ |
||||
name: "q-tube-general", |
||||
}); |
||||
name: "q-tube-general", |
||||
}); |
||||
export function extractIdValue(metadescription) { |
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) { |
||||
const match = str.match(/\*\*(.*?)\*\*/); |
||||
return match ? match[1] : null; |
||||
} |
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) { |
||||
const match = str.match(/\*\*(.*?)\*\*/); |
||||
return match ? match[1] : null; |
||||
} |
||||
|
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) { |
||||
const regex = /id:(.*?)(;|$)/; |
||||
const match = str.match(regex); |
||||
return match ? match[1] : null; |
||||
} |
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) { |
||||
const regex = /id:(.*?)(;|$)/; |
||||
const match = str.match(regex); |
||||
return match ? match[1] : null; |
||||
} |
||||
|
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription); |
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription); |
||||
|
||||
if (relevantSubstring) { |
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring); |
||||
} else { |
||||
return null; |
||||
} |
||||
if (relevantSubstring) { |
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring); |
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
export const Notifications = () => { |
||||
const dispatch = useDispatch() |
||||
const [anchorElNotification, setAnchorElNotification] = useState<HTMLButtonElement | null>(null) |
||||
const [notifications, setNotifications] = useState<any[]>([]) |
||||
const [notificationTimestamp, setNotificationTimestamp] = useState<null | number>(null) |
||||
|
||||
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name); |
||||
const usernameAddress = useSelector((state: RootState) => state.auth?.user?.address); |
||||
const navigate = useNavigate(); |
||||
|
||||
const interval = useRef<any>(null) |
||||
|
||||
const getInitialTimestamp = async ()=> { |
||||
const timestamp: undefined | number = await generalLocal.getItem("notification-timestamp"); |
||||
if(timestamp){ |
||||
setNotificationTimestamp(timestamp) |
||||
} |
||||
const dispatch = useDispatch(); |
||||
const [anchorElNotification, setAnchorElNotification] = |
||||
useState<HTMLButtonElement | null>(null); |
||||
const [notifications, setNotifications] = useState<any[]>([]); |
||||
const [notificationTimestamp, setNotificationTimestamp] = useState< |
||||
null | number |
||||
>(null); |
||||
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name); |
||||
const usernameAddress = useSelector( |
||||
(state: RootState) => state.auth?.user?.address |
||||
); |
||||
const navigate = useNavigate(); |
||||
|
||||
const interval = useRef<any>(null); |
||||
|
||||
const getInitialTimestamp = async () => { |
||||
const timestamp: undefined | number = await generalLocal.getItem( |
||||
"notification-timestamp" |
||||
); |
||||
if (timestamp) { |
||||
setNotificationTimestamp(timestamp); |
||||
} |
||||
|
||||
useEffect(()=> { |
||||
getInitialTimestamp() |
||||
}, []) |
||||
|
||||
|
||||
const openNotificationPopover = (event: any) => { |
||||
const target = event.currentTarget as unknown as HTMLButtonElement | null |
||||
setAnchorElNotification(target) |
||||
} |
||||
const closeNotificationPopover = () => { |
||||
setAnchorElNotification(null) |
||||
} |
||||
const fullNotifications = useMemo(() => { |
||||
return [...notifications].sort( |
||||
(a, b) => b.created - a.created |
||||
) |
||||
}, [notifications]) |
||||
const notificationBadgeLength = useMemo(()=> { |
||||
if(!notificationTimestamp) return fullNotifications.length |
||||
return fullNotifications?.filter((item)=> item.created > notificationTimestamp).length |
||||
}, [fullNotifications, notificationTimestamp]) |
||||
|
||||
const checkNotifications = useCallback(async (username: string) => { |
||||
try { |
||||
// let notificationComments: Item[] =
|
||||
// (await notification.getItem('comments')) || []
|
||||
// notificationComments = notificationComments
|
||||
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
||||
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
||||
|
||||
const timestamp = await generalLocal.getItem("notification-timestamp"); |
||||
|
||||
const after = timestamp || moment().subtract(5, 'days').valueOf(); |
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`; |
||||
const response = await fetch(url, { |
||||
method: "GET", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
}); |
||||
const responseDataSearch = await response.json(); |
||||
let notifys = [] |
||||
for (const comment of responseDataSearch) { |
||||
if (comment.identifier && comment.name && comment?.metadata?.description) { |
||||
|
||||
|
||||
try { |
||||
const result = extractSigValue(comment?.metadata?.description) |
||||
if(!result) continue |
||||
const res = await getPaymentInfo(result); |
||||
if(+res?.amount >= minPriceSuperlike && res.recipient === usernameAddress && isTimestampWithinRange(res?.timestamp, comment.created)){ |
||||
|
||||
let urlReference = null |
||||
try { |
||||
let idForUrl = extractIdValue(comment?.metadata?.description) |
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`; |
||||
const response2 = await fetch(url, { |
||||
method: "GET", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
}); |
||||
const responseSearch = await response2.json(); |
||||
if(responseSearch.length > 0){ |
||||
urlReference = responseSearch[0] |
||||
} |
||||
|
||||
} catch (error) { |
||||
|
||||
} |
||||
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||
// const response = await fetch(url, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// });
|
||||
// if(!response.ok) continue
|
||||
// const responseData2 = await response.text();
|
||||
|
||||
notifys = [...notifys, { |
||||
...comment, |
||||
amount: res.amount, |
||||
urlReference: urlReference || null |
||||
}]; |
||||
|
||||
} |
||||
|
||||
} catch (error) { |
||||
|
||||
}; |
||||
|
||||
useEffect(() => { |
||||
getInitialTimestamp(); |
||||
}, []); |
||||
|
||||
const openNotificationPopover = (event: any) => { |
||||
const target = event.currentTarget as unknown as HTMLButtonElement | null; |
||||
setAnchorElNotification(target); |
||||
}; |
||||
const closeNotificationPopover = () => { |
||||
setAnchorElNotification(null); |
||||
}; |
||||
const fullNotifications = useMemo(() => { |
||||
return [...notifications].sort((a, b) => b.created - a.created); |
||||
}, [notifications]); |
||||
const notificationBadgeLength = useMemo(() => { |
||||
if (!notificationTimestamp) return fullNotifications.length; |
||||
return fullNotifications?.filter( |
||||
item => item.created > notificationTimestamp |
||||
).length; |
||||
}, [fullNotifications, notificationTimestamp]); |
||||
|
||||
const checkNotifications = useCallback(async (username: string) => { |
||||
try { |
||||
// let notificationComments: Item[] =
|
||||
// (await notification.getItem('comments')) || []
|
||||
// notificationComments = notificationComments
|
||||
// .filter((nc) => nc.postId && nc.postName && nc.lastSeen)
|
||||
// .sort((a, b) => b.lastSeen - a.lastSeen)
|
||||
|
||||
const timestamp = await generalLocal.getItem("notification-timestamp"); |
||||
|
||||
const after = timestamp || moment().subtract(5, "days").valueOf(); |
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&identifier=${SUPER_LIKE_BASE}&limit=20&includemetadata=true&reverse=true&excludeblocked=true&offset=0&description=${FOR}:${username}_${FOR_SUPER_LIKE}&after=${after}`; |
||||
const response = await fetch(url, { |
||||
method: "GET", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
}); |
||||
const responseDataSearch = await response.json(); |
||||
let notifys = []; |
||||
for (const comment of responseDataSearch) { |
||||
if ( |
||||
comment.identifier && |
||||
comment.name && |
||||
comment?.metadata?.description |
||||
) { |
||||
try { |
||||
const result = extractSigValue(comment?.metadata?.description); |
||||
if (!result) continue; |
||||
const res = await getPaymentInfo(result); |
||||
if ( |
||||
+res?.amount >= minPriceSuperlike && |
||||
res.recipient === usernameAddress && |
||||
isTimestampWithinRange(res?.timestamp, comment.created) |
||||
) { |
||||
let urlReference = null; |
||||
try { |
||||
let idForUrl = extractIdValue(comment?.metadata?.description); |
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${idForUrl}&limit=1&includemetadata=false&reverse=false&excludeblocked=true&offset=0&name=${username}`; |
||||
const response2 = await fetch(url, { |
||||
method: "GET", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
}); |
||||
const responseSearch = await response2.json(); |
||||
if (responseSearch.length > 0) { |
||||
urlReference = responseSearch[0]; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
} catch (error) {} |
||||
// const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`;
|
||||
// const response = await fetch(url, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// });
|
||||
// if(!response.ok) continue
|
||||
// const responseData2 = await response.text();
|
||||
|
||||
notifys = [ |
||||
...notifys, |
||||
{ |
||||
...comment, |
||||
amount: res.amount, |
||||
urlReference: urlReference || null, |
||||
}, |
||||
]; |
||||
} |
||||
} |
||||
setNotifications((prev) => { |
||||
const allNotifications = [...notifys, ...prev]; |
||||
const uniqueNotifications = Array.from(new Map(allNotifications.map(notif => [notif.identifier, notif])).values()); |
||||
return uniqueNotifications.slice(0, 20); |
||||
}); |
||||
|
||||
} catch (error) { |
||||
console.log({ error }) |
||||
} catch (error) {} |
||||
} |
||||
}, []) |
||||
|
||||
const checkNotificationsFunc = useCallback( |
||||
(username: string) => { |
||||
let isCalling = false |
||||
interval.current = setInterval(async () => { |
||||
if (isCalling) return |
||||
isCalling = true |
||||
const res = await checkNotifications(username) |
||||
isCalling = false |
||||
}, 60000) |
||||
checkNotifications(username) |
||||
}, |
||||
[checkNotifications]) |
||||
|
||||
useEffect(() => { |
||||
if (!username) return |
||||
checkNotificationsFunc(username) |
||||
|
||||
|
||||
|
||||
return () => { |
||||
if (interval?.current) { |
||||
clearInterval(interval.current) |
||||
} |
||||
} |
||||
}, [checkNotificationsFunc, username]) |
||||
} |
||||
setNotifications(prev => { |
||||
const allNotifications = [...notifys, ...prev]; |
||||
const uniqueNotifications = Array.from( |
||||
new Map( |
||||
allNotifications.map(notif => [notif.identifier, notif]) |
||||
).values() |
||||
); |
||||
return uniqueNotifications.slice(0, 20); |
||||
}); |
||||
} catch (error) { |
||||
console.log({ error }); |
||||
} |
||||
}, []); |
||||
|
||||
const checkNotificationsFunc = useCallback( |
||||
(username: string) => { |
||||
let isCalling = false; |
||||
interval.current = setInterval(async () => { |
||||
if (isCalling) return; |
||||
isCalling = true; |
||||
const res = await checkNotifications(username); |
||||
isCalling = false; |
||||
}, 60000); |
||||
checkNotifications(username); |
||||
}, |
||||
[checkNotifications] |
||||
); |
||||
|
||||
useEffect(() => { |
||||
if (!username) return; |
||||
checkNotificationsFunc(username); |
||||
|
||||
return () => { |
||||
if (interval?.current) { |
||||
clearInterval(interval.current); |
||||
} |
||||
}; |
||||
}, [checkNotificationsFunc, username]); |
||||
|
||||
const openPopover = Boolean(anchorElNotification) |
||||
const openPopover = Boolean(anchorElNotification); |
||||
return ( |
||||
<Box |
||||
sx={{ |
||||
display: 'flex', |
||||
alignItems: 'center' |
||||
}} |
||||
> |
||||
|
||||
<Badge |
||||
badgeContent={notificationBadgeLength} |
||||
color="primary" |
||||
sx={{ |
||||
margin: '0px 12px' |
||||
display: "flex", |
||||
alignItems: "center", |
||||
}} |
||||
> |
||||
<Button |
||||
onClick={(e) => { |
||||
openNotificationPopover(e) |
||||
generalLocal.setItem("notification-timestamp", Date.now()); |
||||
setNotificationTimestamp(Date.now) |
||||
}} |
||||
<Badge |
||||
badgeContent={notificationBadgeLength} |
||||
color="primary" |
||||
sx={{ |
||||
margin: '0px', |
||||
padding: '0px', |
||||
height: 'auto', |
||||
width: 'auto', |
||||
minWidth: 'unset' |
||||
margin: "0px 12px", |
||||
}} |
||||
> |
||||
<NotificationsIcon color="action" /> |
||||
</Button> |
||||
</Badge> |
||||
<Popover |
||||
id={'simple-popover-notification'} |
||||
open={openPopover} |
||||
anchorEl={anchorElNotification} |
||||
onClose={closeNotificationPopover} |
||||
anchorOrigin={{ |
||||
vertical: 'bottom', |
||||
horizontal: 'left' |
||||
}} |
||||
> |
||||
<Box> |
||||
<List |
||||
<Button |
||||
onClick={e => { |
||||
openNotificationPopover(e); |
||||
generalLocal.setItem("notification-timestamp", Date.now()); |
||||
setNotificationTimestamp(Date.now); |
||||
}} |
||||
sx={{ |
||||
maxHeight: '300px', |
||||
overflow: 'auto' |
||||
margin: "0px", |
||||
padding: "0px", |
||||
height: "auto", |
||||
width: "auto", |
||||
minWidth: "unset", |
||||
}} |
||||
> |
||||
<NotificationsIcon color="action" /> |
||||
</Button> |
||||
</Badge> |
||||
<Popover |
||||
id={"simple-popover-notification"} |
||||
open={openPopover} |
||||
anchorEl={anchorElNotification} |
||||
onClose={closeNotificationPopover} |
||||
anchorOrigin={{ |
||||
vertical: "bottom", |
||||
horizontal: "left", |
||||
}} |
||||
> |
||||
<Box> |
||||
<List |
||||
sx={{ |
||||
maxHeight: "300px", |
||||
overflow: "auto", |
||||
}} |
||||
> |
||||
{fullNotifications.length === 0 && ( |
||||
<ListItem |
||||
|
||||
<ListItem> |
||||
<ListItemText primary="No new notifications"></ListItemText> |
||||
</ListItem> |
||||
)} |
||||
{fullNotifications.map((notification: any, index: number) => ( |
||||
<ListItem |
||||
key={index} |
||||
divider |
||||
sx={{ |
||||
cursor: notification?.urlReference ? "pointer" : "default", |
||||
}} |
||||
onClick={async () => { |
||||
if (notification?.urlReference) { |
||||
navigate( |
||||
`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}` |
||||
); |
||||
} |
||||
}} |
||||
> |
||||
<ListItemText |
||||
primary="No new notifications"> |
||||
|
||||
</ListItemText> |
||||
</ListItem> |
||||
)} |
||||
{fullNotifications.map((notification: any, index: number) => ( |
||||
<ListItem |
||||
key={index} |
||||
divider |
||||
sx={{ |
||||
cursor: notification?.urlReference ? 'pointer' : 'default' |
||||
}} |
||||
onClick={async () => { |
||||
if(notification?.urlReference){ |
||||
navigate(`/video/${notification?.urlReference?.name}/${notification?.urlReference?.identifier}`); |
||||
} |
||||
}} |
||||
> |
||||
<ListItemText |
||||
primary={ |
||||
<Box sx={{ |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
gap: '5px' |
||||
}}> |
||||
<Typography |
||||
component="span" |
||||
variant="body1" |
||||
color="textPrimary" |
||||
> |
||||
Super Like |
||||
|
||||
</Typography> |
||||
<ThumbUpIcon |
||||
style={{ |
||||
color: "gold", |
||||
|
||||
|
||||
}} |
||||
/> |
||||
</Box> |
||||
} |
||||
secondary={ |
||||
<React.Fragment> |
||||
<Typography |
||||
component="span" |
||||
sx={{ |
||||
fontSize: '16px' |
||||
}} |
||||
color="textSecondary" |
||||
> |
||||
{formatDate(notification.created)} |
||||
</Typography> |
||||
<Typography |
||||
component="span" |
||||
primary={ |
||||
<Box |
||||
sx={{ |
||||
fontSize: '16px' |
||||
display: "flex", |
||||
alignItems: "center", |
||||
gap: "5px", |
||||
}} |
||||
color="textSecondary" |
||||
> |
||||
{` from ${notification.name}`} |
||||
</Typography> |
||||
</React.Fragment> |
||||
} |
||||
/> |
||||
</ListItem> |
||||
))} |
||||
</List> |
||||
</Box> |
||||
</Popover> |
||||
</Box> |
||||
) |
||||
} |
||||
<Typography |
||||
component="span" |
||||
variant="body1" |
||||
color="textPrimary" |
||||
> |
||||
Super Like |
||||
</Typography> |
||||
<ThumbUpIcon |
||||
style={{ |
||||
color: "gold", |
||||
}} |
||||
/> |
||||
</Box> |
||||
} |
||||
secondary={ |
||||
<React.Fragment> |
||||
<Typography |
||||
component="span" |
||||
sx={{ |
||||
fontSize: "16px", |
||||
}} |
||||
color="textSecondary" |
||||
> |
||||
{formatDate(notification.created)} |
||||
</Typography> |
||||
<Typography |
||||
component="span" |
||||
sx={{ |
||||
fontSize: "16px", |
||||
}} |
||||
color="textSecondary" |
||||
> |
||||
{` from ${notification.name}`} |
||||
</Typography> |
||||
</React.Fragment> |
||||
} |
||||
/> |
||||
</ListItem> |
||||
))} |
||||
</List> |
||||
</Box> |
||||
</Popover> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
@ -0,0 +1,107 @@
|
||||
interface SubCategory { |
||||
id: number; |
||||
name: string; |
||||
} |
||||
|
||||
interface CategoryMap { |
||||
[key: number]: SubCategory[]; |
||||
} |
||||
|
||||
const sortCategory = (a: SubCategory, b: SubCategory) => { |
||||
if (a.name === "Other") return 1; |
||||
else if (b.name === "Other") return -1; |
||||
else return a.name.localeCompare(b.name); |
||||
}; |
||||
export const categories = [ |
||||
{ id: 1, name: "Movies" }, |
||||
{ id: 2, name: "Series" }, |
||||
{ id: 3, name: "Music" }, |
||||
{ id: 4, name: "Education" }, |
||||
{ id: 5, name: "Lifestyle" }, |
||||
{ id: 6, name: "Gaming" }, |
||||
{ id: 7, name: "Technology" }, |
||||
{ id: 8, name: "Sports" }, |
||||
{ id: 9, name: "News & Politics" }, |
||||
{ id: 10, name: "Cooking & Food" }, |
||||
{ id: 11, name: "Animation" }, |
||||
{ id: 12, name: "Science" }, |
||||
{ id: 13, name: "Health & Wellness" }, |
||||
{ id: 14, name: "DIY & Crafts" }, |
||||
{ id: 15, name: "Kids & Family" }, |
||||
{ id: 16, name: "Comedy" }, |
||||
{ id: 17, name: "Travel & Adventure" }, |
||||
{ id: 18, name: "Art & Design" }, |
||||
{ id: 19, name: "Nature & Environment" }, |
||||
{ id: 20, name: "Business & Finance" }, |
||||
{ id: 21, name: "Personal Development" }, |
||||
{ id: 22, name: "Other" }, |
||||
{ id: 23, name: "History" }, |
||||
{ id: 24, name: "Anime" }, |
||||
{ id: 25, name: "Cartoons" }, |
||||
{ id: 26, name: "Qortal" }, |
||||
].sort(sortCategory); |
||||
|
||||
export const subCategories: CategoryMap = { |
||||
1: [ |
||||
// Movies
|
||||
{ id: 101, name: "Action & Adventure" }, |
||||
{ id: 102, name: "Comedy" }, |
||||
{ id: 103, name: "Drama" }, |
||||
{ id: 104, name: "Fantasy & Science Fiction" }, |
||||
{ id: 105, name: "Horror & Thriller" }, |
||||
{ id: 106, name: "Documentaries" }, |
||||
{ id: 107, name: "Animated" }, |
||||
{ id: 108, name: "Family & Kids" }, |
||||
{ id: 109, name: "Romance" }, |
||||
{ id: 110, name: "Mystery & Crime" }, |
||||
{ id: 111, name: "Historical & War" }, |
||||
{ id: 112, name: "Musicals & Music Films" }, |
||||
{ id: 113, name: "Indie Films" }, |
||||
{ id: 114, name: "International Films" }, |
||||
{ id: 115, name: "Biographies & True Stories" }, |
||||
{ id: 116, name: "Other" }, |
||||
].sort(sortCategory), |
||||
2: [ |
||||
// Series
|
||||
{ id: 201, name: "Dramas" }, |
||||
{ id: 202, name: "Comedies" }, |
||||
{ id: 203, name: "Reality & Competition" }, |
||||
{ id: 204, name: "Documentaries & Docuseries" }, |
||||
{ id: 205, name: "Sci-Fi & Fantasy" }, |
||||
{ id: 206, name: "Crime & Mystery" }, |
||||
{ id: 207, name: "Animated Series" }, |
||||
{ id: 208, name: "Kids & Family" }, |
||||
{ id: 209, name: "Historical & Period Pieces" }, |
||||
{ id: 210, name: "Action & Adventure" }, |
||||
{ id: 211, name: "Horror & Thriller" }, |
||||
{ id: 212, name: "Romance" }, |
||||
{ id: 213, name: "Anthologies" }, |
||||
{ id: 214, name: "International Series" }, |
||||
{ id: 215, name: "Miniseries" }, |
||||
{ id: 216, name: "Other" }, |
||||
].sort(sortCategory), |
||||
4: [ |
||||
// Education
|
||||
{ id: 400, name: "Tutorial" }, |
||||
{ id: 401, name: "Documentary" }, |
||||
{ id: 401, name: "Qortal" }, |
||||
{ id: 402, name: "Other" }, |
||||
].sort(sortCategory), |
||||
|
||||
24: [ |
||||
{ id: 2401, name: "Kodomomuke" }, |
||||
{ id: 2402, name: "Shonen" }, |
||||
{ id: 2403, name: "Shoujo" }, |
||||
{ id: 2404, name: "Seinen" }, |
||||
{ id: 2405, name: "Josei" }, |
||||
{ id: 2406, name: "Mecha" }, |
||||
{ id: 2407, name: "Mahou Shoujo" }, |
||||
{ id: 2408, name: "Isekai" }, |
||||
{ id: 2409, name: "Yaoi" }, |
||||
{ id: 2410, name: "Yuri" }, |
||||
{ id: 2411, name: "Harem" }, |
||||
{ id: 2412, name: "Ecchi" }, |
||||
{ id: 2413, name: "Idol" }, |
||||
{ id: 2414, name: "Other" }, |
||||
].sort(sortCategory), |
||||
}; |
@ -0,0 +1,15 @@
|
||||
const useTestIdentifiers = false; |
||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers |
||||
? "MYTEST_vid_" |
||||
: "qtube_vid_"; |
||||
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers |
||||
? "MYTEST_playlist_" |
||||
: "qtube_playlist_"; |
||||
export const SUPER_LIKE_BASE = useTestIdentifiers |
||||
? "MYTEST_superlike_" |
||||
: "qtube_superlike_"; |
||||
export const COMMENT_BASE = useTestIdentifiers |
||||
? "qcomment_v1_MYTEST_" |
||||
: "qcomment_v1_qtube_"; |
||||
export const FOR = useTestIdentifiers ? "FORTEST5" : "FOR0962"; |
||||
export const FOR_SUPER_LIKE = useTestIdentifiers ? "MYTEST_sl" : `qtube_sl`; |
@ -0,0 +1,2 @@
|
||||
export const minPriceSuperlike = 10; |
||||
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^&*+=]/g; |
@ -1,121 +0,0 @@
|
||||
const useTestIdentifiers = true; |
||||
|
||||
export const QTUBE_VIDEO_BASE = useTestIdentifiers |
||||
? "MYTEST_vid_" |
||||
: "qtube_vid_"; |
||||
|
||||
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers |
||||
? "MYTEST_playlist_" |
||||
: "qtube_playlist_"; |
||||
|
||||
export const SUPER_LIKE_BASE = useTestIdentifiers |
||||
? "MYTEST_superlike_" |
||||
: "qtube_superlike_"; |
||||
|
||||
export const COMMENT_BASE = useTestIdentifiers |
||||
? "qcomment_v1_MYTEST_" |
||||
: "qcomment_v1_qtube_"; |
||||
|
||||
export const FOR = useTestIdentifiers |
||||
? "FORTEST5" |
||||
: "FOR0962"; |
||||
|
||||
export const FOR_SUPER_LIKE = useTestIdentifiers |
||||
? "MYTEST_sl" |
||||
: `qtube_sl`; |
||||
|
||||
export const minPriceSuperlike = 10 |
||||
|
||||
interface SubCategory { |
||||
id: number; |
||||
name: string; |
||||
} |
||||
|
||||
interface CategoryMap { |
||||
[key: number]: SubCategory[]; |
||||
} |
||||
|
||||
|
||||
export const categories = [ |
||||
{"id": 1, "name": "Movies"}, |
||||
{"id": 2, "name": "Series"}, |
||||
{"id": 3, "name": "Music"}, |
||||
{"id": 4, "name": "Education"}, |
||||
{"id": 5, "name": "Lifestyle"}, |
||||
{"id": 6, "name": "Gaming"}, |
||||
{"id": 7, "name": "Technology"}, |
||||
{"id": 8, "name": "Sports"}, |
||||
{"id": 9, "name": "News & Politics"}, |
||||
{"id": 10, "name": "Cooking & Food"}, |
||||
{"id": 11, "name": "Animation"}, |
||||
{"id": 12, "name": "Science"}, |
||||
{"id": 13, "name": "Health & Wellness"}, |
||||
{"id": 14, "name": "DIY & Crafts"}, |
||||
{"id": 15, "name": "Kids & Family"}, |
||||
{"id": 16, "name": "Comedy"}, |
||||
{"id": 17, "name": "Travel & Adventure"}, |
||||
{"id": 18, "name": "Art & Design"}, |
||||
{"id": 19, "name": "Nature & Environment"}, |
||||
{"id": 20, "name": "Business & Finance"}, |
||||
{"id": 21, "name": "Personal Development"}, |
||||
{"id": 22, "name": "Other"}, |
||||
{"id": 23, "name": "History"}, |
||||
{"id": 24, "name": "Anime"}, |
||||
{"id": 25, "name": "Cartoons"} |
||||
] |
||||
|
||||
|
||||
export const subCategories: CategoryMap = { |
||||
1: [ // Movies
|
||||
{"id": 101, "name": "Action & Adventure"}, |
||||
{"id": 102, "name": "Comedy"}, |
||||
{"id": 103, "name": "Drama"}, |
||||
{"id": 104, "name": "Fantasy & Science Fiction"}, |
||||
{"id": 105, "name": "Horror & Thriller"}, |
||||
{"id": 106, "name": "Documentaries"}, |
||||
{"id": 107, "name": "Animated"}, |
||||
{"id": 108, "name": "Family & Kids"}, |
||||
{"id": 109, "name": "Romance"}, |
||||
{"id": 110, "name": "Mystery & Crime"}, |
||||
{"id": 111, "name": "Historical & War"}, |
||||
{"id": 112, "name": "Musicals & Music Films"}, |
||||
{"id": 113, "name": "Indie Films"}, |
||||
{"id": 114, "name": "International Films"}, |
||||
{"id": 115, "name": "Biographies & True Stories"}, |
||||
{"id": 116, "name": "Other"} |
||||
], |
||||
2: [ // Series
|
||||
{"id": 201, "name": "Dramas"}, |
||||
{"id": 202, "name": "Comedies"}, |
||||
{"id": 203, "name": "Reality & Competition"}, |
||||
{"id": 204, "name": "Documentaries & Docuseries"}, |
||||
{"id": 205, "name": "Sci-Fi & Fantasy"}, |
||||
{"id": 206, "name": "Crime & Mystery"}, |
||||
{"id": 207, "name": "Animated Series"}, |
||||
{"id": 208, "name": "Kids & Family"}, |
||||
{"id": 209, "name": "Historical & Period Pieces"}, |
||||
{"id": 210, "name": "Action & Adventure"}, |
||||
{"id": 211, "name": "Horror & Thriller"}, |
||||
{"id": 212, "name": "Romance"}, |
||||
{"id": 213, "name": "Anthologies"}, |
||||
{"id": 214, "name": "International Series"}, |
||||
{"id": 215, "name": "Miniseries"}, |
||||
{"id": 216, "name": "Other"} |
||||
], |
||||
24: [ |
||||
{"id": 2401, "name": "Kodomomuke"}, |
||||
{"id": 2402, "name": "Shonen"}, |
||||
{"id": 2403, "name": "Shoujo"}, |
||||
{"id": 2404, "name": "Seinen"}, |
||||
{"id": 2405, "name": "Josei"}, |
||||
{"id": 2406, "name": "Mecha"}, |
||||
{"id": 2407, "name": "Mahou Shoujo"}, |
||||
{"id": 2408, "name": "Isekai"}, |
||||
{"id": 2409, "name": "Yaoi"}, |
||||
{"id": 2410, "name": "Yuri"}, |
||||
{"id": 2411, "name": "Harem"}, |
||||
{"id": 2412, "name": "Ecchi"}, |
||||
{"id": 2413, "name": "Idol"}, |
||||
{"id": 2414, "name": "Other"} |
||||
]
|
||||
} |
@ -0,0 +1,154 @@
|
||||
import { |
||||
IconButton, |
||||
InputAdornment, |
||||
TextField, |
||||
TextFieldProps, |
||||
} from "@mui/material"; |
||||
import React, { useRef, useState } from "react"; |
||||
import AddIcon from "@mui/icons-material/Add"; |
||||
import RemoveIcon from "@mui/icons-material/Remove"; |
||||
import { |
||||
removeTrailingZeros, |
||||
setNumberWithinBounds, |
||||
} from "./numberFunctions.ts"; |
||||
|
||||
type eventType = React.ChangeEvent<HTMLInputElement>; |
||||
type BoundedNumericTextFieldProps = { |
||||
minValue: number; |
||||
maxValue: number; |
||||
addIconButtons?: boolean; |
||||
allowDecimals?: boolean; |
||||
allowNegatives?: boolean; |
||||
afterChange?: (s: string) => void; |
||||
initialValue?: string; |
||||
maxSigDigits?: number; |
||||
} & TextFieldProps; |
||||
|
||||
export const BoundedNumericTextField = ({ |
||||
minValue, |
||||
maxValue, |
||||
addIconButtons = true, |
||||
allowDecimals = true, |
||||
allowNegatives = false, |
||||
afterChange, |
||||
initialValue, |
||||
maxSigDigits = 6, |
||||
...props |
||||
}: BoundedNumericTextFieldProps) => { |
||||
const [textFieldValue, setTextFieldValue] = useState<string>( |
||||
initialValue || "" |
||||
); |
||||
const ref = useRef<HTMLInputElement | null>(null); |
||||
|
||||
const stringIsEmpty = (value: string) => { |
||||
return value === ""; |
||||
}; |
||||
const isAllZerosNum = /^0*\.?0*$/; |
||||
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/; |
||||
const isIntegerNum = /^-?[0-9]+$/; |
||||
const skipMinMaxCheck = (value: string) => { |
||||
const lastIndexIsDecimal = value.charAt(value.length - 1) === "."; |
||||
const isEmpty = stringIsEmpty(value); |
||||
const isAllZeros = isAllZerosNum.test(value); |
||||
const isInteger = isIntegerNum.test(value); |
||||
// skipping minMax on all 0s allows values less than 1 to be entered
|
||||
|
||||
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger); |
||||
}; |
||||
|
||||
const setMinMaxValue = (value: string): string => { |
||||
if (skipMinMaxCheck(value)) return value; |
||||
const valueNum = Number(value); |
||||
|
||||
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue); |
||||
|
||||
const numberInBounds = boundedNum === valueNum; |
||||
return numberInBounds ? value : boundedNum.toString(); |
||||
}; |
||||
|
||||
const getSigDigits = (number: string) => { |
||||
if (isIntegerNum.test(number)) return 0; |
||||
const decimalSplit = number.split("."); |
||||
return decimalSplit[decimalSplit.length - 1].length; |
||||
}; |
||||
|
||||
const sigDigitsExceeded = (number: string, sigDigits: number) => { |
||||
return getSigDigits(number) > sigDigits; |
||||
}; |
||||
|
||||
const filterTypes = (value: string) => { |
||||
if (allowDecimals === false) value = value.replace(".", ""); |
||||
if (allowNegatives === false) value = value.replace("-", ""); |
||||
if (sigDigitsExceeded(value, maxSigDigits)) { |
||||
value = value.substring(0, value.length - 1); |
||||
} |
||||
return value; |
||||
}; |
||||
const filterValue = (value: string) => { |
||||
if (stringIsEmpty(value)) return ""; |
||||
value = filterTypes(value); |
||||
if (isFloatNum.test(value)) { |
||||
return setMinMaxValue(value); |
||||
} |
||||
return textFieldValue; |
||||
}; |
||||
|
||||
const listeners = (e: eventType) => { |
||||
// console.log("changeEvent:", e);
|
||||
const newValue = filterValue(e.target.value); |
||||
setTextFieldValue(newValue); |
||||
if (afterChange) afterChange(newValue); |
||||
}; |
||||
|
||||
const changeValueWithIncDecButton = (changeAmount: number) => { |
||||
const changedValue = (+textFieldValue + changeAmount).toString(); |
||||
const inBoundsValue = setMinMaxValue(changedValue); |
||||
setTextFieldValue(inBoundsValue); |
||||
if (afterChange) afterChange(inBoundsValue); |
||||
}; |
||||
|
||||
const formatValueOnBlur = (e: eventType) => { |
||||
let value = e.target.value; |
||||
if (stringIsEmpty(value) || value === ".") { |
||||
setTextFieldValue(""); |
||||
return; |
||||
} |
||||
|
||||
value = setMinMaxValue(value); |
||||
value = removeTrailingZeros(value); |
||||
if (isAllZerosNum.test(value)) value = minValue.toString(); |
||||
|
||||
setTextFieldValue(value); |
||||
}; |
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { onChange, ...noChangeProps } = { ...props }; |
||||
return ( |
||||
<TextField |
||||
{...noChangeProps} |
||||
InputProps={{ |
||||
...props?.InputProps, |
||||
endAdornment: addIconButtons ? ( |
||||
<InputAdornment position="end"> |
||||
<IconButton onClick={() => changeValueWithIncDecButton(1)}> |
||||
<AddIcon />{" "} |
||||
</IconButton> |
||||
<IconButton onClick={() => changeValueWithIncDecButton(-1)}> |
||||
<RemoveIcon />{" "} |
||||
</IconButton> |
||||
</InputAdornment> |
||||
) : ( |
||||
<></> |
||||
), |
||||
}} |
||||
onChange={e => listeners(e as eventType)} |
||||
onBlur={e => { |
||||
formatValueOnBlur(e as eventType); |
||||
}} |
||||
autoComplete="off" |
||||
value={textFieldValue} |
||||
inputRef={ref} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default BoundedNumericTextField; |
@ -0,0 +1,28 @@
|
||||
import * as colorsys from "colorsys"; |
||||
|
||||
export const truncateNumber = (value: string | number, sigDigits: number) => { |
||||
return Number(value).toFixed(sigDigits); |
||||
}; |
||||
|
||||
export const changeLightness = (hexColor: string, amount: number) => { |
||||
const hsl = colorsys.hex2Hsl(hexColor); |
||||
hsl.l += amount; |
||||
return colorsys.hsl2Hex(hsl); |
||||
}; |
||||
export const removeTrailingZeros = (s: string) => { |
||||
return Number(s).toString(); |
||||
}; |
||||
|
||||
export const setNumberWithinBounds = ( |
||||
num: number, |
||||
minValue: number, |
||||
maxValue: number |
||||
) => { |
||||
if (num > maxValue) return maxValue; |
||||
if (num < minValue) return minValue; |
||||
return num; |
||||
}; |
||||
|
||||
export const numberToInt = (num: number) => { |
||||
return Math.floor(num); |
||||
}; |
@ -0,0 +1,48 @@
|
||||
import { |
||||
AccountInfo, |
||||
AccountName, |
||||
GetRequestData, |
||||
SearchTransactionResponse, |
||||
TransactionSearchParams, |
||||
} from "./qortalRequestTypes.ts"; |
||||
|
||||
export const getBalance = async (address: string) => { |
||||
return (await qortalRequest({ |
||||
action: "GET_BALANCE", |
||||
address, |
||||
})) as number; |
||||
}; |
||||
|
||||
export const getUserAccount = async () => { |
||||
return (await qortalRequest({ |
||||
action: "GET_USER_ACCOUNT", |
||||
})) as AccountInfo; |
||||
}; |
||||
export const getUserBalance = async () => { |
||||
const accountInfo = await getUserAccount(); |
||||
return (await getBalance(accountInfo.address)) as number; |
||||
}; |
||||
export const getAccountNames = async ( |
||||
address: string, |
||||
params?: GetRequestData |
||||
) => { |
||||
const names = (await qortalRequest({ |
||||
action: "GET_ACCOUNT_NAMES", |
||||
address, |
||||
...params, |
||||
})) as AccountName[]; |
||||
|
||||
const namelessAddress = { name: "", owner: address }; |
||||
const emptyNamesFilled = names.map(({ name, owner }) => { |
||||
return name ? { name, owner } : namelessAddress; |
||||
}); |
||||
|
||||
return emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress]; |
||||
}; |
||||
|
||||
export const searchTransactions = async (params: TransactionSearchParams) => { |
||||
return (await qortalRequest({ |
||||
action: "SEARCH_TRANSACTIONS", |
||||
...params, |
||||
})) as SearchTransactionResponse[]; |
||||
}; |
@ -0,0 +1,76 @@
|
||||
export type AccountInfo = { address: string; publicKey: string }; |
||||
export type AccountName = { name: string; owner: string }; |
||||
export type ConfirmationStatus = "CONFIRMED" | "UNCONFIRMED" | "BOTH"; |
||||
|
||||
export interface GetRequestData { |
||||
limit?: number; |
||||
offset?: number; |
||||
reverse?: boolean; |
||||
} |
||||
|
||||
export interface SearchTransactionResponse { |
||||
type: string; |
||||
timestamp: number; |
||||
reference: string; |
||||
fee: string; |
||||
signature: string; |
||||
txGroupId: number; |
||||
blockHeight: number; |
||||
approvalStatus: string; |
||||
creatorAddress: string; |
||||
senderPublicKey: string; |
||||
recipient: string; |
||||
amount: string; |
||||
} |
||||
|
||||
export type TransactionType = |
||||
| "GENESIS" |
||||
| "PAYMENT" |
||||
| "REGISTER_NAME" |
||||
| "UPDATE_NAME" |
||||
| "SELL_NAME" |
||||
| "CANCEL_SELL_NAME" |
||||
| "BUY_NAME" |
||||
| "CREATE_POLL" |
||||
| "VOTE_ON_POLL" |
||||
| "ARBITRARY" |
||||
| "ISSUE_ASSET" |
||||
| "TRANSFER_ASSET" |
||||
| "CREATE_ASSET_ORDER" |
||||
| "CANCEL_ASSET_ORDER" |
||||
| "MULTI_PAYMENT" |
||||
| "DEPLOY_AT" |
||||
| "MESSAGE" |
||||
| "CHAT" |
||||
| "PUBLICIZE" |
||||
| "AIRDROP" |
||||
| "AT" |
||||
| "CREATE_GROUP" |
||||
| "UPDATE_GROUP" |
||||
| "ADD_GROUP_ADMIN" |
||||
| "REMOVE_GROUP_ADMIN" |
||||
| "GROUP_BAN" |
||||
| "CANCEL_GROUP_BAN" |
||||
| "GROUP_KICK" |
||||
| "GROUP_INVITE" |
||||
| "CANCEL_GROUP_INVITE" |
||||
| "JOIN_GROUP" |
||||
| "LEAVE_GROUP" |
||||
| "GROUP_APPROVAL" |
||||
| "SET_GROUP" |
||||
| "UPDATE_ASSET" |
||||
| "ACCOUNT_FLAGS" |
||||
| "ENABLE_FORGING" |
||||
| "REWARD_SHARE" |
||||
| "ACCOUNT_LEVEL" |
||||
| "TRANSFER_PRIVS" |
||||
| "PRESENCE"; |
||||
|
||||
export interface TransactionSearchParams extends GetRequestData { |
||||
startBlock?: number; |
||||
blockLimit?: number; |
||||
txGroupId?: number; |
||||
txType: TransactionType[]; |
||||
address: string; |
||||
confirmationStatus: ConfirmationStatus; |
||||
} |
@ -0,0 +1,8 @@
|
||||
export const getFileExtensionIndex = (s: string) => { |
||||
const lastIndex = s.lastIndexOf("."); |
||||
return lastIndex > 0 ? lastIndex : s.length - 1; |
||||
}; |
||||
|
||||
export const getFileName = (s: string) => { |
||||
return s.substring(0, getFileExtensionIndex(s)); |
||||
}; |
Loading…
Reference in new issue