wild west mode
This commit is contained in:
parent
14ce36c1cb
commit
7faae425b3
|
@ -4,20 +4,23 @@ import serialize from './serial'
|
|||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
||||
import domino from 'domino'
|
||||
import { BOOST_MIN, ITEM_SPAM_INTERVAL, MAX_POLL_NUM_CHOICES, MAX_TITLE_LENGTH } from '../../lib/constants'
|
||||
import {
|
||||
BOOST_MIN, ITEM_SPAM_INTERVAL, MAX_POLL_NUM_CHOICES,
|
||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST
|
||||
} from '../../lib/constants'
|
||||
import { mdHas } from '../../lib/md'
|
||||
|
||||
async function comments (models, id, sort) {
|
||||
async function comments (me, models, id, sort) {
|
||||
let orderBy
|
||||
switch (sort) {
|
||||
case 'top':
|
||||
orderBy = 'ORDER BY "Item"."weightedVotes" DESC, "Item".id DESC'
|
||||
orderBy = `ORDER BY ${await orderByNumerator(me, models)} DESC, "Item".id DESC`
|
||||
break
|
||||
case 'recent':
|
||||
orderBy = 'ORDER BY "Item".created_at DESC, "Item".id DESC'
|
||||
break
|
||||
default:
|
||||
orderBy = COMMENTS_ORDER_BY_SATS
|
||||
orderBy = `ORDER BY ${await orderByNumerator(me, models)}/POWER(EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE 'UTC') - "Item".created_at))/3600+2, 1.3) DESC NULLS LAST, "Item".id DESC`
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -26,18 +29,18 @@ async function comments (models, id, sort) {
|
|||
${SELECT}, ARRAY[row_number() OVER (${orderBy}, "Item".path)] AS sort_path
|
||||
FROM "Item"
|
||||
WHERE "parentId" = $1
|
||||
${await filterClause(me, models)}
|
||||
UNION ALL
|
||||
${SELECT}, p.sort_path || row_number() OVER (${orderBy}, "Item".path)
|
||||
FROM base p
|
||||
JOIN "Item" ON "Item"."parentId" = p.id)
|
||||
JOIN "Item" ON "Item"."parentId" = p.id
|
||||
WHERE true
|
||||
${await filterClause(me, models)})
|
||||
SELECT * FROM base ORDER BY sort_path`, Number(id))
|
||||
return nestComments(flat, id)[0]
|
||||
}
|
||||
|
||||
const COMMENTS_ORDER_BY_SATS =
|
||||
'ORDER BY POWER("Item"."weightedVotes", 1.2)/POWER(EXTRACT(EPOCH FROM ((NOW() AT TIME ZONE \'UTC\') - "Item".created_at))/3600+2, 1.3) DESC NULLS LAST, "Item".id DESC'
|
||||
|
||||
export async function getItem (parent, { id }, { models }) {
|
||||
export async function getItem (parent, { id }, { me, models }) {
|
||||
const [item] = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
|
@ -67,6 +70,38 @@ function topClause (within) {
|
|||
return interval
|
||||
}
|
||||
|
||||
export async function orderByNumerator (me, models) {
|
||||
if (me) {
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
if (user.wildWestMode) {
|
||||
return 'GREATEST("Item"."weightedVotes", POWER("Item"."weightedVotes", 1.2))'
|
||||
}
|
||||
}
|
||||
|
||||
return `(CASE WHEN "Item"."weightedVotes" > "Item"."weightedDownVotes"
|
||||
THEN 1
|
||||
ELSE -1 END
|
||||
* GREATEST(ABS("Item"."weightedVotes" - "Item"."weightedDownVotes"), POWER(ABS("Item"."weightedVotes" - "Item"."weightedDownVotes"), 1.2)))`
|
||||
}
|
||||
|
||||
export async function filterClause (me, models) {
|
||||
if (me) {
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
if (user.wildWestMode) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// if the item is above the threshold or is mine
|
||||
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
||||
if (me) {
|
||||
clause += ` OR "Item"."userId" = ${me.id}`
|
||||
}
|
||||
clause += ')'
|
||||
|
||||
return clause
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
itemRepetition: async (parent, { parentId }, { me, models }) => {
|
||||
|
@ -106,6 +141,7 @@ export default {
|
|||
WHERE "userId" = $1 AND "parentId" IS NULL AND created_at <= $2
|
||||
AND "pinId" IS NULL
|
||||
${activeOrMine()}
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $3
|
||||
LIMIT ${LIMIT}`, user.id, decodedCursor.time, decodedCursor.offset)
|
||||
|
@ -117,6 +153,7 @@ export default {
|
|||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
${subClause(3)}
|
||||
${activeOrMine()}
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub || 'NULL')
|
||||
|
@ -128,7 +165,8 @@ export default {
|
|||
WHERE "parentId" IS NULL AND "Item".created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${topClause(within)}
|
||||
${TOP_ORDER_BY_SATS}
|
||||
${await filterClause(me, models)}
|
||||
${await topOrderByWeightedSats(me, models)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
break
|
||||
|
@ -179,7 +217,8 @@ export default {
|
|||
WHERE "parentId" IS NULL AND "Item".created_at <= $1 AND "Item".created_at > $3
|
||||
AND "pinId" IS NULL
|
||||
${subClause(4)}
|
||||
${newTimedOrderByWeightedSats(1)}
|
||||
${await filterClause(me, models)}
|
||||
${await newTimedOrderByWeightedSats(me, models, 1)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, new Date(new Date().setDate(new Date().getDate() - 5)), sub || 'NULL')
|
||||
}
|
||||
|
@ -191,7 +230,8 @@ export default {
|
|||
WHERE "parentId" IS NULL AND "Item".created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${subClause(3)}
|
||||
${newTimedOrderByWeightedSats(1)}
|
||||
${await filterClause(me, models)}
|
||||
${await newTimedOrderByWeightedSats(me, models, 1)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, sub || 'NULL')
|
||||
}
|
||||
|
@ -219,11 +259,12 @@ export default {
|
|||
pins
|
||||
}
|
||||
},
|
||||
allItems: async (parent, { cursor }, { models }) => {
|
||||
allItems: async (parent, { cursor }, { me, models }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
const items = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $1
|
||||
LIMIT ${LIMIT}`, decodedCursor.offset)
|
||||
|
@ -242,6 +283,7 @@ export default {
|
|||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NOT NULL AND created_at <= $1
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
|
@ -261,6 +303,7 @@ export default {
|
|||
FROM "Item"
|
||||
WHERE "userId" = $1 AND "parentId" IS NOT NULL
|
||||
AND created_at <= $2
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $3
|
||||
LIMIT ${LIMIT}`, user.id, decodedCursor.time, decodedCursor.offset)
|
||||
|
@ -272,7 +315,8 @@ export default {
|
|||
WHERE "parentId" IS NOT NULL
|
||||
AND "Item".created_at <= $1
|
||||
${topClause(within)}
|
||||
${TOP_ORDER_BY_SATS}
|
||||
${await filterClause(me, models)}
|
||||
${await topOrderByWeightedSats(me, models)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
break
|
||||
|
@ -322,8 +366,8 @@ export default {
|
|||
ORDER BY created_at DESC
|
||||
LIMIT 3`, similar)
|
||||
},
|
||||
comments: async (parent, { id, sort }, { models }) => {
|
||||
return comments(models, id, sort)
|
||||
comments: async (parent, { id, sort }, { me, models }) => {
|
||||
return comments(me, models, id, sort)
|
||||
},
|
||||
search: async (parent, { q: query, sub, cursor }, { me, models, search }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
|
@ -636,6 +680,25 @@ export default {
|
|||
vote,
|
||||
sats
|
||||
}
|
||||
},
|
||||
dontLikeThis: async (parent, { id }, { me, models }) => {
|
||||
// need to make sure we are logged in
|
||||
if (!me) {
|
||||
throw new AuthenticationError('you must be logged in')
|
||||
}
|
||||
|
||||
// disallow self down votes
|
||||
const [item] = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE id = $1 AND "userId" = $2`, Number(id), me.id)
|
||||
if (item) {
|
||||
throw new UserInputError('cannot downvote your self')
|
||||
}
|
||||
|
||||
await serialize(models, models.$queryRaw`SELECT item_act(${Number(id)}, ${me.id}, 'DONT_LIKE_THIS', ${DONT_LIKE_THIS_COST})`)
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
Item: {
|
||||
|
@ -710,11 +773,11 @@ export default {
|
|||
}
|
||||
return await models.user.findUnique({ where: { id: item.fwdUserId } })
|
||||
},
|
||||
comments: async (item, args, { models }) => {
|
||||
comments: async (item, args, { me, models }) => {
|
||||
if (item.comments) {
|
||||
return item.comments
|
||||
}
|
||||
return comments(models, item.id, 'hot')
|
||||
return comments(me, models, item.id, 'hot')
|
||||
},
|
||||
upvotes: async (item, args, { models }) => {
|
||||
const { sum: { sats } } = await models.itemAct.aggregate({
|
||||
|
@ -768,6 +831,19 @@ export default {
|
|||
|
||||
return sats || 0
|
||||
},
|
||||
meDontLike: async (item, args, { me, models }) => {
|
||||
if (!me) return false
|
||||
|
||||
const dontLike = await models.itemAct.findFirst({
|
||||
where: {
|
||||
itemId: Number(item.id),
|
||||
userId: me.id,
|
||||
act: 'DONT_LIKE_THIS'
|
||||
}
|
||||
})
|
||||
|
||||
return !!dontLike
|
||||
},
|
||||
mine: async (item, args, { me, models }) => {
|
||||
return me?.id === item.userId
|
||||
},
|
||||
|
@ -940,10 +1016,12 @@ export const SELECT =
|
|||
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item"."paidImgLink",
|
||||
"Item".sats, "Item".ncomments, "Item"."commentSats", "Item"."lastCommentAt", ltree2text("Item"."path") AS "path"`
|
||||
|
||||
function newTimedOrderByWeightedSats (num) {
|
||||
async function newTimedOrderByWeightedSats (me, models, num) {
|
||||
return `
|
||||
ORDER BY (POWER("Item"."weightedVotes", 1.2)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.3) +
|
||||
ORDER BY (${await orderByNumerator(me, models)}/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 1.3) +
|
||||
("Item".boost/${BOOST_MIN}::float)/POWER(EXTRACT(EPOCH FROM ($${num} - "Item".created_at))/3600+2, 2.6)) DESC NULLS LAST, "Item".id DESC`
|
||||
}
|
||||
|
||||
const TOP_ORDER_BY_SATS = 'ORDER BY "Item"."weightedVotes" DESC NULLS LAST, "Item".id DESC'
|
||||
async function topOrderByWeightedSats (me, models) {
|
||||
return `ORDER BY ${await orderByNumerator(me, models)} DESC NULLS LAST, "Item".id DESC`
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AuthenticationError } from 'apollo-server-micro'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { getItem } from './item'
|
||||
import { getItem, filterClause } from './item'
|
||||
import { getInvoice } from './wallet'
|
||||
|
||||
export default {
|
||||
|
@ -76,7 +76,8 @@ export default {
|
|||
FROM "Item"
|
||||
JOIN "Item" p ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||
WHERE p."userId" = $1
|
||||
AND "Item"."userId" <> $1 AND "Item".created_at <= $2`
|
||||
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
|
||||
${await filterClause(me, models)}`
|
||||
)
|
||||
} else {
|
||||
queries.push(
|
||||
|
@ -86,6 +87,7 @@ export default {
|
|||
JOIN "Item" p ON ${meFull.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||
WHERE p."userId" = $1
|
||||
AND "Item"."userId" <> $1 AND "Item".created_at <= $2
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY "sortTime" DESC
|
||||
LIMIT ${LIMIT}+$3)`
|
||||
)
|
||||
|
@ -129,6 +131,7 @@ export default {
|
|||
AND "Mention".created_at <= $2
|
||||
AND "Item"."userId" <> $1
|
||||
AND (p."userId" IS NULL OR p."userId" <> $1)
|
||||
${await filterClause(me, models)}
|
||||
ORDER BY "sortTime" DESC
|
||||
LIMIT ${LIMIT}+$3)`
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AuthenticationError, UserInputError } from 'apollo-server-errors'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { mdHas } from '../../lib/md'
|
||||
import { createMentions, getItem, SELECT, updateItem } from './item'
|
||||
import { createMentions, getItem, SELECT, updateItem, filterClause } from './item'
|
||||
import serialize from './serial'
|
||||
|
||||
export function topClause (within) {
|
||||
|
@ -317,6 +317,7 @@ export default {
|
|||
JOIN "Item" p ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
||||
WHERE p."userId" = $1
|
||||
AND "Item".created_at > $2 AND "Item"."userId" <> $1
|
||||
${await filterClause(me, models)}
|
||||
LIMIT 1`, me.id, lastChecked)
|
||||
if (newReplies.length > 0) {
|
||||
return true
|
||||
|
|
|
@ -27,6 +27,7 @@ export default gql`
|
|||
upsertPoll(id: ID, title: String!, text: String, options: [String!]!, boost: Int, forward: String): Item!
|
||||
createComment(text: String!, parentId: ID!): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
dontLikeThis(id: ID!): Boolean!
|
||||
act(id: ID!, sats: Int): ItemActResult!
|
||||
pollVote(id: ID!): ID!
|
||||
}
|
||||
|
@ -78,6 +79,7 @@ export default gql`
|
|||
lastCommentAt: String
|
||||
upvotes: Int!
|
||||
meSats: Int!
|
||||
meDontLike: Boolean!
|
||||
paidImgLink: Boolean
|
||||
ncomments: Int!
|
||||
comments: [Item!]!
|
||||
|
|
|
@ -31,7 +31,7 @@ export default gql`
|
|||
setName(name: String!): Boolean
|
||||
setSettings(tipDefault: Int!, noteItemSats: Boolean!, noteEarning: Boolean!,
|
||||
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
|
||||
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!): User
|
||||
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, wildWestMode: Boolean!): User
|
||||
setPhoto(photoId: ID!): Int!
|
||||
upsertBio(bio: String!): User!
|
||||
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
|
||||
|
@ -72,6 +72,7 @@ export default gql`
|
|||
noteInvites: Boolean!
|
||||
noteJobIndicator: Boolean!
|
||||
hideInvoiceDesc: Boolean!
|
||||
wildWestMode: Boolean!
|
||||
lastCheckedJobs: String
|
||||
authMethods: AuthMethods!
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@ import CommentEdit from './comment-edit'
|
|||
import Countdown from './countdown'
|
||||
import { COMMENT_DEPTH_LIMIT, NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import { ignoreClick } from '../lib/clicks'
|
||||
import { useMe } from './me'
|
||||
import DontLikeThis from './dont-link-this'
|
||||
import Flag from '../svgs/flag-fill.svg'
|
||||
|
||||
function Parent ({ item, rootText }) {
|
||||
const ParentFrag = () => (
|
||||
|
@ -78,6 +81,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
|
||||
|
@ -105,7 +109,7 @@ export default function Comment ({
|
|||
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse ? styles.collapsed : ''}`}
|
||||
>
|
||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||
<UpVote item={item} className={styles.upvote} />
|
||||
{item.meDontLike ? <Flag width={24} height={24} className={`${styles.dontLike}`} /> : <UpVote item={item} className={styles.upvote} />}
|
||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||
<div className='d-flex align-items-center'>
|
||||
<div className={`${itemStyles.other} ${styles.other}`}>
|
||||
|
@ -128,6 +132,7 @@ export default function Comment ({
|
|||
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
|
||||
</Link>
|
||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||
{me && !item.meSats && !item.meDontLike && <DontLikeThis id={item.id} />}
|
||||
{canEdit &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
|
|
|
@ -8,6 +8,14 @@
|
|||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.dontLike {
|
||||
fill: #a5a5a5;
|
||||
margin-right: .2rem;
|
||||
padding: 2px;
|
||||
margin-left: 1px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: .1rem;
|
||||
padding-right: 15px;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { gql, useMutation } from '@apollo/client'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import MoreIcon from '../svgs/more-fill.svg'
|
||||
import { useFundError } from './fund-error'
|
||||
|
||||
export default function DontLikeThis ({ id }) {
|
||||
const { setError } = useFundError()
|
||||
|
||||
const [dontLikeThis] = useMutation(
|
||||
gql`
|
||||
mutation dontLikeThis($id: ID!) {
|
||||
dontLikeThis(id: $id)
|
||||
}`, {
|
||||
update (cache) {
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
meDontLike () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Dropdown className='pointer' as='span'>
|
||||
<Dropdown.Toggle variant='success' id='dropdown-basic' as='a'>
|
||||
<MoreIcon className='fill-grey ml-1' height={16} width={16} />
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
className='text-center'
|
||||
onClick={async () => {
|
||||
try {
|
||||
await dontLikeThis({
|
||||
variables: { id },
|
||||
optimisticResponse: { dontLikeThis: true }
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.toString().includes('insufficient funds')) {
|
||||
setError(true)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
I don't like this
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
|
@ -11,6 +11,9 @@ import Toc from './table-of-contents'
|
|||
import PollIcon from '../svgs/bar-chart-horizontal-fill.svg'
|
||||
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'
|
||||
|
||||
export function SearchTitle ({ title }) {
|
||||
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||
|
@ -36,6 +39,7 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
|
|||
useState(mine && (Date.now() < editThreshold))
|
||||
const [wrap, setWrap] = useState(false)
|
||||
const titleRef = useRef()
|
||||
const me = useMe()
|
||||
const [hasNewComments, setHasNewComments] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -58,7 +62,9 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
|
|||
</div>)
|
||||
: <div />}
|
||||
<div className={styles.item}>
|
||||
{item.position ? <Pin width={24} height={24} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
|
||||
{item.position
|
||||
? <Pin width={24} height={24} className={styles.pin} />
|
||||
: item.meDontLike ? <Flag width={24} height={24} className={`${styles.dontLike}`} /> : <UpVote item={item} className={styles.upvote} />}
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap ${wrap ? 'd-inline' : ''}`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
|
@ -104,6 +110,7 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
|
|||
<Link href={`/items/${item.id}`} passHref>
|
||||
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
|
||||
</Link>
|
||||
{me && !item.meSats && !item.position && !item.meDontLike && <DontLikeThis id={item.id} />}
|
||||
{item.prior &&
|
||||
<>
|
||||
<span> \ </span>
|
||||
|
|
|
@ -30,6 +30,13 @@ a.title:visited {
|
|||
margin-right: .2rem;
|
||||
}
|
||||
|
||||
.dontLike {
|
||||
fill: #a5a5a5;
|
||||
margin-right: .2rem;
|
||||
padding: 2px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.case {
|
||||
fill: #a5a5a5;
|
||||
margin-right: .2rem;
|
||||
|
@ -76,7 +83,7 @@ a.link:visited {
|
|||
}
|
||||
|
||||
.hunk {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
line-height: 1.06rem;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export const COMMENT_FIELDS = gql`
|
|||
upvotes
|
||||
boost
|
||||
meSats
|
||||
meDontLike
|
||||
path
|
||||
commentSats
|
||||
mine
|
||||
|
|
|
@ -21,6 +21,7 @@ export const ITEM_FIELDS = gql`
|
|||
boost
|
||||
path
|
||||
meSats
|
||||
meDontLike
|
||||
ncomments
|
||||
commentSats
|
||||
lastCommentAt
|
||||
|
|
|
@ -25,6 +25,7 @@ export const ME = gql`
|
|||
noteInvites
|
||||
noteJobIndicator
|
||||
hideInvoiceDesc
|
||||
wildWestMode
|
||||
lastCheckedJobs
|
||||
}
|
||||
}`
|
||||
|
@ -50,6 +51,7 @@ export const ME_SSR = gql`
|
|||
noteInvites
|
||||
noteJobIndicator
|
||||
hideInvoiceDesc
|
||||
wildWestMode
|
||||
lastCheckedJobs
|
||||
}
|
||||
}`
|
||||
|
@ -65,6 +67,7 @@ export const SETTINGS_FIELDS = gql`
|
|||
noteInvites
|
||||
noteJobIndicator
|
||||
hideInvoiceDesc
|
||||
wildWestMode
|
||||
authMethods {
|
||||
lightning
|
||||
email
|
||||
|
@ -86,11 +89,11 @@ gql`
|
|||
${SETTINGS_FIELDS}
|
||||
mutation setSettings($tipDefault: Int!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
|
||||
$noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
|
||||
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!) {
|
||||
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $wildWestMode: Boolean!) {
|
||||
setSettings(tipDefault: $tipDefault, noteItemSats: $noteItemSats,
|
||||
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
|
||||
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
|
||||
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc) {
|
||||
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode) {
|
||||
...SettingsFields
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,3 +14,5 @@ export const MAX_TITLE_LENGTH = 80
|
|||
export const MAX_POLL_CHOICE_LENGTH = 30
|
||||
export const ITEM_SPAM_INTERVAL = '10m'
|
||||
export const MAX_POLL_NUM_CHOICES = 10
|
||||
export const ITEM_FILTER_THRESHOLD = 1.2
|
||||
export const DONT_LIKE_THIS_COST = 1
|
||||
|
|
|
@ -61,7 +61,8 @@ export default function Settings ({ data: { settings } }) {
|
|||
noteDeposits: settings?.noteDeposits,
|
||||
noteInvites: settings?.noteInvites,
|
||||
noteJobIndicator: settings?.noteJobIndicator,
|
||||
hideInvoiceDesc: settings?.hideInvoiceDesc
|
||||
hideInvoiceDesc: settings?.hideInvoiceDesc,
|
||||
wildWestMode: settings?.wildWestMode
|
||||
}}
|
||||
schema={SettingsSchema}
|
||||
onSubmit={async ({ tipDefault, ...values }) => {
|
||||
|
@ -115,7 +116,7 @@ export default function Settings ({ data: { settings } }) {
|
|||
<div className='form-label'>privacy</div>
|
||||
<Checkbox
|
||||
label={
|
||||
<>hide invoice descriptions
|
||||
<div className='d-flex align-items-center'>hide invoice descriptions
|
||||
<Info>
|
||||
<ul className='font-weight-bold'>
|
||||
<li>Use this if you don't want funding sources to be linkable to your SN identity.</li>
|
||||
|
@ -127,10 +128,24 @@ export default function Settings ({ data: { settings } }) {
|
|||
</li>
|
||||
</ul>
|
||||
</Info>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
name='hideInvoiceDesc'
|
||||
/>
|
||||
<div className='form-label'>content</div>
|
||||
<Checkbox
|
||||
label={
|
||||
<div className='d-flex align-items-center'>wild west mode
|
||||
<Info>
|
||||
<ul className='font-weight-bold'>
|
||||
<li>Don't hide flagged content</li>
|
||||
<li>Don't down rank flagged content</li>
|
||||
</ul>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='wildWestMode'
|
||||
/>
|
||||
<div className='d-flex'>
|
||||
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "ItemActType" ADD VALUE 'DONT_LIKE_THIS';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Item" ADD COLUMN "weightedDownVotes" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "wildWestMode" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -0,0 +1,74 @@
|
|||
-- modify it to take DONT_LIKE_THIS
|
||||
CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_sats INTEGER;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT (msats / 1000) INTO user_sats FROM users WHERE id = user_id;
|
||||
IF act_sats > user_sats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
-- deduct sats from actor
|
||||
UPDATE users SET msats = msats - (act_sats * 1000) WHERE id = user_id;
|
||||
|
||||
IF act = 'VOTE' OR act = 'TIP' THEN
|
||||
-- add sats to actee's balance and stacked count
|
||||
UPDATE users
|
||||
SET msats = msats + (act_sats * 1000), "stackedMsats" = "stackedMsats" + (act_sats * 1000)
|
||||
WHERE id = (SELECT COALESCE("fwdUserId", "userId") FROM "Item" WHERE id = item_id);
|
||||
|
||||
-- if they have already voted, this is a tip
|
||||
IF EXISTS (SELECT 1 FROM "ItemAct" WHERE "itemId" = item_id AND "userId" = user_id AND "ItemAct".act = 'VOTE') THEN
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_sats, item_id, user_id, 'TIP', now_utc(), now_utc());
|
||||
ELSE
|
||||
-- else this is a vote with a possible extra tip
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (1, item_id, user_id, 'VOTE', now_utc(), now_utc());
|
||||
act_sats := act_sats - 1;
|
||||
|
||||
-- if we have sats left after vote, leave them as a tip
|
||||
IF act_sats > 0 THEN
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_sats, item_id, user_id, 'TIP', now_utc(), now_utc());
|
||||
END IF;
|
||||
|
||||
RETURN 1;
|
||||
END IF;
|
||||
ELSE -- BOOST, POLL, DONT_LIKE_THIS
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_sats, item_id, user_id, act, now_utc(), now_utc());
|
||||
END IF;
|
||||
|
||||
RETURN 0;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION weighted_downvotes_after_act() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
user_trust DOUBLE PRECISION;
|
||||
BEGIN
|
||||
-- grab user's trust who is upvoting
|
||||
SELECT trust INTO user_trust FROM users WHERE id = NEW."userId";
|
||||
-- update item
|
||||
UPDATE "Item"
|
||||
SET "weightedDownVotes" = "weightedDownVotes" + user_trust
|
||||
WHERE id = NEW."itemId" AND "userId" <> NEW."userId";
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS weighted_downvotes_after_act ON "ItemAct";
|
||||
CREATE TRIGGER weighted_downvotes_after_act
|
||||
AFTER INSERT ON "ItemAct"
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.act = 'DONT_LIKE_THIS')
|
||||
EXECUTE PROCEDURE weighted_downvotes_after_act();
|
||||
|
||||
ALTER TABLE "Item" ADD CONSTRAINT "weighted_votes_positive" CHECK ("weightedVotes" >= 0) NOT VALID;
|
||||
ALTER TABLE "Item" ADD CONSTRAINT "weighted_down_votes_positive" CHECK ("weightedDownVotes" >= 0) NOT VALID;
|
|
@ -59,6 +59,9 @@ model User {
|
|||
// privacy settings
|
||||
hideInvoiceDesc Boolean @default(false)
|
||||
|
||||
// content settings
|
||||
wildWestMode Boolean @default(false)
|
||||
|
||||
Earn Earn[]
|
||||
Upload Upload[] @relation(name: "Uploads")
|
||||
PollVote PollVote[]
|
||||
|
@ -183,8 +186,9 @@ model Item {
|
|||
paidImgLink Boolean @default(false)
|
||||
|
||||
// denormalized self stats
|
||||
weightedVotes Float @default(0)
|
||||
sats Int @default(0)
|
||||
weightedVotes Float @default(0)
|
||||
weightedDownVotes Float @default(0)
|
||||
sats Int @default(0)
|
||||
|
||||
// denormalized comment stats
|
||||
ncomments Int @default(0)
|
||||
|
@ -296,6 +300,7 @@ enum ItemActType {
|
|||
TIP
|
||||
STREAM
|
||||
POLL
|
||||
DONT_LIKE_THIS
|
||||
}
|
||||
|
||||
model ItemAct {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M17 7a8.003 8.003 0 0 0-7.493 5.19l1.874.703A6.002 6.002 0 0 1 23 15a6 6 0 0 1-6 6H7A6 6 0 0 1 5.008 9.339a7 7 0 0 1 13.757-2.143A8.027 8.027 0 0 0 17 7z"/></svg>
|
After Width: | Height: | Size: 291 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>
|
After Width: | Height: | Size: 241 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2 3h19.138a.5.5 0 0 1 .435.748L18 10l3.573 6.252a.5.5 0 0 1-.435.748H4v5H2V3z"/></svg>
|
After Width: | Height: | Size: 216 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 3h9.382a1 1 0 0 1 .894.553L14 5h6a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1h-6.382a1 1 0 0 1-.894-.553L12 16H5v6H3V3z"/></svg>
|
After Width: | Height: | Size: 247 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm14 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
After Width: | Height: | Size: 285 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z"/></svg>
|
After Width: | Height: | Size: 385 B |
Loading…
Reference in New Issue