diff --git a/api/resolvers/item.js b/api/resolvers/item.js
index f8a43b47..c1839575 100644
--- a/api/resolvers/item.js
+++ b/api/resolvers/item.js
@@ -484,6 +484,28 @@ export default {
comments
}
},
+ moreBookmarks: async (parent, { cursor, name }, { me, models }) => {
+ const decodedCursor = decodeCursor(cursor)
+
+ const user = await models.user.findUnique({ where: { name } })
+ if (!user) {
+ throw new UserInputError('no user has that name', { argumentName: 'name' })
+ }
+
+ const items = await models.$queryRaw(`
+ ${SELECT}
+ FROM "Item"
+ JOIN "Bookmark" ON "Bookmark"."itemId" = "Item"."id" AND "Bookmark"."userId" = $1
+ AND "Bookmark".created_at <= $2
+ ORDER BY "Bookmark".created_at DESC
+ OFFSET $3
+ LIMIT ${LIMIT}`, user.id, decodedCursor.time, decodedCursor.offset)
+
+ return {
+ cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
+ items
+ }
+ },
item: getItem,
pageTitleAndUnshorted: async (parent, { url }, { models }) => {
const res = {}
@@ -576,6 +598,14 @@ export default {
},
Mutation: {
+ bookmarkItem: async (parent, { id }, { me, models }) => {
+ const data = { itemId: Number(id), userId: me.id }
+ const old = await models.bookmark.findUnique({ where: { userId_itemId: data } })
+ if (old) {
+ await models.bookmark.delete({ where: { userId_itemId: data } })
+ } else await models.bookmark.create({ data })
+ return { id }
+ },
deleteItem: async (parent, { id }, { me, models }) => {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
@@ -924,6 +954,20 @@ export default {
return !!dontLike
},
+ meBookmark: async (item, args, { me, models }) => {
+ if (!me) return false
+
+ const bookmark = await models.bookmark.findUnique({
+ where: {
+ userId_itemId: {
+ itemId: Number(item.id),
+ userId: me.id
+ }
+ }
+ })
+
+ return !!bookmark
+ },
outlawed: async (item, args, { me, models }) => {
if (me && Number(item.userId) === Number(me.id)) {
return false
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 4f690248..a30dad13 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -505,6 +505,20 @@ export default {
}
})
},
+ nbookmarks: async (user, { when }, { models }) => {
+ if (user.nBookmarks) {
+ return user.nBookmarks
+ }
+
+ return await models.bookmark.count({
+ where: {
+ userId: user.id,
+ createdAt: {
+ gte: withinDate(when)
+ }
+ }
+ })
+ },
stacked: async (user, { when }, { models }) => {
if (user.stacked) {
return user.stacked
diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js
index 4a4d5c53..23ee493a 100644
--- a/api/typeDefs/item.js
+++ b/api/typeDefs/item.js
@@ -4,6 +4,7 @@ export default gql`
extend type Query {
items(sub: String, sort: String, type: String, cursor: String, name: String, within: String): Items
moreFlatComments(sort: String!, cursor: String, name: String, within: String): Comments
+ moreBookmarks(cursor: String, name: String!): Items
item(id: ID!): Item
comments(id: ID!, sort: String): [Item!]!
pageTitleAndUnshorted(url: String!): TitleUnshorted
@@ -32,6 +33,7 @@ export default gql`
}
extend type Mutation {
+ bookmarkItem(id: ID): Item
deleteItem(id: ID): Item
upsertLink(id: ID, title: String!, url: String!, boost: Int, forward: String): Item!
upsertDiscussion(id: ID, title: String!, text: String, boost: Int, forward: String): Item!
@@ -99,6 +101,7 @@ export default gql`
wvotes: Float!
meSats: Int!
meDontLike: Boolean!
+ meBookmark: Boolean!
outlawed: Boolean!
freebie: Boolean!
paidImgLink: Boolean
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index a41f67d6..b5172a49 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -45,6 +45,7 @@ export default gql`
name: String
nitems(when: String): Int!
ncomments(when: String): Int!
+ nbookmarks(when: String): Int!
stacked(when: String): Int!
spent(when: String): Int!
referrals(when: String): Int!
diff --git a/components/bookmark.js b/components/bookmark.js
new file mode 100644
index 00000000..dc689ccc
--- /dev/null
+++ b/components/bookmark.js
@@ -0,0 +1,30 @@
+import { useMutation } from '@apollo/client'
+import { gql } from 'apollo-server-micro'
+import { Dropdown } from 'react-bootstrap'
+
+export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
+ const [bookmarkItem] = useMutation(
+ gql`
+ mutation bookmarkItem($id: ID!) {
+ bookmarkItem(id: $id) {
+ meBookmark
+ }
+ }`, {
+ update (cache, { data: { bookmarkItem } }) {
+ cache.modify({
+ id: `Item:${id}`,
+ fields: {
+ meBookmark: () => bookmarkItem.meBookmark
+ }
+ })
+ }
+ }
+ )
+ return (
+ bookmarkItem({ variables: { id } })}
+ >
+ {meBookmark ? 'remove bookmark' : 'bookmark'}
+
+ )
+}
diff --git a/components/comment.js b/components/comment.js
index 222c6993..54c2a04e 100644
--- a/components/comment.js
+++ b/components/comment.js
@@ -4,26 +4,20 @@ import Text from './text'
import Link from 'next/link'
import Reply, { ReplyOnAnotherPage } from './reply'
import { useEffect, useRef, useState } from 'react'
-import { timeSince } from '../lib/time'
import UpVote from './upvote'
import Eye from '../svgs/eye-fill.svg'
import EyeClose from '../svgs/eye-close-line.svg'
import { useRouter } from 'next/router'
import CommentEdit from './comment-edit'
-import Countdown from './countdown'
import { COMMENT_DEPTH_LIMIT, NOFOLLOW_LIMIT } from '../lib/constants'
import { ignoreClick } from '../lib/clicks'
import PayBounty from './pay-bounty'
import BountyIcon from '../svgs/bounty-bag.svg'
import ActionTooltip from './action-tooltip'
-import { useMe } from './me'
-import DontLikeThis from './dont-link-this'
import Flag from '../svgs/flag-fill.svg'
-import { Badge } from 'react-bootstrap'
import { abbrNum } from '../lib/format'
import Share from './share'
-import { DeleteDropdown } from './delete'
-import CowboyHat from './cowboy-hat'
+import ItemInfo from './item-info'
function Parent ({ item, rootText }) {
const ParentFrag = () => (
@@ -89,12 +83,7 @@ export default function Comment ({
const [edit, setEdit] = useState()
const [collapse, setCollapse] = useState(false)
const ref = useRef(null)
- const me = useMe()
const router = useRouter()
- const mine = item.mine
- const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
- const [canEdit, setCanEdit] =
- useState(mine && (Date.now() < editThreshold))
useEffect(() => {
if (Number(router.query.commentId) === Number(item.id)) {
@@ -123,56 +112,23 @@ export default function Comment ({
: }
-
-
{abbrNum(item.sats)} sats
-
\
- {item.boost > 0 &&
+
OP}
+ extraInfo={
<>
- {abbrNum(item.boost)} boost
- \
- >}
-
- {item.ncomments} replies
-
- \
-
-
- @{item.user.name}
- {op && OP}
-
-
-
-
- {timeSince(new Date(item.createdAt))}
-
- {includeParent && }
- {bountyPaid &&
-
-
- }
- {me && !item.meSats && !item.meDontLike && !item.mine && !item.deletedAt && }
- {(item.outlawed && {' '}OUTLAWED) ||
- (item.freebie && !item.mine && (me?.greeterMode) && {' '}FREEBIE)}
- {canEdit && !item.deletedAt &&
- <>
- \
- {
- setEdit(!edit)
- }}
- >
- {edit ? 'cancel' : 'edit'}
- {
- setCanEdit(false)
- }}
- />
-
- >}
- {mine && !canEdit && !item.deletedAt && }
-
+ {includeParent &&
}
+ {bountyPaid &&
+
+
+ }
+ >
+ }
+ onEdit={e => { setEdit(!edit) }}
+ editText={edit ? 'cancel' : 'edit'}
+ />
{!includeParent && (collapse
?
{
@@ -186,7 +142,11 @@ export default function Comment ({
localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
}}
/>)}
- {topLevel && }
+ {topLevel && (
+
+
+
+ )}
{edit
? (
@@ -194,7 +154,6 @@ export default function Comment ({
comment={item}
onSuccess={() => {
setEdit(!edit)
- setCanEdit(mine && (Date.now() < editThreshold))
}}
/>
)
diff --git a/components/delete.js b/components/delete.js
index 80376a5a..bdb5e123 100644
--- a/components/delete.js
+++ b/components/delete.js
@@ -3,7 +3,6 @@ import { gql } from 'apollo-server-micro'
import { useState } from 'react'
import { Alert, Button, Dropdown } from 'react-bootstrap'
import { useShowModal } from './modal'
-import MoreIcon from '../svgs/more-fill.svg'
export default function Delete ({ itemId, children, onDelete }) {
const showModal = useShowModal()
@@ -81,22 +80,12 @@ function DeleteConfirm ({ onConfirm }) {
)
}
-export function DeleteDropdown (props) {
+export function DeleteDropdownItem (props) {
return (
-
-
-
-
-
-
-
-
- delete
-
-
-
-
+
+
+ delete
+
+
)
}
diff --git a/components/dont-link-this.js b/components/dont-link-this.js
index 65da4cde..a40e73ea 100644
--- a/components/dont-link-this.js
+++ b/components/dont-link-this.js
@@ -1,10 +1,9 @@
import { gql, useMutation } from '@apollo/client'
import { Dropdown } from 'react-bootstrap'
-import MoreIcon from '../svgs/more-fill.svg'
import FundError from './fund-error'
import { useShowModal } from './modal'
-export default function DontLikeThis ({ id }) {
+export default function DontLikeThisDropdownItem ({ id }) {
const showModal = useShowModal()
const [dontLikeThis] = useMutation(
@@ -26,32 +25,23 @@ export default function DontLikeThis ({ id }) {
)
return (
-
-
-
-
-
-
- {
- try {
- await dontLikeThis({
- variables: { id },
- optimisticResponse: { dontLikeThis: true }
- })
- } catch (error) {
- if (error.toString().includes('insufficient funds')) {
- showModal(onClose => {
- return
- })
- }
- }
- }}
- >
- flag
-
-
-
+
{
+ try {
+ await dontLikeThis({
+ variables: { id },
+ optimisticResponse: { dontLikeThis: true }
+ })
+ } catch (error) {
+ if (error.toString().includes('insufficient funds')) {
+ showModal(onClose => {
+ return
+ })
+ }
+ }
+ }}
+ >
+ flag
+
)
}
diff --git a/components/header.js b/components/header.js
index 1d80b100..a80d772a 100644
--- a/components/header.js
+++ b/components/header.js
@@ -31,6 +31,7 @@ export default function Header ({ sub }) {
const prefix = sub ? `/~${sub}` : ''
// there's always at least 2 on the split, e.g. '/' yields ['','']
const topNavKey = path.split('/')[sub ? 2 : 1]
+ const dropNavKey = path.split('/').slice(sub ? 2 : 1).join('/')
const { data: subLatestPost } = useQuery(gql`
query subLatestPost($name: ID!) {
subLatestPost(name: $name)
@@ -80,7 +81,7 @@ export default function Header ({ sub }) {
} alignRight
>
-
+
profile
{me && !me.bioId &&
@@ -88,6 +89,9 @@ export default function Header ({ sub }) {
}
+
+ bookmarks
+
wallet
diff --git a/components/item-full.js b/components/item-full.js
index 4318cddf..c5e7704c 100644
--- a/components/item-full.js
+++ b/components/item-full.js
@@ -17,6 +17,9 @@ import { commentsViewed } from '../lib/new-comments'
import Related from './related'
import PastBounties from './past-bounties'
import Check from '../svgs/check-double-line.svg'
+import Share from './share'
+import Toc from './table-of-contents'
+import Link from 'next/link'
function BioItem ({ item, handleClick }) {
const me = useMe()
@@ -91,11 +94,32 @@ function ItemEmbed ({ item }) {
return null
}
+function FwdUser ({ user }) {
+ return (
+
+ )
+}
+
function TopLevelItem ({ item, noReply, ...props }) {
const ItemComponent = item.isJob ? ItemJob : Item
return (
-
+
+
+
+ >
+ }
+ belowTitle={item.fwdUser && }
+ {...props}
+ >
{item.text &&
}
{item.url &&
}
diff --git a/components/item-info.js b/components/item-info.js
new file mode 100644
index 00000000..7d724986
--- /dev/null
+++ b/components/item-info.js
@@ -0,0 +1,116 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import { Badge, Dropdown } from 'react-bootstrap'
+import Countdown from './countdown'
+import { abbrNum } from '../lib/format'
+import { newComments } from '../lib/new-comments'
+import { timeSince } from '../lib/time'
+import CowboyHat from './cowboy-hat'
+import { DeleteDropdownItem } from './delete'
+import styles from './item.module.css'
+import { useMe } from './me'
+import MoreIcon from '../svgs/more-fill.svg'
+import DontLikeThisDropdownItem from './dont-link-this'
+import BookmarkDropdownItem from './bookmark'
+import { CopyLinkDropdownItem } from './share'
+
+export default function ItemInfo ({ item, commentsText, className, embellishUser, extraInfo, onEdit, editText }) {
+ const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
+ const me = useMe()
+ const router = useRouter()
+ const [canEdit, setCanEdit] =
+ useState(item.mine && (Date.now() < editThreshold))
+ const [hasNewComments, setHasNewComments] = useState(false)
+ useEffect(() => {
+ // if we are showing toc, then this is a full item
+ setHasNewComments(newComments(item))
+ }, [item])
+
+ return (
+
+ )
+}
+
+export function ItemDropdown ({ children }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/components/item.js b/components/item.js
index d7a99f9b..50641dae 100644
--- a/components/item.js
+++ b/components/item.js
@@ -1,25 +1,16 @@
import Link from 'next/link'
import styles from './item.module.css'
-import { timeSince } from '../lib/time'
import UpVote from './upvote'
import { useEffect, useRef, useState } from 'react'
-import Countdown from './countdown'
import { NOFOLLOW_LIMIT } from '../lib/constants'
import Pin from '../svgs/pushpin-fill.svg'
import reactStringReplace from 'react-string-replace'
-import Toc from './table-of-contents'
import PollIcon from '../svgs/bar-chart-horizontal-fill.svg'
import BountyIcon from '../svgs/bounty-bag.svg'
import ActionTooltip from './action-tooltip'
-import { Badge } from 'react-bootstrap'
-import { newComments } from '../lib/new-comments'
-import { useMe } from './me'
-import DontLikeThis from './dont-link-this'
import Flag from '../svgs/flag-fill.svg'
-import Share from './share'
import { abbrNum } from '../lib/format'
-import { DeleteDropdown } from './delete'
-import CowboyHat from './cowboy-hat'
+import ItemInfo from './item-info'
export function SearchTitle ({ title }) {
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
@@ -27,26 +18,9 @@ export function SearchTitle ({ title }) {
})
}
-function FwdUser ({ user }) {
- return (
-
- )
-}
-
-export default function Item ({ item, rank, showFwdUser, toc, children }) {
- const mine = item.mine
- const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
- const [canEdit, setCanEdit] =
- useState(mine && (Date.now() < editThreshold))
+export default function Item ({ item, rank, belowTitle, right, children }) {
const [wrap, setWrap] = useState(false)
const titleRef = useRef()
- const me = useMe()
- const [hasNewComments, setHasNewComments] = useState(false)
useEffect(() => {
setWrap(
@@ -54,11 +28,6 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
titleRef.current.clientHeight)
}, [])
- useEffect(() => {
- // if we are showing toc, then this is a full item
- setHasNewComments(!toc && newComments(item))
- }, [item])
-
return (
<>
{rank
@@ -96,69 +65,10 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
>}
-
- {showFwdUser && item.fwdUser && }
+
+ {belowTitle}
- {toc &&
- <>
-
-
- >}
+ {right}
{children && (
diff --git a/components/share.js b/components/share.js
index 846870f6..73bf3b35 100644
--- a/components/share.js
+++ b/components/share.js
@@ -34,7 +34,6 @@ export default function Share ({ item }) {
{
copy(url)
}}
@@ -44,3 +43,26 @@ export default function Share ({ item }) {
)
}
+
+export function CopyLinkDropdownItem ({ item }) {
+ const me = useMe()
+ const url = `https://stacker.news/items/${item.id}${me ? `/r/${me.name}` : ''}`
+ return (
+
{
+ if (navigator.share) {
+ navigator.share({
+ title: item.title || '',
+ text: '',
+ url
+ }).then(() => console.log('Successful share'))
+ .catch((error) => console.log('Error sharing', error))
+ } else {
+ copy(url)
+ }
+ }}
+ >
+ copy link
+
+ )
+}
diff --git a/components/user-header.js b/components/user-header.js
index 4273e726..9ad1436b 100644
--- a/components/user-header.js
+++ b/components/user-header.js
@@ -161,12 +161,11 @@ export default function UserHeader ({ user }) {
{user.ncomments} comments
- {isMe &&
-
-
- satistics
-
- }
+
+
+ {user.nbookmarks} bookmarks
+
+
>
)
diff --git a/fragments/comments.js b/fragments/comments.js
index d598ed7f..43c5de06 100644
--- a/fragments/comments.js
+++ b/fragments/comments.js
@@ -17,6 +17,7 @@ export const COMMENT_FIELDS = gql`
boost
meSats
meDontLike
+ meBookmark
outlawed
freebie
path
diff --git a/fragments/items.js b/fragments/items.js
index 51b0f607..7d06f22e 100644
--- a/fragments/items.js
+++ b/fragments/items.js
@@ -24,6 +24,7 @@ export const ITEM_FIELDS = gql`
path
meSats
meDontLike
+ meBookmark
outlawed
freebie
ncomments
@@ -218,8 +219,8 @@ export const ITEM_WITH_COMMENTS = gql`
export const BOUNTY_ITEMS_BY_USER_NAME = gql`
${ITEM_FIELDS}
- query getBountiesByUserName($name: String!) {
- getBountiesByUserName(name: $name) {
+ query getBountiesByUserName($name: String!, $cursor: String, $limit: Int) {
+ getBountiesByUserName(name: $name, cursor: $cursor, limit: $limit) {
cursor
items {
...ItemFields
diff --git a/fragments/users.js b/fragments/users.js
index 3e89bd03..5d56598b 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -1,6 +1,6 @@
import { gql } from '@apollo/client'
import { COMMENT_FIELDS } from './comments'
-import { ITEM_FIELDS, ITEM_WITH_COMMENTS } from './items'
+import { ITEM_FIELDS, ITEM_FULL_FIELDS, ITEM_WITH_COMMENTS } from './items'
export const ME = gql`
{
@@ -124,6 +124,7 @@ export const USER_FIELDS = gql`
streak
nitems
ncomments
+ nbookmarks
stacked
sats
photoId
@@ -196,6 +197,22 @@ export const USER_WITH_COMMENTS = gql`
}
}`
+export const USER_WITH_BOOKMARKS = gql`
+ ${USER_FIELDS}
+ ${ITEM_FULL_FIELDS}
+ query UserWithBookmarks($name: String!, $cursor: String) {
+ user(name: $name) {
+ ...UserFields
+ }
+ moreBookmarks(name: $name, cursor: $cursor) {
+ cursor
+ items {
+ ...ItemFullFields
+ }
+ }
+ }
+`
+
export const USER_WITH_POSTS = gql`
${USER_FIELDS}
${ITEM_FIELDS}
diff --git a/fragments/wallet.js b/fragments/wallet.js
index c9d29066..62f55d1c 100644
--- a/fragments/wallet.js
+++ b/fragments/wallet.js
@@ -1,6 +1,5 @@
import { gql } from '@apollo/client'
import { ITEM_FULL_FIELDS } from './items'
-import { USER_FIELDS } from './users'
export const INVOICE = gql`
query Invoice($id: ID!) {
@@ -28,12 +27,8 @@ export const WITHDRAWL = gql`
export const WALLET_HISTORY = gql`
${ITEM_FULL_FIELDS}
- ${USER_FIELDS}
query WalletHistory($cursor: String, $inc: String) {
- me {
- ...UserFields
- }
walletHistory(cursor: $cursor, inc: $inc) {
facts {
id
diff --git a/lib/apollo.js b/lib/apollo.js
index 463fb94b..07556363 100644
--- a/lib/apollo.js
+++ b/lib/apollo.js
@@ -181,6 +181,19 @@ function getClient (uri) {
}
}
},
+ moreBookmarks: {
+ keyArgs: ['name'],
+ merge (existing, incoming) {
+ if (isFirstPage(incoming.cursor, existing?.items)) {
+ return incoming
+ }
+
+ return {
+ cursor: incoming.cursor,
+ items: [...(existing?.items || []), ...incoming.items]
+ }
+ }
+ },
notifications: {
keyArgs: ['inc'],
merge (existing, incoming) {
diff --git a/pages/[name]/bookmarks.js b/pages/[name]/bookmarks.js
new file mode 100644
index 00000000..65f05c50
--- /dev/null
+++ b/pages/[name]/bookmarks.js
@@ -0,0 +1,33 @@
+import Layout from '../../components/layout'
+import { useQuery } from '@apollo/client'
+import UserHeader from '../../components/user-header'
+import Seo from '../../components/seo'
+import Items from '../../components/items'
+import { USER_WITH_BOOKMARKS } from '../../fragments/users'
+import { getGetServerSideProps } from '../../api/ssrApollo'
+
+export const getServerSideProps = getGetServerSideProps(USER_WITH_BOOKMARKS)
+
+export default function UserBookmarks ({ data: { user, moreBookmarks: { items, cursor } } }) {
+ const { data } = useQuery(
+ USER_WITH_BOOKMARKS, { variables: { name: user.name } })
+
+ if (data) {
+ ({ user, moreBookmarks: { items, cursor } } = data)
+ }
+
+ return (
+
+
+
+
+ data.moreBookmarks}
+ variables={{ name: user.name }}
+ />
+
+
+ )
+}
diff --git a/pages/satistics.js b/pages/satistics.js
index b1125386..2ac37427 100644
--- a/pages/satistics.js
+++ b/pages/satistics.js
@@ -4,7 +4,6 @@ import { Table } from 'react-bootstrap'
import { getGetServerSideProps } from '../api/ssrApollo'
import Layout from '../components/layout'
import MoreFooter from '../components/more-footer'
-import UserHeader from '../components/user-header'
import { WALLET_HISTORY } from '../fragments/wallet'
import styles from '../styles/satistics.module.css'
import Moon from '../svgs/moon-fill.svg'
@@ -138,7 +137,7 @@ function Detail ({ fact }) {
return
}
-export default function Satistics ({ data: { me, walletHistory: { facts, cursor } } }) {
+export default function Satistics ({ data: { walletHistory: { facts, cursor } } }) {
const router = useRouter()
const { data, fetchMore } = useQuery(WALLET_HISTORY, { variables: { inc: router.query.inc } })
@@ -176,7 +175,7 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
}
if (data) {
- ({ me, walletHistory: { facts, cursor } } = data)
+ ({ walletHistory: { facts, cursor } } = data)
}
const SatisticsSkeleton = () => (
@@ -185,9 +184,9 @@ export default function Satistics ({ data: { me, walletHistory: { facts, cursor
)
return (
-
-
+