wild west mode

This commit is contained in:
keyan 2022-09-21 14:57:36 -05:00
parent 14ce36c1cb
commit 7faae425b3
24 changed files with 316 additions and 35 deletions

View File

@ -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`
}

View File

@ -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)`
)

View File

@ -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

View File

@ -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!]!

View File

@ -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!
}

View File

@ -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>

View File

@ -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;

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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;
}

View File

@ -14,6 +14,7 @@ export const COMMENT_FIELDS = gql`
upvotes
boost
meSats
meDontLike
path
commentSats
mine

View File

@ -21,6 +21,7 @@ export const ITEM_FIELDS = gql`
boost
path
meSats
meDontLike
ncomments
commentSats
lastCommentAt

View File

@ -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
}
}

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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 {

1
svgs/cloud-fill.svg Normal file
View File

@ -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

View File

@ -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

1
svgs/flag-2-fill.svg Normal file
View File

@ -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

1
svgs/flag-fill.svg Normal file
View File

@ -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

1
svgs/more-fill.svg Normal file
View File

@ -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

1
svgs/more-line.svg Normal file
View File

@ -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