diff --git a/api/resolvers/item.js b/api/resolvers/item.js index c4b01830..fd6716b9 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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` +} diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index 9a5a8c2c..aa08603a 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -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)` ) diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 18b38270..fb3277b5 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -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 diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 274b3b36..f9cbab91 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -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!]! diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 86fde278..411fb9b6 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -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! } diff --git a/components/comment.js b/components/comment.js index f8bca31a..0a923ff3 100644 --- a/components/comment.js +++ b/components/comment.js @@ -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 : ''}`} >
- + {item.meDontLike ? : }
@@ -128,6 +132,7 @@ export default function Comment ({ {timeSince(new Date(item.createdAt))} {includeParent && } + {me && !item.meSats && !item.meDontLike && } {canEdit && <> \ diff --git a/components/comment.module.css b/components/comment.module.css index ea9d316d..047a00d4 100644 --- a/components/comment.module.css +++ b/components/comment.module.css @@ -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; diff --git a/components/dont-link-this.js b/components/dont-link-this.js new file mode 100644 index 00000000..1019f5e6 --- /dev/null +++ b/components/dont-link-this.js @@ -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 ( + + + + + + + { + try { + await dontLikeThis({ + variables: { id }, + optimisticResponse: { dontLikeThis: true } + }) + } catch (error) { + if (error.toString().includes('insufficient funds')) { + setError(true) + } + } + }} + > + I don't like this + + + + ) +} diff --git a/components/item.js b/components/item.js index f41559c2..91d11643 100644 --- a/components/item.js +++ b/components/item.js @@ -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 }) {
) :
}
- {item.position ? : } + {item.position + ? + : item.meDontLike ? : }
@@ -104,6 +110,7 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) { {timeSince(new Date(item.createdAt))} + {me && !item.meSats && !item.position && !item.meDontLike && } {item.prior && <> \ diff --git a/components/item.module.css b/components/item.module.css index 41b8515d..eb9bd351 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -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; } diff --git a/fragments/comments.js b/fragments/comments.js index d73fa49f..57718b50 100644 --- a/fragments/comments.js +++ b/fragments/comments.js @@ -14,6 +14,7 @@ export const COMMENT_FIELDS = gql` upvotes boost meSats + meDontLike path commentSats mine diff --git a/fragments/items.js b/fragments/items.js index 3196d896..7b7aa2a5 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -21,6 +21,7 @@ export const ITEM_FIELDS = gql` boost path meSats + meDontLike ncomments commentSats lastCommentAt diff --git a/fragments/users.js b/fragments/users.js index 93ae84a4..8ccbe45c 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -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 } } diff --git a/lib/constants.js b/lib/constants.js index bce39bdc..ec7a9582 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -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 diff --git a/pages/settings.js b/pages/settings.js index 2d260192..cc7a7789 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -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 } }) {
privacy
hide invoice descriptions +
hide invoice descriptions
  • Use this if you don't want funding sources to be linkable to your SN identity.
  • @@ -127,10 +128,24 @@ export default function Settings ({ data: { settings } }) {
- +
} name='hideInvoiceDesc' /> +
content
+ wild west mode + +
    +
  • Don't hide flagged content
  • +
  • Don't down rank flagged content
  • +
+
+
+ } + name='wildWestMode' + />
save
diff --git a/prisma/migrations/20220920152500_downvotes/migration.sql b/prisma/migrations/20220920152500_downvotes/migration.sql new file mode 100644 index 00000000..7a08679b --- /dev/null +++ b/prisma/migrations/20220920152500_downvotes/migration.sql @@ -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; diff --git a/prisma/migrations/20220920195257_dont_like_this/migration.sql b/prisma/migrations/20220920195257_dont_like_this/migration.sql new file mode 100644 index 00000000..d6c4912f --- /dev/null +++ b/prisma/migrations/20220920195257_dont_like_this/migration.sql @@ -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; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bb0350a1..64e4ed54 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 { diff --git a/svgs/cloud-fill.svg b/svgs/cloud-fill.svg new file mode 100644 index 00000000..ba229a29 --- /dev/null +++ b/svgs/cloud-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/error-warning-fill.svg b/svgs/error-warning-fill.svg new file mode 100644 index 00000000..a0e4ce1a --- /dev/null +++ b/svgs/error-warning-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/flag-2-fill.svg b/svgs/flag-2-fill.svg new file mode 100644 index 00000000..db4089ec --- /dev/null +++ b/svgs/flag-2-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/flag-fill.svg b/svgs/flag-fill.svg new file mode 100644 index 00000000..cfc536a6 --- /dev/null +++ b/svgs/flag-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/more-fill.svg b/svgs/more-fill.svg new file mode 100644 index 00000000..087b4440 --- /dev/null +++ b/svgs/more-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svgs/more-line.svg b/svgs/more-line.svg new file mode 100644 index 00000000..aafdf470 --- /dev/null +++ b/svgs/more-line.svg @@ -0,0 +1 @@ + \ No newline at end of file