support pinned posts + recurring pins
This commit is contained in:
parent
e950b0df7f
commit
c3e6627cea
|
@ -75,7 +75,7 @@ export default {
|
|||
Query: {
|
||||
moreItems: async (parent, { sort, cursor, name, within }, { me, models }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
let items; let user
|
||||
let items; let user; let pins
|
||||
|
||||
switch (sort) {
|
||||
case 'user':
|
||||
|
@ -92,6 +92,7 @@ export default {
|
|||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "userId" = $1 AND "parentId" IS NULL AND created_at <= $2
|
||||
AND "pinId" IS NULL
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $3
|
||||
LIMIT ${LIMIT}`, user.id, decodedCursor.time, decodedCursor.offset)
|
||||
|
@ -110,6 +111,7 @@ export default {
|
|||
FROM "Item"
|
||||
${timedLeftJoinSats(1)}
|
||||
WHERE "parentId" IS NULL AND created_at <= $1 AND created_at > $3
|
||||
AND "pinId" IS NULL
|
||||
${timedOrderBySats(1)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset, new Date(new Date() - 7))
|
||||
|
@ -121,10 +123,25 @@ export default {
|
|||
FROM "Item"
|
||||
${timedLeftJoinSats(1)}
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${timedOrderBySats(1)}
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
}
|
||||
|
||||
if (decodedCursor.offset === 0) {
|
||||
// get pins for the page and return those separately
|
||||
pins = await models.$queryRaw(`SELECT rank_filter.*
|
||||
FROM (
|
||||
${SELECT},
|
||||
rank() OVER (
|
||||
PARTITION BY "pinId"
|
||||
ORDER BY created_at DESC
|
||||
)
|
||||
FROM "Item"
|
||||
WHERE "pinId" IS NOT NULL
|
||||
) rank_filter WHERE RANK = 1`)
|
||||
}
|
||||
break
|
||||
case 'top':
|
||||
items = await models.$queryRaw(`
|
||||
|
@ -132,6 +149,7 @@ export default {
|
|||
FROM "Item"
|
||||
${timedLeftJoinSats(1)}
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
AND "pinId" IS NULL
|
||||
${topClause(within)}
|
||||
ORDER BY x.sats DESC NULLS LAST, created_at DESC
|
||||
OFFSET $2
|
||||
|
@ -141,7 +159,7 @@ export default {
|
|||
items = await models.$queryRaw(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE "parentId" IS NULL AND created_at <= $1
|
||||
WHERE "parentId" IS NULL AND created_at <= $1 AND "pinId" IS NULL
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
||||
|
@ -149,7 +167,8 @@ export default {
|
|||
}
|
||||
return {
|
||||
cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
|
||||
items
|
||||
items,
|
||||
pins
|
||||
}
|
||||
},
|
||||
moreFlatComments: async (parent, { cursor, name, sort, within }, { me, models }) => {
|
||||
|
@ -367,6 +386,18 @@ export default {
|
|||
},
|
||||
|
||||
Item: {
|
||||
position: async (item, args, { models }) => {
|
||||
if (!item.pinId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const pin = await models.pin.findUnique({ where: { id: item.pinId } })
|
||||
if (!pin) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pin.position
|
||||
},
|
||||
user: async (item, args, { models }) =>
|
||||
await models.user.findUnique({ where: { id: item.userId } }),
|
||||
ncomments: async (item, args, { models }) => {
|
||||
|
@ -591,7 +622,7 @@ function nestComments (flat, parentId) {
|
|||
// we have to do our own query because ltree is unsupported
|
||||
export const SELECT =
|
||||
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
|
||||
"Item".text, "Item".url, "Item"."userId", "Item"."parentId", ltree2text("Item"."path") AS "path"`
|
||||
"Item".text, "Item".url, "Item"."userId", "Item"."parentId", "Item"."pinId", ltree2text("Item"."path") AS "path"`
|
||||
|
||||
const LEFT_JOIN_SATS_SELECT = 'SELECT i.id, SUM(CASE WHEN "ItemAct".act = \'VOTE\' THEN "ItemAct".sats ELSE 0 END) as sats, SUM(CASE WHEN "ItemAct".act = \'BOOST\' THEN "ItemAct".sats ELSE 0 END) as boost'
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export default gql`
|
|||
type Items {
|
||||
cursor: String
|
||||
items: [Item!]!
|
||||
pins: [Item!]
|
||||
}
|
||||
|
||||
type Comments {
|
||||
|
@ -62,5 +63,6 @@ export default gql`
|
|||
ncomments: Int!
|
||||
comments: [Item!]!
|
||||
path: String
|
||||
position: Int
|
||||
}
|
||||
`
|
||||
|
|
|
@ -5,6 +5,7 @@ import UpVote from './upvote'
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import Countdown from './countdown'
|
||||
import { NOFOLLOW_LIMIT } from '../lib/constants'
|
||||
import Pin from '../svgs/pin.svg'
|
||||
|
||||
export default function Item ({ item, rank, children }) {
|
||||
const mine = item.mine
|
||||
|
@ -29,7 +30,7 @@ export default function Item ({ item, rank, children }) {
|
|||
</div>)
|
||||
: <div />}
|
||||
<div className={styles.item}>
|
||||
<UpVote item={item} className={styles.upvote} />
|
||||
{item.position ? <Pin width={24} height={24} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
|
||||
<div className={styles.hunk}>
|
||||
<div className={`${styles.main} flex-wrap ${wrap ? 'd-inline' : ''}`}>
|
||||
<Link href={`/items/${item.id}`} passHref>
|
||||
|
@ -47,8 +48,11 @@ export default function Item ({ item, rank, children }) {
|
|||
</>}
|
||||
</div>
|
||||
<div className={`${styles.other}`}>
|
||||
<span title={`${item.sats} upvotes \\ ${item.tips} tipped${item.meSats > 0 ? ` (${item.meSats} from me)` : ''}`}>{item.sats + item.tips} sats</span>
|
||||
<span> \ </span>
|
||||
{!item.position &&
|
||||
<>
|
||||
<span title={`${item.sats} upvotes \\ ${item.tips} tipped${item.meSats > 0 ? ` (${item.meSats} from me)` : ''}`}>{item.sats + item.tips} sats</span>
|
||||
<span> \ </span>
|
||||
</>}
|
||||
{item.boost > 0 &&
|
||||
<>
|
||||
<span>{item.boost} boost</span>
|
||||
|
|
|
@ -16,6 +16,12 @@
|
|||
flex: 1 0 128px;
|
||||
}
|
||||
|
||||
.pin {
|
||||
fill: var(--theme-body);
|
||||
stroke: var(--theme-color);
|
||||
margin-right: .2rem;
|
||||
}
|
||||
|
||||
.linkSmall {
|
||||
width: 128px;
|
||||
display: inline-block;
|
||||
|
|
|
@ -3,8 +3,9 @@ import Item, { ItemSkeleton } from './item'
|
|||
import styles from './items.module.css'
|
||||
import { MORE_ITEMS } from '../fragments/items'
|
||||
import MoreFooter from './more-footer'
|
||||
import React from 'react'
|
||||
|
||||
export default function Items ({ variables, rank, items, cursor }) {
|
||||
export default function Items ({ variables, rank, items, pins, cursor }) {
|
||||
const { data, fetchMore } = useQuery(MORE_ITEMS, { variables })
|
||||
|
||||
if (!data && !items) {
|
||||
|
@ -12,14 +13,19 @@ export default function Items ({ variables, rank, items, cursor }) {
|
|||
}
|
||||
|
||||
if (data) {
|
||||
({ moreItems: { items, cursor } } = data)
|
||||
({ moreItems: { items, pins, cursor } } = data)
|
||||
}
|
||||
|
||||
const pinMap = pins?.reduce((a, p) => { a[p.position] = p; return a }, {})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.grid}>
|
||||
{items.map((item, i) => (
|
||||
<Item item={item} rank={rank && i + 1} key={item.id} />
|
||||
<React.Fragment key={item.id}>
|
||||
{pinMap && pinMap[i + 1] && <Item item={pinMap[i + 1]} key={pinMap[i + 1].id} />}
|
||||
<Item item={item} rank={rank && i + 1} key={item.id} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<MoreFooter
|
||||
|
|
|
@ -38,6 +38,10 @@ export const MORE_ITEMS = gql`
|
|||
cursor
|
||||
items {
|
||||
...ItemFields
|
||||
},
|
||||
pins {
|
||||
...ItemFields
|
||||
position
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
@ -68,6 +72,7 @@ export const ITEM_FULL = gql`
|
|||
query Item($id: ID!) {
|
||||
item(id: $id) {
|
||||
...ItemFields
|
||||
position
|
||||
text
|
||||
comments {
|
||||
...CommentsRecursive
|
||||
|
|
|
@ -47,7 +47,8 @@ export default function getApolloClient () {
|
|||
|
||||
return {
|
||||
cursor: incoming.cursor,
|
||||
items: [...(existing?.items || []), ...incoming.items]
|
||||
items: [...(existing?.items || []), ...incoming.items],
|
||||
pins: existing?.pins
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -6,11 +6,11 @@ import { MORE_ITEMS } from '../fragments/items'
|
|||
const variables = { sort: 'hot' }
|
||||
export const getServerSideProps = getGetServerSideProps(MORE_ITEMS, variables)
|
||||
|
||||
export default function Index ({ data: { moreItems: { items, cursor } } }) {
|
||||
export default function Index ({ data: { moreItems: { items, pins, cursor } } }) {
|
||||
return (
|
||||
<Layout>
|
||||
<Items
|
||||
items={items} cursor={cursor}
|
||||
items={items} pins={pins} cursor={cursor}
|
||||
variables={variables} rank
|
||||
/>
|
||||
</Layout>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Item" ADD COLUMN "pinId" INTEGER,
|
||||
ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Pin" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"cron" TEXT,
|
||||
"timezone" TEXT,
|
||||
"position" INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Item" ADD FOREIGN KEY ("pinId") REFERENCES "Pin"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- pin upserts add new pgboss.schedule
|
||||
CREATE OR REPLACE FUNCTION pin_upsert_trigger_func() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- only schedule if pin has new.cron set
|
||||
IF new.cron IS NOT NULL THEN
|
||||
-- pgboss updates when inserts have the same name
|
||||
INSERT INTO pgboss.schedule (name, cron, timezone)
|
||||
VALUES ('repin-' || new.id, new.cron, new.timezone)
|
||||
ON CONFLICT (name) DO UPDATE SET
|
||||
cron = EXCLUDED.cron,
|
||||
timezone = EXCLUDED.timezone,
|
||||
data = EXCLUDED.data,
|
||||
options = EXCLUDED.options,
|
||||
updated_on = now();
|
||||
-- if old.cron is set but new.cron isn't ... we need to delete the job
|
||||
ELSIF old.cron IS NOT NULL AND new.cron IS NULL THEN
|
||||
DELETE FROM pgboss.schedule where name = 'repin-' || new.id;
|
||||
END IF;
|
||||
|
||||
RETURN new;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS pin_upsert_trigger ON "Pin";
|
||||
CREATE TRIGGER pin_upsert_trigger
|
||||
AFTER INSERT OR UPDATE ON "Pin"
|
||||
FOR EACH ROW EXECUTE PROCEDURE pin_upsert_trigger_func();
|
||||
|
||||
-- pin delete removes from pgboss.schedule
|
||||
CREATE OR REPLACE FUNCTION pin_delete_trigger_func() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
DELETE FROM pgboss.schedule where name = 'repin-' || old.id;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS pin_delete_trigger ON "Pin";
|
||||
CREATE TRIGGER pin_delete_trigger
|
||||
AFTER DELETE ON "Pin"
|
||||
FOR EACH ROW EXECUTE PROCEDURE pin_delete_trigger_func();
|
|
@ -83,7 +83,7 @@ model Message {
|
|||
model Item {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
title String?
|
||||
text String?
|
||||
url String?
|
||||
|
@ -95,13 +95,27 @@ model Item {
|
|||
actions ItemAct[]
|
||||
mentions Mention[]
|
||||
path Unsupported("LTREE")?
|
||||
pin Pin? @relation(fields: [pinId], references: [id])
|
||||
pinId Int?
|
||||
|
||||
User User[] @relation("Item")
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([userId])
|
||||
@@index([parentId])
|
||||
}
|
||||
|
||||
// the active pin is the latest one when it's a recurring cron
|
||||
model Pin {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
|
||||
cron String?
|
||||
timezone String?
|
||||
position Int
|
||||
Item Item[]
|
||||
}
|
||||
|
||||
enum ItemActType {
|
||||
VOTE
|
||||
BOOST
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="302" height="308" viewBox="0 0 302 308" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M129.115 160.364L142.992 173.981L50.3695 268.367L30.7354 274.38L36.4819 254.761L129.115 160.364Z" stroke-width="9"/>
|
||||
<path d="M207.857 104.935C228.879 129.791 243.94 155.699 249.202 178.77C254.417 201.631 249.984 221.211 232.696 234.98C205.344 256.765 177.645 263.158 151.824 258.363C125.865 253.542 101.218 237.297 80.4255 212.713C59.7384 188.254 48.7432 160.128 48.9871 133.081C49.23 106.127 60.6273 79.8929 85.406 58.9359C102.378 44.5815 122.065 43.5273 143.128 52.3031C164.436 61.1806 186.812 80.0517 207.857 104.935Z" stroke-width="9"/>
|
||||
<path d="M221.575 93.935C242.294 118.433 253.781 146.518 255.776 171.976C257.773 197.443 250.288 219.901 233.584 234.029C216.88 248.157 193.491 251.811 168.708 245.616C143.934 239.423 118.145 223.434 97.4255 198.936C76.706 174.439 65.2192 146.354 63.2235 120.895C61.2272 95.4283 68.7123 72.9704 85.4164 58.8426C102.12 44.7148 125.509 41.0605 150.292 47.2557C175.066 53.4488 200.855 69.4373 221.575 93.935Z" stroke-width="9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -21,9 +21,49 @@ async function work () {
|
|||
await boss.start()
|
||||
await boss.work('checkInvoice', checkInvoice)
|
||||
await boss.work('checkWithdrawal', checkWithdrawal)
|
||||
await boss.work('repin-*', repin)
|
||||
console.log('working jobs')
|
||||
}
|
||||
|
||||
async function repin ({ name }) {
|
||||
console.log(name)
|
||||
// get the id
|
||||
const id = name.slice('repin-'.length)
|
||||
if (id.length === 0 || isNaN(id)) {
|
||||
console.log('repin id not found in', name)
|
||||
return
|
||||
}
|
||||
|
||||
// get the latest item with this id
|
||||
const pinId = Number(id)
|
||||
const current = await models.item.findFirst(
|
||||
{
|
||||
where: {
|
||||
pinId
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!current) {
|
||||
console.log('could not find existing item for', name)
|
||||
return
|
||||
}
|
||||
|
||||
// create a new item with matching 1) title, text, and url and 2) setting pinId
|
||||
await models.item.create({
|
||||
data: {
|
||||
title: current.title,
|
||||
text: current.text,
|
||||
url: current.url,
|
||||
userId: current.userId,
|
||||
pinId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function checkInvoice ({ data: { hash } }) {
|
||||
const inv = await getInvoice({ id: hash, lnd })
|
||||
console.log(inv)
|
||||
|
@ -44,7 +84,7 @@ async function checkInvoice ({ data: { hash } }) {
|
|||
}))
|
||||
} else if (new Date(inv.expires_at) > new Date()) {
|
||||
// not expired, recheck in 5 seconds
|
||||
boss.send('checkInvoice', { hash }, walletOptions)
|
||||
await boss.send('checkInvoice', { hash }, walletOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +123,7 @@ async function checkWithdrawal ({ data: { id, hash } }) {
|
|||
SELECT reverse_withdrawl(${id}, ${status})`)
|
||||
} else {
|
||||
// we need to requeue to check again in 5 seconds
|
||||
boss.send('checkWithdrawal', { id, hash }, walletOptions)
|
||||
await boss.send('checkWithdrawal', { id, hash }, walletOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue