)
}
@@ -377,7 +382,7 @@ export function AnonDropdown ({ path }) {
- @anon
+ @anon
diff --git a/components/notifications.js b/components/notifications.js
index 22478990..7fb10dbe 100644
--- a/components/notifications.js
+++ b/components/notifications.js
@@ -14,6 +14,8 @@ import UserAdd from '@/svgs/user-add-fill.svg'
import { LOST_BLURBS, FOUND_BLURBS, UNKNOWN_LINK_REL } from '@/lib/constants'
import CowboyHatIcon from '@/svgs/cowboy.svg'
import BaldIcon from '@/svgs/bald.svg'
+import GunIcon from '@/svgs/revolver.svg'
+import HorseIcon from '@/svgs/horse.svg'
import { RootProvider } from './root'
import Alert from 'react-bootstrap/Alert'
import styles from './notifications.module.css'
@@ -39,6 +41,8 @@ import { useRetryCreateItem } from './use-item-submit'
import { payBountyCacheMods } from './pay-bounty'
import { useToast } from './toast'
import classNames from 'classnames'
+import HolsterIcon from '@/svgs/holster.svg'
+import SaddleIcon from '@/svgs/saddle.svg'
function Notification ({ n, fresh }) {
const type = n.__typename
@@ -168,23 +172,28 @@ const defaultOnClick = n => {
function Streak ({ n }) {
function blurb (n) {
- const index = Number(n.id) % Math.min(FOUND_BLURBS.length, LOST_BLURBS.length)
+ const type = n.type ?? 'COWBOY_HAT'
+ const index = Number(n.id) % Math.min(FOUND_BLURBS[type].length, LOST_BLURBS[type].length)
if (n.days) {
return `After ${numWithUnits(n.days, {
abbreviate: false,
unitSingular: 'day',
unitPlural: 'days'
- })}, ` + LOST_BLURBS[index]
+ })}, ` + LOST_BLURBS[type][index]
}
- return FOUND_BLURBS[index]
+ return FOUND_BLURBS[type][index]
}
+ const Icon = n.days
+ ? n.type === 'GUN' ? HolsterIcon : n.type === 'HORSE' ? SaddleIcon : BaldIcon
+ : n.type === 'GUN' ? GunIcon : n.type === 'HORSE' ? HorseIcon : CowboyHatIcon
+
return (
-
{n.days ? : }
+
- you {n.days ? 'lost your' : 'found a'} cowboy hat
+ you {n.days ? 'lost your' : 'found a'} {n.type.toLowerCase().replace('_', ' ')}
{blurb(n)}
diff --git a/components/territory-header.js b/components/territory-header.js
index f204dacc..6e63d392 100644
--- a/components/territory-header.js
+++ b/components/territory-header.js
@@ -5,7 +5,7 @@ import Link from 'next/link'
import Text from './text'
import { numWithUnits } from '@/lib/format'
import styles from './item.module.css'
-import Hat from './hat'
+import Badges from './badge'
import { useMe } from './me'
import Share from './share'
import { gql, useMutation } from '@apollo/client'
@@ -41,7 +41,7 @@ export function TerritoryInfo ({ sub }) {
founded by
- @{sub.user.name}
+ @{sub.user.name} on {new Date(sub.createdAt).toDateString()}
diff --git a/components/user-header.js b/components/user-header.js
index f7971060..75409918 100644
--- a/components/user-header.js
+++ b/components/user-header.js
@@ -17,7 +17,7 @@ import Avatar from './avatar'
import { userSchema } from '@/lib/validate'
import { useShowModal } from './modal'
import { numWithUnits } from '@/lib/format'
-import Hat from './hat'
+import Badges from './badge'
import SubscribeUserDropdownItem from './subscribeUser'
import ActionDropdown from './action-dropdown'
import CodeIcon from '@/svgs/terminal-box-fill.svg'
@@ -178,7 +178,7 @@ function NymView ({ user, isMe, setEditting }) {
const { me } = useMe()
return (
-
@{user.name}
+
@{user.name}
{isMe &&
}
{!isMe && me && }
diff --git a/components/user-list.js b/components/user-list.js
index ea452e74..c2c2f953 100644
--- a/components/user-list.js
+++ b/components/user-list.js
@@ -7,7 +7,7 @@ import React, { useEffect, useMemo, useState } from 'react'
import { useQuery } from '@apollo/client'
import MoreFooter from './more-footer'
import { useData } from './use-data'
-import Hat from './hat'
+import Badges from './badge'
import { useMe } from './me'
import { MEDIA_URL } from '@/lib/constants'
import { NymActionDropdown } from '@/components/user-header'
@@ -57,7 +57,7 @@ export function UserListRow ({ user, stats, className, onNymClick, showHat = tru
style={{ textUnderlineOffset: '0.25em' }}
onClick={onNymClick}
>
- @{user.name}{showHat && }{selected && }
+ @{user.name}{showHat && }{selected && }
{stats && (
diff --git a/fragments/comments.js b/fragments/comments.js
index 2abe51ee..0813dc9c 100644
--- a/fragments/comments.js
+++ b/fragments/comments.js
@@ -1,6 +1,18 @@
import { gql } from '@apollo/client'
+// we can't import from users because of circular dependency
+const STREAK_FIELDS = gql`
+ fragment StreakFields on User {
+ optional {
+ streak
+ gunStreak
+ horseStreak
+ }
+ }
+`
+
export const COMMENT_FIELDS = gql`
+ ${STREAK_FIELDS}
fragment CommentFields on Item {
id
position
@@ -11,10 +23,8 @@ export const COMMENT_FIELDS = gql`
user {
id
name
- optional {
- streak
- }
meMute
+ ...StreakFields
}
sats
meAnonSats @client
@@ -45,6 +55,7 @@ export const COMMENT_FIELDS = gql`
`
export const COMMENTS_ITEM_EXT_FIELDS = gql`
+ ${STREAK_FIELDS}
fragment CommentItemExtFields on Item {
text
root {
@@ -61,10 +72,8 @@ export const COMMENTS_ITEM_EXT_FIELDS = gql`
}
user {
name
- optional {
- streak
- }
id
+ ...StreakFields
}
}
}`
diff --git a/fragments/invites.js b/fragments/invites.js
index 7642aa81..038ba53f 100644
--- a/fragments/invites.js
+++ b/fragments/invites.js
@@ -1,6 +1,8 @@
import { gql } from '@apollo/client'
+import { STREAK_FIELDS } from './users'
export const INVITE_FIELDS = gql`
+ ${STREAK_FIELDS}
fragment InviteFields on Invite {
id
createdAt
@@ -14,9 +16,7 @@ export const INVITE_FIELDS = gql`
user {
id
name
- optional {
- streak
- }
+ ...StreakFields
}
poor
}
diff --git a/fragments/items.js b/fragments/items.js
index 195693d7..6e1a9f40 100644
--- a/fragments/items.js
+++ b/fragments/items.js
@@ -1,7 +1,19 @@
import { gql } from '@apollo/client'
import { COMMENTS } from './comments'
+// we can't import from users because of circular dependency
+const STREAK_FIELDS = gql`
+ fragment StreakFields on User {
+ optional {
+ streak
+ gunStreak
+ horseStreak
+ }
+ }
+`
+
export const ITEM_FIELDS = gql`
+ ${STREAK_FIELDS}
fragment ItemFields on Item {
id
parentId
@@ -12,10 +24,8 @@ export const ITEM_FIELDS = gql`
user {
id
name
- optional {
- streak
- }
meMute
+ ...StreakFields
}
sub {
name
@@ -69,6 +79,7 @@ export const ITEM_FIELDS = gql`
export const ITEM_FULL_FIELDS = gql`
${ITEM_FIELDS}
+ ${STREAK_FIELDS}
fragment ItemFullFields on Item {
...ItemFields
text
@@ -82,9 +93,7 @@ export const ITEM_FULL_FIELDS = gql`
user {
id
name
- optional {
- streak
- }
+ ...StreakFields
}
sub {
name
diff --git a/fragments/notifications.js b/fragments/notifications.js
index 1b09f081..ce588ccc 100644
--- a/fragments/notifications.js
+++ b/fragments/notifications.js
@@ -86,6 +86,7 @@ export const NOTIFICATIONS = gql`
id
sortTime
days
+ type
}
... on Earn {
id
diff --git a/fragments/subs.js b/fragments/subs.js
index c9c4423e..8b84f8d7 100644
--- a/fragments/subs.js
+++ b/fragments/subs.js
@@ -2,6 +2,17 @@ import { gql } from '@apollo/client'
import { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items'
import { COMMENTS_ITEM_EXT_FIELDS } from './comments'
+// we can't import from users because of circular dependency
+const STREAK_FIELDS = gql`
+ fragment StreakFields on User {
+ optional {
+ streak
+ gunStreak
+ horseStreak
+ }
+ }
+`
+
export const SUB_FIELDS = gql`
fragment SubFields on Sub {
name
@@ -26,15 +37,13 @@ export const SUB_FIELDS = gql`
export const SUB_FULL_FIELDS = gql`
${SUB_FIELDS}
-
+ ${STREAK_FIELDS}
fragment SubFullFields on Sub {
...SubFields
user {
name
id
- optional {
- streak
- }
+ ...StreakFields
}
}`
diff --git a/fragments/users.js b/fragments/users.js
index 8e0f9203..26d92144 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -3,47 +3,58 @@ import { COMMENTS, COMMENTS_ITEM_EXT_FIELDS } from './comments'
import { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items'
import { SUB_FULL_FIELDS } from './subs'
-export const ME = gql`
- {
- me {
- id
- name
- bioId
- photoId
- privates {
- autoDropBolt11s
- diagnostics
- noReferralLinks
- fiatCurrency
- autoWithdrawMaxFeePercent
- autoWithdrawThreshold
- withdrawMaxFeeDefault
- satsFilter
- hideFromTopUsers
- hideWalletBalance
- hideWelcomeBanner
- imgproxyOnly
- showImagesAndVideos
- nostrCrossposting
- sats
- tipDefault
- tipRandom
- tipRandomMin
- tipRandomMax
- tipPopover
- turboTipping
- zapUndos
- upvotePopover
- wildWestMode
- disableFreebies
- }
- optional {
- isContributor
- stacked
- streak
- }
+export const STREAK_FIELDS = gql`
+ fragment StreakFields on User {
+ optional {
+ streak
+ gunStreak
+ horseStreak
}
- }`
+ }
+`
+
+export const ME = gql`
+${STREAK_FIELDS}
+{
+ me {
+ id
+ name
+ bioId
+ photoId
+ privates {
+ autoDropBolt11s
+ diagnostics
+ noReferralLinks
+ fiatCurrency
+ autoWithdrawMaxFeePercent
+ autoWithdrawThreshold
+ withdrawMaxFeeDefault
+ satsFilter
+ hideFromTopUsers
+ hideWalletBalance
+ hideWelcomeBanner
+ imgproxyOnly
+ showImagesAndVideos
+ nostrCrossposting
+ sats
+ tipDefault
+ tipRandom
+ tipRandomMin
+ tipRandomMax
+ tipPopover
+ turboTipping
+ zapUndos
+ upvotePopover
+ wildWestMode
+ disableFreebies
+ }
+ optional {
+ isContributor
+ stacked
+ }
+ ...StreakFields
+ }
+}`
export const SETTINGS_FIELDS = gql`
fragment SettingsFields on User {
@@ -101,61 +112,52 @@ export const SETTINGS_FIELDS = gql`
}`
export const SETTINGS = gql`
-${SETTINGS_FIELDS}
-query Settings {
- settings {
- ...SettingsFields
- }
-}`
+ ${SETTINGS_FIELDS}
+ query Settings {
+ settings {
+ ...SettingsFields
+ }
+ }`
-export const SET_SETTINGS =
-gql`
-${SETTINGS_FIELDS}
-mutation setSettings($settings: SettingsInput!) {
- setSettings(settings: $settings) {
- ...SettingsFields
- }
-}
-`
+export const SET_SETTINGS = gql`
+ ${SETTINGS_FIELDS}
+ mutation setSettings($settings: SettingsInput!) {
+ setSettings(settings: $settings) {
+ ...SettingsFields
+ }
+ }`
-export const DELETE_WALLET =
-gql`
-mutation removeWallet {
- removeWallet
-}
-`
+export const DELETE_WALLET = gql`
+ mutation removeWallet {
+ removeWallet
+ }`
-export const NAME_QUERY =
-gql`
+export const NAME_QUERY = gql`
query nameAvailable($name: String!) {
nameAvailable(name: $name)
- }
-`
+ }`
-export const NAME_MUTATION =
-gql`
+export const NAME_MUTATION = gql`
mutation setName($name: String!) {
setName(name: $name)
}
`
-export const WELCOME_BANNER_MUTATION =
-gql`
+export const WELCOME_BANNER_MUTATION = gql`
mutation hideWelcomeBanner {
hideWelcomeBanner
}
`
-export const USER_SUGGESTIONS =
-gql`
+export const USER_SUGGESTIONS = gql`
query userSuggestions($q: String!, $limit: Limit) {
userSuggestions(q: $q, limit: $limit) {
name
}
}`
-export const USER_SEARCH =
-gql`
+export const USER_SEARCH = gql`
+${STREAK_FIELDS}
query searchUsers($q: String!, $limit: Limit, $similarity: Float) {
searchUsers(q: $q, limit: $limit, similarity: $similarity) {
id
@@ -165,15 +167,16 @@ gql`
nposts
optional {
- streak
stacked
spent
referrals
}
+ ...StreakFields
}
}`
export const USER_FIELDS = gql`
+ ${STREAK_FIELDS}
fragment UserFields on User {
id
name
@@ -187,16 +190,17 @@ export const USER_FIELDS = gql`
optional {
stacked
- streak
maxStreak
isContributor
githubId
nostrAuthPubkey
twitterId
}
+ ...StreakFields
}`
export const MY_SUBSCRIBED_USERS = gql`
+ ${STREAK_FIELDS}
query MySubscribedUsers($cursor: String) {
mySubscribedUsers(cursor: $cursor) {
users {
@@ -207,9 +211,7 @@ export const MY_SUBSCRIBED_USERS = gql`
meSubscriptionComments
meMute
- optional {
- streak
- }
+ ...StreakFields
}
cursor
}
@@ -217,6 +219,7 @@ export const MY_SUBSCRIBED_USERS = gql`
`
export const MY_MUTED_USERS = gql`
+ ${STREAK_FIELDS}
query MyMutedUsers($cursor: String) {
myMutedUsers(cursor: $cursor) {
users {
@@ -226,10 +229,7 @@ export const MY_MUTED_USERS = gql`
meSubscriptionPosts
meSubscriptionComments
meMute
-
- optional {
- streak
- }
+ ...StreakFields
}
cursor
}
@@ -237,6 +237,7 @@ export const MY_MUTED_USERS = gql`
`
export const TOP_USERS = gql`
+ ${STREAK_FIELDS}
query TopUsers($cursor: String, $when: String, $from: String, $to: String, $by: String, ) {
topUsers(cursor: $cursor, when: $when, from: $from, to: $to, by: $by) {
users {
@@ -247,11 +248,11 @@ export const TOP_USERS = gql`
nposts(when: $when, from: $from, to: $to)
optional {
- streak
stacked(when: $when, from: $from, to: $to)
spent(when: $when, from: $from, to: $to)
referrals(when: $when, from: $from, to: $to)
}
+ ...StreakFields
}
cursor
}
@@ -259,6 +260,7 @@ export const TOP_USERS = gql`
`
export const TOP_COWBOYS = gql`
+ ${STREAK_FIELDS}
query TopCowboys($cursor: String) {
topCowboys(cursor: $cursor) {
users {
@@ -269,11 +271,11 @@ export const TOP_COWBOYS = gql`
nposts(when: "forever")
optional {
- streak
stacked(when: "forever")
spent(when: "forever")
referrals(when: "forever")
}
+ ...StreakFields
}
cursor
}
diff --git a/lib/constants.js b/lib/constants.js
index 76640fa2..ad327236 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -119,22 +119,54 @@ export const TERRITORY_PERIOD_COST = (billingType) => {
}
}
-export 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.'
-]
-export 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.'
-]
+export const FOUND_BLURBS = {
+ COWBOY_HAT: [
+ '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.'
+ ],
+ GUN: [
+ 'A gun is a tool, and like all tools, it can be used for good or evil. Use it wisely.',
+ 'In these wild lands, a gun can be your best friend or worst enemy. Handle it with care and respect.',
+ 'This firearm is more than just a weapon; it\'s your lifeline in the untamed West. Treat it well.',
+ 'A gun in the right hands can mean the difference between life and death. Make sure your aim is true.',
+ 'This gun is your ticket to survival in the frontier. Treat it with care and respect.'
+ ],
+ HORSE: [
+ 'A loyal steed is worth its weight in gold. Treat this horse well, and it\'ll carry you through thick and thin.',
+ 'From dusty trails to raging rivers, this horse will be your constant companion. Treat it with respect.',
+ 'This horse has chosen you as much as you\'ve chosen it. Together, you\'ll forge a path through the frontier.',
+ 'Your new horse is both transportation and friend. In the loneliness of the prairie, you\'ll be glad for its company.',
+ 'Swift hooves and a sturdy back - this horse has the spirit of the West. Ride it with pride and care.'
+ ]
+}
+export const LOST_BLURBS = {
+ COWBOY_HAT: [
+ '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.'
+ ],
+ GUN: [
+ 'your gun slipped from its holster while crossing a treacherous ravine. It\'s lost to the depths, but a new one awaits in the next town.',
+ 'you were forced to toss your gun to distract a grizzly bear. It saved your life, but now you\'ll need to find a new firearm.',
+ 'your gun was confiscated by the local sheriff after a misunderstanding. Time to clear your name and find a new sidearm.',
+ 'your trusty six-shooter jammed beyond repair during a shootout. Luckily you survived, but now you need a replacement.',
+ 'you traded your gun for medicine to save a sick child. A noble deed, but the frontier is unforgiving - best find a new weapon soon.'
+ ],
+ HORSE: [
+ 'your horse spooked at a rattlesnake and bolted into the night. You\'ll need to find a new steed to continue your journey.',
+ 'you lost your horse in a game of chance. The stakes were high, but now you\'re on foot until you can acquire a new mount.',
+ 'your horse was stolen by bandits while you slept. Time to track down a new companion for the long road ahead.',
+ 'your loyal steed fell ill and you had to leave it at a ranch to recover. You\'ll need a new horse to press on with your travels.',
+ 'your horse was requisitioned by the cavalry for an urgent mission. They left you with a voucher, but you\'ll need to find a new mount soon.'
+ ]
+}
export const ADMIN_ITEMS = [
// FAQ, old privacy policy, changelog, content guidelines, tos, new privacy policy, copyright policy
diff --git a/lib/webPush.js b/lib/webPush.js
index 3cb2b3c8..5d054aed 100644
--- a/lib/webPush.js
+++ b/lib/webPush.js
@@ -363,14 +363,14 @@ export async function notifyWithdrawal (userId, wdrwl) {
}
export async function notifyNewStreak (userId, streak) {
- const index = streak.id % FOUND_BLURBS.length
- const blurb = FOUND_BLURBS[index]
+ const index = streak.id % FOUND_BLURBS[streak.type].length
+ const blurb = FOUND_BLURBS[streak.type][index]
try {
await sendUserNotification(userId, {
- title: 'you found a cowboy hat',
+ title: `you found a ${streak.type.toLowerCase().replace('_', ' ')}`,
body: blurb,
- tag: 'STREAK-FOUND'
+ tag: `STREAK-FOUND-${streak.type}`
})
} catch (err) {
console.error(err)
@@ -378,14 +378,14 @@ export async function notifyNewStreak (userId, streak) {
}
export async function notifyStreakLost (userId, streak) {
- const index = streak.id % LOST_BLURBS.length
- const blurb = LOST_BLURBS[index]
+ const index = streak.id % LOST_BLURBS[streak.type].length
+ const blurb = LOST_BLURBS[streak.type][index]
try {
await sendUserNotification(userId, {
- title: 'you lost your cowboy hat',
+ title: `you lost your ${streak.type.toLowerCase().replace('_', ' ')}`,
body: blurb,
- tag: 'STREAK-LOST'
+ tag: `STREAK-LOST-${streak.type}`
})
} catch (err) {
console.error(err)
diff --git a/pages/rewards/index.js b/pages/rewards/index.js
index 5db87856..164710f4 100644
--- a/pages/rewards/index.js
+++ b/pages/rewards/index.js
@@ -53,6 +53,8 @@ ${ITEM_FULL_FIELDS}
optional {
streak
+ gunStreak
+ horseStreak
stacked
spent
referrals
diff --git a/pages/settings/index.js b/pages/settings/index.js
index a5e72eb7..d0bcb10e 100644
--- a/pages/settings/index.js
+++ b/pages/settings/index.js
@@ -324,7 +324,7 @@ export default function Settings ({ ssrData }) {
groupClassName='mb-0'
/>
privacy
diff --git a/prisma/migrations/20241009200148_wallet_streaks/migration.sql b/prisma/migrations/20241009200148_wallet_streaks/migration.sql
new file mode 100644
index 00000000..4dc30058
--- /dev/null
+++ b/prisma/migrations/20241009200148_wallet_streaks/migration.sql
@@ -0,0 +1,27 @@
+-- CreateEnum
+CREATE TYPE "StreakType" AS ENUM ('COWBOY_HAT', 'GUN', 'HORSE');
+
+-- AlterTable
+ALTER TABLE "Streak" ADD COLUMN "type" "StreakType" NOT NULL DEFAULT 'COWBOY_HAT';
+
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "gunStreak" INTEGER,
+ADD COLUMN "horseStreak" INTEGER;
+
+-- CreateIndex
+CREATE INDEX "Streak_type_idx" ON "Streak"("type");
+
+-- CreateIndex
+CREATE INDEX "users_streak_idx" ON "users"("streak");
+
+-- CreateIndex
+CREATE INDEX "users_gunStreak_idx" ON "users"("gunStreak");
+
+-- CreateIndex
+CREATE INDEX "users_horseStreak_idx" ON "users"("horseStreak");
+
+-- DropIndex
+DROP INDEX "Streak.startedAt_userId_unique";
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Streak_startedAt_userId_type_key" ON "Streak"("startedAt", "userId", "type");
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 1a9a3795..7a629e44 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -74,6 +74,8 @@ model User {
slashtagId String? @unique(map: "users.slashtagId_unique")
noteCowboyHat Boolean @default(true)
streak Int?
+ gunStreak Int?
+ horseStreak Int?
subs String[]
hideCowboyHat Boolean @default(false)
Bookmarks Bookmark[]
@@ -138,6 +140,9 @@ model User {
@@index([photoId])
@@index([createdAt], map: "users.created_at_index")
@@index([inviteId], map: "users.inviteId_index")
+ @@index([streak])
+ @@index([gunStreak])
+ @@index([horseStreak])
@@map("users")
}
@@ -300,17 +305,25 @@ model Arc {
@@index([toId, fromId])
}
-model Streak {
- id Int @id @default(autoincrement())
- createdAt DateTime @default(now()) @map("created_at")
- updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
- startedAt DateTime @db.Date
- endedAt DateTime? @db.Date
- userId Int
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+enum StreakType {
+ COWBOY_HAT
+ GUN
+ HORSE
+}
- @@unique([startedAt, userId], map: "Streak.startedAt_userId_unique")
+model Streak {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
+ startedAt DateTime @db.Date
+ endedAt DateTime? @db.Date
+ userId Int
+ type StreakType @default(COWBOY_HAT)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([startedAt, userId, type])
@@index([userId], map: "Streak.userId_index")
+ @@index([type])
}
model NostrRelay {
diff --git a/scripts/newsletter.js b/scripts/newsletter.js
index 1238c77f..9a249ac9 100644
--- a/scripts/newsletter.js
+++ b/scripts/newsletter.js
@@ -32,6 +32,8 @@ query TopCowboys($cursor: String) {
name
optional {
streak
+ gunStreak
+ horseStreak
}
}
cursor
diff --git a/styles/globals.scss b/styles/globals.scss
index 15f0b5ee..86f33c5c 100644
--- a/styles/globals.scss
+++ b/styles/globals.scss
@@ -220,6 +220,10 @@ $zindex-sticky: 900;
scroll-margin-top: 60px;
}
+.ms-xs {
+ margin-left: 0.125rem;
+}
+
.text-monospace {
font-family: monospace;
}
diff --git a/svgs/holster.svg b/svgs/holster.svg
new file mode 100644
index 00000000..ecbd697d
--- /dev/null
+++ b/svgs/holster.svg
@@ -0,0 +1,3 @@
+
diff --git a/svgs/horse.svg b/svgs/horse.svg
new file mode 100644
index 00000000..fc04c67e
--- /dev/null
+++ b/svgs/horse.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/svgs/revolver.svg b/svgs/revolver.svg
new file mode 100644
index 00000000..d40f0ded
--- /dev/null
+++ b/svgs/revolver.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgs/saddle.svg b/svgs/saddle.svg
new file mode 100644
index 00000000..12009e09
--- /dev/null
+++ b/svgs/saddle.svg
@@ -0,0 +1,3 @@
+
diff --git a/worker/paidAction.js b/worker/paidAction.js
index ff6e5469..b2015c3b 100644
--- a/worker/paidAction.js
+++ b/worker/paidAction.js
@@ -144,9 +144,19 @@ export async function paidActionPaid ({ data: { invoiceId, ...args }, models, ln
}
await paidActions[dbInvoice.actionType].onPaid?.({ invoice: dbInvoice }, { models, tx, lnd })
+
+ // any paid action is eligible for a cowboy hat streak
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data)
- VALUES ('checkStreak', jsonb_build_object('id', ${dbInvoice.userId}))`
+ VALUES ('checkStreak', jsonb_build_object('id', ${dbInvoice.userId}, 'type', 'COWBOY_HAT'))`
+ if (dbInvoice.invoiceForward) {
+ // only paid forwards are eligible for a gun streak
+ await tx.$executeRaw`
+ INSERT INTO pgboss.job (name, data)
+ VALUES
+ ('checkStreak', jsonb_build_object('id', ${dbInvoice.userId}, 'type', 'GUN')),
+ ('checkStreak', jsonb_build_object('id', ${dbInvoice.invoiceForward.withdrawl.userId}, 'type', 'HORSE'))`
+ }
return {
confirmedAt: new Date(lndInvoice.confirmed_at),
diff --git a/worker/streak.js b/worker/streak.js
index 2b70f7c4..e38a3d87 100644
--- a/worker/streak.js
+++ b/worker/streak.js
@@ -1,124 +1,169 @@
import { notifyNewStreak, notifyStreakLost } from '@/lib/webPush'
+import { Prisma } from '@prisma/client'
-const STREAK_THRESHOLD = 100
+const COWBOY_HAT_STREAK_THRESHOLD = 100
+const GUN_STREAK_THRESHOLD = 1000
+const HORSE_STREAK_THRESHOLD = 1000
export async function computeStreaks ({ models }) {
// 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
- const endingStreaks = await models.$queryRaw`
- 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
- AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
- 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
+ for (const type of ['COWBOY_HAT', 'GUN', 'HORSE']) {
+ const endingStreaks = await models.$queryRaw`
+ WITH day_streaks (id) AS (
+ ${getStreakQuery(type)}
+ ), existing_streaks (id, started_at) AS (
+ SELECT "userId", "startedAt"
+ FROM "Streak"
+ WHERE "Streak"."endedAt" IS NULL
+ AND "type" = ${type}::"StreakType"
+ ), 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
+ ), extending_streaks (id, started_at) AS (
+ SELECT existing_streaks.id, existing_streaks.started_at
+ FROM existing_streaks
+ JOIN day_streaks ON existing_streaks.id = day_streaks.id
+ ),
+ -- a bunch of mutations
+ streak_insert AS (
+ INSERT INTO "Streak" ("userId", "startedAt", "type", created_at, updated_at)
+ SELECT id, (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date, ${type}::"StreakType", now_utc(), now_utc()
+ FROM new_streaks
+ ), user_update_new_streaks AS (
+ UPDATE users SET ${getStreakColumn(type)} = 1 FROM new_streaks WHERE new_streaks.id = users.id
+ ), user_update_end_streaks AS (
+ UPDATE users SET ${getStreakColumn(type)} = NULL FROM ending_streaks WHERE ending_streaks.id = users.id
+ ), user_update_extend_streaks AS (
+ UPDATE users
+ SET ${getStreakColumn(type)} = (now() AT TIME ZONE 'America/Chicago')::date - extending_streaks.started_at
+ FROM extending_streaks WHERE extending_streaks.id = users.id
)
- UNION ALL
- (SELECT "userId", floor(sum("SubAct".msats)/1000) as sats_spent
- FROM "SubAct"
- WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= (now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date
- AND "type" = 'BILLING'
- GROUP BY "userId")) spending
- GROUP BY "userId"
- HAVING sum(sats_spent) >= 100
- ), existing_streaks (id, started_at) AS (
- SELECT "userId", "startedAt"
- 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
- ), extending_streaks (id, started_at) AS (
- SELECT existing_streaks.id, existing_streaks.started_at
- FROM existing_streaks
- JOIN day_streaks ON existing_streaks.id = day_streaks.id
- ),
- -- a bunch of mutations
- 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
- ), user_update_new_streaks AS (
- UPDATE users SET streak = 1 FROM new_streaks WHERE new_streaks.id = users.id
- ), user_update_end_streaks AS (
- UPDATE users SET streak = NULL FROM ending_streaks WHERE ending_streaks.id = users.id
- ), user_update_extend_streaks AS (
- UPDATE users
- SET streak = (now() AT TIME ZONE 'America/Chicago')::date - extending_streaks.started_at
- FROM extending_streaks WHERE extending_streaks.id = users.id
- )
- 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" AND "endedAt" IS NULL
- RETURNING "Streak".id, ending_streaks."id" AS "userId"`
+ 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" AND "endedAt" IS NULL AND "type" = ${type}::"StreakType"
+ RETURNING "Streak".*`
- Promise.allSettled(endingStreaks.map(streak => notifyStreakLost(streak.userId, streak)))
+ Promise.allSettled(endingStreaks.map(streak => notifyStreakLost(streak.userId, streak)))
+ }
}
-export async function checkStreak ({ data: { id }, models }) {
+export async function checkStreak ({ data: { id, type = 'COWBOY_HAT' }, models }) {
// if user is actively streaking skip
- let streak = await models.streak.findFirst({
+ const user = await models.user.findUnique({
where: {
- userId: Number(id),
- endedAt: null
+ id: Number(id)
}
})
- if (streak) {
+ console.log('checking streak', id, type, isStreakActive(type, user))
+
+ if (isStreakActive(type, user)) {
return
}
- [streak] = await models.$queryRaw`
+ const [streak] = await models.$queryRaw`
WITH streak_started (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')::date
- AND "userId" = ${Number(id)}
- AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
- 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')::date
- AND "userId" = ${Number(id)}
- )
- UNION ALL
- (SELECT "userId", floor(sum("SubAct".msats)/1000) as sats_spent
- FROM "SubAct"
- WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= (now() AT TIME ZONE 'America/Chicago')::date
- AND "userId" = ${Number(id)}
- AND "type" = 'BILLING'
- GROUP BY "userId")
- ) spending
- GROUP BY "userId"
- HAVING sum(sats_spent) >= ${STREAK_THRESHOLD}
+ ${getStreakQuery(type, id)}
), user_start_streak AS (
- UPDATE users SET streak = 0 FROM streak_started WHERE streak_started.id = users.id
+ UPDATE users SET ${getStreakColumn(type)} = 0 FROM streak_started WHERE streak_started.id = users.id
)
- INSERT INTO "Streak" ("userId", "startedAt", created_at, updated_at)
- SELECT id, (now() AT TIME ZONE 'America/Chicago')::date, now_utc(), now_utc()
+ INSERT INTO "Streak" ("userId", "startedAt", "type", created_at, updated_at)
+ SELECT id, (now() AT TIME ZONE 'America/Chicago')::date, ${type}::"StreakType", now_utc(), now_utc()
FROM streak_started
- RETURNING "Streak".id`
+ RETURNING "Streak".*`
if (!streak) return
// new streak started for user
notifyNewStreak(id, streak)
}
+
+function getStreakQuery (type, userId) {
+ const dayFragment = userId
+ ? Prisma.sql`(now() AT TIME ZONE 'America/Chicago')::date`
+ : Prisma.sql`(now() AT TIME ZONE 'America/Chicago' - interval '1 day')::date`
+
+ if (type === 'GUN') {
+ return Prisma.sql`
+ SELECT "Invoice"."userId"
+ FROM "Invoice"
+ JOIN "InvoiceForward" ON "Invoice".id = "InvoiceForward"."invoiceId"
+ WHERE ("Invoice"."created_at" AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= ${dayFragment}
+ AND "Invoice"."actionState" = 'PAID'
+ ${userId ? Prisma.sql`AND "Invoice"."userId" = ${userId}` : Prisma.empty}
+ GROUP BY "Invoice"."userId"
+ HAVING sum(floor("Invoice"."msatsReceived"/1000)) >= ${GUN_STREAK_THRESHOLD}`
+ }
+
+ if (type === 'HORSE') {
+ return Prisma.sql`
+ SELECT "Withdrawl"."userId"
+ FROM "Withdrawl"
+ JOIN "InvoiceForward" ON "Withdrawl".id = "InvoiceForward"."withdrawlId"
+ JOIN "Invoice" ON "InvoiceForward"."invoiceId" = "Invoice".id
+ WHERE ("Withdrawl"."created_at" AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= ${dayFragment}
+ AND "Invoice"."actionState" = 'PAID'
+ ${userId ? Prisma.sql`AND "Withdrawl"."userId" = ${userId}` : Prisma.empty}
+ GROUP BY "Withdrawl"."userId"
+ HAVING sum(floor("Invoice"."msatsReceived"/1000)) >= ${HORSE_STREAK_THRESHOLD}`
+ }
+
+ return Prisma.sql`
+ 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 >= ${dayFragment}
+ AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
+ ${userId ? Prisma.sql`AND "userId" = ${userId}` : Prisma.empty}
+ 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 >= ${dayFragment}
+ ${userId ? Prisma.sql`AND "userId" = ${userId}` : Prisma.empty}
+ )
+ UNION ALL
+ (SELECT "userId", floor(sum("SubAct".msats)/1000) as sats_spent
+ FROM "SubAct"
+ WHERE (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= ${dayFragment}
+ ${userId ? Prisma.sql`AND "userId" = ${userId}` : Prisma.empty}
+ AND "type" = 'BILLING'
+ GROUP BY "userId")) spending
+ GROUP BY "userId"
+ HAVING sum(sats_spent) >= ${COWBOY_HAT_STREAK_THRESHOLD}`
+}
+
+function isStreakActive (type, user) {
+ if (type === 'GUN') {
+ return typeof user.gunStreak === 'number'
+ }
+
+ if (type === 'HORSE') {
+ return typeof user.horseStreak === 'number'
+ }
+
+ return typeof user.streak === 'number'
+}
+
+function getStreakColumn (type) {
+ if (type === 'GUN') {
+ return Prisma.sql`"gunStreak"`
+ }
+
+ if (type === 'HORSE') {
+ return Prisma.sql`"horseStreak"`
+ }
+
+ return Prisma.sql`"streak"`
+}
diff --git a/worker/thisDay.js b/worker/thisDay.js
index cb4dcfd3..cbdabaf0 100644
--- a/worker/thisDay.js
+++ b/worker/thisDay.js
@@ -177,6 +177,8 @@ const THIS_DAY = gql`
id
optional {
streak
+ gunStreak
+ horseStreak
}
}
ncomments(when: "custom", from: $from, to: $to)