notifications done
This commit is contained in:
parent
f7b4618b4a
commit
01922e4b88
@ -37,18 +37,21 @@ function nextCursorEncoded (cursor) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
moreItems: async (parent, { sort, cursor, userId }, { models }) => {
|
moreItems: async (parent, { sort, cursor, userId }, { me, models }) => {
|
||||||
const decodedCursor = decodeCursor(cursor)
|
const decodedCursor = decodeCursor(cursor)
|
||||||
const items = userId
|
let items
|
||||||
? await models.$queryRaw(`
|
switch (sort) {
|
||||||
${SELECT}
|
case 'user':
|
||||||
FROM "Item"
|
items = await models.$queryRaw(`
|
||||||
WHERE "userId" = $1 AND "parentId" IS NULL AND created_at <= $2
|
${SELECT}
|
||||||
ORDER BY created_at DESC
|
FROM "Item"
|
||||||
OFFSET $3
|
WHERE "userId" = $1 AND "parentId" IS NULL AND created_at <= $2
|
||||||
LIMIT ${LIMIT}`, Number(userId), decodedCursor.time, decodedCursor.offset)
|
ORDER BY created_at DESC
|
||||||
: sort === 'hot'
|
OFFSET $3
|
||||||
? await models.$queryRaw(`
|
LIMIT ${LIMIT}`, Number(userId), decodedCursor.time, decodedCursor.offset)
|
||||||
|
break
|
||||||
|
case 'hot':
|
||||||
|
items = await models.$queryRaw(`
|
||||||
${SELECT}
|
${SELECT}
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
${timedLeftJoinSats(1)}
|
${timedLeftJoinSats(1)}
|
||||||
@ -56,18 +59,66 @@ export default {
|
|||||||
${timedOrderBySats(1)}
|
${timedOrderBySats(1)}
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||||
: await models.$queryRaw(`
|
break
|
||||||
|
default:
|
||||||
|
items = await models.$queryRaw(`
|
||||||
${SELECT}
|
${SELECT}
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE "parentId" IS NULL AND created_at <= $1
|
WHERE "parentId" IS NULL AND created_at <= $1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||||
|
break
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
|
cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
moreFlatComments: async (parent, { cursor, userId }, { me, models }) => {
|
||||||
|
const decodedCursor = decodeCursor(cursor)
|
||||||
|
let comments
|
||||||
|
if (userId) {
|
||||||
|
comments = await models.$queryRaw(`
|
||||||
|
${SELECT}
|
||||||
|
FROM "Item"
|
||||||
|
WHERE "userId" = $1 AND "parentId" IS NOT NULL
|
||||||
|
AND created_at <= $2
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
OFFSET $3
|
||||||
|
LIMIT ${LIMIT}`, Number(userId), decodedCursor.time, decodedCursor.offset)
|
||||||
|
} else {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
const user = await models.user.findUnique({ where: { name: me.name } })
|
||||||
|
comments = await models.$queryRaw(`
|
||||||
|
${SELECT}
|
||||||
|
From "Item"
|
||||||
|
JOIN "Item" p ON "Item"."parentId" = p.id AND p."userId" = $1
|
||||||
|
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
|
||||||
|
ORDER BY "Item".created_at DESC
|
||||||
|
OFFSET $3
|
||||||
|
LIMIT ${LIMIT}`, user.id, decodedCursor.time, decodedCursor.offset)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
cursor: comments.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
|
||||||
|
comments
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notifications: async (parent, args, { me, models }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
const user = await models.user.findUnique({ where: { name: me.name } })
|
||||||
|
|
||||||
|
return await models.$queryRaw(`
|
||||||
|
${SELECT}
|
||||||
|
From "Item"
|
||||||
|
JOIN "Item" p ON "Item"."parentId" = p.id AND p."userId" = $1
|
||||||
|
AND "Item"."userId" <> $1
|
||||||
|
ORDER BY "Item".created_at DESC`, user.id)
|
||||||
|
},
|
||||||
item: async (parent, { id }, { models }) => {
|
item: async (parent, { id }, { models }) => {
|
||||||
const [item] = await models.$queryRaw(`
|
const [item] = await models.$queryRaw(`
|
||||||
${SELECT}
|
${SELECT}
|
||||||
@ -104,8 +155,6 @@ export default {
|
|||||||
throw new UserInputError('link must have url', { argumentName: 'url' })
|
throw new UserInputError('link must have url', { argumentName: 'url' })
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(ensureProtocol(url))
|
|
||||||
|
|
||||||
return await createItem(parent, { title, url: ensureProtocol(url) }, { me, models })
|
return await createItem(parent, { title, url: ensureProtocol(url) }, { me, models })
|
||||||
},
|
},
|
||||||
createDiscussion: async (parent, { title, text }, { me, models }) => {
|
createDiscussion: async (parent, { title, text }, { me, models }) => {
|
||||||
|
@ -15,6 +15,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return me.name === name || !(await models.user.findUnique({ where: { name } }))
|
return me.name === name || !(await models.user.findUnique({ where: { name } }))
|
||||||
|
},
|
||||||
|
recentlyStacked: async (parent, args, { models, me }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new AuthenticationError('you must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await models.user.findUnique({ where: { name: me.name } })
|
||||||
|
|
||||||
|
const [{ sum }] = await models.$queryRaw(`
|
||||||
|
SELECT sum("Vote".sats)
|
||||||
|
FROM "Item"
|
||||||
|
LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id
|
||||||
|
AND "Vote"."userId" <> $1
|
||||||
|
AND ("Vote".created_at > $2 OR $2 IS NULL)
|
||||||
|
AND "Vote".boost = false
|
||||||
|
WHERE "Item"."userId" = $1`, user.id, user.checkedNotesAt)
|
||||||
|
|
||||||
|
await models.user.update({ where: { name: me.name }, data: { checkedNotesAt: new Date() } })
|
||||||
|
return sum || 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -46,12 +65,36 @@ export default {
|
|||||||
const [{ sum }] = await models.$queryRaw`
|
const [{ sum }] = await models.$queryRaw`
|
||||||
SELECT sum("Vote".sats)
|
SELECT sum("Vote".sats)
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id AND "Vote"."userId" <> ${user.id}
|
LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id AND "Vote"."userId" <> ${user.id} AND boost = false
|
||||||
WHERE "Item"."userId" = ${user.id}`
|
WHERE "Item"."userId" = ${user.id}`
|
||||||
return sum || 0
|
return sum || 0
|
||||||
},
|
},
|
||||||
sats: async (user, args, { models }) => {
|
sats: async (user, args, { models }) => {
|
||||||
return Math.floor(user.msats / 1000)
|
return Math.floor(user.msats / 1000)
|
||||||
|
},
|
||||||
|
hasNewNotes: async (user, args, { models }) => {
|
||||||
|
// check if any votes have been cast for them since checkedNotesAt
|
||||||
|
const votes = await models.$queryRaw(`
|
||||||
|
SELECT "Vote".id, "Vote".created_at
|
||||||
|
FROM "Vote"
|
||||||
|
LEFT JOIN "Item" on "Vote"."itemId" = "Item".id
|
||||||
|
AND "Vote"."userId" <> $1
|
||||||
|
AND ("Vote".created_at > $2 OR $2 IS NULL)
|
||||||
|
AND "Vote".boost = false
|
||||||
|
WHERE "Item"."userId" = $1
|
||||||
|
LIMIT 1`, user.id, user.checkedNotesAt)
|
||||||
|
if (votes.length > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if they have any replies since checkedNotesAt
|
||||||
|
const newReplies = await models.$queryRaw(`
|
||||||
|
SELECT "Item".id, "Item".created_at
|
||||||
|
From "Item"
|
||||||
|
JOIN "Item" p ON "Item"."parentId" = p.id AND p."userId" = $1
|
||||||
|
AND ("Item".created_at > $2 OR $2 IS NULL) AND "Item"."userId" <> $1
|
||||||
|
LIMIT 1`, user.id, user.checkedNotesAt)
|
||||||
|
return !!newReplies.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import { gql } from 'apollo-server-micro'
|
|||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
moreItems(sort: String!, cursor: String, userId: ID): Items
|
moreItems(sort: String!, cursor: String, userId: ID): Items
|
||||||
|
moreFlatComments(cursor: String, userId: ID): Comments
|
||||||
|
notifications: [Item!]!
|
||||||
item(id: ID!): Item
|
item(id: ID!): Item
|
||||||
userComments(userId: ID!): [Item!]
|
userComments(userId: ID!): [Item!]
|
||||||
root(id: ID!): Item
|
root(id: ID!): Item
|
||||||
@ -20,6 +22,11 @@ export default gql`
|
|||||||
items: [Item!]!
|
items: [Item!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Comments {
|
||||||
|
cursor: String
|
||||||
|
comments: [Item!]!
|
||||||
|
}
|
||||||
|
|
||||||
type Item {
|
type Item {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
|
@ -6,6 +6,7 @@ export default gql`
|
|||||||
user(name: String!): User
|
user(name: String!): User
|
||||||
users: [User!]
|
users: [User!]
|
||||||
nameAvailable(name: String!): Boolean!
|
nameAvailable(name: String!): Boolean!
|
||||||
|
recentlyStacked: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
@ -20,6 +21,7 @@ export default gql`
|
|||||||
stacked: Int!
|
stacked: Int!
|
||||||
freePosts: Int!
|
freePosts: Int!
|
||||||
freeComments: Int!
|
freeComments: Int!
|
||||||
|
hasNewNotes: Boolean!
|
||||||
sats: Int!
|
sats: Int!
|
||||||
msats: Int!
|
msats: Int!
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,13 @@ import styles from './comment.module.css'
|
|||||||
import Text from './text'
|
import Text from './text'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Reply from './reply'
|
import Reply from './reply'
|
||||||
import { useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import { timeSince } from '../lib/time'
|
import { timeSince } from '../lib/time'
|
||||||
import UpVote from './upvote'
|
import UpVote from './upvote'
|
||||||
import Eye from '../svgs/eye-fill.svg'
|
import Eye from '../svgs/eye-fill.svg'
|
||||||
import EyeClose from '../svgs/eye-close-line.svg'
|
import EyeClose from '../svgs/eye-close-line.svg'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
function Parent ({ item }) {
|
function Parent ({ item }) {
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
@ -24,7 +25,7 @@ function Parent ({ item }) {
|
|||||||
<>
|
<>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<Link href={`/items/${item.parentId}`} passHref>
|
<Link href={`/items/${item.parentId}`} passHref>
|
||||||
<a className='text-reset'>parent</a>
|
<a onClick={e => e.stopPropagation()} className='text-reset'>parent</a>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -38,18 +39,33 @@ function Parent ({ item }) {
|
|||||||
{data.root.id !== item.parentId && <ParentFrag />}
|
{data.root.id !== item.parentId && <ParentFrag />}
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<Link href={`/items/${data.root.id}`} passHref>
|
<Link href={`/items/${data.root.id}`} passHref>
|
||||||
<a className='text-reset'>{data.root.title}</a>
|
<a onClick={e => e.stopPropagation()} className='text-reset'>root: {data.root.title}</a>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply }) {
|
export default function Comment ({ item, children, replyOpen, includeParent, cacheId, noComments, noReply, clickToContext }) {
|
||||||
const [reply, setReply] = useState(replyOpen)
|
const [reply, setReply] = useState(replyOpen)
|
||||||
const [collapse, setCollapse] = useState(false)
|
const [collapse, setCollapse] = useState(false)
|
||||||
|
const ref = useRef(null)
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Number(router.query.commentId) === Number(item.id)) {
|
||||||
|
ref.current.scrollIntoView()
|
||||||
|
// ref.current.classList.add('flash-it')
|
||||||
|
}
|
||||||
|
}, [item])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={includeParent ? '' : `${styles.comment} ${collapse ? styles.collapsed : ''}`}>
|
<div
|
||||||
|
ref={ref} onClick={() => {
|
||||||
|
if (clickToContext) {
|
||||||
|
router.push(`/items/${item.parentId}?commentId=${item.id}`, `/items/${item.parentId}`)
|
||||||
|
}
|
||||||
|
}} className={includeParent ? `${clickToContext ? styles.clickToContext : ''}` : `${styles.comment} ${collapse ? styles.collapsed : ''}`}
|
||||||
|
>
|
||||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||||
<UpVote itemId={item.id} meSats={item.meSats} className={styles.upvote} />
|
<UpVote itemId={item.id} meSats={item.meSats} className={styles.upvote} />
|
||||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||||
@ -60,11 +76,11 @@ export default function Comment ({ item, children, replyOpen, includeParent, cac
|
|||||||
<span>{item.boost} boost</span>
|
<span>{item.boost} boost</span>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<Link href={`/items/${item.id}`} passHref>
|
<Link href={`/items/${item.id}`} passHref>
|
||||||
<a className='text-reset'>{item.ncomments} replies</a>
|
<a onClick={e => e.stopPropagation()} className='text-reset'>{item.ncomments} replies</a>
|
||||||
</Link>
|
</Link>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<Link href={`/${item.user.name}`} passHref>
|
<Link href={`/${item.user.name}`} passHref>
|
||||||
<a>@{item.user.name}</a>
|
<a onClick={e => e.stopPropagation()}>@{item.user.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<span>{timeSince(new Date(item.createdAt))}</span>
|
<span>{timeSince(new Date(item.createdAt))}</span>
|
||||||
|
@ -80,6 +80,15 @@
|
|||||||
padding-left: .2rem;
|
padding-left: .2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickToContext {
|
||||||
|
border-radius: .4rem;
|
||||||
|
padding: .2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickToContext:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
.comment:not(:last-child) {
|
.comment:not(:last-child) {
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
@ -90,3 +99,8 @@
|
|||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickToContext {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
70
components/comments-flat.js
Normal file
70
components/comments-flat.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { useQuery } from '@apollo/client'
|
||||||
|
import Button from 'react-bootstrap/Button'
|
||||||
|
import { MORE_FLAT_COMMENTS } from '../fragments/comments'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Comment, { CommentSkeleton } from './comment'
|
||||||
|
|
||||||
|
export default function CommentsFlat ({ variables, ...props }) {
|
||||||
|
const { loading, error, data, fetchMore } = useQuery(MORE_FLAT_COMMENTS, {
|
||||||
|
variables
|
||||||
|
})
|
||||||
|
if (error) return <div>Failed to load!</div>
|
||||||
|
if (loading) {
|
||||||
|
return <CommentsFlatSkeleton />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { moreFlatComments: { comments, cursor } } = data
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{comments.map(item => (
|
||||||
|
<Comment key={item.id} item={item} {...props} />
|
||||||
|
))}
|
||||||
|
<MoreFooter cursor={cursor} fetchMore={fetchMore} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommentsFlatSkeleton () {
|
||||||
|
const comments = new Array(21).fill(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>{comments.map((_, i) => (
|
||||||
|
<CommentSkeleton key={i} skeletonChildren={0} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoreFooter ({ cursor, fetchMore }) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div><CommentsFlatSkeleton /></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
let Footer
|
||||||
|
if (cursor) {
|
||||||
|
Footer = () => (
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
size='md'
|
||||||
|
onClick={async () => {
|
||||||
|
setLoading(true)
|
||||||
|
await fetchMore({
|
||||||
|
variables: {
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
}}
|
||||||
|
>more
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Footer = () => (
|
||||||
|
<div className='text-muted' style={{ fontFamily: 'lightning', fontSize: '2rem', opacity: '0.6' }}>GENISIS</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='d-flex justify-content-center mt-4 mb-2'><Footer /></div>
|
||||||
|
}
|
@ -1,7 +1,16 @@
|
|||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
|
import { useEffect } from 'react'
|
||||||
import Comment, { CommentSkeleton } from './comment'
|
import Comment, { CommentSkeleton } from './comment'
|
||||||
|
|
||||||
export default function Comments ({ comments, ...props }) {
|
export default function Comments ({ comments, ...props }) {
|
||||||
|
useEffect(() => {
|
||||||
|
// Your code here
|
||||||
|
const hash = window.location.hash
|
||||||
|
if (hash) {
|
||||||
|
document.querySelector(hash).scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return comments.map(item => (
|
return comments.map(item => (
|
||||||
<Comment key={item.id} item={item} {...props} />
|
<Comment key={item.id} item={item} {...props} />
|
||||||
))
|
))
|
||||||
|
@ -7,11 +7,9 @@ import { useRouter } from 'next/router'
|
|||||||
import { Button, Container, NavDropdown } from 'react-bootstrap'
|
import { Button, Container, NavDropdown } from 'react-bootstrap'
|
||||||
import Price from './price'
|
import Price from './price'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
import { useApolloClient } from '@apollo/client'
|
||||||
|
|
||||||
function WalletSummary () {
|
function WalletSummary ({ me }) {
|
||||||
const me = useMe()
|
|
||||||
if (!me) return null
|
|
||||||
|
|
||||||
return `[${me.stacked},${me.sats}]`
|
return `[${me.stacked},${me.sats}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,6 +17,8 @@ export default function Header () {
|
|||||||
const [session, loading] = useSession()
|
const [session, loading] = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const path = router.asPath.split('?')[0]
|
const path = router.asPath.split('?')[0]
|
||||||
|
const me = useMe()
|
||||||
|
const client = useApolloClient()
|
||||||
|
|
||||||
const Corner = () => {
|
const Corner = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -28,35 +28,56 @@ export default function Header () {
|
|||||||
if (session) {
|
if (session) {
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<NavDropdown className='pl-0' title={`@${session.user.name}`} alignRight>
|
<div className='position-relative'>
|
||||||
<Link href={'/' + session.user.name} passHref>
|
<NavDropdown className='pl-0' title={`@${session.user.name}`} alignRight>
|
||||||
<NavDropdown.Item>profile</NavDropdown.Item>
|
<Link href={'/' + session.user.name} passHref>
|
||||||
</Link>
|
<NavDropdown.Item>profile</NavDropdown.Item>
|
||||||
<Link href='/wallet' passHref>
|
|
||||||
<NavDropdown.Item>wallet</NavDropdown.Item>
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
<NavDropdown.Divider />
|
|
||||||
<Link href='/recent' passHref>
|
|
||||||
<NavDropdown.Item>recent</NavDropdown.Item>
|
|
||||||
</Link>
|
</Link>
|
||||||
{session
|
<Link href='/notifications' passHref>
|
||||||
? (
|
<NavDropdown.Item onClick={() => {
|
||||||
<Link href='/post' passHref>
|
// when it's a fresh click evict old notification cache
|
||||||
<NavDropdown.Item>post</NavDropdown.Item>
|
client.cache.evict({ id: 'ROOT_QUERY', fieldName: 'moreFlatComments:{}' })
|
||||||
</Link>
|
client.cache.evict({ id: 'ROOT_QUERY', fieldName: 'recentlyStacked' })
|
||||||
)
|
}}
|
||||||
: <NavDropdown.Item onClick={signIn}>post</NavDropdown.Item>}
|
>
|
||||||
<NavDropdown.Item href='https://bitcoinerjobs.co' target='_blank'>jobs</NavDropdown.Item>
|
notifications
|
||||||
</div>
|
{me && me.hasNewNotes &&
|
||||||
<NavDropdown.Divider />
|
<div className='p-1 d-inline-block bg-danger rounded-circle ml-1'>
|
||||||
<NavDropdown.Item onClick={signOut}>logout</NavDropdown.Item>
|
<span className='invisible'>{' '}</span>
|
||||||
</NavDropdown>
|
</div>}
|
||||||
<Nav.Item>
|
</NavDropdown.Item>
|
||||||
<Link href='/wallet' passHref>
|
</Link>
|
||||||
<Nav.Link className='text-success px-0'><WalletSummary /></Nav.Link>
|
<Link href='/wallet' passHref>
|
||||||
</Link>
|
<NavDropdown.Item>wallet</NavDropdown.Item>
|
||||||
</Nav.Item>
|
</Link>
|
||||||
|
<div>
|
||||||
|
<NavDropdown.Divider />
|
||||||
|
<Link href='/recent' passHref>
|
||||||
|
<NavDropdown.Item>recent</NavDropdown.Item>
|
||||||
|
</Link>
|
||||||
|
{session
|
||||||
|
? (
|
||||||
|
<Link href='/post' passHref>
|
||||||
|
<NavDropdown.Item>post</NavDropdown.Item>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
: <NavDropdown.Item onClick={signIn}>post</NavDropdown.Item>}
|
||||||
|
<NavDropdown.Item href='https://bitcoinerjobs.co' target='_blank'>jobs</NavDropdown.Item>
|
||||||
|
</div>
|
||||||
|
<NavDropdown.Divider />
|
||||||
|
<NavDropdown.Item onClick={signOut}>logout</NavDropdown.Item>
|
||||||
|
</NavDropdown>
|
||||||
|
{me && me.hasNewNotes &&
|
||||||
|
<span className='position-absolute p-1 bg-danger rounded-circle' style={{ top: '5px', right: '0px' }}>
|
||||||
|
<span className='invisible'>{' '}</span>
|
||||||
|
</span>}
|
||||||
|
</div>
|
||||||
|
{me &&
|
||||||
|
<Nav.Item>
|
||||||
|
<Link href='/wallet' passHref>
|
||||||
|
<Nav.Link className='text-success px-0'><WalletSummary me={me} /></Nav.Link>
|
||||||
|
</Link>
|
||||||
|
</Nav.Item>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -50,8 +50,8 @@ function MoreFooter ({ cursor, fetchMore, offset }) {
|
|||||||
if (cursor) {
|
if (cursor) {
|
||||||
Footer = () => (
|
Footer = () => (
|
||||||
<Button
|
<Button
|
||||||
variant='secondary'
|
variant='primary'
|
||||||
size='sm'
|
size='md'
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
await fetchMore({
|
await fetchMore({
|
||||||
@ -61,7 +61,7 @@ function MoreFooter ({ cursor, fetchMore, offset }) {
|
|||||||
})
|
})
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}}
|
}}
|
||||||
>Load more
|
>more
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,6 +14,7 @@ export function MeProvider ({ children }) {
|
|||||||
stacked
|
stacked
|
||||||
freePosts
|
freePosts
|
||||||
freeComments
|
freeComments
|
||||||
|
hasNewNotes
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
const { data } = useQuery(query, { pollInterval: 1000 })
|
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||||
|
@ -45,7 +45,9 @@ export default function UpVote ({ itemId, meSats, className }) {
|
|||||||
}
|
}
|
||||||
onClick={
|
onClick={
|
||||||
session
|
session
|
||||||
? async () => {
|
? async (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
strike()
|
||||||
if (!itemId) return
|
if (!itemId) return
|
||||||
try {
|
try {
|
||||||
await vote({ variables: { id: itemId, sats: 1 } })
|
await vote({ variables: { id: itemId, sats: 1 } })
|
||||||
@ -56,8 +58,6 @@ export default function UpVote ({ itemId, meSats, className }) {
|
|||||||
}
|
}
|
||||||
throw new Error({ message: error.toString() })
|
throw new Error({ message: error.toString() })
|
||||||
}
|
}
|
||||||
|
|
||||||
strike()
|
|
||||||
}
|
}
|
||||||
: signIn
|
: signIn
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.upvote.stimi {
|
.upvote.stimi {
|
||||||
fill: #993DF5;
|
/* fill: #993DF5;
|
||||||
filter: drop-shadow(0 0 7px #C28BF9);
|
filter: drop-shadow(0 0 7px #C28BF9); */
|
||||||
|
fill: #F6911D;
|
||||||
|
filter: drop-shadow(0 0 7px #F6911D);
|
||||||
}
|
}
|
@ -16,6 +16,19 @@ export const COMMENT_FIELDS = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const MORE_FLAT_COMMENTS = gql`
|
||||||
|
${COMMENT_FIELDS}
|
||||||
|
|
||||||
|
query MoreFlatComments($cursor: String, $userId: ID) {
|
||||||
|
moreFlatComments(cursor: $cursor, userId: $userId) {
|
||||||
|
cursor
|
||||||
|
comments {
|
||||||
|
...CommentFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const COMMENTS = gql`
|
export const COMMENTS = gql`
|
||||||
${COMMENT_FIELDS}
|
${COMMENT_FIELDS}
|
||||||
|
|
||||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -656,9 +656,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@prisma/engines": {
|
"@prisma/engines": {
|
||||||
"version": "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b",
|
"version": "2.25.0-36.c838e79f39885bc8e1611849b1eb28b5bb5bc922",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-2.25.0-36.c838e79f39885bc8e1611849b1eb28b5bb5bc922.tgz",
|
||||||
"integrity": "sha512-Tgk3kggO5B9IT6mimJAw6HSxbFoDAuDKL3sHHSS41EnQm76j/nf4uhGZFPzOQwZWOLeT5ZLO2khr4/FCA9Nkhw=="
|
"integrity": "sha512-vjLCk8AFRZu3D8h/SMcWDzTo0xkMuUDyXQzXekn8gzAGjb47B6LQXGR6rDoZ3/uPM13JNTLPvF62mtVaY6fVeQ=="
|
||||||
},
|
},
|
||||||
"@prisma/engines-version": {
|
"@prisma/engines-version": {
|
||||||
"version": "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b",
|
"version": "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b",
|
||||||
@ -5285,11 +5285,11 @@
|
|||||||
"integrity": "sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U="
|
"integrity": "sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U="
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"version": "2.23.0",
|
"version": "2.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-2.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-2.25.0.tgz",
|
||||||
"integrity": "sha512-3c/lmDy8nsPcEsfCufvCTJUEuwmAcTPbeGg9fL1qjlvS314duLUA/k2nm3n1rq4ImKqzeC5uaKfvI2IoAfwrJA==",
|
"integrity": "sha512-AdAlP+PShvugljIx62Omu+eLKu6Cozz06dehmClIHSb0/yFiVnyBtrRVV4LZus+QX6Ayg7CTDvtzroACAWl+Zw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@prisma/engines": "2.23.0-36.adf5e8cba3daf12d456d911d72b6e9418681b28b"
|
"@prisma/engines": "2.25.0-36.c838e79f39885bc8e1611849b1eb28b5bb5bc922"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"process": {
|
"process": {
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"next": "10.0.9",
|
"next": "10.0.9",
|
||||||
"next-auth": "^3.13.3",
|
"next-auth": "^3.13.3",
|
||||||
"next-seo": "^4.24.0",
|
"next-seo": "^4.24.0",
|
||||||
"prisma": "^2.23.0",
|
"prisma": "^2.25.0",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-bootstrap": "^1.5.2",
|
"react-bootstrap": "^1.5.2",
|
||||||
|
@ -37,7 +37,7 @@ export default function User ({ user }) {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<UserHeader user={user} />
|
<UserHeader user={user} />
|
||||||
<Items variables={{ sort: 'recent', userId: user.id }} />
|
<Items variables={{ sort: 'user', userId: user.id }} />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import Layout from '../../components/layout'
|
import Layout from '../../components/layout'
|
||||||
import { CommentsQuery } from '../../components/comments'
|
|
||||||
import { COMMENT_FIELDS } from '../../fragments/comments'
|
|
||||||
import { gql } from '@apollo/client'
|
import { gql } from '@apollo/client'
|
||||||
import ApolloClient from '../../api/client'
|
import ApolloClient from '../../api/client'
|
||||||
import UserHeader from '../../components/user-header'
|
import UserHeader from '../../components/user-header'
|
||||||
|
import CommentsFlat from '../../components/comments-flat'
|
||||||
|
|
||||||
export async function getServerSideProps ({ req, params }) {
|
export async function getServerSideProps ({ req, params }) {
|
||||||
const { error, data: { user } } = await (await ApolloClient(req)).query({
|
const { error, data: { user } } = await (await ApolloClient(req)).query({
|
||||||
@ -34,19 +33,11 @@ export async function getServerSideProps ({ req, params }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function User ({ user }) {
|
export default function UserComments ({ user }) {
|
||||||
const query = gql`
|
|
||||||
${COMMENT_FIELDS}
|
|
||||||
{
|
|
||||||
comments: userComments(userId: ${user.id}) {
|
|
||||||
...CommentFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<UserHeader user={user} />
|
<UserHeader user={user} />
|
||||||
<CommentsQuery query={query} includeParent noReply />
|
<CommentsFlat variables={{ userId: user.id }} includeParent noReply clickToContext />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,25 @@ const client = new ApolloClient({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
moreFlatComments: {
|
||||||
|
keyArgs: ['userId'],
|
||||||
|
merge (existing, incoming, { readField }) {
|
||||||
|
const comments = existing ? existing.comments : []
|
||||||
|
return {
|
||||||
|
cursor: incoming.cursor,
|
||||||
|
comments: [...comments, ...incoming.comments]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
read (existing) {
|
||||||
|
if (existing) {
|
||||||
|
return {
|
||||||
|
cursor: existing.cursor,
|
||||||
|
comments: existing.comments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
pages/notifications.js
Normal file
28
pages/notifications.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { gql, useQuery } from '@apollo/client'
|
||||||
|
import CommentsFlat from '../components/comments-flat'
|
||||||
|
import Layout from '../components/layout'
|
||||||
|
|
||||||
|
export function RecentlyStacked () {
|
||||||
|
const query = gql`
|
||||||
|
{
|
||||||
|
recentlyStacked
|
||||||
|
}`
|
||||||
|
const { data } = useQuery(query)
|
||||||
|
if (!data || !data.recentlyStacked) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2 className='visible text-success text-center py-3'>
|
||||||
|
you stacked <span className='text-monospace'>{data.recentlyStacked}</span> sats
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Notifications ({ user }) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<RecentlyStacked />
|
||||||
|
<h6 className='text-muted'>replies</h6>
|
||||||
|
<CommentsFlat noReply includeParent clickToContext />
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
@ -123,7 +123,6 @@ export function WithdrawlForm () {
|
|||||||
initialError={error ? error.toString() : undefined}
|
initialError={error ? error.toString() : undefined}
|
||||||
schema={WithdrawlSchema}
|
schema={WithdrawlSchema}
|
||||||
onSubmit={async ({ invoice, maxFee }) => {
|
onSubmit={async ({ invoice, maxFee }) => {
|
||||||
console.log('calling')
|
|
||||||
const { data } = await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } })
|
const { data } = await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } })
|
||||||
router.push(`/withdrawls/${data.createWithdrawl.id}`)
|
router.push(`/withdrawls/${data.createWithdrawl.id}`)
|
||||||
}}
|
}}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "checkedNotesAt" TIMESTAMP(3);
|
@ -11,21 +11,22 @@ generator client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||||
name String? @unique
|
name String? @unique
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime? @map(name: "email_verified")
|
emailVerified DateTime? @map(name: "email_verified")
|
||||||
image String?
|
image String?
|
||||||
items Item[]
|
items Item[]
|
||||||
messages Message[]
|
messages Message[]
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
invoices Invoice[]
|
invoices Invoice[]
|
||||||
withdrawls Withdrawl[]
|
withdrawls Withdrawl[]
|
||||||
msats Int @default(0)
|
msats Int @default(0)
|
||||||
freeComments Int @default(5)
|
freeComments Int @default(5)
|
||||||
freePosts Int @default(2)
|
freePosts Int @default(2)
|
||||||
|
checkedNotesAt DateTime?
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
@ -160,6 +160,10 @@ footer {
|
|||||||
opacity: .2;
|
opacity: .2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flash-it {
|
||||||
|
animation: flash 2s linear 2;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(359deg); }
|
100% { transform: rotate(359deg); }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user