streaks
This commit is contained in:
parent
a6ce93c2bb
commit
072e60c954
|
@ -181,6 +181,15 @@ export default {
|
|||
GROUP BY "userId", created_at`
|
||||
)
|
||||
}
|
||||
|
||||
if (meFull.noteCowboyHat) {
|
||||
queries.push(
|
||||
`SELECT id::text, updated_at AS "sortTime", 0 as "earnedSats", 'Streak' AS type
|
||||
FROM "Streak"
|
||||
WHERE "userId" = $1
|
||||
AND updated_at <= $2`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// we do all this crazy subquery stuff to make 'reward' islands
|
||||
|
@ -227,6 +236,17 @@ export default {
|
|||
JobChanged: {
|
||||
item: async (n, args, { models }) => getItem(n, { id: n.id }, { models })
|
||||
},
|
||||
Streak: {
|
||||
days: async (n, args, { models }) => {
|
||||
const res = await models.$queryRaw`
|
||||
SELECT "endedAt" - "startedAt" AS days
|
||||
FROM "Streak"
|
||||
WHERE id = ${Number(n.id)} AND "endedAt" IS NOT NULL
|
||||
`
|
||||
|
||||
return res.length ? res[0].days : null
|
||||
}
|
||||
},
|
||||
Earn: {
|
||||
sources: async (n, args, { me, models }) => {
|
||||
const [sources] = await models.$queryRaw(`
|
||||
|
|
|
@ -304,6 +304,21 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
if (user.noteCowboyHat) {
|
||||
const streak = await models.streak.findFirst({
|
||||
where: {
|
||||
userId: me.id,
|
||||
updatedAt: {
|
||||
gt: lastChecked
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (streak) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
searchUsers: async (parent, { q, limit, similarity }, { models }) => {
|
||||
|
@ -475,6 +490,15 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
streak: async (user, args, { models }) => {
|
||||
const res = await models.$queryRaw`
|
||||
SELECT (now_utc() at time zone 'America/Chicago')::date - "startedAt" AS days
|
||||
FROM "Streak"
|
||||
WHERE "userId" = ${user.id} AND "endedAt" IS NULL
|
||||
`
|
||||
|
||||
return res.length ? res[0].days : null
|
||||
},
|
||||
stacked: async (user, { when }, { models }) => {
|
||||
if (user.stacked) {
|
||||
return user.stacked
|
||||
|
|
|
@ -38,6 +38,12 @@ export default gql`
|
|||
tips: Int!
|
||||
}
|
||||
|
||||
type Streak {
|
||||
sortTime: String!
|
||||
days: Int
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Earn {
|
||||
earnedSats: Int!
|
||||
sortTime: String!
|
||||
|
@ -56,6 +62,7 @@ export default gql`
|
|||
|
||||
union Notification = Reply | Votification | Mention
|
||||
| Invitification | Earn | JobChanged | InvoicePaid | Referral
|
||||
| Streak
|
||||
|
||||
type Notifications {
|
||||
lastChecked: String
|
||||
|
|
|
@ -21,7 +21,7 @@ export default gql`
|
|||
setName(name: String!): Boolean
|
||||
setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, noteItemSats: Boolean!,
|
||||
noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
|
||||
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!,
|
||||
noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!, hideFromTopUsers: Boolean!,
|
||||
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrRelays: [String!]): User
|
||||
setPhoto(photoId: ID!): Int!
|
||||
upsertBio(bio: String!): User!
|
||||
|
@ -58,6 +58,7 @@ export default gql`
|
|||
bio: Item
|
||||
bioId: Int
|
||||
photoId: Int
|
||||
streak: Int
|
||||
sats: Int!
|
||||
upvotePopover: Boolean!
|
||||
tipPopover: Boolean!
|
||||
|
@ -68,6 +69,7 @@ export default gql`
|
|||
noteDeposits: Boolean!
|
||||
noteInvites: Boolean!
|
||||
noteJobIndicator: Boolean!
|
||||
noteCowboyHat: Boolean!
|
||||
hideInvoiceDesc: Boolean!
|
||||
hideFromTopUsers: Boolean!
|
||||
wildWestMode: Boolean!
|
||||
|
|
|
@ -23,6 +23,7 @@ import { Badge } from 'react-bootstrap'
|
|||
import { abbrNum } from '../lib/format'
|
||||
import Share from './share'
|
||||
import { DeleteDropdown } from './delete'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
|
||||
function Parent ({ item, rootText }) {
|
||||
const ParentFrag = () => (
|
||||
|
@ -135,7 +136,10 @@ export default function Comment ({
|
|||
</Link>
|
||||
<span> \ </span>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}<span className='text-boost font-weight-bold'>{op && ' OP'}</span></a>
|
||||
<a className='d-inline-flex align-items-center'>
|
||||
@{item.user.name}<CowboyHat className='ml-1 fill-grey' streak={item.user.streak} height={12} width={12} />
|
||||
{op && <span className='text-boost font-weight-bold ml-1'>OP</span>}
|
||||
</a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import CowboyHatIcon from '../svgs/cowboy.svg'
|
||||
|
||||
export default function CowboyHat ({ streak, badge, className = 'ml-1', height = 16, width = 16 }) {
|
||||
if (!streak) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<HatTooltip overlayText={`${streak} days`}>
|
||||
{badge
|
||||
? (
|
||||
<Badge variant='grey-medium' className='ml-2 d-inline-flex align-items-center'>
|
||||
<CowboyHatIcon className={className} height={height} width={width} />
|
||||
<span className='ml-1'>{streak}</span>
|
||||
</Badge>)
|
||||
: <CowboyHatIcon className={className} height={height} width={width} />}
|
||||
</HatTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function HatTooltip ({ children, overlayText, placement }) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement={placement || 'bottom'}
|
||||
overlay={
|
||||
<Tooltip>
|
||||
{overlayText || '1 sat'}
|
||||
</Tooltip>
|
||||
}
|
||||
trigger={['hover', 'focus']}
|
||||
>
|
||||
{children}
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
|
@ -15,6 +15,7 @@ import { abbrNum } from '../lib/format'
|
|||
import NoteIcon from '../svgs/notification-4-fill.svg'
|
||||
import { useQuery, gql } from '@apollo/client'
|
||||
import LightningIcon from '../svgs/bolt.svg'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
|
||||
function WalletSummary ({ me }) {
|
||||
if (!me) return null
|
||||
|
@ -72,7 +73,9 @@ export default function Header ({ sub }) {
|
|||
<NavDropdown
|
||||
className={styles.dropdown} title={
|
||||
<Link href={`/${me?.name}`} passHref>
|
||||
<Nav.Link eventKey={me?.name} as='div' className='p-0' onClick={e => e.preventDefault()}>{`@${me?.name}`}</Nav.Link>
|
||||
<Nav.Link eventKey={me?.name} as='div' className='p-0 d-flex align-items-center' onClick={e => e.preventDefault()}>
|
||||
{`@${me?.name}`}<CowboyHat streak={me.streak} />
|
||||
</Nav.Link>
|
||||
</Link>
|
||||
} alignRight
|
||||
>
|
||||
|
|
|
@ -7,6 +7,7 @@ import Link from 'next/link'
|
|||
import { timeSince } from '../lib/time'
|
||||
import EmailIcon from '../svgs/mail-open-line.svg'
|
||||
import Share from './share'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
|
||||
export default function ItemJob ({ item, toc, rank, children }) {
|
||||
const isEmail = Yup.string().email().isValidSync(item.url)
|
||||
|
@ -52,7 +53,9 @@ export default function ItemJob ({ item, toc, rank, children }) {
|
|||
<span> \ </span>
|
||||
<span>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
<a className='d-inline-flex align-items-center'>
|
||||
@{item.user.name}<CowboyHat className='ml-1 fill-grey' streak={item.user.streak} height={12} width={12} />
|
||||
</a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
|
|
|
@ -19,6 +19,7 @@ import Flag from '../svgs/flag-fill.svg'
|
|||
import Share from './share'
|
||||
import { abbrNum } from '../lib/format'
|
||||
import { DeleteDropdown } from './delete'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
|
||||
export function SearchTitle ({ title }) {
|
||||
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||
|
@ -115,7 +116,7 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
|
|||
<span> \ </span>
|
||||
<span>
|
||||
<Link href={`/${item.user.name}`} passHref>
|
||||
<a>@{item.user.name}</a>
|
||||
<a className='d-inline-flex align-items-center'>@{item.user.name}<CowboyHat className='ml-1 fill-grey' streak={item.user.streak} height={12} width={12} /></a>
|
||||
</Link>
|
||||
<span> </span>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
|
|
|
@ -12,6 +12,8 @@ import Link from 'next/link'
|
|||
import Check from '../svgs/check-double-line.svg'
|
||||
import HandCoin from '../svgs/hand-coin-fill.svg'
|
||||
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
||||
import CowboyHatIcon from '../svgs/cowboy.svg'
|
||||
import BaldIcon from '../svgs/bald.svg'
|
||||
|
||||
// TODO: oh man, this is a mess ... each notification type should just be a component ...
|
||||
function Notification ({ n }) {
|
||||
|
@ -20,7 +22,7 @@ function Notification ({ n }) {
|
|||
<div
|
||||
className='clickToContext'
|
||||
onClick={e => {
|
||||
if (n.__typename === 'Earn' || n.__typename === 'Referral') {
|
||||
if (n.__typename === 'Earn' || n.__typename === 'Referral' || n.__typename === 'Streak') {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -103,35 +105,76 @@ function Notification ({ n }) {
|
|||
<Check className='fill-info mr-1' />{n.earnedSats} sats were deposited in your account
|
||||
<small className='text-muted ml-1'>{timeSince(new Date(n.sortTime))}</small>
|
||||
</div>)
|
||||
: (
|
||||
<>
|
||||
{n.__typename === 'Votification' &&
|
||||
<small className='font-weight-bold text-success ml-2'>
|
||||
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
||||
</small>}
|
||||
{n.__typename === 'Mention' &&
|
||||
<small className='font-weight-bold text-info ml-2'>
|
||||
you were mentioned in
|
||||
</small>}
|
||||
{n.__typename === 'JobChanged' &&
|
||||
<small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
|
||||
{n.item.status === 'ACTIVE'
|
||||
? 'your job is active again'
|
||||
: (n.item.status === 'NOSATS'
|
||||
? 'your job promotion ran out of sats'
|
||||
: 'your job has been stopped')}
|
||||
</small>}
|
||||
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
||||
{n.item.isJob
|
||||
? <ItemJob item={n.item} />
|
||||
: n.item.title
|
||||
? <Item item={n.item} />
|
||||
: (
|
||||
<div className='pb-2'>
|
||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||
</div>)}
|
||||
</div>
|
||||
</>)}
|
||||
: n.__typename === 'Streak'
|
||||
? <Streak n={n} />
|
||||
: (
|
||||
<>
|
||||
{n.__typename === 'Votification' &&
|
||||
<small className='font-weight-bold text-success ml-2'>
|
||||
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
||||
</small>}
|
||||
{n.__typename === 'Mention' &&
|
||||
<small className='font-weight-bold text-info ml-2'>
|
||||
you were mentioned in
|
||||
</small>}
|
||||
{n.__typename === 'JobChanged' &&
|
||||
<small className={`font-weight-bold text-${n.item.status === 'ACTIVE' ? 'success' : 'boost'} ml-1`}>
|
||||
{n.item.status === 'ACTIVE'
|
||||
? 'your job is active again'
|
||||
: (n.item.status === 'NOSATS'
|
||||
? 'your job promotion ran out of sats'
|
||||
: 'your job has been stopped')}
|
||||
</small>}
|
||||
<div className={n.__typename === 'Votification' || n.__typename === 'Mention' || n.__typename === 'JobChanged' ? '' : 'py-2'}>
|
||||
{n.item.isJob
|
||||
? <ItemJob item={n.item} />
|
||||
: n.item.title
|
||||
? <Item item={n.item} />
|
||||
: (
|
||||
<div className='pb-2'>
|
||||
<Comment item={n.item} noReply includeParent rootText={n.__typename === 'Reply' ? 'replying on:' : undefined} clickToContext />
|
||||
</div>)}
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Streak ({ n }) {
|
||||
function blurb (n) {
|
||||
const index = Number(n.id) % 6
|
||||
const FOUND_BLURBS = [
|
||||
'The harsh frontier is no place for the unprepared. This hat will protect you from the sun, dust, and other elements Mother Nature throws your way.',
|
||||
'A cowboy is nothing without a cowboy hat. Take good care of it, and it will protect you from the sun, dust, and other elements on your journey.',
|
||||
"This is not just a hat, it's a matter of survival. Take care of this essential tool, and it will shield you from the scorching sun and the elements.",
|
||||
"A cowboy hat isn't just a fashion statement. It's your last defense against the unforgiving elements of the Wild West. Hang onto it tight.",
|
||||
"A good cowboy hat is worth its weight in gold, shielding you from the sun, wind, and dust of the western frontier. Don't lose it.",
|
||||
'Your cowboy hat is the key to your survival in the wild west. Treat it with respect and it will protect you from the elements.'
|
||||
]
|
||||
|
||||
const LOST_BLURBS = [
|
||||
'your cowboy hat was taken by the wind storm that blew in from the west. No worries, a true cowboy always finds another hat.',
|
||||
"you left your trusty cowboy hat in the saloon before leaving town. You'll need a replacement for the long journey west.",
|
||||
'you lost your cowboy hat in a wild shoot-out on the outskirts of town. Tough luck, tIme to start searching for another one.',
|
||||
'you ran out of food and had to trade your hat for supplies. Better start looking for another hat.',
|
||||
"your hat was stolen by a mischievous prairie dog. You won't catch the dog, but you can always find another hat.",
|
||||
'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.'
|
||||
]
|
||||
|
||||
if (n.days) {
|
||||
return `After ${n.days} days, ` + LOST_BLURBS[index]
|
||||
}
|
||||
|
||||
return FOUND_BLURBS[index]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='d-flex font-weight-bold ml-2 py-1'>
|
||||
<div style={{ fontSize: '2rem' }}>{n.days ? <BaldIcon className='fill-grey' height={40} width={40} /> : <CowboyHatIcon className='fill-grey' height={40} width={40} />}</div>
|
||||
<div className='ml-1 p-1'>
|
||||
you {n.days ? 'lost your' : 'found a'} cowboy hat
|
||||
<div><small style={{ lineHeight: '140%', display: 'inline-block' }}>{blurb(n)}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import LightningIcon from '../svgs/bolt.svg'
|
|||
import ModalButton from './modal-button'
|
||||
import { encodeLNUrl } from '../lib/lnurl'
|
||||
import Avatar from './avatar'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
|
||||
export default function UserHeader ({ user }) {
|
||||
const [editting, setEditting] = useState(false)
|
||||
|
@ -132,7 +133,7 @@ export default function UserHeader ({ user }) {
|
|||
)
|
||||
: (
|
||||
<div className='d-flex align-items-center mb-2'>
|
||||
<div className={styles.username}>@{user.name}</div>
|
||||
<div className={styles.username}>@{user.name}<CowboyHat className='' streak={user.streak} badge /></div>
|
||||
{isMe &&
|
||||
<Button className='py-0' style={{ lineHeight: '1.25' }} variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import { Image } from 'react-bootstrap'
|
||||
import { abbrNum } from '../lib/format'
|
||||
import CowboyHat from './cowboy-hat'
|
||||
import styles from './item.module.css'
|
||||
import userStyles from './user-header.module.css'
|
||||
|
||||
|
@ -18,8 +19,8 @@ export default function UserList ({ users }) {
|
|||
</Link>
|
||||
<div className={styles.hunk}>
|
||||
<Link href={`/${user.name}`} passHref>
|
||||
<a className={`${styles.title} text-reset`}>
|
||||
@{user.name}
|
||||
<a className={`${styles.title} d-inline-flex align-items-center text-reset`}>
|
||||
@{user.name}<CowboyHat className='ml-1 fill-grey' height={14} width={14} streak={user.streak} />
|
||||
</a>
|
||||
</Link>
|
||||
<div className={styles.other}>
|
||||
|
|
|
@ -9,6 +9,7 @@ export const COMMENT_FIELDS = gql`
|
|||
text
|
||||
user {
|
||||
name
|
||||
streak
|
||||
id
|
||||
}
|
||||
sats
|
||||
|
@ -29,6 +30,7 @@ export const COMMENT_FIELDS = gql`
|
|||
bountyPaidTo
|
||||
user {
|
||||
name
|
||||
streak
|
||||
id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export const INVITE_FIELDS = gql`
|
|||
revoked
|
||||
user {
|
||||
name
|
||||
streak
|
||||
id
|
||||
}
|
||||
poor
|
||||
|
|
|
@ -11,10 +11,12 @@ export const ITEM_FIELDS = gql`
|
|||
url
|
||||
user {
|
||||
name
|
||||
streak
|
||||
id
|
||||
}
|
||||
fwdUser {
|
||||
name
|
||||
streak
|
||||
id
|
||||
}
|
||||
sats
|
||||
|
@ -51,6 +53,7 @@ export const ITEM_FIELDS = gql`
|
|||
}
|
||||
user {
|
||||
name
|
||||
streak
|
||||
id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,11 @@ export const NOTIFICATIONS = gql`
|
|||
text
|
||||
}
|
||||
}
|
||||
... on Streak {
|
||||
id
|
||||
sortTime
|
||||
days
|
||||
}
|
||||
... on Earn {
|
||||
sortTime
|
||||
earnedSats
|
||||
|
|
|
@ -7,6 +7,7 @@ export const ME = gql`
|
|||
me {
|
||||
id
|
||||
name
|
||||
streak
|
||||
sats
|
||||
stacked
|
||||
freePosts
|
||||
|
@ -24,6 +25,7 @@ export const ME = gql`
|
|||
noteDeposits
|
||||
noteInvites
|
||||
noteJobIndicator
|
||||
noteCowboyHat
|
||||
hideInvoiceDesc
|
||||
hideFromTopUsers
|
||||
wildWestMode
|
||||
|
@ -44,6 +46,7 @@ export const SETTINGS_FIELDS = gql`
|
|||
noteDeposits
|
||||
noteInvites
|
||||
noteJobIndicator
|
||||
noteCowboyHat
|
||||
hideInvoiceDesc
|
||||
hideFromTopUsers
|
||||
nostrPubkey
|
||||
|
@ -72,12 +75,12 @@ gql`
|
|||
${SETTINGS_FIELDS}
|
||||
mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $noteItemSats: Boolean!,
|
||||
$noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
|
||||
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!,
|
||||
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!, $hideFromTopUsers: Boolean!,
|
||||
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrRelays: [String!]) {
|
||||
setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency,
|
||||
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
|
||||
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
|
||||
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
|
||||
noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc, hideFromTopUsers: $hideFromTopUsers,
|
||||
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrRelays: $nostrRelays) {
|
||||
...SettingsFields
|
||||
}
|
||||
|
@ -103,6 +106,7 @@ gql`
|
|||
query searchUsers($q: String!, $limit: Int, $similarity: Float) {
|
||||
searchUsers(q: $q, limit: $limit, similarity: $similarity) {
|
||||
name
|
||||
streak
|
||||
photoId
|
||||
stacked
|
||||
spent
|
||||
|
@ -117,6 +121,7 @@ export const USER_FIELDS = gql`
|
|||
id
|
||||
createdAt
|
||||
name
|
||||
streak
|
||||
nitems
|
||||
ncomments
|
||||
stacked
|
||||
|
@ -133,6 +138,7 @@ export const TOP_USERS = gql`
|
|||
topUsers(cursor: $cursor, when: $when, sort: $sort) {
|
||||
users {
|
||||
name
|
||||
streak
|
||||
photoId
|
||||
stacked(when: $when)
|
||||
spent(when: $when)
|
||||
|
|
|
@ -102,6 +102,7 @@ export default function Settings ({ data: { settings } }) {
|
|||
noteDeposits: settings?.noteDeposits,
|
||||
noteInvites: settings?.noteInvites,
|
||||
noteJobIndicator: settings?.noteJobIndicator,
|
||||
noteCowboyHat: settings?.noteCowboyHat,
|
||||
hideInvoiceDesc: settings?.hideInvoiceDesc,
|
||||
hideFromTopUsers: settings?.hideFromTopUsers,
|
||||
wildWestMode: settings?.wildWestMode,
|
||||
|
@ -216,6 +217,11 @@ export default function Settings ({ data: { settings } }) {
|
|||
<Checkbox
|
||||
label='there is a new job'
|
||||
name='noteJobIndicator'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label='I find or lose a cowboy hat'
|
||||
name='noteCowboyHat'
|
||||
/>
|
||||
<div className='form-label'>privacy</div>
|
||||
<Checkbox
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "Streak" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"startedAt" DATE NOT NULL,
|
||||
"endedAt" DATE,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Streak.userId_index" ON "Streak"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Streak.startedAt_userId_unique" ON "Streak"("startedAt", "userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Streak" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "noteCowboyHat" BOOLEAN NOT NULL DEFAULT true;
|
|
@ -70,6 +70,7 @@ model User {
|
|||
noteDeposits Boolean @default(true)
|
||||
noteInvites Boolean @default(true)
|
||||
noteJobIndicator Boolean @default(true)
|
||||
noteCowboyHat Boolean @default(true)
|
||||
|
||||
// privacy settings
|
||||
hideInvoiceDesc Boolean @default(false)
|
||||
|
@ -84,12 +85,26 @@ model User {
|
|||
PollVote PollVote[]
|
||||
Donation Donation[]
|
||||
ReferralAct ReferralAct[]
|
||||
Streak Streak[]
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([inviteId])
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model Streak {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
startedAt DateTime @db.Date
|
||||
endedAt DateTime? @db.Date
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([startedAt, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model NostrRelay {
|
||||
addr String @id
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288.69 325.52">
|
||||
<path class="cls-1" d="m275.55,123.09c-2.36-8.21-11.36-2.84-11.36-2.84,0,0-.16-.79-1.42-15.31-1.26-14.52-5.21-41.81-34.71-68.16-29.51-26.36-77.51-25.09-77.51-25.09h-12.28c-12.27-.02-38.34,4.73-59.96,16.56-21.61,11.84-33.29,34.56-39.6,51.13-6.31,16.57-8.52,47.18-8.52,47.18,0,0-6.31-3.32-8.84-2.05-2.52,1.26-6.94,3.15-2.52,21.46,4.42,18.3,15.15,30.92,18.14,36.76,3,5.84,8.52,4.42,9.63,3.47,1.1-.94,1.58-3.78,1.58-3.78,0,0,1.1,29.35,2.21,36.29,1.1,6.94,5.2,19.56,8.67,24.14,3.48,4.58,6.79,8.21,6.79,8.21l6.31-.32s-.32,3.47,0,6.15c.32,2.69,2.05,5.84,4.1,7.74,2.05,1.89,23.36,26.03,30.61,30.92,7.26,4.89,13.73,10.57,44,10.57s36.95-8.2,40.74-11.2,20.04-18.46,29.19-29.03c9.15-10.57,21.93-26.98,24.93-35.66s5.37-45.45,5.37-45.45c0,0,.63,1.27,2.68,1.11,2.05-.16,5.84-3.47,7.73-7.58,1.89-4.1,2.68-4.1,8.68-20.19,5.99-16.1,7.73-26.83,5.36-35.03Zm-200.2-.65c-1.08.06-1.56-.72-1.8-1.32-.24-.59.08-2.79.77-3.71s4.37-6.44,15.99-12.77c11.62-6.33,20.02-4.83,20.02-4.83,0,0,2.8.45,5.31,1.11,2.51.65,4.31,2.99,4.73,3.76.42.78.59,1.62,0,2.75-.6,1.14-1.92,1.26-8.37,1.44-6.46.18-11.9,1.49-17.7,3.65-5.8,2.15-10.58,5.91-13.69,7.95-3.11,2.03-4.19,1.91-5.26,1.97Zm27.73,47.96c-8.57,0-15.52-7.91-15.52-17.66s6.95-17.66,15.52-17.66,15.52,7.91,15.52,17.66-6.95,17.66-15.52,17.66Zm88.88,74.66c-3.22,2.29-7.61,2.6-18.21,0-10.59-2.6-33.26-1.24-36.17-1.05-2.91.18-9.47,1.18-12.07,1.79-2.6.62-4.96,1.51-8.92,1.1-3.97-.41-6.57-3.82-6.94-7.17-.37-3.34.62-9.84,5.58-17.96,4.95-8.11,13.76-18.58,37.32-18.58,0,0,11.05-1.11,22.5,4.71,11.46,5.82,18.71,13.81,20.38,20.69,1.67,6.87-.25,14.18-3.47,16.47Zm7.17-74.66c-8.57,0-15.52-7.91-15.52-17.66s6.95-17.66,15.52-17.66,15.52,7.91,15.52,17.66-6.95,17.66-15.52,17.66Zm28.96-49.28c-.24.6-.72,1.38-1.8,1.32-1.07-.06-2.15.06-5.26-1.97-3.11-2.04-7.89-5.8-13.69-7.95-5.8-2.16-11.24-3.47-17.7-3.65-6.45-.18-7.77-.3-8.37-1.44-.59-1.13-.41-1.97,0-2.75.42-.77,2.22-3.11,4.73-3.76,2.51-.66,5.31-1.11,5.31-1.11,0,0,8.4-1.5,20.02,4.83,11.62,6.33,15.3,11.85,15.99,12.77s1.01,3.12.77,3.71Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,21 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="80 200 450 400">
|
||||
<path class="st0" d="M499.9,315.6c-17.2-10.5-28.7,0.6-36.6,7.9c-7.9,7.2-26.6,30.8-36.6,38.1c-10,7.2-27.5,13.6-53.9,16.7
|
||||
c-26.3,3.1-50.5,1.4-47.8,1c2.7-0.4,10.6-2.2,12.4-3c1.8-0.9,11.3-4.3,33.7-11.9c22.4-7.6,36.5-17,39.4-18.7
|
||||
c2.9-1.7,5.9-3.9,5.4-5.2s-2.2-8.1-5.1-15.8c-2.9-7.7-8.5-29.4-10.1-34.2c-1.6-4.8-3-13.1-5.9-19.5s-4.4-21.3-11.3-38.5
|
||||
c-6.9-17.2-11.8-19.5-31.7-27.7c-19.8-8.2-57.8-6.9-57.8-6.9s-41.1,1.3-59.5,9.2c-18.4,7.9-22.8,19-23.4,20.6
|
||||
c-0.6,1.6-12.9,44.2-12.9,44.2l-27.4,93.1c0,0-6-5.6-10.7-10.8c-4.5-5-29.9-36.9-44-41.1c-14.1-4.2-20.4,0.8-26.2,5.5
|
||||
c-5.8,4.7-11.1,18.3-8.5,32.7c2.6,14.4,29.8,45.3,51,59.7c21.2,14.4,40.8,21.5,43.6,23.1c2.8,1.6,6.5,2.8,16,15.2
|
||||
s32.5,32.3,45.8,41.6c13.4,9.4,34.8,22.1,44.6,26.8c9.8,4.7,14.9,3.6,14.9,3.6c8.8,0,36.1-15.4,50.6-25.1
|
||||
c14.4-9.7,32.3-25.1,34.1-26.6c1.6-1.3,3.2-3.4,3.7-3.9c0.7-0.8,6-8.1,15.3-17s14.6-12.4,14.6-12.4l0.2-1.8c0,0,27-9.2,53.4-30.3
|
||||
c26.5-21.1,39.1-43.5,41.4-48.5C512.8,350.8,517.1,326,499.9,315.6L499.9,315.6z M366.5,413.2c-17,7.4-32,14.7-53.7,17.6
|
||||
c-21.7,2.9-39.3,0.3-57.4-5.8c-18.1-6.1-38.3-16.6-49.2-23.6c-10.9-7-22.7-16.6-22.7-16.6s74.7,8.7,110.6,8.4
|
||||
c36-0.3,71.5-2.9,83.3-4.2c11.8-1.3,32.8-4.2,32.8-4.2S383.5,405.7,366.5,413.2L366.5,413.2z"/>
|
||||
<path class="st0" d="M415.7,449.1c0,0.1-0.2,1.2-6,4.5c-6.3,3.6-21.6,22.6-25.8,27.2c-4.2,4.7-27.5,22.3-27.5,22.3
|
||||
S310.7,535,295.9,535s-43.1-21.3-58.4-31.3c-15.3-10-26.9-21.4-31.2-25.9c-4.2-4.5-13.8-15-20.5-21.7c-6.7-6.7-8.7-7-8.7-7
|
||||
s-0.4,2.3,0,5.1c0.4,2.8,3.5,16.6,5.1,20.1s4.4,8.3,4.4,8.3s13.1,49.1,15.1,53.5s11.9,16.9,11.9,16.9l6,0.2l0.7,9
|
||||
c0,0,0.7,0.9,2.5,2.5c1.8,1.6,18.6,20.4,22.3,23.6c3.6,3.2,15.6,15.6,28.3,17.8c12.7,2.2,23.3,2,40.8,0.9
|
||||
c17.5-1.2,36.7-18.8,54.6-39.9c17.9-21.1,24.3-31.8,26.4-40.5s3.1-40.5,3.2-41.4c0.1-0.9,1.9,1.3,4.7,1.5c2.8,0.1,6.9-7.9,10.4-19.4
|
||||
C416.6,456,415.8,449.5,415.7,449.1L415.7,449.1z M318.4,556.4c-6.8,7-16.8,12-20.7,12s-11.4-2.5-19.9-10.2
|
||||
c-8.5-7.7-11.9-13-11.9-13s15.9,1.8,31,1.8s31-1.8,31-1.8S325.2,549.4,318.4,556.4L318.4,556.4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -10,6 +10,7 @@ const { earn } = require('./earn')
|
|||
const { ApolloClient, HttpLink, InMemoryCache } = require('@apollo/client')
|
||||
const { indexItem, indexAllItems } = require('./search')
|
||||
const { timestampItem } = require('./ots')
|
||||
const { computeStreaks } = require('./streak')
|
||||
const fetch = require('cross-fetch')
|
||||
|
||||
async function work () {
|
||||
|
@ -47,6 +48,7 @@ async function work () {
|
|||
await boss.work('indexAllItems', indexAllItems(args))
|
||||
await boss.work('auction', auction(args))
|
||||
await boss.work('earn', earn(args))
|
||||
await boss.work('streak', computeStreaks(args))
|
||||
|
||||
console.log('working jobs')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
function computeStreaks ({ models }) {
|
||||
return async function () {
|
||||
console.log('computing streaks')
|
||||
|
||||
// get all eligible users in the last day
|
||||
// if the user doesn't have an active streak, add one
|
||||
// if they have an active streak but didn't maintain it, end it
|
||||
await models.$executeRaw(
|
||||
`WITH day_streaks (id) AS (
|
||||
SELECT "userId"
|
||||
FROM
|
||||
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
|
||||
FROM "ItemAct"
|
||||
WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date
|
||||
GROUP BY "userId")
|
||||
UNION ALL
|
||||
(SELECT "userId", sats as sats_spent
|
||||
FROM "Donation"
|
||||
WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date
|
||||
)) spending
|
||||
GROUP BY "userId"
|
||||
HAVING sum(sats_spent) >= 100
|
||||
), existing_streaks (id) AS (
|
||||
SELECT "userId"
|
||||
FROM "Streak"
|
||||
WHERE "Streak"."endedAt" IS NULL
|
||||
), new_streaks (id) AS (
|
||||
SELECT day_streaks.id
|
||||
FROM day_streaks
|
||||
LEFT JOIN existing_streaks ON existing_streaks.id = day_streaks.id
|
||||
WHERE existing_streaks.id IS NULL
|
||||
), ending_streaks (id) AS (
|
||||
SELECT existing_streaks.id
|
||||
FROM existing_streaks
|
||||
LEFT JOIN day_streaks ON existing_streaks.id = day_streaks.id
|
||||
WHERE day_streaks.id IS NULL
|
||||
), streak_insert AS (
|
||||
INSERT INTO "Streak" ("userId", "startedAt", created_at, updated_at)
|
||||
SELECT id, (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, now_utc(), now_utc()
|
||||
FROM new_streaks
|
||||
)
|
||||
UPDATE "Streak"
|
||||
SET "endedAt" = (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, updated_at = now_utc()
|
||||
FROM ending_streaks
|
||||
WHERE ending_streaks.id = "Streak"."userId"`)
|
||||
|
||||
console.log('done computing streaks')
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { computeStreaks }
|
Loading…
Reference in New Issue