Compare commits

...

18 Commits

Author SHA1 Message Date
ekzyis 18fbd17025
CTRL+U for uploads (#1423) 2024-09-23 20:08:37 -05:00
k00b 2b5a1cbfe9 more reply placeholders 2024-09-22 12:05:03 -05:00
k00b b6dcee4f26 add booosts to newsletter script 2024-09-22 12:05:03 -05:00
ekzyis d30dace266
Fix missing dependency for useZap (#1420)
This didn't cause any bugs because useWallet returns everything we need on first render.

This caused a bug with E2EE device sync branch though since there the wallet is loaded async.

This meant that during payment, the wallet config was undefined.
2024-09-22 11:03:38 -05:00
k00b 894d02a196 allow top sorting by boost 2024-09-21 14:58:25 -05:00
k00b 83458fdc9e add amas to newsletter script 2024-09-21 14:44:17 -05:00
k00b 101574b605 fix territory revenue notification 2024-09-20 11:07:15 -05:00
k00b 59d5fd60f2 wider top boost on rewards page 2024-09-20 10:46:07 -05:00
k00b f90a9905ba unzapped bolt shouldn't glow 2024-09-20 10:41:46 -05:00
k00b 8447a4a8b2 boost icon refinement 2024-09-20 10:15:44 -05:00
k00b 4981d572bb show boost for when forwardee 2024-09-20 09:58:53 -05:00
ekzyis 4e7b4ee571
Fix upvote hover style not showing for first zap (#1418) 2024-09-20 09:44:15 -05:00
k00b 8c9d4aa59b fix auction based ranking 2024-09-19 17:07:36 -05:00
k00b ad9a65ce78 fix expire boost unit 2024-09-19 16:10:04 -05:00
ekzyis b4e143460b
Fix nwc error message (#1417)
* Fix this.error undefined on relay error

* Also use arrow function for ws.onmessage
2024-09-19 15:53:42 -05:00
k00b 731df5fc67 better err message ek suggestion 2024-09-19 15:32:18 -05:00
k00b 2f191e04f9 ek boost hint suggestions 2024-09-19 15:27:09 -05:00
k00b 09be42844e boosted items aren't freebies 2024-09-19 15:18:07 -05:00
20 changed files with 118 additions and 55 deletions

View File

@ -190,7 +190,7 @@ export async function retryPaidAction (actionType, args, context) {
const failedInvoice = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
if (!failedInvoice) {
throw new Error(`retryPaidAction - invoice not found or not in failed state ${actionType}`)
throw new Error(`retryPaidAction ${actionType} - invoice ${invoiceId} not found or not in failed state`)
}
const { msatsRequested, actionId } = failedInvoice

View File

@ -105,6 +105,8 @@ const orderByClause = (by, me, models, type) => {
return 'ORDER BY "Item".msats DESC'
case 'zaprank':
return topOrderByWeightedSats(me, models)
case 'boost':
return 'ORDER BY "Item".boost DESC'
case 'random':
return 'ORDER BY RANDOM()'
default:
@ -378,6 +380,7 @@ export default {
activeOrMine(me),
nsfwClause(showNsfw),
typeClause(type),
by === 'boost' && '"Item".boost > 0',
whenClause(when || 'forever', table))}
${orderByClause(by, me, models, type)}
OFFSET $4
@ -421,6 +424,7 @@ export default {
typeClause(type),
whenClause(when, 'Item'),
await filterClause(me, models, type),
by === 'boost' && '"Item".boost > 0',
muteClause(me))}
${orderByClause(by || 'zaprank', me, models, type)}
OFFSET $3
@ -479,7 +483,7 @@ export default {
'"pinId" IS NULL',
subClause(sub, 4)
)}
ORDER BY group_rank, rank
ORDER BY group_rank DESC, rank
OFFSET $2
LIMIT $3`,
orderBy: 'ORDER BY group_rank DESC, rank'
@ -690,7 +694,8 @@ export default {
boost: { gte: boost },
status: 'ACTIVE',
deletedAt: null,
outlawed: false
outlawed: false,
parentId: null
}
if (id) {
where.id = { not: Number(id) }
@ -698,7 +703,7 @@ export default {
return {
home: await models.item.count({ where }) === 0,
sub: await models.item.count({ where: { ...where, subName: sub } }) === 0
sub: sub ? await models.item.count({ where: { ...where, subName: sub } }) === 0 : false
}
}
},
@ -1139,7 +1144,7 @@ export default {
return item.weightedVotes - item.weightedDownVotes > 0
},
freebie: async (item) => {
return item.cost === 0
return item.cost === 0 && item.boost === 0
},
meSats: async (item, args, { me, models }) => {
if (!me) return 0

View File

@ -90,8 +90,8 @@ 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) {
query BoostPosition($sub: String, $id: ID, $boost: Int) {
boostPosition(sub: $sub, id: $id, boost: $boost) {
home
sub
}
@ -101,10 +101,10 @@ export function BoostItemInput ({ item, sub, act = false, ...props }) {
const getPositionDebounce = useDebounceCallback((...args) => getBoostPosition(...args), 1000, [getBoostPosition])
useEffect(() => {
if (boost) {
getPositionDebounce({ variables: { boost: Number(boost), id: item?.id } })
if (boost >= 0 && !item?.parentId) {
getPositionDebounce({ variables: { sub: item?.subName || sub?.name, boost: Number(boost), id: item?.id } })
}
}, [boost, item?.id])
}, [boost, item?.id, !item?.parentId, item?.subName || sub?.name])
const boostMessage = useMemo(() => {
if (!item?.parentId) {

View File

@ -2,9 +2,9 @@ import { useShowModal } from './modal'
import { useToast } from './toast'
import ItemAct from './item-act'
import AccordianItem from './accordian-item'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import getColor from '@/lib/rainbow'
import UpBolt from '@/svgs/bolt.svg'
import BoostIcon from '@/svgs/arrow-up-double-line.svg'
import styles from './upvote.module.css'
import { BoostHelp } from './adv-post-form'
import { BOOST_MULT } from '@/lib/constants'
@ -12,15 +12,17 @@ import classNames from 'classnames'
export default function Boost ({ item, className, ...props }) {
const { boost } = item
const style = useMemo(() => (boost
const [hover, setHover] = useState(false)
const [color, nextColor] = useMemo(() => [getColor(boost), getColor(boost + BOOST_MULT)], [boost])
const style = useMemo(() => (hover || boost
? {
fill: getColor(boost),
filter: `drop-shadow(0 0 6px ${getColor(boost)}90)`,
transform: 'scaleX(-1)'
fill: hover ? nextColor : color,
filter: `drop-shadow(0 0 6px ${hover ? nextColor : color}90)`
}
: {
transform: 'scaleX(-1)'
}), [boost])
: undefined), [boost, hover])
return (
<Booster
item={item} As={({ ...oprops }) =>
@ -28,11 +30,14 @@ export default function Boost ({ item, className, ...props }) {
<div
className={styles.upvoteWrapper}
>
<UpBolt
<BoostIcon
{...props} {...oprops} style={style}
width={26}
height={26}
className={classNames(styles.upvote, className, boost && styles.voted)}
onPointerEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onTouchEnd={() => setHover(false)}
className={classNames(styles.boost, className, boost && styles.voted)}
/>
</div>
</div>}

View File

@ -255,6 +255,11 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
e.preventDefault()
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
}
if (e.key === 'u') {
// some browsers might use CTRL+U to do something else so prevent that behavior too
e.preventDefault()
imageUploadRef.current?.click()
}
if (e.key === 'Tab' && e.altKey) {
e.preventDefault()
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)

View File

@ -277,7 +277,7 @@ export function useZap () {
const reason = error?.message || error?.toString?.()
toaster.danger(reason)
}
}, [me?.id, strike])
}, [act, me?.id, strike])
}
export class ActCanceledError extends Error {

View File

@ -113,7 +113,6 @@ function TopLevelItem ({ item, noReply, ...props }) {
<Reply
item={item}
replyOpen
placeholder={item.ncomments > 3 ? 'fractions of a penny for your thoughts?' : 'early comments get more zaps'}
onCancelQuote={cancelQuote}
onQuoteReply={quoteReply}
quote={quote}

View File

@ -106,7 +106,7 @@ export default function Item ({
<div className={classNames(styles.item, itemClassName)}>
{item.position && (pinnable || !item.subName)
? <Pin width={24} height={24} className={styles.pin} />
: item.mine
: item.mine || item.meForward
? <Boost item={item} className={styles.upvote} />
: item.meDontLikeSats > item.meSats
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />

View File

@ -242,12 +242,12 @@ function RevenueNotification ({ n }) {
return (
<div className='d-flex'>
<BountyIcon className='align-self-center fill-success mx-1' width={24} height={24} style={{ flex: '0 0 24px' }} />
<div className=' pb-1'>
<div className='fw-bold text-success'>
<div className='ms-2'>
<NoteHeader color='success' big>
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in territory revenue<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
</div>
</NoteHeader>
<div style={{ lineHeight: '140%' }}>
As the founder of territory <Link href={`/~${n.subName}`}>~{n.subName}</Link>, you receive 50% of the revenue it generates and the other 50% go to <Link href='/rewards'>rewards</Link>.
As the founder of territory <Link href={`/~${n.subName}`}>~{n.subName}</Link>, you receive 70% of the post, comment, boost, and zap fees. The other 30% go to <Link href='/rewards'>rewards</Link>.
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@ import { Form, MarkdownInput } from '@/components/form'
import styles from './reply.module.css'
import { COMMENTS } from '@/fragments/comments'
import { useMe } from './me'
import { forwardRef, useCallback, useEffect, useState, useRef } from 'react'
import { forwardRef, useCallback, useEffect, useState, useRef, useMemo } from 'react'
import Link from 'next/link'
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
import { commentsViewedAfterComment } from '@/lib/new-comments'
@ -34,7 +34,6 @@ export default forwardRef(function Reply ({
item,
replyOpen,
children,
placeholder,
onQuoteReply,
onCancelQuote,
quote
@ -53,6 +52,14 @@ export default forwardRef(function Reply ({
}
}, [replyOpen, quote, parentId])
const placeholder = useMemo(() => {
return [
'comment for currency?',
'fractions of a penny for your thoughts?',
'put your money where your mouth is?'
][parentId % 3]
}, [parentId])
const onSubmit = useItemSubmit(CREATE_COMMENT, {
extraValues: { parentId },
paidMutationOptions: {

View File

@ -227,14 +227,15 @@ export default function UpVote ({ item, className }) {
}
}
const fillColor = meSats && (hover || pending ? nextColor : color)
const style = useMemo(() => (fillColor
? {
fill: fillColor,
filter: `drop-shadow(0 0 6px ${fillColor}90)`
}
: undefined), [fillColor])
const style = useMemo(() => {
const fillColor = pending || hover ? nextColor : color
return meSats || hover || pending
? {
fill: fillColor,
filter: `drop-shadow(0 0 6px ${fillColor}90)`
}
: undefined
}, [hover, pending, nextColor, color, meSats])
return (
<div ref={ref} className='upvoteParent'>

View File

@ -5,6 +5,13 @@
-webkit-touch-callout: none;
}
.boost {
fill: var(--theme-clickToContextColor);
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
.upvoteWrapper {
position: relative;
padding-right: .2rem;

View File

@ -62,7 +62,7 @@ export const ITEM_FILTER_THRESHOLD = 1.2
export const DONT_LIKE_THIS_COST = 1
export const COMMENT_TYPE_QUERY = ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks']
export const USER_SORTS = ['value', 'stacking', 'spending', 'comments', 'posts', 'referrals']
export const ITEM_SORTS = ['zaprank', 'comments', 'sats']
export const ITEM_SORTS = ['zaprank', 'comments', 'sats', 'boost']
export const SUB_SORTS = ['stacking', 'revenue', 'spending', 'posts', 'comments']
export const WHENS = ['day', 'week', 'month', 'year', 'forever', 'custom']
export const ITEM_TYPES_USER = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls', 'freebies', 'jobs', 'bookmarks']

View File

@ -22,14 +22,14 @@ export class Relay {
constructor (relayUrl) {
const ws = new WebSocket(relayUrl)
ws.onmessage = function (msg) {
ws.onmessage = (msg) => {
const [type, notice] = JSON.parse(msg.data)
if (type === 'NOTICE') {
console.log('relay notice:', notice)
}
}
ws.onerror = function (err) {
ws.onerror = (err) => {
console.error('websocket error:', err.message)
this.error = err.message
}

View File

@ -1,5 +1,9 @@
export default function getColor (meSats) {
if (!meSats || meSats <= 10) {
if (!meSats || meSats === 0) {
return '#a5a5a5'
}
if (meSats <= 10) {
return 'var(--bs-secondary)'
}

View File

@ -131,7 +131,7 @@ export default function Rewards ({ ssrData }) {
return (
<Layout footerLinks>
{ad &&
<div className='pt-3 align-self-center' style={{ maxWidth: '480px', width: '100%' }}>
<div className='pt-3 align-self-center' style={{ maxWidth: '500px', width: '100%' }}>
<div className='fw-bold text-muted pb-2'>
top boost this month
</div>

View File

@ -0,0 +1,18 @@
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;

View File

@ -1,8 +1,8 @@
const { ApolloClient, InMemoryCache, HttpLink, gql } = require('@apollo/client')
const ITEMS = gql`
query items ($sort: String, $when: String, $sub: String) {
items (sort: $sort, when: $when, sub: $sub) {
query items ($sort: String, $when: String, $sub: String, $by: String) {
items (sort: $sort, when: $when, sub: $sub, by: $by) {
cursor
items {
id
@ -15,6 +15,7 @@ const ITEMS = gql`
location
remote
boost
subName
user {
id
name
@ -152,15 +153,15 @@ async function main () {
variables: { sort: 'top', when: 'week', sub: 'meta' }
})
const jobs = await client.query({
const ama = await client.query({
query: ITEMS,
variables: { sub: 'jobs' }
variables: { sort: 'top', when: 'week', sub: 'ama' }
})
// const thisDay = await client.query({
// query: SEARCH,
// variables: { q: 'This Day in Stacker News @Undisciplined', sort: 'recent', what: 'posts', when: 'week' }
// })
const boosts = await client.query({
query: ITEMS,
variables: { sort: 'top', when: 'forever', by: 'boost' }
})
const topMeme = await bountyWinner('meme monday')
const topFact = await bountyWinner('fun fact')
@ -179,6 +180,13 @@ ${top.data.items.items.map((item, i) =>
`${i + 1}. [${item.title}](https://stacker.news/items/${item.id})
- ${abbrNum(item.sats)} sats${item.boost ? ` \\ ${abbrNum(item.boost)} boost` : ''} \\ ${item.ncomments} comments \\ [@${item.user.name}](https://stacker.news/${item.user.name})\n`).join('')}
##### Top AMAs
${ama.data.items.items.slice(0, 3).map((item, i) =>
`${i + 1}. [${item.title}](https://stacker.news/items/${item.id})
- ${abbrNum(item.sats)} sats${item.boost ? ` \\ ${abbrNum(item.boost)} boost` : ''} \\ ${item.ncomments} comments \\ [@${item.user.name}](https://stacker.news/${item.user.name})\n`).join('')}
[**all AMAs**](https://stacker.news/~meta/top/posts/forever)
##### Don't miss
${top.data.items.items.map((item, i) =>
`- [${item.title}](https://stacker.news/items/${item.id})\n`).join('')}
@ -230,9 +238,12 @@ ${topCowboys.map((user, i) =>
------
##### Promoted jobs
${jobs.data.items.items.filter(i => i.boost > 0).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('')}
##### Top Boosts
${boosts.data.items.items.map((item, i) =>
item.subName === 'jobs'
? `${i + 1}. [${item.title.trim()} \\ ${item.company} \\ ${item.location}${item.remote ? ' or Remote' : ''}](https://stacker.news/items/${item.id})\n`
: `${i + 1}. [${item.title.trim()}](https://stacker.news/items/${item.id})\n`
).join('')}
[**all jobs**](https://stacker.news/~jobs)

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.83582L5.79291 11.0429L7.20712 12.4571L12 7.66424L16.7929 12.4571L18.2071 11.0429L12 4.83582ZM12 10.4857L5.79291 16.6928L7.20712 18.107L12 13.3141L16.7929 18.107L18.2071 16.6928L12 10.4857Z"></path></svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@ -16,7 +16,7 @@ export async function expireBoost ({ data: { id }, models }) {
AND "itemId" = ${Number(id)}::INTEGER
)
UPDATE "Item"
SET boost = COALESCE(boost.cur_msats, 0), "oldBoost" = COALESCE(boost.old_msats, 0)
SET boost = COALESCE(boost.cur_msats, 0) / 1000, "oldBoost" = COALESCE(boost.old_msats, 0) / 1000
FROM boost
WHERE "Item".id = ${Number(id)}::INTEGER`
],