Compare commits
No commits in common. "3310925155f5796656158fafb93846e52a23115d" and "ae579cbec3f491a96960b3f0cc748db8d3eace63" have entirely different histories.
3310925155
...
ae579cbec3
@ -1,77 +0,0 @@
|
|||||||
import { msatsToSats, satsToMsats } from '@/lib/format'
|
|
||||||
|
|
||||||
export const anonable = false
|
|
||||||
export const supportsPessimism = false
|
|
||||||
export const supportsOptimism = true
|
|
||||||
|
|
||||||
export async function getCost ({ sats }) {
|
|
||||||
return satsToMsats(sats)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) {
|
|
||||||
itemId = parseInt(itemId)
|
|
||||||
|
|
||||||
let invoiceData = {}
|
|
||||||
if (invoiceId) {
|
|
||||||
invoiceData = { invoiceId, invoiceActionState: 'PENDING' }
|
|
||||||
// store a reference to the item in the invoice
|
|
||||||
await tx.invoice.update({
|
|
||||||
where: { id: invoiceId },
|
|
||||||
data: { actionId: itemId }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const act = await tx.itemAct.create({ data: { msats: cost, itemId, userId: me.id, act: 'BOOST', ...invoiceData } })
|
|
||||||
|
|
||||||
const [{ path }] = await tx.$queryRaw`
|
|
||||||
SELECT ltree2text(path) as path FROM "Item" WHERE id = ${itemId}::INTEGER`
|
|
||||||
return { id: itemId, sats, act: 'BOOST', path, actId: act.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) {
|
|
||||||
await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } })
|
|
||||||
const [{ id, path }] = await tx.$queryRaw`
|
|
||||||
SELECT "Item".id, ltree2text(path) as path
|
|
||||||
FROM "Item"
|
|
||||||
JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId"
|
|
||||||
WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER`
|
|
||||||
return { id, sats: msatsToSats(cost), act: 'BOOST', path }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function onPaid ({ invoice, actId }, { models, tx }) {
|
|
||||||
let itemAct
|
|
||||||
if (invoice) {
|
|
||||||
await tx.itemAct.updateMany({
|
|
||||||
where: { invoiceId: invoice.id },
|
|
||||||
data: {
|
|
||||||
invoiceActionState: 'PAID'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
itemAct = await tx.itemAct.findFirst({ where: { invoiceId: invoice.id } })
|
|
||||||
} else if (actId) {
|
|
||||||
itemAct = await tx.itemAct.findFirst({ where: { id: actId } })
|
|
||||||
} else {
|
|
||||||
throw new Error('No invoice or actId')
|
|
||||||
}
|
|
||||||
|
|
||||||
// increment boost on item
|
|
||||||
await tx.item.update({
|
|
||||||
where: { id: itemAct.itemId },
|
|
||||||
data: {
|
|
||||||
boost: { increment: msatsToSats(itemAct.msats) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await tx.$executeRaw`
|
|
||||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
|
|
||||||
VALUES ('expireBoost', jsonb_build_object('id', ${itemAct.itemId}::INTEGER), 21, true,
|
|
||||||
now() + interval '30 days', interval '40 days')`
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function onFail ({ invoice }, { tx }) {
|
|
||||||
await tx.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'FAILED' } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function describe ({ id: itemId, sats }, { actionId, cost }) {
|
|
||||||
return `SN: boost ${sats ?? msatsToSats(cost)} sats to #${itemId ?? actionId}`
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ import * as TERRITORY_UPDATE from './territoryUpdate'
|
|||||||
import * as TERRITORY_BILLING from './territoryBilling'
|
import * as TERRITORY_BILLING from './territoryBilling'
|
||||||
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
||||||
import * as DONATE from './donate'
|
import * as DONATE from './donate'
|
||||||
import * as BOOST from './boost'
|
|
||||||
import wrapInvoice from 'wallets/wrap'
|
import wrapInvoice from 'wallets/wrap'
|
||||||
import { createInvoice as createUserInvoice } from 'wallets/server'
|
import { createInvoice as createUserInvoice } from 'wallets/server'
|
||||||
|
|
||||||
@ -22,7 +21,6 @@ export const paidActions = {
|
|||||||
ITEM_UPDATE,
|
ITEM_UPDATE,
|
||||||
ZAP,
|
ZAP,
|
||||||
DOWN_ZAP,
|
DOWN_ZAP,
|
||||||
BOOST,
|
|
||||||
POLL_VOTE,
|
POLL_VOTE,
|
||||||
TERRITORY_CREATE,
|
TERRITORY_CREATE,
|
||||||
TERRITORY_UPDATE,
|
TERRITORY_UPDATE,
|
||||||
@ -188,12 +186,7 @@ export async function retryPaidAction (actionType, args, context) {
|
|||||||
context.optimistic = true
|
context.optimistic = true
|
||||||
context.me = await models.user.findUnique({ where: { id: me.id } })
|
context.me = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
const failedInvoice = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
|
const { msatsRequested, actionId } = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
|
||||||
if (!failedInvoice) {
|
|
||||||
throw new Error(`retryPaidAction - invoice not found or not in failed state ${actionType}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { msatsRequested, actionId } = failedInvoice
|
|
||||||
context.cost = BigInt(msatsRequested)
|
context.cost = BigInt(msatsRequested)
|
||||||
context.actionId = actionId
|
context.actionId = actionId
|
||||||
const invoiceArgs = await createSNInvoice(actionType, args, context)
|
const invoiceArgs = await createSNInvoice(actionType, args, context)
|
||||||
@ -252,8 +245,8 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||||||
try {
|
try {
|
||||||
const description = await paidActions[actionType].describe(args, context)
|
const description = await paidActions[actionType].describe(args, context)
|
||||||
const { invoice: bolt11, wallet } = await createUserInvoice(userId, {
|
const { invoice: bolt11, wallet } = await createUserInvoice(userId, {
|
||||||
// this is the amount the stacker will receive, the other 3/10ths is the sybil fee
|
// this is the amount the stacker will receive, the other 1/10th is the fee
|
||||||
msats: cost * BigInt(7) / BigInt(10),
|
msats: cost * BigInt(9) / BigInt(10),
|
||||||
description,
|
description,
|
||||||
expiry: INVOICE_EXPIRE_SECS
|
expiry: INVOICE_EXPIRE_SECS
|
||||||
}, { models })
|
}, { models })
|
||||||
|
@ -195,13 +195,6 @@ export async function onPaid ({ invoice, id }, context) {
|
|||||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
||||||
VALUES ('imgproxy', jsonb_build_object('id', ${item.id}::INTEGER), 21, true, now() + interval '5 seconds')`
|
VALUES ('imgproxy', jsonb_build_object('id', ${item.id}::INTEGER), 21, true, now() + interval '5 seconds')`
|
||||||
|
|
||||||
if (item.boost > 0) {
|
|
||||||
await tx.$executeRaw`
|
|
||||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
|
|
||||||
VALUES ('expireBoost', jsonb_build_object('id', ${item.id}::INTEGER), 21, true,
|
|
||||||
now() + interval '30 days', interval '40 days')`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.parentId) {
|
if (item.parentId) {
|
||||||
// denormalize ncomments, lastCommentAt, and "weightedComments" for ancestors, and insert into reply table
|
// denormalize ncomments, lastCommentAt, and "weightedComments" for ancestors, and insert into reply table
|
||||||
await tx.$executeRaw`
|
await tx.$executeRaw`
|
||||||
|
@ -13,7 +13,7 @@ export async function getCost ({ id, boost = 0, uploadIds }, { me, models }) {
|
|||||||
// or more boost
|
// or more boost
|
||||||
const old = await models.item.findUnique({ where: { id: parseInt(id) } })
|
const old = await models.item.findUnique({ where: { id: parseInt(id) } })
|
||||||
const { totalFeesMsats } = await uploadFees(uploadIds, { models, me })
|
const { totalFeesMsats } = await uploadFees(uploadIds, { models, me })
|
||||||
return BigInt(totalFeesMsats) + satsToMsats(boost - old.boost)
|
return BigInt(totalFeesMsats) + satsToMsats(boost - (old.boost || 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function perform (args, context) {
|
export async function perform (args, context) {
|
||||||
@ -30,10 +30,9 @@ export async function perform (args, context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const newBoost = boost - old.boost
|
const boostMsats = satsToMsats(boost - (old.boost || 0))
|
||||||
const itemActs = []
|
const itemActs = []
|
||||||
if (newBoost > 0) {
|
if (boostMsats > 0) {
|
||||||
const boostMsats = satsToMsats(newBoost)
|
|
||||||
itemActs.push({
|
itemActs.push({
|
||||||
msats: boostMsats, act: 'BOOST', userId: me?.id || USER_ID.anon
|
msats: boostMsats, act: 'BOOST', userId: me?.id || USER_ID.anon
|
||||||
})
|
})
|
||||||
@ -55,19 +54,15 @@ export async function perform (args, context) {
|
|||||||
data: { paid: true }
|
data: { paid: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
// we put boost in the where clause because we don't want to update the boost
|
|
||||||
// if it has changed concurrently
|
|
||||||
const item = await tx.item.update({
|
const item = await tx.item.update({
|
||||||
where: { id: parseInt(id), boost: old.boost },
|
where: { id: parseInt(id) },
|
||||||
include: {
|
include: {
|
||||||
mentions: true,
|
mentions: true,
|
||||||
itemReferrers: { include: { refereeItem: true } }
|
itemReferrers: { include: { refereeItem: true } }
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
boost: {
|
boost,
|
||||||
increment: newBoost
|
|
||||||
},
|
|
||||||
pollOptions: {
|
pollOptions: {
|
||||||
createMany: {
|
createMany: {
|
||||||
data: pollOptions?.map(option => ({ option }))
|
data: pollOptions?.map(option => ({ option }))
|
||||||
@ -131,17 +126,8 @@ export async function perform (args, context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await tx.$executeRaw`
|
await tx.$executeRaw`INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
||||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
|
VALUES ('imgproxy', jsonb_build_object('id', ${id}::INTEGER), 21, true, now() + interval '5 seconds')`
|
||||||
VALUES ('imgproxy', jsonb_build_object('id', ${id}::INTEGER), 21, true,
|
|
||||||
now() + interval '5 seconds', interval '1 day')`
|
|
||||||
|
|
||||||
if (newBoost > 0) {
|
|
||||||
await tx.$executeRaw`
|
|
||||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
|
|
||||||
VALUES ('expireBoost', jsonb_build_object('id', ${id}::INTEGER), 21, true,
|
|
||||||
now() + interval '30 days', interval '40 days')`
|
|
||||||
}
|
|
||||||
|
|
||||||
await performBotBehavior(args, context)
|
await performBotBehavior(args, context)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ export async function invoiceablePeer ({ id }, { models }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) {
|
export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, cost, tx }) {
|
||||||
const feeMsats = 3n * (cost / BigInt(10)) // 30% fee
|
const feeMsats = cost / BigInt(10) // 10% fee
|
||||||
const zapMsats = cost - feeMsats
|
const zapMsats = cost - feeMsats
|
||||||
itemId = parseInt(itemId)
|
itemId = parseInt(itemId)
|
||||||
|
|
||||||
|
@ -6,9 +6,8 @@ import domino from 'domino'
|
|||||||
import {
|
import {
|
||||||
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
|
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
|
||||||
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||||
USER_ID, POLL_COST, ADMIN_ITEMS, GLOBAL_SEED,
|
USER_ID, POLL_COST,
|
||||||
NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_ADMIN_IDS,
|
ADMIN_ITEMS, GLOBAL_SEED, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_ADMIN_IDS
|
||||||
BOOST_MULT
|
|
||||||
} from '@/lib/constants'
|
} from '@/lib/constants'
|
||||||
import { msatsToSats } from '@/lib/format'
|
import { msatsToSats } from '@/lib/format'
|
||||||
import { parse } from 'tldts'
|
import { parse } from 'tldts'
|
||||||
@ -31,13 +30,13 @@ function commentsOrderByClause (me, models, sort) {
|
|||||||
if (me && sort === 'hot') {
|
if (me && sort === 'hot') {
|
||||||
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, COALESCE(
|
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, COALESCE(
|
||||||
personal_hot_score,
|
personal_hot_score,
|
||||||
${orderByNumerator({ models, commentScaler: 0, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
|
${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
|
||||||
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
} else {
|
} else {
|
||||||
if (sort === 'top') {
|
if (sort === 'top') {
|
||||||
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator({ models, commentScaler: 0 })} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
} else {
|
} else {
|
||||||
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator({ models, commentScaler: 0, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,29 +73,6 @@ export async function getItem (parent, { id }, { me, models }) {
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAd (parent, { sub, subArr = [], showNsfw = false }, { me, models }) {
|
|
||||||
return (await itemQueryWithMeta({
|
|
||||||
me,
|
|
||||||
models,
|
|
||||||
query: `
|
|
||||||
${SELECT}
|
|
||||||
FROM "Item"
|
|
||||||
LEFT JOIN "Sub" ON "Sub"."name" = "Item"."subName"
|
|
||||||
${whereClause(
|
|
||||||
'"parentId" IS NULL',
|
|
||||||
'"Item"."pinId" IS NULL',
|
|
||||||
'"Item"."deletedAt" IS NULL',
|
|
||||||
'"Item"."parentId" IS NULL',
|
|
||||||
'"Item".bio = false',
|
|
||||||
'"Item".boost > 0',
|
|
||||||
activeOrMine(),
|
|
||||||
subClause(sub, 1, 'Item', me, showNsfw),
|
|
||||||
muteClause(me))}
|
|
||||||
ORDER BY boost desc, "Item".created_at ASC
|
|
||||||
LIMIT 1`
|
|
||||||
}, ...subArr))?.[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderByClause = (by, me, models, type) => {
|
const orderByClause = (by, me, models, type) => {
|
||||||
switch (by) {
|
switch (by) {
|
||||||
case 'comments':
|
case 'comments':
|
||||||
@ -112,12 +88,12 @@ const orderByClause = (by, me, models, type) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function orderByNumerator ({ models, commentScaler = 0.5, considerBoost = false }) {
|
export function orderByNumerator (models, commentScaler = 0.5) {
|
||||||
return `(CASE WHEN "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0 THEN
|
return `(CASE WHEN "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0 THEN
|
||||||
GREATEST("Item"."weightedVotes" - "Item"."weightedDownVotes", POWER("Item"."weightedVotes" - "Item"."weightedDownVotes", 1.2))
|
GREATEST("Item"."weightedVotes" - "Item"."weightedDownVotes", POWER("Item"."weightedVotes" - "Item"."weightedDownVotes", 1.2))
|
||||||
ELSE
|
ELSE
|
||||||
"Item"."weightedVotes" - "Item"."weightedDownVotes"
|
"Item"."weightedVotes" - "Item"."weightedDownVotes"
|
||||||
END + "Item"."weightedComments"*${commentScaler}) + ${considerBoost ? `("Item".boost / ${BOOST_MULT})` : 0}`
|
END + "Item"."weightedComments"*${commentScaler})`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function joinZapRankPersonalView (me, models) {
|
export function joinZapRankPersonalView (me, models) {
|
||||||
@ -328,7 +304,7 @@ export default {
|
|||||||
},
|
},
|
||||||
items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit = LIMIT }, { me, models }) => {
|
items: async (parent, { sub, sort, type, cursor, name, when, from, to, by, limit = LIMIT }, { me, models }) => {
|
||||||
const decodedCursor = decodeCursor(cursor)
|
const decodedCursor = decodeCursor(cursor)
|
||||||
let items, user, pins, subFull, table, ad
|
let items, user, pins, subFull, table
|
||||||
|
|
||||||
// special authorization for bookmarks depending on owning users' privacy settings
|
// special authorization for bookmarks depending on owning users' privacy settings
|
||||||
if (type === 'bookmarks' && name && me?.name !== name) {
|
if (type === 'bookmarks' && name && me?.name !== name) {
|
||||||
@ -466,54 +442,27 @@ export default {
|
|||||||
models,
|
models,
|
||||||
query: `
|
query: `
|
||||||
${SELECT},
|
${SELECT},
|
||||||
(boost IS NOT NULL AND boost > 0)::INT AS group_rank,
|
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
|
||||||
CASE WHEN boost IS NOT NULL AND boost > 0
|
THEN 0 ELSE 1 END AS group_rank,
|
||||||
THEN rank() OVER (ORDER BY boost DESC, created_at ASC)
|
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
|
||||||
|
THEN rank() OVER (ORDER BY "maxBid" DESC, created_at ASC)
|
||||||
ELSE rank() OVER (ORDER BY created_at DESC) END AS rank
|
ELSE rank() OVER (ORDER BY created_at DESC) END AS rank
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
${whereClause(
|
${whereClause(
|
||||||
'"parentId" IS NULL',
|
'"parentId" IS NULL',
|
||||||
'"Item"."deletedAt" IS NULL',
|
'"Item"."deletedAt" IS NULL',
|
||||||
'"Item"."status" = \'ACTIVE\'',
|
|
||||||
'created_at <= $1',
|
'created_at <= $1',
|
||||||
'"pinId" IS NULL',
|
'"pinId" IS NULL',
|
||||||
subClause(sub, 4)
|
subClause(sub, 4),
|
||||||
|
"status IN ('ACTIVE', 'NOSATS')"
|
||||||
)}
|
)}
|
||||||
ORDER BY group_rank, rank
|
ORDER BY group_rank, rank
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
LIMIT $3`,
|
LIMIT $3`,
|
||||||
orderBy: 'ORDER BY group_rank DESC, rank'
|
orderBy: 'ORDER BY group_rank, rank'
|
||||||
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
|
}, decodedCursor.time, decodedCursor.offset, limit, ...subArr)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if (decodedCursor.offset === 0) {
|
|
||||||
// get pins for the page and return those separately
|
|
||||||
pins = await itemQueryWithMeta({
|
|
||||||
me,
|
|
||||||
models,
|
|
||||||
query: `
|
|
||||||
SELECT rank_filter.*
|
|
||||||
FROM (
|
|
||||||
${SELECT}, position,
|
|
||||||
rank() OVER (
|
|
||||||
PARTITION BY "pinId"
|
|
||||||
ORDER BY "Item".created_at DESC
|
|
||||||
)
|
|
||||||
FROM "Item"
|
|
||||||
JOIN "Pin" ON "Item"."pinId" = "Pin".id
|
|
||||||
${whereClause(
|
|
||||||
'"pinId" IS NOT NULL',
|
|
||||||
'"parentId" IS NULL',
|
|
||||||
sub ? '"subName" = $1' : '"subName" IS NULL',
|
|
||||||
muteClause(me))}
|
|
||||||
) rank_filter WHERE RANK = 1
|
|
||||||
ORDER BY position ASC`,
|
|
||||||
orderBy: 'ORDER BY position ASC'
|
|
||||||
}, ...subArr)
|
|
||||||
|
|
||||||
ad = await getAd(parent, { sub, subArr, showNsfw }, { me, models })
|
|
||||||
}
|
|
||||||
|
|
||||||
items = await itemQueryWithMeta({
|
items = await itemQueryWithMeta({
|
||||||
me,
|
me,
|
||||||
models,
|
models,
|
||||||
@ -529,7 +478,6 @@ export default {
|
|||||||
'"Item"."parentId" IS NULL',
|
'"Item"."parentId" IS NULL',
|
||||||
'"Item".outlawed = false',
|
'"Item".outlawed = false',
|
||||||
'"Item".bio = false',
|
'"Item".bio = false',
|
||||||
ad ? `"Item".id <> ${ad.id}` : '',
|
|
||||||
activeOrMine(me),
|
activeOrMine(me),
|
||||||
subClause(sub, 3, 'Item', me, showNsfw),
|
subClause(sub, 3, 'Item', me, showNsfw),
|
||||||
muteClause(me))}
|
muteClause(me))}
|
||||||
@ -539,8 +487,8 @@ export default {
|
|||||||
orderBy: 'ORDER BY rank DESC'
|
orderBy: 'ORDER BY rank DESC'
|
||||||
}, decodedCursor.offset, limit, ...subArr)
|
}, decodedCursor.offset, limit, ...subArr)
|
||||||
|
|
||||||
// XXX this is mostly for subs that are really empty
|
// XXX this is just for subs that are really empty
|
||||||
if (items.length < limit) {
|
if (decodedCursor.offset === 0 && items.length < limit) {
|
||||||
items = await itemQueryWithMeta({
|
items = await itemQueryWithMeta({
|
||||||
me,
|
me,
|
||||||
models,
|
models,
|
||||||
@ -556,17 +504,40 @@ export default {
|
|||||||
'"Item"."deletedAt" IS NULL',
|
'"Item"."deletedAt" IS NULL',
|
||||||
'"Item"."parentId" IS NULL',
|
'"Item"."parentId" IS NULL',
|
||||||
'"Item".bio = false',
|
'"Item".bio = false',
|
||||||
ad ? `"Item".id <> ${ad.id}` : '',
|
|
||||||
activeOrMine(me),
|
activeOrMine(me),
|
||||||
await filterClause(me, models, type))}
|
await filterClause(me, models, type))}
|
||||||
ORDER BY ${orderByNumerator({ models, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST,
|
ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC
|
||||||
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC
|
|
||||||
OFFSET $1
|
OFFSET $1
|
||||||
LIMIT $2`,
|
LIMIT $2`,
|
||||||
orderBy: `ORDER BY ${orderByNumerator({ models, considerBoost: true })}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST,
|
orderBy: `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
|
||||||
}, decodedCursor.offset, limit, ...subArr)
|
}, decodedCursor.offset, limit, ...subArr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (decodedCursor.offset === 0) {
|
||||||
|
// get pins for the page and return those separately
|
||||||
|
pins = await itemQueryWithMeta({
|
||||||
|
me,
|
||||||
|
models,
|
||||||
|
query: `
|
||||||
|
SELECT rank_filter.*
|
||||||
|
FROM (
|
||||||
|
${SELECT}, position,
|
||||||
|
rank() OVER (
|
||||||
|
PARTITION BY "pinId"
|
||||||
|
ORDER BY "Item".created_at DESC
|
||||||
|
)
|
||||||
|
FROM "Item"
|
||||||
|
JOIN "Pin" ON "Item"."pinId" = "Pin".id
|
||||||
|
${whereClause(
|
||||||
|
'"pinId" IS NOT NULL',
|
||||||
|
'"parentId" IS NULL',
|
||||||
|
sub ? '"subName" = $1' : '"subName" IS NULL',
|
||||||
|
muteClause(me))}
|
||||||
|
) rank_filter WHERE RANK = 1
|
||||||
|
ORDER BY position ASC`,
|
||||||
|
orderBy: 'ORDER BY position ASC'
|
||||||
|
}, ...subArr)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -574,8 +545,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
cursor: items.length === limit ? nextCursorEncoded(decodedCursor) : null,
|
cursor: items.length === limit ? nextCursorEncoded(decodedCursor) : null,
|
||||||
items,
|
items,
|
||||||
pins,
|
pins
|
||||||
ad
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
item: getItem,
|
item: getItem,
|
||||||
@ -645,17 +615,18 @@ export default {
|
|||||||
LIMIT 3`
|
LIMIT 3`
|
||||||
}, similar)
|
}, similar)
|
||||||
},
|
},
|
||||||
auctionPosition: async (parent, { id, sub, boost }, { models, me }) => {
|
auctionPosition: async (parent, { id, sub, bid }, { models, me }) => {
|
||||||
const createdAt = id ? (await getItem(parent, { id }, { models, me })).createdAt : new Date()
|
const createdAt = id ? (await getItem(parent, { id }, { models, me })).createdAt : new Date()
|
||||||
let where
|
let where
|
||||||
if (boost > 0) {
|
if (bid > 0) {
|
||||||
// if there's boost
|
// if there's a bid
|
||||||
// has a larger boost than ours, or has an equal boost and is older
|
// it's ACTIVE and has a larger bid than ours, or has an equal bid and is older
|
||||||
// count items: (boost > ours.boost OR (boost = ours.boost AND create_at < ours.created_at))
|
// count items: (bid > ours.bid OR (bid = ours.bid AND create_at < ours.created_at)) AND status = 'ACTIVE'
|
||||||
where = {
|
where = {
|
||||||
|
status: 'ACTIVE',
|
||||||
OR: [
|
OR: [
|
||||||
{ boost: { gt: boost } },
|
{ maxBid: { gt: bid } },
|
||||||
{ boost, createdAt: { lt: createdAt } }
|
{ maxBid: bid, createdAt: { lt: createdAt } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -664,42 +635,18 @@ export default {
|
|||||||
// count items: ((bid > ours.bid AND status = 'ACTIVE') OR (created_at > ours.created_at AND status <> 'STOPPED'))
|
// count items: ((bid > ours.bid AND status = 'ACTIVE') OR (created_at > ours.created_at AND status <> 'STOPPED'))
|
||||||
where = {
|
where = {
|
||||||
OR: [
|
OR: [
|
||||||
{ boost: { gt: 0 } },
|
{ maxBid: { gt: 0 }, status: 'ACTIVE' },
|
||||||
{ createdAt: { gt: createdAt } }
|
{ createdAt: { gt: createdAt }, status: { not: 'STOPPED' } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
where.AND = {
|
where.subName = sub
|
||||||
subName: sub,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
deletedAt: null
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
where.AND.id = { not: Number(id) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return await models.item.count({ where }) + 1
|
|
||||||
},
|
|
||||||
boostPosition: async (parent, { id, sub, boost }, { models, me }) => {
|
|
||||||
if (boost <= 0) {
|
|
||||||
throw new GqlInputError('boost must be greater than 0')
|
|
||||||
}
|
|
||||||
|
|
||||||
const where = {
|
|
||||||
boost: { gte: boost },
|
|
||||||
status: 'ACTIVE',
|
|
||||||
deletedAt: null,
|
|
||||||
outlawed: false
|
|
||||||
}
|
|
||||||
if (id) {
|
if (id) {
|
||||||
where.id = { not: Number(id) }
|
where.id = { not: Number(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return await models.item.count({ where }) + 1
|
||||||
home: await models.item.count({ where }) === 0,
|
|
||||||
sub: await models.item.count({ where: { ...where, subName: sub } }) === 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -878,6 +825,7 @@ export default {
|
|||||||
item.uploadId = item.logo
|
item.uploadId = item.logo
|
||||||
delete item.logo
|
delete item.logo
|
||||||
}
|
}
|
||||||
|
item.maxBid ??= 0
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
||||||
@ -914,7 +862,7 @@ export default {
|
|||||||
|
|
||||||
return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd })
|
return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd })
|
||||||
},
|
},
|
||||||
act: async (parent, { id, sats, act = 'TIP' }, { me, models, lnd, headers }) => {
|
act: async (parent, { id, sats, act = 'TIP', idempotent }, { me, models, lnd, headers }) => {
|
||||||
assertApiKeyNotPermitted({ me })
|
assertApiKeyNotPermitted({ me })
|
||||||
await ssValidate(actSchema, { sats, act })
|
await ssValidate(actSchema, { sats, act })
|
||||||
await assertGofacYourself({ models, headers })
|
await assertGofacYourself({ models, headers })
|
||||||
@ -933,7 +881,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// disallow self tips except anons
|
// disallow self tips except anons
|
||||||
if (me && ['TIP', 'DONT_LIKE_THIS'].includes(act)) {
|
if (me) {
|
||||||
if (Number(item.userId) === Number(me.id)) {
|
if (Number(item.userId) === Number(me.id)) {
|
||||||
throw new GqlInputError('cannot zap yourself')
|
throw new GqlInputError('cannot zap yourself')
|
||||||
}
|
}
|
||||||
@ -951,8 +899,6 @@ export default {
|
|||||||
return await performPaidAction('ZAP', { id, sats }, { me, models, lnd })
|
return await performPaidAction('ZAP', { id, sats }, { me, models, lnd })
|
||||||
} else if (act === 'DONT_LIKE_THIS') {
|
} else if (act === 'DONT_LIKE_THIS') {
|
||||||
return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd })
|
return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd })
|
||||||
} else if (act === 'BOOST') {
|
|
||||||
return await performPaidAction('BOOST', { id, sats }, { me, models, lnd })
|
|
||||||
} else {
|
} else {
|
||||||
throw new GqlInputError('unknown act')
|
throw new GqlInputError('unknown act')
|
||||||
}
|
}
|
||||||
@ -1375,7 +1321,7 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
|
|||||||
item = { id: Number(item.id), text: item.text, title: `@${user.name}'s bio` }
|
item = { id: Number(item.id), text: item.text, title: `@${user.name}'s bio` }
|
||||||
} else if (old.parentId) {
|
} else if (old.parentId) {
|
||||||
// prevent editing a comment like a post
|
// prevent editing a comment like a post
|
||||||
item = { id: Number(item.id), text: item.text, boost: item.boost }
|
item = { id: Number(item.id), text: item.text }
|
||||||
} else {
|
} else {
|
||||||
item = { subName, ...item }
|
item = { subName, ...item }
|
||||||
item.forwardUsers = await getForwardUsers(models, forward)
|
item.forwardUsers = await getForwardUsers(models, forward)
|
||||||
@ -1444,5 +1390,5 @@ export const SELECT =
|
|||||||
ltree2text("Item"."path") AS "path"`
|
ltree2text("Item"."path") AS "path"`
|
||||||
|
|
||||||
function topOrderByWeightedSats (me, models) {
|
function topOrderByWeightedSats (me, models) {
|
||||||
return `ORDER BY ${orderByNumerator({ models })} DESC NULLS LAST, "Item".id DESC`
|
return `ORDER BY ${orderByNumerator(models)} DESC NULLS LAST, "Item".id DESC`
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,17 @@ export default {
|
|||||||
)`
|
)`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
queries.push(
|
||||||
|
`(SELECT "Item".id::text, "Item"."statusUpdatedAt" AS "sortTime", NULL as "earnedSats",
|
||||||
|
'JobChanged' AS type
|
||||||
|
FROM "Item"
|
||||||
|
WHERE "Item"."userId" = $1
|
||||||
|
AND "maxBid" IS NOT NULL
|
||||||
|
AND "statusUpdatedAt" < $2 AND "statusUpdatedAt" <> created_at
|
||||||
|
ORDER BY "sortTime" DESC
|
||||||
|
LIMIT ${LIMIT})`
|
||||||
|
)
|
||||||
|
|
||||||
// territory transfers
|
// territory transfers
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT "TerritoryTransfer".id::text, "TerritoryTransfer"."created_at" AS "sortTime", NULL as "earnedSats",
|
`(SELECT "TerritoryTransfer".id::text, "TerritoryTransfer"."created_at" AS "sortTime", NULL as "earnedSats",
|
||||||
@ -343,8 +354,7 @@ export default {
|
|||||||
"Invoice"."actionType" = 'ITEM_CREATE' OR
|
"Invoice"."actionType" = 'ITEM_CREATE' OR
|
||||||
"Invoice"."actionType" = 'ZAP' OR
|
"Invoice"."actionType" = 'ZAP' OR
|
||||||
"Invoice"."actionType" = 'DOWN_ZAP' OR
|
"Invoice"."actionType" = 'DOWN_ZAP' OR
|
||||||
"Invoice"."actionType" = 'POLL_VOTE' OR
|
"Invoice"."actionType" = 'POLL_VOTE'
|
||||||
"Invoice"."actionType" = 'BOOST'
|
|
||||||
)
|
)
|
||||||
ORDER BY "sortTime" DESC
|
ORDER BY "sortTime" DESC
|
||||||
LIMIT ${LIMIT})`
|
LIMIT ${LIMIT})`
|
||||||
|
@ -8,7 +8,6 @@ function paidActionType (actionType) {
|
|||||||
return 'ItemPaidAction'
|
return 'ItemPaidAction'
|
||||||
case 'ZAP':
|
case 'ZAP':
|
||||||
case 'DOWN_ZAP':
|
case 'DOWN_ZAP':
|
||||||
case 'BOOST':
|
|
||||||
return 'ItemActPaidAction'
|
return 'ItemActPaidAction'
|
||||||
case 'TERRITORY_CREATE':
|
case 'TERRITORY_CREATE':
|
||||||
case 'TERRITORY_UPDATE':
|
case 'TERRITORY_UPDATE':
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { amountSchema, ssValidate } from '@/lib/validate'
|
import { amountSchema, ssValidate } from '@/lib/validate'
|
||||||
import { getAd, getItem } from './item'
|
import { getItem } from './item'
|
||||||
import { topUsers } from './user'
|
import { topUsers } from './user'
|
||||||
import performPaidAction from '../paidAction'
|
import performPaidAction from '../paidAction'
|
||||||
import { GqlInputError } from '@/lib/error'
|
import { GqlInputError } from '@/lib/error'
|
||||||
@ -164,9 +164,6 @@ export default {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return parent.total
|
return parent.total
|
||||||
},
|
|
||||||
ad: async (parent, args, { me, models }) => {
|
|
||||||
return await getAd(parent, { }, { me, models })
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
@ -396,6 +396,22 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const job = await models.item.findFirst({
|
||||||
|
where: {
|
||||||
|
maxBid: {
|
||||||
|
not: null
|
||||||
|
},
|
||||||
|
userId: me.id,
|
||||||
|
statusUpdatedAt: {
|
||||||
|
gt: lastChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (job && job.statusUpdatedAt > job.createdAt) {
|
||||||
|
foundNotes()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (user.noteEarning) {
|
if (user.noteEarning) {
|
||||||
const earn = await models.earn.findFirst({
|
const earn = await models.earn.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -516,7 +516,6 @@ const resolvers = {
|
|||||||
case 'ZAP':
|
case 'ZAP':
|
||||||
case 'DOWN_ZAP':
|
case 'DOWN_ZAP':
|
||||||
case 'POLL_VOTE':
|
case 'POLL_VOTE':
|
||||||
case 'BOOST':
|
|
||||||
return (await itemQueryWithMeta({
|
return (await itemQueryWithMeta({
|
||||||
me,
|
me,
|
||||||
models,
|
models,
|
||||||
@ -533,14 +532,12 @@ const resolvers = {
|
|||||||
const action2act = {
|
const action2act = {
|
||||||
ZAP: 'TIP',
|
ZAP: 'TIP',
|
||||||
DOWN_ZAP: 'DONT_LIKE_THIS',
|
DOWN_ZAP: 'DONT_LIKE_THIS',
|
||||||
POLL_VOTE: 'POLL',
|
POLL_VOTE: 'POLL'
|
||||||
BOOST: 'BOOST'
|
|
||||||
}
|
}
|
||||||
switch (invoice.actionType) {
|
switch (invoice.actionType) {
|
||||||
case 'ZAP':
|
case 'ZAP':
|
||||||
case 'DOWN_ZAP':
|
case 'DOWN_ZAP':
|
||||||
case 'POLL_VOTE':
|
case 'POLL_VOTE':
|
||||||
case 'BOOST':
|
|
||||||
return (await models.$queryRaw`
|
return (await models.$queryRaw`
|
||||||
SELECT id, act, "invoiceId", "invoiceActionState", msats
|
SELECT id, act, "invoiceId", "invoiceActionState", msats
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
|
@ -8,16 +8,10 @@ export default gql`
|
|||||||
dupes(url: String!): [Item!]
|
dupes(url: String!): [Item!]
|
||||||
related(cursor: String, title: String, id: ID, minMatch: String, limit: Limit): Items
|
related(cursor: String, title: String, id: ID, minMatch: String, limit: Limit): Items
|
||||||
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String, from: String, to: String): Items
|
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String, from: String, to: String): Items
|
||||||
auctionPosition(sub: String, id: ID, boost: Int): Int!
|
auctionPosition(sub: String, id: ID, bid: Int!): Int!
|
||||||
boostPosition(sub: String, id: ID, boost: Int): BoostPositions!
|
|
||||||
itemRepetition(parentId: ID): Int!
|
itemRepetition(parentId: ID): Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoostPositions {
|
|
||||||
home: Boolean!
|
|
||||||
sub: Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
type TitleUnshorted {
|
type TitleUnshorted {
|
||||||
title: String
|
title: String
|
||||||
unshorted: String
|
unshorted: String
|
||||||
@ -52,12 +46,12 @@ export default gql`
|
|||||||
hash: String, hmac: String): ItemPaidAction!
|
hash: String, hmac: String): ItemPaidAction!
|
||||||
upsertJob(
|
upsertJob(
|
||||||
id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||||
text: String!, url: String!, boost: Int, status: String, logo: Int): ItemPaidAction!
|
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): ItemPaidAction!
|
||||||
upsertPoll(
|
upsertPoll(
|
||||||
id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], pollExpiresAt: Date,
|
id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], pollExpiresAt: Date,
|
||||||
hash: String, hmac: String): ItemPaidAction!
|
hash: String, hmac: String): ItemPaidAction!
|
||||||
updateNoteId(id: ID!, noteId: String!): Item!
|
updateNoteId(id: ID!, noteId: String!): Item!
|
||||||
upsertComment(id: ID, text: String!, parentId: ID, boost: Int, hash: String, hmac: String): ItemPaidAction!
|
upsertComment(id: ID, text: String!, parentId: ID, hash: String, hmac: String): ItemPaidAction!
|
||||||
act(id: ID!, sats: Int, act: String, idempotent: Boolean): ItemActPaidAction!
|
act(id: ID!, sats: Int, act: String, idempotent: Boolean): ItemActPaidAction!
|
||||||
pollVote(id: ID!): PollVotePaidAction!
|
pollVote(id: ID!): PollVotePaidAction!
|
||||||
toggleOutlaw(id: ID!): Item!
|
toggleOutlaw(id: ID!): Item!
|
||||||
@ -85,7 +79,6 @@ export default gql`
|
|||||||
cursor: String
|
cursor: String
|
||||||
items: [Item!]!
|
items: [Item!]!
|
||||||
pins: [Item!]
|
pins: [Item!]
|
||||||
ad: Item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Comments {
|
type Comments {
|
||||||
@ -143,6 +136,7 @@ export default gql`
|
|||||||
path: String
|
path: String
|
||||||
position: Int
|
position: Int
|
||||||
prior: Int
|
prior: Int
|
||||||
|
maxBid: Int
|
||||||
isJob: Boolean!
|
isJob: Boolean!
|
||||||
pollCost: Int
|
pollCost: Int
|
||||||
poll: Poll
|
poll: Poll
|
||||||
|
@ -19,7 +19,6 @@ export default gql`
|
|||||||
time: Date!
|
time: Date!
|
||||||
sources: [NameValue!]!
|
sources: [NameValue!]!
|
||||||
leaderboard: UsersNullable
|
leaderboard: UsersNullable
|
||||||
ad: Item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reward {
|
type Reward {
|
||||||
|
@ -127,4 +127,3 @@ brugeman,issue,#1311,#864,medium,high,,,50k,brugeman@stacker.news,2024-08-27
|
|||||||
riccardobl,pr,#1342,#1141,hard,high,,pending unrelated rearchitecture,1m,rblb@getalby.com,2024-09-09
|
riccardobl,pr,#1342,#1141,hard,high,,pending unrelated rearchitecture,1m,rblb@getalby.com,2024-09-09
|
||||||
SatsAllDay,issue,#1368,#1331,medium,,,,25k,weareallsatoshi@getalby.com,2024-09-16
|
SatsAllDay,issue,#1368,#1331,medium,,,,25k,weareallsatoshi@getalby.com,2024-09-16
|
||||||
benalleng,helpfulness,#1368,#1170,medium,,,did a lot of it in #1175,25k,BenAllenG@stacker.news,2024-09-16
|
benalleng,helpfulness,#1368,#1170,medium,,,did a lot of it in #1175,25k,BenAllenG@stacker.news,2024-09-16
|
||||||
humble-GOAT,issue,#1412,#1407,good-first-issue,,,,2k,humble_GOAT@stacker.news,2024-09-18
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import { Input, InputUserSuggest, VariableInput, Checkbox } from './form'
|
import { Input, InputUserSuggest, VariableInput, Checkbox } from './form'
|
||||||
import InputGroup from 'react-bootstrap/InputGroup'
|
import InputGroup from 'react-bootstrap/InputGroup'
|
||||||
@ -11,8 +11,6 @@ import { useMe } from './me'
|
|||||||
import { useFeeButton } from './fee-button'
|
import { useFeeButton } from './fee-button'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
import { gql, useLazyQuery } from '@apollo/client'
|
|
||||||
import useDebounceCallback from './use-debounce-callback'
|
|
||||||
|
|
||||||
const EMPTY_FORWARD = { nym: '', pct: '' }
|
const EMPTY_FORWARD = { nym: '', pct: '' }
|
||||||
|
|
||||||
@ -28,118 +26,9 @@ const FormStatus = {
|
|||||||
ERROR: 'error'
|
ERROR: 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BoostHelp () {
|
export default function AdvPostForm ({ children, item, storageKeyPrefix }) {
|
||||||
return (
|
|
||||||
<ol style={{ lineHeight: 1.25 }}>
|
|
||||||
<li>Boost ranks items higher based on the amount</li>
|
|
||||||
<li>The highest boost in a territory over the last 30 days is pinned to the top of the territory</li>
|
|
||||||
<li>The highest boost across all territories over the last 30 days is pinned to the top of the homepage</li>
|
|
||||||
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
|
|
||||||
<li>Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to a zap-vote from a maximally trusted stacker
|
|
||||||
<ul>
|
|
||||||
<li>e.g. {numWithUnits(BOOST_MULT * 5, { abbreviate: false })} is like five zap-votes from a maximally trusted stacker</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>The decay of boost "votes" increases at 1.25x the rate of organic votes
|
|
||||||
<ul>
|
|
||||||
<li>i.e. boost votes fall out of ranking faster</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>boost can take a few minutes to show higher ranking in feed</li>
|
|
||||||
<li>100% of boost goes to the territory founder and top stackers as rewards</li>
|
|
||||||
</ol>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BoostInput ({ onChange, ...props }) {
|
|
||||||
const feeButton = useFeeButton()
|
|
||||||
let merge
|
|
||||||
if (feeButton) {
|
|
||||||
({ merge } = feeButton)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
label={
|
|
||||||
<div className='d-flex align-items-center'>boost
|
|
||||||
<Info>
|
|
||||||
<BoostHelp />
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
name='boost'
|
|
||||||
onChange={(_, e) => {
|
|
||||||
merge?.({
|
|
||||||
boost: {
|
|
||||||
term: `+ ${e.target.value}`,
|
|
||||||
label: 'boost',
|
|
||||||
op: '+',
|
|
||||||
modifier: cost => cost + Number(e.target.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onChange && onChange(_, e)
|
|
||||||
}}
|
|
||||||
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
|
|
||||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// act means we are adding to existing boost
|
|
||||||
export function BoostItemInput ({ item, sub, act = false, ...props }) {
|
|
||||||
const [boost, setBoost] = useState(Number(item?.boost) + (act ? BOOST_MULT : 0))
|
|
||||||
|
|
||||||
const [getBoostPosition, { data }] = useLazyQuery(gql`
|
|
||||||
query BoostPosition($id: ID, $boost: Int) {
|
|
||||||
boostPosition(sub: "${item?.subName || sub?.name}", id: $id, boost: $boost) {
|
|
||||||
home
|
|
||||||
sub
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{ fetchPolicy: 'cache-and-network' })
|
|
||||||
|
|
||||||
const getPositionDebounce = useDebounceCallback((...args) => getBoostPosition(...args), 1000, [getBoostPosition])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (boost) {
|
|
||||||
getPositionDebounce({ variables: { boost: Number(boost), id: item?.id } })
|
|
||||||
}
|
|
||||||
}, [boost, item?.id])
|
|
||||||
|
|
||||||
const boostMessage = useMemo(() => {
|
|
||||||
if (!item?.parentId) {
|
|
||||||
if (data?.boostPosition?.home || data?.boostPosition?.sub) {
|
|
||||||
const boostPinning = []
|
|
||||||
if (data?.boostPosition?.home) {
|
|
||||||
boostPinning.push('homepage')
|
|
||||||
}
|
|
||||||
if (data?.boostPosition?.sub) {
|
|
||||||
boostPinning.push(`~${item?.subName || sub?.name}`)
|
|
||||||
}
|
|
||||||
return `pins to the top of ${boostPinning.join(' and ')}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (boost >= 0 && boost % BOOST_MULT === 0) {
|
|
||||||
return `${act ? 'brings to' : 'equivalent to'} ${numWithUnits(boost / BOOST_MULT, { unitPlural: 'zapvotes', unitSingular: 'zapvote' })}`
|
|
||||||
}
|
|
||||||
return 'ranks posts higher based on the amount'
|
|
||||||
}, [boost, data?.boostPosition?.home, data?.boostPosition?.sub, item?.subName, sub?.name])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BoostInput
|
|
||||||
hint={<span className='text-muted'>{boostMessage}</span>}
|
|
||||||
onChange={(_, e) => {
|
|
||||||
if (e.target.value >= 0) {
|
|
||||||
setBoost(Number(e.target.value) + (act ? Number(item?.boost) : 0))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdvPostForm ({ children, item, sub, storageKeyPrefix }) {
|
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
|
const { merge } = useFeeButton()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [itemType, setItemType] = useState()
|
const [itemType, setItemType] = useState()
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
@ -222,7 +111,39 @@ export default function AdvPostForm ({ children, item, sub, storageKeyPrefix })
|
|||||||
body={
|
body={
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
<BoostItemInput item={item} sub={sub} />
|
<Input
|
||||||
|
label={
|
||||||
|
<div className='d-flex align-items-center'>boost
|
||||||
|
<Info>
|
||||||
|
<ol>
|
||||||
|
<li>Boost ranks posts higher temporarily based on the amount</li>
|
||||||
|
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
|
||||||
|
<li>Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to one trusted upvote
|
||||||
|
<ul>
|
||||||
|
<li>e.g. {numWithUnits(BOOST_MULT * 5, { abbreviate: false })} is like 5 votes</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>The decay of boost "votes" increases at 1.25x the rate of organic votes
|
||||||
|
<ul>
|
||||||
|
<li>i.e. boost votes fall out of ranking faster</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>100% of sats from boost are given back to top stackers as rewards</li>
|
||||||
|
</ol>
|
||||||
|
</Info>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
name='boost'
|
||||||
|
onChange={(_, e) => merge({
|
||||||
|
boost: {
|
||||||
|
term: `+ ${e.target.value}`,
|
||||||
|
label: 'boost',
|
||||||
|
modifier: cost => cost + Number(e.target.value)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
|
||||||
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
|
/>
|
||||||
<VariableInput
|
<VariableInput
|
||||||
label='forward sats to'
|
label='forward sats to'
|
||||||
name='forward'
|
name='forward'
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import { useShowModal } from './modal'
|
|
||||||
import { useToast } from './toast'
|
|
||||||
import ItemAct from './item-act'
|
|
||||||
import AccordianItem from './accordian-item'
|
|
||||||
import { useMemo } from 'react'
|
|
||||||
import getColor from '@/lib/rainbow'
|
|
||||||
import UpBolt from '@/svgs/bolt.svg'
|
|
||||||
import styles from './upvote.module.css'
|
|
||||||
import { BoostHelp } from './adv-post-form'
|
|
||||||
import { BOOST_MULT } from '@/lib/constants'
|
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
export default function Boost ({ item, className, ...props }) {
|
|
||||||
const { boost } = item
|
|
||||||
const style = useMemo(() => (boost
|
|
||||||
? {
|
|
||||||
fill: getColor(boost),
|
|
||||||
filter: `drop-shadow(0 0 6px ${getColor(boost)}90)`,
|
|
||||||
transform: 'scaleX(-1)'
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
transform: 'scaleX(-1)'
|
|
||||||
}), [boost])
|
|
||||||
return (
|
|
||||||
<Booster
|
|
||||||
item={item} As={({ ...oprops }) =>
|
|
||||||
<div className='upvoteParent'>
|
|
||||||
<div
|
|
||||||
className={styles.upvoteWrapper}
|
|
||||||
>
|
|
||||||
<UpBolt
|
|
||||||
{...props} {...oprops} style={style}
|
|
||||||
width={26}
|
|
||||||
height={26}
|
|
||||||
className={classNames(styles.upvote, className, boost && styles.voted)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Booster ({ item, As, children }) {
|
|
||||||
const toaster = useToast()
|
|
||||||
const showModal = useShowModal()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<As
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
showModal(onClose =>
|
|
||||||
<ItemAct onClose={onClose} item={item} act='BOOST' step={BOOST_MULT}>
|
|
||||||
<AccordianItem header='what is boost?' body={<BoostHelp />} />
|
|
||||||
</ItemAct>)
|
|
||||||
} catch (error) {
|
|
||||||
toaster.danger('failed to boost item')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</As>
|
|
||||||
)
|
|
||||||
}
|
|
@ -80,7 +80,7 @@ export function BountyForm ({
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} sub={sub} />
|
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} />
|
||||||
<ItemButtonBar itemId={item?.id} canDelete={false} />
|
<ItemButtonBar itemId={item?.id} canDelete={false} />
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
@ -25,7 +25,6 @@ import Skull from '@/svgs/death-skull.svg'
|
|||||||
import { commentSubTreeRootId } from '@/lib/item'
|
import { commentSubTreeRootId } from '@/lib/item'
|
||||||
import Pin from '@/svgs/pushpin-fill.svg'
|
import Pin from '@/svgs/pushpin-fill.svg'
|
||||||
import LinkToContext from './link-to-context'
|
import LinkToContext from './link-to-context'
|
||||||
import Boost from './boost-button'
|
|
||||||
|
|
||||||
function Parent ({ item, rootText }) {
|
function Parent ({ item, rootText }) {
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
@ -145,11 +144,9 @@ export default function Comment ({
|
|||||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||||
{item.outlawed && !me?.privates?.wildWestMode
|
{item.outlawed && !me?.privates?.wildWestMode
|
||||||
? <Skull className={styles.dontLike} width={24} height={24} />
|
? <Skull className={styles.dontLike} width={24} height={24} />
|
||||||
: item.mine
|
: item.meDontLikeSats > item.meSats
|
||||||
? <Boost item={item} className={styles.upvote} />
|
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
|
||||||
: item.meDontLikeSats > item.meSats
|
: pin ? <Pin width={22} height={22} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
|
||||||
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
|
|
||||||
: pin ? <Pin width={22} height={22} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
|
|
||||||
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
{item.user?.meMute && !includeParent && collapse === 'yep'
|
{item.user?.meMute && !includeParent && collapse === 'yep'
|
||||||
|
@ -79,7 +79,7 @@ export function DiscussionForm ({
|
|||||||
? <div className='text-muted fw-bold'><Countdown date={editThreshold} /></div>
|
? <div className='text-muted fw-bold'><Countdown date={editThreshold} /></div>
|
||||||
: null}
|
: null}
|
||||||
/>
|
/>
|
||||||
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} sub={sub} />
|
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} />
|
||||||
<ItemButtonBar itemId={item?.id} />
|
<ItemButtonBar itemId={item?.id} />
|
||||||
{!item &&
|
{!item &&
|
||||||
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
|
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
|
||||||
|
@ -17,12 +17,7 @@ export function DownZap ({ item, ...props }) {
|
|||||||
}
|
}
|
||||||
: undefined), [meDontLikeSats])
|
: undefined), [meDontLikeSats])
|
||||||
return (
|
return (
|
||||||
<DownZapper
|
<DownZapper item={item} As={({ ...oprops }) => <Flag {...props} {...oprops} style={style} />} />
|
||||||
item={item} As={({ ...oprops }) =>
|
|
||||||
<div className='upvoteParent'>
|
|
||||||
<Flag {...props} {...oprops} style={style} />
|
|
||||||
</div>}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +31,7 @@ function DownZapper ({ item, As, children }) {
|
|||||||
try {
|
try {
|
||||||
showModal(onClose =>
|
showModal(onClose =>
|
||||||
<ItemAct
|
<ItemAct
|
||||||
onClose={onClose} item={item} act='DONT_LIKE_THIS'
|
onClose={onClose} item={item} down
|
||||||
>
|
>
|
||||||
<AccordianItem
|
<AccordianItem
|
||||||
header='what is a downzap?' body={
|
header='what is a downzap?' body={
|
||||||
|
@ -21,7 +21,6 @@ export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me })
|
|||||||
anonCharge: {
|
anonCharge: {
|
||||||
term: `x ${ANON_FEE_MULTIPLIER}`,
|
term: `x ${ANON_FEE_MULTIPLIER}`,
|
||||||
label: 'anon mult',
|
label: 'anon mult',
|
||||||
op: '*',
|
|
||||||
modifier: (cost) => cost * ANON_FEE_MULTIPLIER
|
modifier: (cost) => cost * ANON_FEE_MULTIPLIER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,7 +28,6 @@ export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me })
|
|||||||
baseCost: {
|
baseCost: {
|
||||||
term: baseCost,
|
term: baseCost,
|
||||||
label: `${comment ? 'comment' : 'post'} cost`,
|
label: `${comment ? 'comment' : 'post'} cost`,
|
||||||
op: '_',
|
|
||||||
modifier: (cost) => cost + baseCost,
|
modifier: (cost) => cost + baseCost,
|
||||||
allowFreebies: comment
|
allowFreebies: comment
|
||||||
},
|
},
|
||||||
@ -50,12 +48,10 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const repetition = data?.itemRepetition
|
const repetition = data?.itemRepetition
|
||||||
if (!repetition) return setLine({})
|
if (!repetition) return setLine({})
|
||||||
console.log('repetition', repetition)
|
|
||||||
setLine({
|
setLine({
|
||||||
itemRepetition: {
|
itemRepetition: {
|
||||||
term: <>x 10<sup>{repetition}</sup></>,
|
term: <>x 10<sup>{repetition}</sup></>,
|
||||||
label: <>{repetition} {parentId ? 'repeat or self replies' : 'posts'} in 10m</>,
|
label: <>{repetition} {parentId ? 'repeat or self replies' : 'posts'} in 10m</>,
|
||||||
op: '*',
|
|
||||||
modifier: (cost) => cost * Math.pow(10, repetition)
|
modifier: (cost) => cost * Math.pow(10, repetition)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -65,35 +61,6 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortHelper (a, b) {
|
|
||||||
if (a.op === '_') {
|
|
||||||
return -1
|
|
||||||
} else if (b.op === '_') {
|
|
||||||
return 1
|
|
||||||
} else if (a.op === '*' || a.op === '/') {
|
|
||||||
if (b.op === '*' || b.op === '/') {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
// a is higher precedence
|
|
||||||
return -1
|
|
||||||
} else {
|
|
||||||
if (b.op === '*' || b.op === '/') {
|
|
||||||
// b is higher precedence
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// postive first
|
|
||||||
if (a.op === '+' && b.op === '-') {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a.op === '-' && b.op === '+') {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// both are + or -
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
||||||
const [lineItems, setLineItems] = useState({})
|
const [lineItems, setLineItems] = useState({})
|
||||||
const [disabled, setDisabled] = useState(false)
|
const [disabled, setDisabled] = useState(false)
|
||||||
@ -110,7 +77,7 @@ export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = ()
|
|||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
const lines = { ...baseLineItems, ...lineItems, ...remoteLineItems }
|
const lines = { ...baseLineItems, ...lineItems, ...remoteLineItems }
|
||||||
const total = Object.values(lines).sort(sortHelper).reduce((acc, { modifier }) => modifier(acc), 0)
|
const total = Object.values(lines).reduce((acc, { modifier }) => modifier(acc), 0)
|
||||||
// freebies: there's only a base cost and we don't have enough sats
|
// freebies: there's only a base cost and we don't have enough sats
|
||||||
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total && !me?.privates?.disableFreebies
|
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total && !me?.privates?.disableFreebies
|
||||||
return {
|
return {
|
||||||
@ -178,7 +145,7 @@ function Receipt ({ lines, total }) {
|
|||||||
return (
|
return (
|
||||||
<Table className={styles.receipt} borderless size='sm'>
|
<Table className={styles.receipt} borderless size='sm'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(lines).sort(([, a], [, b]) => sortHelper(a, b)).map(([key, { term, label, omit }]) => (
|
{Object.entries(lines).map(([key, { term, label, omit }]) => (
|
||||||
!omit &&
|
!omit &&
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
<td>{term}</td>
|
<td>{term}</td>
|
||||||
|
@ -42,7 +42,7 @@ export class SessionRequiredError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SubmitButton ({
|
export function SubmitButton ({
|
||||||
children, variant, valueName = 'submit', value, onClick, disabled, appendText, submittingText,
|
children, variant, value, onClick, disabled, appendText, submittingText,
|
||||||
className, ...props
|
className, ...props
|
||||||
}) {
|
}) {
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
@ -58,7 +58,7 @@ export function SubmitButton ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={value
|
onClick={value
|
||||||
? e => {
|
? e => {
|
||||||
formik.setFieldValue(valueName, value)
|
formik.setFieldValue('submit', value)
|
||||||
onClick && onClick(e)
|
onClick && onClick(e)
|
||||||
}
|
}
|
||||||
: onClick}
|
: onClick}
|
||||||
@ -141,7 +141,6 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
uploadFees: {
|
uploadFees: {
|
||||||
term: `+ ${numWithUnits(uploadFees.totalFees, { abbreviate: false })}`,
|
term: `+ ${numWithUnits(uploadFees.totalFees, { abbreviate: false })}`,
|
||||||
label: 'upload fee',
|
label: 'upload fee',
|
||||||
op: '+',
|
|
||||||
modifier: cost => cost + uploadFees.totalFees,
|
modifier: cost => cost + uploadFees.totalFees,
|
||||||
omit: !uploadFees.totalFees
|
omit: !uploadFees.totalFees
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'
|
|||||||
import { Form, Input, SubmitButton } from './form'
|
import { Form, Input, SubmitButton } from './form'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import UpBolt from '@/svgs/bolt.svg'
|
import UpBolt from '@/svgs/bolt.svg'
|
||||||
import { amountSchema, boostSchema } from '@/lib/validate'
|
import { amountSchema } from '@/lib/validate'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { useLightning } from './lightning'
|
import { useLightning } from './lightning'
|
||||||
import { nextTip, defaultTipIncludingRandom } from './upvote'
|
import { nextTip, defaultTipIncludingRandom } from './upvote'
|
||||||
@ -12,7 +12,6 @@ import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
|
|||||||
import { usePaidMutation } from './use-paid-mutation'
|
import { usePaidMutation } from './use-paid-mutation'
|
||||||
import { ACT_MUTATION } from '@/fragments/paidAction'
|
import { ACT_MUTATION } from '@/fragments/paidAction'
|
||||||
import { meAnonSats } from '@/lib/apollo'
|
import { meAnonSats } from '@/lib/apollo'
|
||||||
import { BoostItemInput } from './adv-post-form'
|
|
||||||
|
|
||||||
const defaultTips = [100, 1000, 10_000, 100_000]
|
const defaultTips = [100, 1000, 10_000, 100_000]
|
||||||
|
|
||||||
@ -54,39 +53,7 @@ const setItemMeAnonSats = ({ id, amount }) => {
|
|||||||
window.localStorage.setItem(storageKey, existingAmount + amount)
|
window.localStorage.setItem(storageKey, existingAmount + amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BoostForm ({ step, onSubmit, children, item, oValue, inputRef, act = 'BOOST' }) {
|
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
initial={{
|
|
||||||
amount: step
|
|
||||||
}}
|
|
||||||
schema={boostSchema}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<BoostItemInput
|
|
||||||
label='add boost'
|
|
||||||
act
|
|
||||||
name='amount'
|
|
||||||
type='number'
|
|
||||||
innerRef={inputRef}
|
|
||||||
overrideValue={oValue}
|
|
||||||
sub={item.sub}
|
|
||||||
step={step}
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
item={item}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
<div className='d-flex mt-3'>
|
|
||||||
<SubmitButton variant='success' className='ms-auto mt-1 px-4' value={act}>
|
|
||||||
boost
|
|
||||||
</SubmitButton>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ItemAct ({ onClose, item, act = 'TIP', step, children, abortSignal }) {
|
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
@ -95,7 +62,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
|
|||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}, [onClose, item.id])
|
}, [onClose, item.id])
|
||||||
|
|
||||||
const actor = useAct()
|
const act = useAct()
|
||||||
const strike = useLightning()
|
const strike = useLightning()
|
||||||
|
|
||||||
const onSubmit = useCallback(async ({ amount }) => {
|
const onSubmit = useCallback(async ({ amount }) => {
|
||||||
@ -109,18 +76,18 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { error } = await actor({
|
const { error } = await act({
|
||||||
variables: {
|
variables: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
sats: Number(amount),
|
sats: Number(amount),
|
||||||
act
|
act: down ? 'DONT_LIKE_THIS' : 'TIP'
|
||||||
},
|
},
|
||||||
optimisticResponse: me
|
optimisticResponse: me
|
||||||
? {
|
? {
|
||||||
act: {
|
act: {
|
||||||
__typename: 'ItemActPaidAction',
|
__typename: 'ItemActPaidAction',
|
||||||
result: {
|
result: {
|
||||||
id: item.id, sats: Number(amount), act, path: item.path
|
id: item.id, sats: Number(amount), act: down ? 'DONT_LIKE_THIS' : 'TIP', path: item.path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,40 +101,36 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
|
|||||||
})
|
})
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
addCustomTip(Number(amount))
|
addCustomTip(Number(amount))
|
||||||
}, [me, actor, act, item.id, onClose, abortSignal, strike])
|
}, [me, act, down, item.id, onClose, abortSignal, strike])
|
||||||
|
|
||||||
return act === 'BOOST'
|
return (
|
||||||
? <BoostForm step={step} onSubmit={onSubmit} item={item} oValue={oValue} inputRef={inputRef} act={act}>{children}</BoostForm>
|
<Form
|
||||||
: (
|
initial={{
|
||||||
<Form
|
amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0],
|
||||||
initial={{
|
default: false
|
||||||
amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0]
|
}}
|
||||||
}}
|
schema={amountSchema}
|
||||||
schema={amountSchema}
|
onSubmit={onSubmit}
|
||||||
onSubmit={onSubmit}
|
>
|
||||||
>
|
<Input
|
||||||
<Input
|
label='amount'
|
||||||
label='amount'
|
name='amount'
|
||||||
name='amount'
|
type='number'
|
||||||
type='number'
|
innerRef={inputRef}
|
||||||
innerRef={inputRef}
|
overrideValue={oValue}
|
||||||
overrideValue={oValue}
|
required
|
||||||
step={step}
|
autoFocus
|
||||||
required
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
autoFocus
|
/>
|
||||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
<div>
|
||||||
/>
|
<Tips setOValue={setOValue} />
|
||||||
|
</div>
|
||||||
<div>
|
{children}
|
||||||
<Tips setOValue={setOValue} />
|
<div className='d-flex mt-3'>
|
||||||
</div>
|
<SubmitButton variant={down ? 'danger' : 'success'} className='ms-auto mt-1 px-4' value='TIP'>{down && 'down'}zap</SubmitButton>
|
||||||
{children}
|
</div>
|
||||||
<div className='d-flex mt-3'>
|
</Form>
|
||||||
<SubmitButton variant={act === 'DONT_LIKE_THIS' ? 'danger' : 'success'} className='ms-auto mt-1 px-4' value={act}>
|
)
|
||||||
{act === 'DONT_LIKE_THIS' ? 'downzap' : 'zap'}
|
|
||||||
</SubmitButton>
|
|
||||||
</div>
|
|
||||||
</Form>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifyActCache (cache, { result, invoice }) {
|
function modifyActCache (cache, { result, invoice }) {
|
||||||
@ -193,12 +156,6 @@ function modifyActCache (cache, { result, invoice }) {
|
|||||||
return existingSats + sats
|
return existingSats + sats
|
||||||
}
|
}
|
||||||
return existingSats
|
return existingSats
|
||||||
},
|
|
||||||
boost: (existingBoost = 0) => {
|
|
||||||
if (act === 'BOOST') {
|
|
||||||
return existingBoost + sats
|
|
||||||
}
|
|
||||||
return existingBoost
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { string } from 'yup'
|
import { string } from 'yup'
|
||||||
import Toc from './table-of-contents'
|
import Toc from './table-of-contents'
|
||||||
|
import Badge from 'react-bootstrap/Badge'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import Image from 'react-bootstrap/Image'
|
import Image from 'react-bootstrap/Image'
|
||||||
import { SearchTitle } from './item'
|
import { SearchTitle } from './item'
|
||||||
@ -10,8 +11,6 @@ import EmailIcon from '@/svgs/mail-open-line.svg'
|
|||||||
import Share from './share'
|
import Share from './share'
|
||||||
import Hat from './hat'
|
import Hat from './hat'
|
||||||
import { MEDIA_URL } from '@/lib/constants'
|
import { MEDIA_URL } from '@/lib/constants'
|
||||||
import { abbrNum } from '@/lib/format'
|
|
||||||
import { Badge } from 'react-bootstrap'
|
|
||||||
|
|
||||||
export default function ItemJob ({ item, toc, rank, children }) {
|
export default function ItemJob ({ item, toc, rank, children }) {
|
||||||
const isEmail = string().email().isValidSync(item.url)
|
const isEmail = string().email().isValidSync(item.url)
|
||||||
@ -51,7 +50,6 @@ export default function ItemJob ({ item, toc, rank, children }) {
|
|||||||
</>}
|
</>}
|
||||||
<wbr />
|
<wbr />
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
{item.boost > 0 && <span>{abbrNum(item.boost)} boost \ </span>}
|
|
||||||
<span>
|
<span>
|
||||||
<Link href={`/${item.user.name}`} className='d-inline-flex align-items-center'>
|
<Link href={`/${item.user.name}`} className='d-inline-flex align-items-center'>
|
||||||
@{item.user.name}<Hat className='ms-1 fill-grey' user={item.user} height={12} width={12} />
|
@{item.user.name}<Hat className='ms-1 fill-grey' user={item.user} height={12} width={12} />
|
||||||
@ -61,21 +59,17 @@ export default function ItemJob ({ item, toc, rank, children }) {
|
|||||||
{timeSince(new Date(item.createdAt))}
|
{timeSince(new Date(item.createdAt))}
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
{item.subName &&
|
{item.mine &&
|
||||||
<Link href={`/~${item.subName}`}>
|
|
||||||
{' '}<Badge className={styles.newComment} bg={null}>{item.subName}</Badge>
|
|
||||||
</Link>}
|
|
||||||
{item.status === 'STOPPED' &&
|
|
||||||
<>{' '}<Badge bg='info' className={styles.badge}>stopped</Badge></>}
|
|
||||||
{item.mine && !item.deletedAt &&
|
|
||||||
(
|
(
|
||||||
<>
|
<>
|
||||||
<wbr />
|
<wbr />
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<Link href={`/items/${item.id}/edit`} className='text-reset fw-bold'>
|
<Link href={`/items/${item.id}/edit`} className='text-reset'>
|
||||||
edit
|
edit
|
||||||
</Link>
|
</Link>
|
||||||
|
{item.status !== 'ACTIVE' && <span className='ms-1 fw-bold text-boost'> {item.status}</span>}
|
||||||
</>)}
|
</>)}
|
||||||
|
{item.maxBid > 0 && item.status === 'ACTIVE' && <Badge className={`${styles.newComment} ms-1`}>PROMOTED</Badge>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{toc &&
|
{toc &&
|
||||||
|
@ -24,7 +24,6 @@ import removeMd from 'remove-markdown'
|
|||||||
import { decodeProxyUrl, IMGPROXY_URL_REGEXP, parseInternalLinks } from '@/lib/url'
|
import { decodeProxyUrl, IMGPROXY_URL_REGEXP, parseInternalLinks } from '@/lib/url'
|
||||||
import ItemPopover from './item-popover'
|
import ItemPopover from './item-popover'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import Boost from './boost-button'
|
|
||||||
|
|
||||||
function onItemClick (e, router, item) {
|
function onItemClick (e, router, item) {
|
||||||
const viewedAt = commentsViewedAt(item)
|
const viewedAt = commentsViewedAt(item)
|
||||||
@ -106,13 +105,11 @@ export default function Item ({
|
|||||||
<div className={classNames(styles.item, itemClassName)}>
|
<div className={classNames(styles.item, itemClassName)}>
|
||||||
{item.position && (pinnable || !item.subName)
|
{item.position && (pinnable || !item.subName)
|
||||||
? <Pin width={24} height={24} className={styles.pin} />
|
? <Pin width={24} height={24} className={styles.pin} />
|
||||||
: item.mine
|
: item.meDontLikeSats > item.meSats
|
||||||
? <Boost item={item} className={styles.upvote} />
|
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
|
||||||
: item.meDontLikeSats > item.meSats
|
: Number(item.user?.id) === USER_ID.ad
|
||||||
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
|
? <AdIcon width={24} height={24} className={styles.ad} />
|
||||||
: Number(item.user?.id) === USER_ID.ad
|
: <UpVote item={item} className={styles.upvote} />}
|
||||||
? <AdIcon width={24} height={24} className={styles.ad} />
|
|
||||||
: <UpVote item={item} className={styles.upvote} />}
|
|
||||||
<div className={styles.hunk}>
|
<div className={styles.hunk}>
|
||||||
<div className={`${styles.main} flex-wrap`}>
|
<div className={`${styles.main} flex-wrap`}>
|
||||||
<Link
|
<Link
|
||||||
|
@ -46,12 +46,6 @@ a.title:visited {
|
|||||||
margin-bottom: .15rem;
|
margin-bottom: .15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: -1px;
|
|
||||||
margin-left: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.newComment {
|
.newComment {
|
||||||
color: var(--theme-grey) !important;
|
color: var(--theme-grey) !important;
|
||||||
background: var(--theme-clickToContextColor) !important;
|
background: var(--theme-clickToContextColor) !important;
|
||||||
|
@ -15,7 +15,7 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
|
|||||||
const Foooter = Footer || MoreFooter
|
const Foooter = Footer || MoreFooter
|
||||||
const dat = useData(data, ssrData)
|
const dat = useData(data, ssrData)
|
||||||
|
|
||||||
const { items, pins, ad, cursor } = useMemo(() => {
|
const { items, pins, cursor } = useMemo(() => {
|
||||||
if (!dat) return {}
|
if (!dat) return {}
|
||||||
if (destructureData) {
|
if (destructureData) {
|
||||||
return destructureData(dat)
|
return destructureData(dat)
|
||||||
@ -50,7 +50,6 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{ad && <ListItem item={ad} ad />}
|
|
||||||
{itemsWithPins.filter(filter).map((item, i) => (
|
{itemsWithPins.filter(filter).map((item, i) => (
|
||||||
<ListItem key={`${item.id}-${i + 1}`} item={item} rank={rank && i + 1} itemClassName={variables.includeComments ? 'py-2' : ''} pinnable={isHome ? false : pins?.length > 0} />
|
<ListItem key={`${item.id}-${i + 1}`} item={item} rank={rank && i + 1} itemClassName={variables.includeComments ? 'py-2' : ''} pinnable={isHome ? false : pins?.length > 0} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,41 +1,46 @@
|
|||||||
import { Checkbox, Form, Input, MarkdownInput, SubmitButton } from './form'
|
import { Checkbox, Form, Input, MarkdownInput } from './form'
|
||||||
import Row from 'react-bootstrap/Row'
|
import Row from 'react-bootstrap/Row'
|
||||||
import Col from 'react-bootstrap/Col'
|
import Col from 'react-bootstrap/Col'
|
||||||
|
import InputGroup from 'react-bootstrap/InputGroup'
|
||||||
import Image from 'react-bootstrap/Image'
|
import Image from 'react-bootstrap/Image'
|
||||||
|
import BootstrapForm from 'react-bootstrap/Form'
|
||||||
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
|
import AccordianItem from './accordian-item'
|
||||||
import styles from '@/styles/post.module.css'
|
import styles from '@/styles/post.module.css'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
import { useLazyQuery, gql } from '@apollo/client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePrice } from './price'
|
||||||
import Avatar from './avatar'
|
import Avatar from './avatar'
|
||||||
import { jobSchema } from '@/lib/validate'
|
import { jobSchema } from '@/lib/validate'
|
||||||
import { BOOST_MIN, BOOST_MULT, MAX_TITLE_LENGTH, MEDIA_URL } from '@/lib/constants'
|
import { MAX_TITLE_LENGTH, MEDIA_URL } from '@/lib/constants'
|
||||||
|
import { ItemButtonBar } from './post'
|
||||||
|
import { useFormikContext } from 'formik'
|
||||||
import { UPSERT_JOB } from '@/fragments/paidAction'
|
import { UPSERT_JOB } from '@/fragments/paidAction'
|
||||||
import useItemSubmit from './use-item-submit'
|
import useItemSubmit from './use-item-submit'
|
||||||
import { BoostInput } from './adv-post-form'
|
|
||||||
import { numWithUnits, giveOrdinalSuffix } from '@/lib/format'
|
function satsMin2Mo (minute) {
|
||||||
import useDebounceCallback from './use-debounce-callback'
|
return minute * 30 * 24 * 60
|
||||||
import FeeButton from './fee-button'
|
}
|
||||||
import CancelButton from './cancel-button'
|
|
||||||
|
function PriceHint ({ monthly }) {
|
||||||
|
const { price, fiatSymbol } = usePrice()
|
||||||
|
|
||||||
|
if (!price || !monthly) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const fixed = (n, f) => Number.parseFloat(n).toFixed(f)
|
||||||
|
const fiat = fixed((price / 100000000) * monthly, 0)
|
||||||
|
|
||||||
|
return <span className='text-muted'>{monthly} sats/mo which is {fiatSymbol}{fiat}/mo</span>
|
||||||
|
}
|
||||||
|
|
||||||
// need to recent list items
|
// need to recent list items
|
||||||
export default function JobForm ({ item, sub }) {
|
export default function JobForm ({ item, sub }) {
|
||||||
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
const storageKeyPrefix = item ? undefined : `${sub.name}-job`
|
||||||
const [logoId, setLogoId] = useState(item?.uploadId)
|
const [logoId, setLogoId] = useState(item?.uploadId)
|
||||||
|
|
||||||
const [getAuctionPosition, { data }] = useLazyQuery(gql`
|
|
||||||
query AuctionPosition($id: ID, $boost: Int) {
|
|
||||||
auctionPosition(sub: "${item?.subName || sub?.name}", id: $id, boost: $boost)
|
|
||||||
}`,
|
|
||||||
{ fetchPolicy: 'cache-and-network' })
|
|
||||||
|
|
||||||
const getPositionDebounce = useDebounceCallback((...args) => getAuctionPosition(...args), 1000, [getAuctionPosition])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (item?.boost) {
|
|
||||||
getPositionDebounce({ variables: { boost: item.boost, id: item.id } })
|
|
||||||
}
|
|
||||||
}, [item?.boost])
|
|
||||||
|
|
||||||
const extraValues = logoId ? { logo: Number(logoId) } : {}
|
const extraValues = logoId ? { logo: Number(logoId) } : {}
|
||||||
const onSubmit = useItemSubmit(UPSERT_JOB, { item, sub, extraValues })
|
const onSubmit = useItemSubmit(UPSERT_JOB, { item, sub, extraValues })
|
||||||
|
|
||||||
@ -48,13 +53,13 @@ export default function JobForm ({ item, sub }) {
|
|||||||
company: item?.company || '',
|
company: item?.company || '',
|
||||||
location: item?.location || '',
|
location: item?.location || '',
|
||||||
remote: item?.remote || false,
|
remote: item?.remote || false,
|
||||||
boost: item?.boost || '',
|
|
||||||
text: item?.text || '',
|
text: item?.text || '',
|
||||||
url: item?.url || '',
|
url: item?.url || '',
|
||||||
|
maxBid: item?.maxBid || 0,
|
||||||
stop: false,
|
stop: false,
|
||||||
start: false
|
start: false
|
||||||
}}
|
}}
|
||||||
schema={jobSchema({ existingBoost: item?.boost })}
|
schema={jobSchema}
|
||||||
storageKeyPrefix={storageKeyPrefix}
|
storageKeyPrefix={storageKeyPrefix}
|
||||||
requireSession
|
requireSession
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
@ -110,46 +115,130 @@ export default function JobForm ({ item, sub }) {
|
|||||||
required
|
required
|
||||||
clear
|
clear
|
||||||
/>
|
/>
|
||||||
<BoostInput
|
<PromoteJob item={item} sub={sub} />
|
||||||
label={
|
{item && <StatusControl item={item} />}
|
||||||
<div className='d-flex align-items-center'>boost
|
<ItemButtonBar itemId={item?.id} canDelete={false} />
|
||||||
<Info>
|
|
||||||
<ol className='line-height-md'>
|
|
||||||
<li>Boost ranks jobs higher based on the amount</li>
|
|
||||||
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
|
|
||||||
<li>Boost must be divisible by {numWithUnits(BOOST_MULT, { abbreviate: false })}</li>
|
|
||||||
<li>100% of boost goes to the territory founder and top stackers as rewards</li>
|
|
||||||
</ol>
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
hint={<span className='text-muted'>{data?.auctionPosition ? `your job will rank ${giveOrdinalSuffix(data.auctionPosition)}` : 'higher boost ranks your job higher'}</span>}
|
|
||||||
onChange={(_, e) => getPositionDebounce({ variables: { boost: Number(e.target.value), id: item?.id } })}
|
|
||||||
/>
|
|
||||||
<JobButtonBar itemId={item?.id} />
|
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JobButtonBar ({
|
const FormStatus = {
|
||||||
itemId, disable, className, children, handleStop, onCancel, hasCancel = true,
|
DIRTY: 'dirty',
|
||||||
createText = 'post', editText = 'save', stopText = 'remove'
|
ERROR: 'error'
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function PromoteJob ({ item, sub }) {
|
||||||
|
const formik = useFormikContext()
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
const [monthly, setMonthly] = useState(satsMin2Mo(item?.maxBid || 0))
|
||||||
|
const [getAuctionPosition, { data }] = useLazyQuery(gql`
|
||||||
|
query AuctionPosition($id: ID, $bid: Int!) {
|
||||||
|
auctionPosition(sub: "${item?.subName || sub?.name}", id: $id, bid: $bid)
|
||||||
|
}`,
|
||||||
|
{ fetchPolicy: 'cache-and-network' })
|
||||||
|
const position = data?.auctionPosition
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialMaxBid = Number(item?.maxBid) || 0
|
||||||
|
getAuctionPosition({ variables: { id: item?.id, bid: initialMaxBid } })
|
||||||
|
setMonthly(satsMin2Mo(initialMaxBid))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formik?.values?.maxBid !== 0) {
|
||||||
|
setShow(FormStatus.DIRTY)
|
||||||
|
}
|
||||||
|
}, [formik?.values])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hasMaxBidError = !!formik?.errors?.maxBid
|
||||||
|
// if it's open we don't want to collapse on submit
|
||||||
|
setShow(show => show || (hasMaxBidError && formik?.isSubmitting && FormStatus.ERROR))
|
||||||
|
}, [formik?.isSubmitting])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`mt-3 ${className}`}>
|
<AccordianItem
|
||||||
<div className='d-flex justify-content-between'>
|
show={show}
|
||||||
{itemId &&
|
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>promote</div>}
|
||||||
<SubmitButton valueName='status' value='STOPPED' variant='grey-medium'>{stopText}</SubmitButton>}
|
body={
|
||||||
{children}
|
<>
|
||||||
<div className='d-flex align-items-center ms-auto'>
|
<Input
|
||||||
{hasCancel && <CancelButton onClick={onCancel} />}
|
label={
|
||||||
<FeeButton
|
<div className='d-flex align-items-center'>bid
|
||||||
text={itemId ? editText : createText}
|
<Info>
|
||||||
variant='secondary'
|
<ol>
|
||||||
disabled={disable}
|
<li>The higher your bid the higher your job will rank</li>
|
||||||
|
<li>You can increase, decrease, or remove your bid at anytime</li>
|
||||||
|
<li>You can edit or stop your job at anytime</li>
|
||||||
|
<li>If you run out of sats, your job will stop being promoted until you fill your wallet again</li>
|
||||||
|
</ol>
|
||||||
|
</Info>
|
||||||
|
<small className='text-muted ms-2'>optional</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
name='maxBid'
|
||||||
|
onChange={async (formik, e) => {
|
||||||
|
if (e.target.value >= 0 && e.target.value <= 100000000) {
|
||||||
|
setMonthly(satsMin2Mo(e.target.value))
|
||||||
|
getAuctionPosition({ variables: { id: item?.id, bid: Number(e.target.value) } })
|
||||||
|
} else {
|
||||||
|
setMonthly(satsMin2Mo(0))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
append={<InputGroup.Text className='text-monospace'>sats/min</InputGroup.Text>}
|
||||||
|
hint={<PriceHint monthly={monthly} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
<><div className='fw-bold text-muted'>This bid puts your job in position: {position}</div></>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusControl ({ item }) {
|
||||||
|
let StatusComp
|
||||||
|
|
||||||
|
if (item.status !== 'STOPPED') {
|
||||||
|
StatusComp = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<AccordianItem
|
||||||
|
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>I want to stop my job</div>}
|
||||||
|
headerColor='var(--bs-danger)'
|
||||||
|
body={
|
||||||
|
<Checkbox
|
||||||
|
label={<div className='fw-bold text-danger'>stop my job</div>} name='stop' inline
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (item.status === 'STOPPED') {
|
||||||
|
StatusComp = () => {
|
||||||
|
return (
|
||||||
|
<AccordianItem
|
||||||
|
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>I want to resume my job</div>}
|
||||||
|
headerColor='var(--bs-success)'
|
||||||
|
body={
|
||||||
|
<Checkbox
|
||||||
|
label={<div className='fw-bold text-success'>resume my job</div>} name='start' inline
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='my-3 border border-3 rounded'>
|
||||||
|
<div className='p-3'>
|
||||||
|
<BootstrapForm.Label>job control</BootstrapForm.Label>
|
||||||
|
{item.status === 'NOSATS' &&
|
||||||
|
<Alert variant='warning'>your promotion ran out of sats. <Link href='/wallet?type=fund' className='text-reset text-underline'>fund your wallet</Link> or reduce bid to continue promoting your job</Alert>}
|
||||||
|
<StatusComp />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -163,7 +163,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} sub={sub}>
|
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item}>
|
||||||
<MarkdownInput
|
<MarkdownInput
|
||||||
label='context'
|
label='context'
|
||||||
name='text'
|
name='text'
|
||||||
|
@ -401,24 +401,22 @@ export function Sorts ({ sub, prefix, className }) {
|
|||||||
<Nav.Link eventKey='recent' className={styles.navLink}>recent</Nav.Link>
|
<Nav.Link eventKey='recent' className={styles.navLink}>recent</Nav.Link>
|
||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
|
<Nav.Item className={className}>
|
||||||
|
<Link href={prefix + '/random'} passHref legacyBehavior>
|
||||||
|
<Nav.Link eventKey='random' className={styles.navLink}>random</Nav.Link>
|
||||||
|
</Link>
|
||||||
|
</Nav.Item>
|
||||||
{sub !== 'jobs' &&
|
{sub !== 'jobs' &&
|
||||||
<>
|
<Nav.Item className={className}>
|
||||||
<Nav.Item className={className}>
|
<Link
|
||||||
<Link href={prefix + '/random'} passHref legacyBehavior>
|
href={{
|
||||||
<Nav.Link eventKey='random' className={styles.navLink}>random</Nav.Link>
|
pathname: '/~/top/[type]/[when]',
|
||||||
</Link>
|
query: { type: 'posts', when: 'day', sub }
|
||||||
</Nav.Item>
|
}} as={prefix + '/top/posts/day'} passHref legacyBehavior
|
||||||
<Nav.Item className={className}>
|
>
|
||||||
<Link
|
<Nav.Link eventKey='top' className={styles.navLink}>top</Nav.Link>
|
||||||
href={{
|
</Link>
|
||||||
pathname: '/~/top/[type]/[when]',
|
</Nav.Item>}
|
||||||
query: { type: 'posts', when: 'day', sub }
|
|
||||||
}} as={prefix + '/top/posts/day'} passHref legacyBehavior
|
|
||||||
>
|
|
||||||
<Nav.Link eventKey='top' className={styles.navLink}>top</Nav.Link>
|
|
||||||
</Link>
|
|
||||||
</Nav.Item>
|
|
||||||
</>}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -406,18 +406,9 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
|||||||
invoiceId = invoice.item.poll?.meInvoiceId
|
invoiceId = invoice.item.poll?.meInvoiceId
|
||||||
invoiceActionState = invoice.item.poll?.meInvoiceActionState
|
invoiceActionState = invoice.item.poll?.meInvoiceActionState
|
||||||
} else {
|
} else {
|
||||||
if (invoice.actionType === 'ZAP') {
|
actionString = `${invoice.actionType === 'ZAP'
|
||||||
if (invoice.item.root?.bounty === invoice.satsRequested && invoice.item.root.mine) {
|
? invoice.item.root?.bounty ? 'bounty payment' : 'zap'
|
||||||
actionString = 'bounty payment'
|
: 'downzap'} on ${itemType} `
|
||||||
} else {
|
|
||||||
actionString = 'zap'
|
|
||||||
}
|
|
||||||
} else if (invoice.actionType === 'DOWN_ZAP') {
|
|
||||||
actionString = 'downzap'
|
|
||||||
} else if (invoice.actionType === 'BOOST') {
|
|
||||||
actionString = 'boost'
|
|
||||||
}
|
|
||||||
actionString = `${actionString} on ${itemType} `
|
|
||||||
retry = actRetry;
|
retry = actRetry;
|
||||||
({ id: invoiceId, actionState: invoiceActionState } = invoice.itemAct.invoice)
|
({ id: invoiceId, actionState: invoiceActionState } = invoice.itemAct.invoice)
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||||||
: null}
|
: null}
|
||||||
maxLength={MAX_POLL_CHOICE_LENGTH}
|
maxLength={MAX_POLL_CHOICE_LENGTH}
|
||||||
/>
|
/>
|
||||||
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item} sub={sub}>
|
<AdvPostForm storageKeyPrefix={storageKeyPrefix} item={item}>
|
||||||
<DateTimeInput
|
<DateTimeInput
|
||||||
isClearable
|
isClearable
|
||||||
label='poll expiration'
|
label='poll expiration'
|
||||||
|
@ -187,7 +187,7 @@ export default function Post ({ sub }) {
|
|||||||
export function ItemButtonBar ({
|
export function ItemButtonBar ({
|
||||||
itemId, canDelete = true, disable,
|
itemId, canDelete = true, disable,
|
||||||
className, children, onDelete, onCancel, hasCancel = true,
|
className, children, onDelete, onCancel, hasCancel = true,
|
||||||
createText = 'post', editText = 'save', deleteText = 'delete'
|
createText = 'post', editText = 'save'
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ export function ItemButtonBar ({
|
|||||||
itemId={itemId}
|
itemId={itemId}
|
||||||
onDelete={onDelete || (() => router.push(`/items/${itemId}`))}
|
onDelete={onDelete || (() => router.push(`/items/${itemId}`))}
|
||||||
>
|
>
|
||||||
<Button variant='grey-medium'>{deleteText}</Button>
|
<Button variant='grey-medium'>delete</Button>
|
||||||
</Delete>}
|
</Delete>}
|
||||||
{children}
|
{children}
|
||||||
<div className='d-flex align-items-center ms-auto'>
|
<div className='d-flex align-items-center ms-auto'>
|
||||||
|
@ -77,7 +77,6 @@ export default function TerritoryForm ({ sub }) {
|
|||||||
lines.paid = {
|
lines.paid = {
|
||||||
term: `- ${abbrNum(alreadyBilled)} sats`,
|
term: `- ${abbrNum(alreadyBilled)} sats`,
|
||||||
label: 'already paid',
|
label: 'already paid',
|
||||||
op: '-',
|
|
||||||
modifier: cost => cost - alreadyBilled
|
modifier: cost => cost - alreadyBilled
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
|
@ -12,7 +12,6 @@ import Popover from 'react-bootstrap/Popover'
|
|||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
import { Dropdown } from 'react-bootstrap'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
import classNames from 'classnames'
|
|
||||||
|
|
||||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
@ -227,14 +226,7 @@ export default function UpVote ({ item, className }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fillColor = meSats && (hover || pending ? nextColor : color)
|
const fillColor = hover || pending ? nextColor : color
|
||||||
|
|
||||||
const style = useMemo(() => (fillColor
|
|
||||||
? {
|
|
||||||
fill: fillColor,
|
|
||||||
filter: `drop-shadow(0 0 6px ${fillColor}90)`
|
|
||||||
}
|
|
||||||
: undefined), [fillColor])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className='upvoteParent'>
|
<div ref={ref} className='upvoteParent'>
|
||||||
@ -244,7 +236,7 @@ export default function UpVote ({ item, className }) {
|
|||||||
>
|
>
|
||||||
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
|
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
|
||||||
<div
|
<div
|
||||||
className={classNames(disabled && styles.noSelfTips, styles.upvoteWrapper)}
|
className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`}
|
||||||
>
|
>
|
||||||
<UpBolt
|
<UpBolt
|
||||||
onPointerEnter={() => setHover(true)}
|
onPointerEnter={() => setHover(true)}
|
||||||
@ -252,12 +244,19 @@ export default function UpVote ({ item, className }) {
|
|||||||
onTouchEnd={() => setHover(false)}
|
onTouchEnd={() => setHover(false)}
|
||||||
width={26}
|
width={26}
|
||||||
height={26}
|
height={26}
|
||||||
className={classNames(styles.upvote,
|
className={
|
||||||
className,
|
`${styles.upvote}
|
||||||
disabled && styles.noSelfTips,
|
${className || ''}
|
||||||
meSats && styles.voted,
|
${disabled ? styles.noSelfTips : ''}
|
||||||
pending && styles.pending)}
|
${meSats ? styles.voted : ''}
|
||||||
style={style}
|
${pending ? styles.pending : ''}`
|
||||||
|
}
|
||||||
|
style={meSats || hover || pending
|
||||||
|
? {
|
||||||
|
fill: fillColor,
|
||||||
|
filter: `drop-shadow(0 0 6px ${fillColor}90)`
|
||||||
|
}
|
||||||
|
: undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.noSelfTips {
|
.noSelfTips {
|
||||||
transform: scaleX(-1);
|
fill: transparent !important;
|
||||||
|
filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upvoteWrapper:not(.noSelfTips):hover {
|
.upvoteWrapper:not(.noSelfTips):hover {
|
||||||
|
@ -24,7 +24,7 @@ export default function useItemSubmit (mutation,
|
|||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async ({ boost, crosspost, title, options, bounty, status, ...values }, { resetForm }) => {
|
async ({ boost, crosspost, title, options, bounty, maxBid, start, stop, ...values }, { resetForm }) => {
|
||||||
if (options) {
|
if (options) {
|
||||||
// remove existing poll options since else they will be appended as duplicates
|
// remove existing poll options since else they will be appended as duplicates
|
||||||
options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0)
|
options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0)
|
||||||
@ -44,9 +44,10 @@ export default function useItemSubmit (mutation,
|
|||||||
variables: {
|
variables: {
|
||||||
id: item?.id,
|
id: item?.id,
|
||||||
sub: item?.subName || sub?.name,
|
sub: item?.subName || sub?.name,
|
||||||
boost: boost ? Number(boost) : item?.boost ? Number(item.boost) : undefined,
|
boost: boost ? Number(boost) : undefined,
|
||||||
bounty: bounty ? Number(bounty) : undefined,
|
bounty: bounty ? Number(bounty) : undefined,
|
||||||
status: status === 'STOPPED' ? 'STOPPED' : 'ACTIVE',
|
maxBid: (maxBid || Number(maxBid) === 0) ? Number(maxBid) : undefined,
|
||||||
|
status: start ? 'ACTIVE' : stop ? 'STOPPED' : undefined,
|
||||||
title: title?.trim(),
|
title: title?.trim(),
|
||||||
options,
|
options,
|
||||||
...values,
|
...values,
|
||||||
|
@ -46,14 +46,15 @@ export const ITEM_FIELDS = gql`
|
|||||||
ncomments
|
ncomments
|
||||||
commentSats
|
commentSats
|
||||||
lastCommentAt
|
lastCommentAt
|
||||||
|
maxBid
|
||||||
isJob
|
isJob
|
||||||
status
|
|
||||||
company
|
company
|
||||||
location
|
location
|
||||||
remote
|
remote
|
||||||
subName
|
subName
|
||||||
pollCost
|
pollCost
|
||||||
pollExpiresAt
|
pollExpiresAt
|
||||||
|
status
|
||||||
uploadId
|
uploadId
|
||||||
mine
|
mine
|
||||||
imgproxyUrls
|
imgproxyUrls
|
||||||
@ -78,7 +79,6 @@ export const ITEM_FULL_FIELDS = gql`
|
|||||||
bounty
|
bounty
|
||||||
bountyPaidTo
|
bountyPaidTo
|
||||||
subName
|
subName
|
||||||
mine
|
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
@ -133,11 +133,11 @@ export const UPSERT_DISCUSSION = gql`
|
|||||||
export const UPSERT_JOB = gql`
|
export const UPSERT_JOB = gql`
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertJob($sub: String!, $id: ID, $title: String!, $company: String!,
|
mutation upsertJob($sub: String!, $id: ID, $title: String!, $company: String!,
|
||||||
$location: String, $remote: Boolean, $text: String!, $url: String!, $boost: Int,
|
$location: String, $remote: Boolean, $text: String!, $url: String!, $maxBid: Int!,
|
||||||
$status: String, $logo: Int) {
|
$status: String, $logo: Int) {
|
||||||
upsertJob(sub: $sub, id: $id, title: $title, company: $company,
|
upsertJob(sub: $sub, id: $id, title: $title, company: $company,
|
||||||
location: $location, remote: $remote, text: $text,
|
location: $location, remote: $remote, text: $text,
|
||||||
url: $url, boost: $boost, status: $status, logo: $logo) {
|
url: $url, maxBid: $maxBid, status: $status, logo: $logo) {
|
||||||
result {
|
result {
|
||||||
id
|
id
|
||||||
deleteScheduledAt
|
deleteScheduledAt
|
||||||
@ -218,8 +218,8 @@ export const CREATE_COMMENT = gql`
|
|||||||
export const UPDATE_COMMENT = gql`
|
export const UPDATE_COMMENT = gql`
|
||||||
${ITEM_PAID_ACTION_FIELDS}
|
${ITEM_PAID_ACTION_FIELDS}
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertComment($id: ID!, $text: String!, $boost: Int, ${HASH_HMAC_INPUT_1}) {
|
mutation upsertComment($id: ID!, $text: String!, ${HASH_HMAC_INPUT_1}) {
|
||||||
upsertComment(id: $id, text: $text, boost: $boost, ${HASH_HMAC_INPUT_2}) {
|
upsertComment(id: $id, text: $text, ${HASH_HMAC_INPUT_2}) {
|
||||||
...ItemPaidActionFields
|
...ItemPaidActionFields
|
||||||
...PaidActionFields
|
...PaidActionFields
|
||||||
}
|
}
|
||||||
|
@ -87,9 +87,6 @@ export const SUB_ITEMS = gql`
|
|||||||
...CommentItemExtFields @include(if: $includeComments)
|
...CommentItemExtFields @include(if: $includeComments)
|
||||||
position
|
position
|
||||||
}
|
}
|
||||||
ad {
|
|
||||||
...ItemFields
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -180,8 +180,7 @@ function getClient (uri) {
|
|||||||
return {
|
return {
|
||||||
cursor: incoming.cursor,
|
cursor: incoming.cursor,
|
||||||
items: [...(existing?.items || []), ...incoming.items],
|
items: [...(existing?.items || []), ...incoming.items],
|
||||||
pins: [...(existing?.pins || []), ...(incoming.pins || [])],
|
pins: [...(existing?.pins || []), ...(incoming.pins || [])]
|
||||||
ad: incoming?.ad || existing?.ad
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6,10 +6,10 @@ export const DEFAULT_SUBS_NO_JOBS = DEFAULT_SUBS.filter(s => s !== 'jobs')
|
|||||||
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
|
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
|
||||||
export const NOFOLLOW_LIMIT = 1000
|
export const NOFOLLOW_LIMIT = 1000
|
||||||
export const UNKNOWN_LINK_REL = 'noreferrer nofollow noopener'
|
export const UNKNOWN_LINK_REL = 'noreferrer nofollow noopener'
|
||||||
|
export const BOOST_MULT = 5000
|
||||||
|
export const BOOST_MIN = BOOST_MULT * 10
|
||||||
export const UPLOAD_SIZE_MAX = 50 * 1024 * 1024
|
export const UPLOAD_SIZE_MAX = 50 * 1024 * 1024
|
||||||
export const UPLOAD_SIZE_MAX_AVATAR = 5 * 1024 * 1024
|
export const UPLOAD_SIZE_MAX_AVATAR = 5 * 1024 * 1024
|
||||||
export const BOOST_MULT = 10000
|
|
||||||
export const BOOST_MIN = BOOST_MULT
|
|
||||||
export const IMAGE_PIXELS_MAX = 35000000
|
export const IMAGE_PIXELS_MAX = 35000000
|
||||||
// backwards compatibile with old media domain env var and precedence for docker url if set
|
// backwards compatibile with old media domain env var and precedence for docker url if set
|
||||||
export const MEDIA_URL = process.env.MEDIA_URL_DOCKER || process.env.NEXT_PUBLIC_MEDIA_URL || `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}`
|
export const MEDIA_URL = process.env.MEDIA_URL_DOCKER || process.env.NEXT_PUBLIC_MEDIA_URL || `https://${process.env.NEXT_PUBLIC_MEDIA_DOMAIN}`
|
||||||
@ -25,7 +25,7 @@ export const UPLOAD_TYPES_ALLOW = [
|
|||||||
'video/webm'
|
'video/webm'
|
||||||
]
|
]
|
||||||
export const AVATAR_TYPES_ALLOW = UPLOAD_TYPES_ALLOW.filter(t => t.startsWith('image/'))
|
export const AVATAR_TYPES_ALLOW = UPLOAD_TYPES_ALLOW.filter(t => t.startsWith('image/'))
|
||||||
export const INVOICE_ACTION_NOTIFICATION_TYPES = ['ITEM_CREATE', 'ZAP', 'DOWN_ZAP', 'POLL_VOTE', 'BOOST']
|
export const INVOICE_ACTION_NOTIFICATION_TYPES = ['ITEM_CREATE', 'ZAP', 'DOWN_ZAP', 'POLL_VOTE']
|
||||||
export const BOUNTY_MIN = 1000
|
export const BOUNTY_MIN = 1000
|
||||||
export const BOUNTY_MAX = 10000000
|
export const BOUNTY_MAX = 10000000
|
||||||
export const POST_TYPES = ['LINK', 'DISCUSSION', 'BOUNTY', 'POLL']
|
export const POST_TYPES = ['LINK', 'DISCUSSION', 'BOUNTY', 'POLL']
|
||||||
@ -91,19 +91,16 @@ export const TERRITORY_BILLING_OPTIONS = (labelPrefix) => ({
|
|||||||
monthly: {
|
monthly: {
|
||||||
term: '+ 100k',
|
term: '+ 100k',
|
||||||
label: `${labelPrefix} month`,
|
label: `${labelPrefix} month`,
|
||||||
op: '+',
|
|
||||||
modifier: cost => cost + TERRITORY_COST_MONTHLY
|
modifier: cost => cost + TERRITORY_COST_MONTHLY
|
||||||
},
|
},
|
||||||
yearly: {
|
yearly: {
|
||||||
term: '+ 1m',
|
term: '+ 1m',
|
||||||
label: `${labelPrefix} year`,
|
label: `${labelPrefix} year`,
|
||||||
op: '+',
|
|
||||||
modifier: cost => cost + TERRITORY_COST_YEARLY
|
modifier: cost => cost + TERRITORY_COST_YEARLY
|
||||||
},
|
},
|
||||||
once: {
|
once: {
|
||||||
term: '+ 3m',
|
term: '+ 3m',
|
||||||
label: 'one time',
|
label: 'one time',
|
||||||
op: '+',
|
|
||||||
modifier: cost => cost + TERRITORY_COST_ONCE
|
modifier: cost => cost + TERRITORY_COST_ONCE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -110,18 +110,3 @@ export const ensureB64 = hexOrB64Url => {
|
|||||||
|
|
||||||
throw new Error('not a valid hex or base64 url or base64 encoded string')
|
throw new Error('not a valid hex or base64 url or base64 encoded string')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function giveOrdinalSuffix (i) {
|
|
||||||
const j = i % 10
|
|
||||||
const k = i % 100
|
|
||||||
if (j === 1 && k !== 11) {
|
|
||||||
return i + 'st'
|
|
||||||
}
|
|
||||||
if (j === 2 && k !== 12) {
|
|
||||||
return i + 'nd'
|
|
||||||
}
|
|
||||||
if (j === 3 && k !== 13) {
|
|
||||||
return i + 'rd'
|
|
||||||
}
|
|
||||||
return i + 'th'
|
|
||||||
}
|
|
||||||
|
@ -10,7 +10,7 @@ export const defaultCommentSort = (pinned, bio, createdAt) => {
|
|||||||
return 'hot'
|
return 'hot'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isJob = item => item.subName !== 'jobs'
|
export const isJob = item => item.maxBid !== null && typeof item.maxBid !== 'undefined'
|
||||||
|
|
||||||
// a delete directive preceded by a non word character that isn't a backtick
|
// a delete directive preceded by a non word character that isn't a backtick
|
||||||
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
|
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
|
||||||
|
@ -550,13 +550,14 @@ export const commentSchema = object({
|
|||||||
text: textValidator(MAX_COMMENT_TEXT_LENGTH).required('required')
|
text: textValidator(MAX_COMMENT_TEXT_LENGTH).required('required')
|
||||||
})
|
})
|
||||||
|
|
||||||
export const jobSchema = args => object({
|
export const jobSchema = object({
|
||||||
title: titleValidator,
|
title: titleValidator,
|
||||||
company: string().required('required').trim(),
|
company: string().required('required').trim(),
|
||||||
text: textValidator(MAX_POST_TEXT_LENGTH).required('required'),
|
text: textValidator(MAX_POST_TEXT_LENGTH).required('required'),
|
||||||
url: string()
|
url: string()
|
||||||
.or([string().email(), string().url()], 'invalid url or email')
|
.or([string().email(), string().url()], 'invalid url or email')
|
||||||
.required('required'),
|
.required('required'),
|
||||||
|
maxBid: intValidator.min(0, 'must be at least 0').required('required'),
|
||||||
location: string().test(
|
location: string().test(
|
||||||
'no-remote',
|
'no-remote',
|
||||||
"don't write remote, just check the box",
|
"don't write remote, just check the box",
|
||||||
@ -564,8 +565,7 @@ export const jobSchema = args => object({
|
|||||||
.when('remote', {
|
.when('remote', {
|
||||||
is: (value) => !value,
|
is: (value) => !value,
|
||||||
then: schema => schema.required('required').trim()
|
then: schema => schema.required('required').trim()
|
||||||
}),
|
})
|
||||||
...advPostSchemaMembers(args)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const emailSchema = object({
|
export const emailSchema = object({
|
||||||
@ -585,26 +585,9 @@ export const amountSchema = object({
|
|||||||
amount: intValidator.required('required').positive('must be positive')
|
amount: intValidator.required('required').positive('must be positive')
|
||||||
})
|
})
|
||||||
|
|
||||||
export const boostValidator = intValidator
|
|
||||||
.min(BOOST_MULT, `must be at least ${BOOST_MULT}`).test({
|
|
||||||
name: 'boost',
|
|
||||||
test: async boost => boost % BOOST_MULT === 0,
|
|
||||||
message: `must be divisble be ${BOOST_MULT}`
|
|
||||||
})
|
|
||||||
|
|
||||||
export const boostSchema = object({
|
|
||||||
amount: boostValidator.required('required').positive('must be positive')
|
|
||||||
})
|
|
||||||
|
|
||||||
export const actSchema = object({
|
export const actSchema = object({
|
||||||
sats: intValidator.required('required').positive('must be positive')
|
sats: intValidator.required('required').positive('must be positive'),
|
||||||
.when(['act'], ([act], schema) => {
|
act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS'])
|
||||||
if (act === 'BOOST') {
|
|
||||||
return boostValidator
|
|
||||||
}
|
|
||||||
return schema
|
|
||||||
}),
|
|
||||||
act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS', 'BOOST'])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const settingsSchema = object().shape({
|
export const settingsSchema = object().shape({
|
||||||
@ -635,7 +618,7 @@ export const settingsSchema = object().shape({
|
|||||||
string().nullable().matches(NOSTR_PUBKEY_HEX, 'must be 64 hex chars'),
|
string().nullable().matches(NOSTR_PUBKEY_HEX, 'must be 64 hex chars'),
|
||||||
string().nullable().matches(NOSTR_PUBKEY_BECH32, 'invalid bech32 encoding')], 'invalid pubkey'),
|
string().nullable().matches(NOSTR_PUBKEY_BECH32, 'invalid bech32 encoding')], 'invalid pubkey'),
|
||||||
nostrRelays: array().of(
|
nostrRelays: array().of(
|
||||||
string().ws().transform(relay => relay.startsWith('wss://') ? relay : `wss://${relay}`)
|
string().ws()
|
||||||
).max(NOSTR_MAX_RELAY_NUM,
|
).max(NOSTR_MAX_RELAY_NUM,
|
||||||
({ max, value }) => `${Math.abs(max - value.length)} too many`),
|
({ max, value }) => `${Math.abs(max - value.length)} too many`),
|
||||||
hideBookmarks: boolean(),
|
hideBookmarks: boolean(),
|
||||||
|
@ -49,7 +49,6 @@ export default function PostEdit ({ ssrData }) {
|
|||||||
existingBoost: {
|
existingBoost: {
|
||||||
label: 'old boost',
|
label: 'old boost',
|
||||||
term: `- ${item.boost}`,
|
term: `- ${item.boost}`,
|
||||||
op: '-',
|
|
||||||
modifier: cost => cost - item.boost
|
modifier: cost => cost - item.boost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,12 @@ import { useMemo } from 'react'
|
|||||||
import { CompactLongCountdown } from '@/components/countdown'
|
import { CompactLongCountdown } from '@/components/countdown'
|
||||||
import { usePaidMutation } from '@/components/use-paid-mutation'
|
import { usePaidMutation } from '@/components/use-paid-mutation'
|
||||||
import { DONATE } from '@/fragments/paidAction'
|
import { DONATE } from '@/fragments/paidAction'
|
||||||
import { ITEM_FULL_FIELDS } from '@/fragments/items'
|
|
||||||
import { ListItem } from '@/components/items'
|
|
||||||
|
|
||||||
const GrowthPieChart = dynamic(() => import('@/components/charts').then(mod => mod.GrowthPieChart), {
|
const GrowthPieChart = dynamic(() => import('@/components/charts').then(mod => mod.GrowthPieChart), {
|
||||||
loading: () => <GrowthPieChartSkeleton />
|
loading: () => <GrowthPieChartSkeleton />
|
||||||
})
|
})
|
||||||
|
|
||||||
const REWARDS_FULL = gql`
|
const REWARDS_FULL = gql`
|
||||||
${ITEM_FULL_FIELDS}
|
|
||||||
{
|
{
|
||||||
rewards {
|
rewards {
|
||||||
total
|
total
|
||||||
@ -40,9 +37,6 @@ ${ITEM_FULL_FIELDS}
|
|||||||
name
|
name
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
ad {
|
|
||||||
...ItemFullFields
|
|
||||||
}
|
|
||||||
leaderboard {
|
leaderboard {
|
||||||
users {
|
users {
|
||||||
id
|
id
|
||||||
@ -103,7 +97,7 @@ export default function Rewards ({ ssrData }) {
|
|||||||
const { data } = useQuery(REWARDS_FULL)
|
const { data } = useQuery(REWARDS_FULL)
|
||||||
const dat = useData(data, ssrData)
|
const dat = useData(data, ssrData)
|
||||||
|
|
||||||
let { rewards: [{ total, sources, time, leaderboard, ad }] } = useMemo(() => {
|
let { rewards: [{ total, sources, time, leaderboard }] } = useMemo(() => {
|
||||||
return dat || { rewards: [{}] }
|
return dat || { rewards: [{}] }
|
||||||
}, [dat])
|
}, [dat])
|
||||||
|
|
||||||
@ -130,13 +124,12 @@ export default function Rewards ({ ssrData }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout footerLinks>
|
<Layout footerLinks>
|
||||||
{ad &&
|
<h4 className='pt-3 align-self-center text-reset'>
|
||||||
<div className='pt-3 align-self-center' style={{ maxWidth: '480px', width: '100%' }}>
|
<small className='text-muted'>rewards are sponsored by ...</small>
|
||||||
<div className='fw-bold text-muted pb-2'>
|
<Link className='text-reset ms-2' href='/items/141924' style={{ lineHeight: 1.5, textDecoration: 'underline' }}>
|
||||||
top boost this month
|
SN is hiring
|
||||||
</div>
|
</Link>
|
||||||
<ListItem item={ad} />
|
</h4>
|
||||||
</div>}
|
|
||||||
<Row className='pb-3'>
|
<Row className='pb-3'>
|
||||||
<Col lg={leaderboard?.users && 5}>
|
<Col lg={leaderboard?.users && 5}>
|
||||||
<div
|
<div
|
||||||
|
@ -170,9 +170,7 @@ export default function Settings ({ ssrData }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nostrRelaysFiltered = nostrRelays
|
const nostrRelaysFiltered = nostrRelays?.filter(word => word.trim().length > 0)
|
||||||
?.filter(word => word.trim().length > 0)
|
|
||||||
.map(relay => relay.startsWith('wss://') ? relay : `wss://${relay}`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await setSettings({
|
await setSettings({
|
||||||
|
@ -2,13 +2,16 @@
|
|||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
gap: 0 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav :global .nav-link {
|
.nav :global .nav-link {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav :global .nav-item:not(:first-child) {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nav :global .active {
|
.nav :global .active {
|
||||||
border-bottom: 2px solid var(--bs-primary);
|
border-bottom: 2px solid var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
UPDATE "Sub" SET "rewardsPct" = 30;
|
|
||||||
|
|
||||||
-- account for comments in rewards
|
|
||||||
CREATE OR REPLACE FUNCTION rewards(min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT)
|
|
||||||
RETURNS TABLE (
|
|
||||||
t TIMESTAMP(3), total BIGINT, donations BIGINT, fees BIGINT, boost BIGINT, jobs BIGINT, anons_stack BIGINT
|
|
||||||
)
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
BEGIN
|
|
||||||
RETURN QUERY
|
|
||||||
SELECT period.t,
|
|
||||||
coalesce(FLOOR(sum(msats)), 0)::BIGINT as total,
|
|
||||||
coalesce(FLOOR(sum(msats) FILTER(WHERE type = 'DONATION')), 0)::BIGINT as donations,
|
|
||||||
coalesce(FLOOR(sum(msats) FILTER(WHERE type NOT IN ('BOOST', 'STREAM', 'DONATION', 'ANON'))), 0)::BIGINT as fees,
|
|
||||||
coalesce(FLOOR(sum(msats) FILTER(WHERE type = 'BOOST')), 0)::BIGINT as boost,
|
|
||||||
coalesce(FLOOR(sum(msats) FILTER(WHERE type = 'STREAM')), 0)::BIGINT as jobs,
|
|
||||||
coalesce(FLOOR(sum(msats) FILTER(WHERE type = 'ANON')), 0)::BIGINT as anons_stack
|
|
||||||
FROM generate_series(min, max, ival) period(t),
|
|
||||||
LATERAL
|
|
||||||
(
|
|
||||||
(SELECT
|
|
||||||
("ItemAct".msats - COALESCE("ReferralAct".msats, 0)) * COALESCE("Sub"."rewardsPct", 100) * 0.01 as msats,
|
|
||||||
act::text as type
|
|
||||||
FROM "ItemAct"
|
|
||||||
JOIN "Item" ON "Item"."id" = "ItemAct"."itemId"
|
|
||||||
LEFT JOIN "Item" root ON "Item"."rootId" = root.id
|
|
||||||
JOIN "Sub" ON "Sub"."name" = COALESCE(root."subName", "Item"."subName")
|
|
||||||
LEFT JOIN "ReferralAct" ON "ReferralAct"."itemActId" = "ItemAct".id
|
|
||||||
WHERE date_trunc(date_part, "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t
|
|
||||||
AND "ItemAct".act <> 'TIP'
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID'))
|
|
||||||
UNION ALL
|
|
||||||
(SELECT sats * 1000 as msats, 'DONATION' as type
|
|
||||||
FROM "Donation"
|
|
||||||
WHERE date_trunc(date_part, "Donation".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t)
|
|
||||||
UNION ALL
|
|
||||||
-- any earnings from anon's stack that are not forwarded to other users
|
|
||||||
(SELECT "ItemAct".msats, 'ANON' as type
|
|
||||||
FROM "Item"
|
|
||||||
JOIN "ItemAct" ON "ItemAct"."itemId" = "Item".id
|
|
||||||
LEFT JOIN "ItemForward" ON "ItemForward"."itemId" = "Item".id
|
|
||||||
WHERE "Item"."userId" = 27 AND "ItemAct".act = 'TIP'
|
|
||||||
AND date_trunc(date_part, "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t
|
|
||||||
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
|
|
||||||
GROUP BY "ItemAct".id, "ItemAct".msats
|
|
||||||
HAVING COUNT("ItemForward".id) = 0)
|
|
||||||
) x
|
|
||||||
GROUP BY period.t;
|
|
||||||
END;
|
|
||||||
$$;
|
|
@ -1,54 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Item" ADD COLUMN "oldBoost" INTEGER NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION expire_boost_jobs()
|
|
||||||
RETURNS INTEGER
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $$
|
|
||||||
DECLARE
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
|
|
||||||
SELECT 'expireBoost', jsonb_build_object('id', "Item".id), 21, true, now(), interval '1 days'
|
|
||||||
FROM "Item"
|
|
||||||
WHERE "Item".boost > 0 ON CONFLICT DO NOTHING;
|
|
||||||
return 0;
|
|
||||||
EXCEPTION WHEN OTHERS THEN
|
|
||||||
return 0;
|
|
||||||
END;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
SELECT expire_boost_jobs();
|
|
||||||
DROP FUNCTION IF EXISTS expire_boost_jobs;
|
|
||||||
|
|
||||||
-- fold all STREAM "ItemAct" into a single row per item (it's defunct)
|
|
||||||
INSERT INTO "ItemAct" (created_at, updated_at, msats, act, "itemId", "userId")
|
|
||||||
SELECT MAX("ItemAct".created_at), MAX("ItemAct".updated_at), sum("ItemAct".msats), 'BOOST', "ItemAct"."itemId", "ItemAct"."userId"
|
|
||||||
FROM "ItemAct"
|
|
||||||
WHERE "ItemAct".act = 'STREAM'
|
|
||||||
GROUP BY "ItemAct"."itemId", "ItemAct"."userId";
|
|
||||||
|
|
||||||
-- drop all STREAM "ItemAct" rows
|
|
||||||
DELETE FROM "ItemAct"
|
|
||||||
WHERE "ItemAct".act = 'STREAM';
|
|
||||||
|
|
||||||
-- AlterEnum
|
|
||||||
ALTER TYPE "InvoiceActionType" ADD VALUE 'BOOST';
|
|
||||||
|
|
||||||
-- increase boost per vote
|
|
||||||
CREATE OR REPLACE VIEW zap_rank_personal_constants AS
|
|
||||||
SELECT
|
|
||||||
10000.0 AS boost_per_vote,
|
|
||||||
1.2 AS vote_power,
|
|
||||||
1.3 AS vote_decay,
|
|
||||||
3.0 AS age_wait_hours,
|
|
||||||
0.5 AS comment_scaler,
|
|
||||||
1.2 AS boost_power,
|
|
||||||
1.6 AS boost_decay,
|
|
||||||
616 AS global_viewer_id,
|
|
||||||
interval '7 days' AS item_age_bound,
|
|
||||||
interval '7 days' AS user_last_seen_bound,
|
|
||||||
0.9 AS max_personal_viewer_vote_ratio,
|
|
||||||
0.1 AS min_viewer_votes;
|
|
||||||
|
|
||||||
DROP FUNCTION IF EXISTS run_auction(item_id INTEGER);
|
|
@ -457,7 +457,6 @@ model Item {
|
|||||||
company String?
|
company String?
|
||||||
weightedVotes Float @default(0)
|
weightedVotes Float @default(0)
|
||||||
boost Int @default(0)
|
boost Int @default(0)
|
||||||
oldBoost Int @default(0)
|
|
||||||
pollCost Int?
|
pollCost Int?
|
||||||
paidImgLink Boolean @default(false)
|
paidImgLink Boolean @default(false)
|
||||||
commentMsats BigInt @default(0)
|
commentMsats BigInt @default(0)
|
||||||
@ -805,7 +804,6 @@ enum InvoiceActionType {
|
|||||||
ITEM_UPDATE
|
ITEM_UPDATE
|
||||||
ZAP
|
ZAP
|
||||||
DOWN_ZAP
|
DOWN_ZAP
|
||||||
BOOST
|
|
||||||
DONATE
|
DONATE
|
||||||
POLL_VOTE
|
POLL_VOTE
|
||||||
TERRITORY_CREATE
|
TERRITORY_CREATE
|
||||||
|
@ -11,6 +11,7 @@ const ITEMS = gql`
|
|||||||
ncomments
|
ncomments
|
||||||
sats
|
sats
|
||||||
company
|
company
|
||||||
|
maxBid
|
||||||
status
|
status
|
||||||
location
|
location
|
||||||
remote
|
remote
|
||||||
@ -231,7 +232,7 @@ ${topCowboys.map((user, i) =>
|
|||||||
------
|
------
|
||||||
|
|
||||||
##### Promoted jobs
|
##### Promoted jobs
|
||||||
${jobs.data.items.items.filter(i => i.boost > 0).slice(0, 5).map((item, i) =>
|
${jobs.data.items.items.filter(i => i.maxBid > 0 && i.status === 'ACTIVE').slice(0, 5).map((item, i) =>
|
||||||
`${i + 1}. [${item.title.trim()} \\ ${item.company} \\ ${item.location}${item.remote ? ' or Remote' : ''}](https://stacker.news/items/${item.id})\n`).join('')}
|
`${i + 1}. [${item.title.trim()} \\ ${item.company} \\ ${item.location}${item.remote ? ' or Remote' : ''}](https://stacker.news/items/${item.id})\n`).join('')}
|
||||||
|
|
||||||
[**all jobs**](https://stacker.news/~jobs)
|
[**all jobs**](https://stacker.news/~jobs)
|
||||||
|
@ -245,14 +245,6 @@ $zindex-sticky: 900;
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-height-sm {
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-height-md {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (display-mode: standalone) {
|
@media (display-mode: standalone) {
|
||||||
.standalone {
|
.standalone {
|
||||||
display: flex
|
display: flex
|
||||||
|
@ -10,7 +10,7 @@ const MAX_OUTGOING_CLTV_DELTA = 500 // the maximum cltv delta we'll allow for th
|
|||||||
export const MIN_SETTLEMENT_CLTV_DELTA = 80 // the minimum blocks we'll leave for settling the incoming invoice
|
export const MIN_SETTLEMENT_CLTV_DELTA = 80 // the minimum blocks we'll leave for settling the incoming invoice
|
||||||
const FEE_ESTIMATE_TIMEOUT_SECS = 5 // the timeout for the fee estimate request
|
const FEE_ESTIMATE_TIMEOUT_SECS = 5 // the timeout for the fee estimate request
|
||||||
const MAX_FEE_ESTIMATE_PERCENT = 0.025 // the maximum fee relative to outgoing we'll allow for the fee estimate
|
const MAX_FEE_ESTIMATE_PERCENT = 0.025 // the maximum fee relative to outgoing we'll allow for the fee estimate
|
||||||
const ZAP_SYBIL_FEE_MULT = 10 / 7 // the fee for the zap sybil service
|
const ZAP_SYBIL_FEE_MULT = 10 / 9 // the fee for the zap sybil service
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The wrapInvoice function is used to wrap an outgoing invoice with the necessary parameters for an incoming hold invoice.
|
The wrapInvoice function is used to wrap an outgoing invoice with the necessary parameters for an incoming hold invoice.
|
||||||
|
22
worker/auction.js
Normal file
22
worker/auction.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import serialize from '@/api/resolvers/serial.js'
|
||||||
|
|
||||||
|
export async function auction ({ models }) {
|
||||||
|
// get all items we need to check
|
||||||
|
const items = await models.item.findMany(
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
maxBid: {
|
||||||
|
not: null
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
not: 'STOPPED'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// for each item, run serialized auction function
|
||||||
|
items.forEach(async item => {
|
||||||
|
await serialize(models.$executeRaw`SELECT run_auction(${item.id}::INTEGER)`, { models })
|
||||||
|
})
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
import { Prisma } from '@prisma/client'
|
|
||||||
|
|
||||||
export async function expireBoost ({ data: { id }, models }) {
|
|
||||||
// reset boost 30 days after last boost
|
|
||||||
// run in serializable because we use an aggregate here
|
|
||||||
// and concurrent boosts could be double counted
|
|
||||||
// serialization errors will cause pgboss retries
|
|
||||||
await models.$transaction(
|
|
||||||
[
|
|
||||||
models.$executeRaw`
|
|
||||||
WITH boost AS (
|
|
||||||
SELECT sum(msats) FILTER (WHERE created_at <= now() - interval '30 days') as old_msats,
|
|
||||||
sum(msats) FILTER (WHERE created_at > now() - interval '30 days') as cur_msats
|
|
||||||
FROM "ItemAct"
|
|
||||||
WHERE act = 'BOOST'
|
|
||||||
AND "itemId" = ${Number(id)}::INTEGER
|
|
||||||
)
|
|
||||||
UPDATE "Item"
|
|
||||||
SET boost = COALESCE(boost.cur_msats, 0), "oldBoost" = COALESCE(boost.old_msats, 0)
|
|
||||||
FROM boost
|
|
||||||
WHERE "Item".id = ${Number(id)}::INTEGER`
|
|
||||||
],
|
|
||||||
{
|
|
||||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||||||
} from './wallet.js'
|
} from './wallet.js'
|
||||||
import { repin } from './repin.js'
|
import { repin } from './repin.js'
|
||||||
import { trust } from './trust.js'
|
import { trust } from './trust.js'
|
||||||
|
import { auction } from './auction.js'
|
||||||
import { earn } from './earn.js'
|
import { earn } from './earn.js'
|
||||||
import apolloClient from '@apollo/client'
|
import apolloClient from '@apollo/client'
|
||||||
import { indexItem, indexAllItems } from './search.js'
|
import { indexItem, indexAllItems } from './search.js'
|
||||||
@ -34,7 +35,6 @@ import {
|
|||||||
import { thisDay } from './thisDay.js'
|
import { thisDay } from './thisDay.js'
|
||||||
import { isServiceEnabled } from '@/lib/sndev.js'
|
import { isServiceEnabled } from '@/lib/sndev.js'
|
||||||
import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts.js'
|
import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts.js'
|
||||||
import { expireBoost } from './expireBoost.js'
|
|
||||||
|
|
||||||
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
|
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
|
||||||
|
|
||||||
@ -117,12 +117,12 @@ async function work () {
|
|||||||
await boss.work('imgproxy', jobWrapper(imgproxy))
|
await boss.work('imgproxy', jobWrapper(imgproxy))
|
||||||
await boss.work('deleteUnusedImages', jobWrapper(deleteUnusedImages))
|
await boss.work('deleteUnusedImages', jobWrapper(deleteUnusedImages))
|
||||||
}
|
}
|
||||||
await boss.work('expireBoost', jobWrapper(expireBoost))
|
|
||||||
await boss.work('weeklyPost-*', jobWrapper(weeklyPost))
|
await boss.work('weeklyPost-*', jobWrapper(weeklyPost))
|
||||||
await boss.work('payWeeklyPostBounty', jobWrapper(payWeeklyPostBounty))
|
await boss.work('payWeeklyPostBounty', jobWrapper(payWeeklyPostBounty))
|
||||||
await boss.work('repin-*', jobWrapper(repin))
|
await boss.work('repin-*', jobWrapper(repin))
|
||||||
await boss.work('trust', jobWrapper(trust))
|
await boss.work('trust', jobWrapper(trust))
|
||||||
await boss.work('timestampItem', jobWrapper(timestampItem))
|
await boss.work('timestampItem', jobWrapper(timestampItem))
|
||||||
|
await boss.work('auction', jobWrapper(auction))
|
||||||
await boss.work('earn', jobWrapper(earn))
|
await boss.work('earn', jobWrapper(earn))
|
||||||
await boss.work('streak', jobWrapper(computeStreaks))
|
await boss.work('streak', jobWrapper(computeStreaks))
|
||||||
await boss.work('checkStreak', jobWrapper(checkStreak))
|
await boss.work('checkStreak', jobWrapper(checkStreak))
|
||||||
|
@ -22,6 +22,7 @@ const ITEM_SEARCH_FIELDS = gql`
|
|||||||
subName
|
subName
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
|
maxBid
|
||||||
company
|
company
|
||||||
location
|
location
|
||||||
remote
|
remote
|
||||||
|
Loading…
x
Reference in New Issue
Block a user