feat: comments nostr tooltip, relays connection status, wrap nav content in page-container, show total comments count on story card, created prefernces service

This commit is contained in:
MTG2000
2022-07-30 11:11:29 +03:00
parent a140208995
commit d43b3215bb
14 changed files with 347 additions and 234 deletions

View File

@@ -377,6 +377,7 @@ export interface NexusGenFieldTypes {
author: NexusGenRootTypes['Author']; // Author!
body: string; // String!
comments: NexusGenRootTypes['PostComment'][]; // [PostComment!]!
comments_count: number; // Int!
cover_image: string | null; // String
createdAt: NexusGenScalars['Date']; // Date!
excerpt: string; // String!
@@ -585,6 +586,7 @@ export interface NexusGenFieldTypeNames {
author: 'Author'
body: 'String'
comments: 'PostComment'
comments_count: 'Int'
cover_image: 'String'
createdAt: 'Date'
excerpt: 'String'

View File

@@ -187,6 +187,7 @@ type Story implements PostBase {
author: Author!
body: String!
comments: [PostComment!]!
comments_count: Int!
cover_image: String
createdAt: Date!
excerpt: String!

View File

@@ -80,21 +80,21 @@ const Story = objectType({
type: "Tag",
resolve: (parent) => prisma.story.findUnique({ where: { id: parent.id } }).tags()
});
// t.nonNull.int('comments_count', {
// resolve: async (parent) => {
// const post = await prisma.story.findUnique({
// where: { id: parent.id },
// include: {
// _count: {
// select: {
// comments: true
// }
// }
// }
// })
// return post._count.comments;
// }
// });
t.nonNull.int('comments_count', {
resolve: async (parent) => {
const post = await prisma.story.findUnique({
where: { id: parent.id },
include: {
_count: {
select: {
comments: true
}
}
}
})
return post._count.comments;
}
});
t.nonNull.field('author', {
type: "Author",
resolve: (parent) =>

View File

@@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
import { UnionToObjectKeys } from 'src/utils/types/utils';
import { Link } from 'react-router-dom'
import { FallingLines, LineWave, TailSpin } from 'react-loader-spinner';
import { TailSpin } from 'react-loader-spinner';
type Props = {
color?: 'primary' | 'red' | 'white' | 'gray' | "black" | 'none',
@@ -107,8 +107,10 @@ const Button = React.forwardRef<any, Props>(({ color = 'white',
disabled={disabled}
{...props}
>
{children}
{isLoading && <div className="text-body5 absolute inset-1 bg-inherit flex flex-col justify-center items-center">
<span className={isLoading ? "opacity-0" : ""}>
{children}
</span>
{isLoading && <div className="text-body5 absolute inset-0 rounded-lg bg-inherit flex flex-col justify-center items-center">
{loadingText ?? <TailSpin
width="24"
color={loadingColor[color]}

View File

@@ -40,113 +40,114 @@ export default function NavDesktop() {
return (
<nav className="bg-white flex py-16 px-32 items-center w-full min-w-full">
<Link to="/">
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
</h2>
</Link>
<ul className="flex gap-32 xl:gap-64">
<li className="relative">
<Link to={'/products'} className='text-body4 font-bold hover:text-primary-600'>
Products
</Link>
</li>
<li>
<Menu
offsetY={28}
menuButton={
<MenuButton
className='text-body4 font-bold hover:text-primary-600'>Community <FiChevronDown className="ml-8" />
</MenuButton>
}
menuClassName='!rounded-12 !p-8 !border-gray-200'
menuStyle={{ border: '1px solid' }}
>
<MenuItem
href="/blog"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/blog");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 '
<nav className="bg-white py-16 flex items-center w-full min-w-full">
<div className="page-container flex items-center !p-0">
<Link to="/">
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
</h2>
</Link>
<ul className="flex gap-32 xl:gap-64">
<li className="relative">
<Link to={'/products'} className='text-body4 font-bold hover:text-primary-600'>
Products
</Link>
</li>
<li>
<Menu
offsetY={28}
menuButton={
<MenuButton
className='text-body4 font-bold hover:text-primary-600'>Community <FiChevronDown className="ml-8" />
</MenuButton>
}
menuClassName='!rounded-12 !p-8 !border-gray-200'
menuStyle={{ border: '1px solid' }}
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
{/* <FiFeather className={`text-body1 inline-block text-primary-600 `} /> */}
<span className="text-body2">🏼</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Stories
</p>
<p className="text-body5 text-gray-600 mt-4">
Tales from the maker community
</p>
</div>
</MenuItem>
<MenuItem
<MenuItem
href="/blog"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/blog");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 '
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
{/* <FiFeather className={`text-body1 inline-block text-primary-600 `} /> */}
<span className="text-body2">🏼</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Stories
</p>
<p className="text-body5 text-gray-600 mt-4">
Tales from the maker community
</p>
</div>
</MenuItem>
<MenuItem
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 opacity-40'
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12 opacity-40'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">💬</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Discussions
</p>
<p className="text-body5 text-gray-600 mt-4">
Coming soon
</p>
</div>
</MenuItem>
<MenuItem
href="/hackathons"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/hackathons");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">🏆</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Hackathons
</p>
<p className="text-body5 text-gray-600 mt-4">
Take part in hackathons & tournaments
</p>
</div>
</MenuItem>
</Menu>
</li>
<li className="relative">
<a
href={'https://bolt.fun/guide/'}
target="_blank"
rel="noreferrer"
className='text-body4 font-bold hover:text-primary-600'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">💬</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Discussions
</p>
<p className="text-body5 text-gray-600 mt-4">
Coming soon
</p>
</div>
</MenuItem>
<MenuItem
href="/hackathons"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/hackathons");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">🏆</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
Hackathons
</p>
<p className="text-body5 text-gray-600 mt-4">
Take part in hackathons & tournaments
</p>
</div>
</MenuItem>
</Menu>
</li>
<li className="relative">
<a
href={'https://bolt.fun/guide/'}
target="_blank"
rel="noreferrer"
className='text-body4 font-bold hover:text-primary-600'
>
Guide
</a>
</li>
<li className="relative">
<Link to={'/donate'} className='text-body4 font-bold hover:text-primary-600'>
Donate
</Link>
</li>
</ul>
Guide
</a>
</li>
<li className="relative">
<Link to={'/donate'} className='text-body4 font-bold hover:text-primary-600'>
Donate
</Link>
</li>
</ul>
<div className="flex-1"></div>
<div className="flex-1"></div>
<motion.div
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
className="flex"
>
<motion.div
animate={searchOpen ? { opacity: 0 } : { opacity: 1 }}
className="flex"
>
{/* <Button
{/* <Button
color="primary"
size="md"
className="lg:px-40"
@@ -155,75 +156,76 @@ export default function NavDesktop() {
>
Submit App
</Button> */}
{/* {isWalletConnected ?
{/* {isWalletConnected ?
<Button className="ml-16 py-12 px-16 lg:px-20">Connected <AiFillThunderbolt className='inline-block text-thunder transform scale-125' /></Button>
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
} */}
{currentSection === 'products' && <IconButton className='mr-16 self-center' onClick={openSearch}>
<BsSearch className='scale-125 text-gray-400' />
</IconButton>}
</motion.div>
{curUser !== undefined &&
(curUser ?
<Menu
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Profile
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Logout
</MenuItem>
</Menu>
:
<Button color="primary" href="/login">
Connect
</Button>
)
}
<div className="relative h-36">
<motion.div
initial={{
opacity: 0,
y: '0'
}}
animate={searchOpen ? {
opacity: 1,
y: '0',
transition: { type: "spring", stiffness: 70 }
} : {
opacity: 0,
y: '-120px',
transition: {
ease: "easeIn"
}
}}
className='absolute top-0 right-0 flex items-center h-full'
>
<Search
width={326}
isOpen={searchOpen}
onClose={() => setSearchOpen(false)}
onResultClick={() => setSearchOpen(false)}
/>
{currentSection === 'products' && <IconButton className='mr-16 self-center' onClick={openSearch}>
<BsSearch className='scale-125 text-gray-400' />
</IconButton>}
</motion.div>
{curUser !== undefined &&
(curUser ?
<Menu
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Profile
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Logout
</MenuItem>
</Menu>
:
<Button color="primary" href="/login">
Connect
</Button>
)
}
<div className="relative h-36">
<motion.div
initial={{
opacity: 0,
y: '0'
}}
animate={searchOpen ? {
opacity: 1,
y: '0',
transition: { type: "spring", stiffness: 70 }
} : {
opacity: 0,
y: '-120px',
transition: {
ease: "easeIn"
}
}}
className='absolute top-0 right-0 flex items-center h-full'
>
<Search
width={326}
isOpen={searchOpen}
onClose={() => setSearchOpen(false)}
onResultClick={() => setSearchOpen(false)}
/>
</motion.div>
</div>
</div>
</nav>
);

View File

@@ -74,45 +74,47 @@ export default function NavMobile() {
return (
<div className={`${styles.navMobile}`}>
<nav className={`bg-white h-[67px] w-full p-16 px-32 flex justify-between items-center`}>
<Link to="/">
<img className='h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
</Link>
<nav className={`bg-white h-[67px] w-full py-16`}>
<div className="page-container flex justify-between items-center !p-0">
<Link to="/">
<img className='h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
</Link>
<div className="ml-auto"></div>
{curUser !== undefined &&
(curUser &&
<Menu
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Profile
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Logout
</MenuItem>
</Menu>
<div className="ml-auto"></div>
{curUser !== undefined &&
(curUser &&
<Menu
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
<MenuItem
href={createRoute({ type: 'profile', id: curUser.id, username: curUser.name })}
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate(createRoute({ type: 'profile', id: curUser.id, username: curUser.name }));
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Profile
</MenuItem>
<MenuItem
href="/logout"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/logout");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Logout
</MenuItem>
</Menu>
)
}
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
</IconButton>
)
}
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
</IconButton>
</div>
</nav>
<div className="fixed left-0 top-[67px] pointer-events-none z-[2010] w-full min-h-[calc(100vh-67px)]">

View File

@@ -33,6 +33,7 @@ export default function Comment({ comment, canReply, isRoot, onClickedReply, onR
const handleReply = async (text: string) => {
try {
await onReply?.(text);
toggleRepliesCollapsed(false);
setReplyOpen(false);
return true;
} catch (error) {
@@ -58,7 +59,7 @@ export default function Comment({ comment, canReply, isRoot, onClickedReply, onR
key={reply.id}
comment={reply}
onClickedReply={clickReply}
canReply={false}
canReply={!!isRoot}
/>)}
{replyOpen && <AddComment
avatar={user?.avatar!}

View File

@@ -8,6 +8,11 @@ import { useAppSelector } from "src/utils/hooks";
import * as CommentsWorker from './comments.worker'
import { Post_Type } from 'src/graphql'
import useComments from './useComments'
import IconButton from 'src/Components/IconButton/IconButton'
import { AiOutlineClose } from 'react-icons/ai'
import { Link } from 'react-router-dom'
import { createRoute } from 'src/utils/routing'
import Preferences from 'src/services/preferences.service'
// const createWorker = createWorkerFactory(() => import('./comments.worker'));
@@ -23,7 +28,8 @@ export default function CommentsSection({ type, id }: Props) {
// const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments])
// const [commentsTree, setCommentsTree] = useState<Comment[]>([])
const user = useAppSelector(state => state.user.me)
const user = useAppSelector(state => state.user.me);
const [showTooltip, setShowTooltip] = useState(Preferences.get('showNostrCommentsTooltip'))
// const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [id, type])
// useEffect(() => {
@@ -36,7 +42,7 @@ export default function CommentsSection({ type, id }: Props) {
// unsub();
// }
// }, [filter]);
const { commentsTree, postComment } = useComments({ type, id })
const { commentsTree, postComment, connectionStatus } = useComments({ type, id })
const handleNewComment = async (content: string, parentId?: string) => {
try {
@@ -47,10 +53,25 @@ export default function CommentsSection({ type, id }: Props) {
}
}
const closeTooltip = () => {
Preferences.update('showNostrCommentsTooltip', false);
setShowTooltip(false);
}
return (
<div className="border-2 border-gray-200 rounded-12 md:rounded-16 p-32 bg-white">
<h6 className="text-body2 font-bolder">Discussion</h6>
<div className="flex flex-wrap justify-between">
<h6 className="text-body2 font-bolder">Discussion</h6>
{connectionStatus.status === 'Connected' && <div className="bg-green-50 text-green-500 text-body5 font-medium py-4 px-12 rounded-48"> &#8226; Connected to {connectionStatus.connectedRelaysCount} relays 📡</div>}
{connectionStatus.status === 'Connecting' && <div className="bg-amber-50 text-amber-500 text-body5 font-medium py-4 px-12 rounded-48"> &#8226; Connecting to relays </div>}
{connectionStatus.status === 'Not Connected' && <div className="bg-red-50 text-red-500 text-body5 font-medium py-4 px-12 rounded-48"> &#8226; Not connected 📡</div>}
</div>
{showTooltip && <div className="bg-gray-900 text-white p-16 rounded-12 my-24 flex items-center justify-between gap-12">
<span>💬</span>
<p className="text-body4 font-medium">Learn about <Link to={createRoute({ type: "story", title: "What is Nostr", id: 999 })} className='underline'>how your data is stored</Link> with Nostr comments and relays</p>
<IconButton className='shrink-0 self-start' onClick={closeTooltip}><AiOutlineClose className='text-gray-600' /></IconButton>
</div>}
{!!user && <div className="mt-24">
<AddComment
placeholder='Leave a comment...'

View File

@@ -16,6 +16,7 @@ const useComments = (config: {
const commentsEventsTemp = useRef<Record<string, Required<NostrEvent>>>({})
const [commentsEvents, setCommentsEvents] = useDebouncedState<Record<string, Required<NostrEvent>>>({}, 1000)
const pendingResolvers = useRef<Record<string, () => void>>({});
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>({ status: "Connecting", connectedRelaysCount: 0 })
const filter = useMemo(() => `boltfun ${config.type}_comment ${config.id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [config.id, config.type])
const [commentsTree, setCommentsTree] = useState<Comment[]>([])
@@ -55,7 +56,21 @@ const useComments = (config: {
}
});
})();
}, [commentsEvents])
}, [commentsEvents]);
useEffect(() => {
const interval = setInterval(() => {
const newStatus = getConnectionStatus();
if (newStatus.connectedRelaysCount !== connectionStatus.connectedRelaysCount || newStatus.status !== connectionStatus.status)
setConnectionStatus(newStatus);
}, 5000)
return () => {
clearInterval(interval)
}
}, [connectionStatus.connectedRelaysCount, connectionStatus.status])
const postComment = useCallback(async ({ content, parentId }: {
content: string,
@@ -110,7 +125,7 @@ const useComments = (config: {
}, [filter]);
return { commentsTree, postComment }
return { commentsTree, postComment, connectionStatus }
}
export default useComments;
@@ -249,4 +264,30 @@ async function buildTree(events: Record<string, Required<NostrEvent>>) {
const sortedTree = Object.values(eventsTree).sort((a, b) => b.created_at - a.created_at)
return sortedTree;
}
type ConnectionStatus = {
status: 'Connected' | "Connecting" | "Not Connected",
connectedRelaysCount: number
}
function getConnectionStatus(): ConnectionStatus {
let openedCnt = 0, reconnectingCnt = 0;
for (const relayUrl in pool.relays) {
const relayStatus = pool.relays[relayUrl].relay.status;
if (relayStatus === 1) openedCnt += 1;
if (relayStatus === 0) reconnectingCnt += 1;
}
const finalStatus = openedCnt > 0 ?
"Connected" :
reconnectingCnt > 0 ?
"Connecting" :
"Not Connected";
return {
status: finalStatus,
connectedRelaysCount: openedCnt
}
}

View File

@@ -6,6 +6,7 @@ import { useVote } from "src/utils/hooks"
import { Author, Tag, Vote_Item_Type } from 'src/graphql';
import Badge from "src/Components/Badge/Badge"
import { createRoute } from "src/utils/routing"
import { BiComment } from "react-icons/bi"
export type StoryCardType = Pick<Story,
@@ -16,6 +17,7 @@ export type StoryCardType = Pick<Story,
| 'createdAt'
| 'excerpt'
| 'votes_count'
| 'comments_count'
> & {
tags: Array<Pick<Tag, 'id' | "title">>,
author: Pick<Author, 'id' | 'name' | 'avatar' | 'join_date'>
@@ -49,9 +51,9 @@ export default function StoryCard({ story }: Props) {
<hr className="my-16 bg-gray-200" />
<div className="flex gap-24 items-center">
<VoteButton votes={story.votes_count} dense onVote={vote} />
{/* <div className="text-gray-600">
<div className="text-gray-600">
<BiComment /> <span className="align-middle text-body5">{story.comments_count} Comments</span>
</div> */}
</div>
</div>
</div>
</div>

View File

@@ -18,7 +18,7 @@ query Feed($take: Int, $skip: Int, $sortBy: String, $tag: Int) {
votes_count
type
cover_image
# comments_count
comments_count
}
... on Bounty {
id

View File

@@ -330,6 +330,7 @@ export type Story = PostBase & {
author: Author;
body: Scalars['String'];
comments: Array<PostComment>;
comments_count: Scalars['Int'];
cover_image: Maybe<Scalars['String']>;
createdAt: Scalars['Date'];
excerpt: Scalars['String'];
@@ -504,7 +505,7 @@ export type FeedQueryVariables = Exact<{
}>;
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
export type FeedQuery = { __typename?: 'Query', getFeed: Array<{ __typename?: 'Bounty', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, deadline: string, reward_amount: number, applicants_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Question', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> } | { __typename?: 'Story', id: number, title: string, createdAt: any, excerpt: string, votes_count: number, type: string, cover_image: string | null, comments_count: number, author: { __typename?: 'Author', id: number, name: string, avatar: string, join_date: any }, tags: Array<{ __typename?: 'Tag', id: number, title: string }> }> };
export type PostDetailsQueryVariables = Exact<{
id: Scalars['Int'];
@@ -1124,6 +1125,7 @@ export const FeedDocument = gql`
votes_count
type
cover_image
comments_count
}
... on Bounty {
id

View File

@@ -0,0 +1,40 @@
type PreferencesType = {
showNostrCommentsTooltip: boolean;
themeMode: 'light' | 'dark'
}
const defaultPrefernces: PreferencesType = {
showNostrCommentsTooltip: true,
themeMode: 'light'
}
export default class Preferences {
private static preferencesObject: PreferencesType;
static init() {
const str = localStorage.getItem('preferences');
if (!str)
this.preferencesObject = defaultPrefernces;
else
this.preferencesObject = JSON.parse(str)
}
static update<T extends keyof PreferencesType>(key: T, value: PreferencesType[T]) {
if (!this.preferencesObject)
this.init();
this.preferencesObject[key] = value;
localStorage.setItem('preferences', JSON.stringify(this.preferencesObject))
}
static get<T extends keyof PreferencesType>(key: T): PreferencesType[T] {
if (!this.preferencesObject)
this.init();
return this.preferencesObject[key];
}
}

View File

@@ -1,10 +1,7 @@
const DEFAULT_RELAYS = [
'wss://nostr.drss.io',
'wss://nostr-relay.freeberty.net',
'wss://nostr.unknown.place',
'wss://nostr-relay.untethr.me',
'wss://relay.damus.io'
'wss://relay.damus.io',
];
const CONSTS = {