2021-04-12 18:05:09 +00:00
import { UserInputError , AuthenticationError } from 'apollo-server-micro'
2022-11-15 23:51:00 +00:00
import { ensureProtocol , removeTracking } from '../../lib/url'
2021-05-20 01:09:32 +00:00
import serialize from './serial'
2021-09-06 22:36:08 +00:00
import { decodeCursor , LIMIT , nextCursorEncoded } from '../../lib/cursor'
2021-08-22 15:25:17 +00:00
import { getMetadata , metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'
2022-09-21 19:57:36 +00:00
import {
2023-02-08 19:38:04 +00:00
BOOST _MIN , ITEM _SPAM _INTERVAL ,
2023-05-06 21:51:17 +00:00
MAX _TITLE _LENGTH , ITEM _FILTER _THRESHOLD , DONT _LIKE _THIS _COST , COMMENT _DEPTH _LIMIT
2022-09-21 19:57:36 +00:00
} from '../../lib/constants'
2022-11-15 20:51:55 +00:00
import { msatsToSats } from '../../lib/format'
2023-01-11 22:20:14 +00:00
import { parse } from 'tldts'
2023-01-12 18:05:47 +00:00
import uu from 'url-unshort'
2023-02-08 19:38:04 +00:00
import { amountSchema , bountySchema , commentSchema , discussionSchema , jobSchema , linkSchema , pollSchema , ssValidate } from '../../lib/validate'
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
import { sendUserNotification } from '../webPush'
2021-06-22 17:47:49 +00:00
2023-05-07 01:25:00 +00:00
async function comments ( me , models , id , sort ) {
2021-12-21 21:29:42 +00:00
let orderBy
switch ( sort ) {
case 'top' :
2023-06-04 01:01:50 +00:00
orderBy = ` ORDER BY ${ await orderByNumerator ( me , models ) } DESC, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC `
2021-12-21 21:29:42 +00:00
break
case 'recent' :
2023-06-04 01:01:50 +00:00
orderBy = 'ORDER BY "Item".created_at DESC, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC'
2021-12-21 21:29:42 +00:00
break
default :
2023-06-04 01:01:50 +00:00
orderBy = ` ORDER BY ${ await orderByNumerator ( me , models ) } /POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC `
2021-12-21 21:29:42 +00:00
break
}
2023-06-04 01:01:50 +00:00
const filter = await commentFilterClause ( me , models )
2023-05-07 01:25:00 +00:00
if ( me ) {
const [ { item _comments _with _me : comments } ] = await models . $queryRaw (
'SELECT item_comments_with_me($1, $2, $3, $4, $5)' , Number ( id ) , Number ( me . id ) , COMMENT _DEPTH _LIMIT , filter , orderBy )
return comments
}
const [ { item _comments : comments } ] = await models . $queryRaw (
'SELECT item_comments($1, $2, $3, $4)' , Number ( id ) , COMMENT _DEPTH _LIMIT , filter , orderBy )
2023-05-06 21:51:17 +00:00
return comments
2021-04-29 15:56:28 +00:00
}
2022-09-21 19:57:36 +00:00
export async function getItem ( parent , { id } , { me , models } ) {
2023-05-08 20:06:42 +00:00
const [ item ] = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE id = $1 `
} , Number ( id ) )
2021-09-23 17:42:00 +00:00
return item
}
2021-12-16 23:05:31 +00:00
function topClause ( within ) {
2022-04-02 22:39:15 +00:00
let interval = ' AND "Item".created_at >= $1 - INTERVAL '
2021-12-16 23:05:31 +00:00
switch ( within ) {
2022-10-25 21:35:32 +00:00
case 'forever' :
interval = ''
2021-12-16 23:05:31 +00:00
break
case 'week' :
interval += "'7 days'"
break
case 'month' :
interval += "'1 month'"
break
case 'year' :
interval += "'1 year'"
break
default :
2022-10-25 21:35:32 +00:00
interval += "'1 day'"
2021-12-16 23:05:31 +00:00
break
}
return interval
}
2022-10-25 21:35:32 +00:00
async function topOrderClause ( sort , me , models ) {
switch ( sort ) {
case 'comments' :
return 'ORDER BY ncomments DESC'
case 'sats' :
2022-11-15 20:51:55 +00:00
return 'ORDER BY msats DESC'
2022-10-25 21:35:32 +00:00
default :
return await topOrderByWeightedSats ( me , models )
}
}
2022-09-21 19:57:36 +00:00
export async function orderByNumerator ( me , models ) {
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
if ( user . wildWestMode ) {
2023-05-09 20:07:23 +00:00
return '(GREATEST("Item"."weightedVotes", POWER("Item"."weightedVotes", 1.2)) + "Item"."weightedComments"/2)'
2022-09-21 19:57:36 +00:00
}
}
return ` (CASE WHEN "Item"."weightedVotes" > "Item"."weightedDownVotes"
THEN 1
ELSE - 1 END
2023-05-09 18:52:35 +00:00
* GREATEST ( ABS ( "Item" . "weightedVotes" - "Item" . "weightedDownVotes" ) , POWER ( ABS ( "Item" . "weightedVotes" - "Item" . "weightedDownVotes" ) , 1.2 ) )
+ "Item" . "weightedComments" / 2 ) `
2022-09-21 19:57:36 +00:00
}
2023-05-23 14:21:04 +00:00
export async function joinSatRankView ( me , models ) {
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
if ( user . wildWestMode ) {
return 'JOIN sat_rank_wwm_view ON "Item".id = sat_rank_wwm_view.id'
}
}
return 'JOIN sat_rank_tender_view ON "Item".id = sat_rank_tender_view.id'
}
2023-06-04 01:01:50 +00:00
export async function commentFilterClause ( me , models ) {
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > - ${ ITEM _FILTER _THRESHOLD } `
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
// wild west mode has everything
if ( user . wildWestMode ) {
return ''
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${ me . id } `
}
// close the clause
clause += ')'
return clause
}
2022-09-21 19:57:36 +00:00
export async function filterClause ( me , models ) {
2022-09-27 21:19:15 +00:00
// by default don't include freebies unless they have upvotes
let clause = ' AND (NOT "Item".freebie OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0'
2022-09-21 19:57:36 +00:00
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-09-27 21:19:15 +00:00
// wild west mode has everything
2022-09-21 19:57:36 +00:00
if ( user . wildWestMode ) {
return ''
}
2022-09-27 21:19:15 +00:00
// greeter mode includes freebies if feebies haven't been flagged
if ( user . greeterMode ) {
2023-05-06 23:17:47 +00:00
clause = ' AND (NOT "Item".freebie OR ("Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0 AND "Item".freebie)'
2022-09-27 21:19:15 +00:00
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${ me . id } ) `
} else {
// close default freebie clause
clause += ')'
2022-09-21 19:57:36 +00:00
}
// if the item is above the threshold or is mine
2022-09-27 21:19:15 +00:00
clause += ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > - ${ ITEM _FILTER _THRESHOLD } `
2022-09-21 19:57:36 +00:00
if ( me ) {
clause += ` OR "Item"."userId" = ${ me . id } `
}
clause += ')'
return clause
}
2022-12-01 22:22:13 +00:00
function recentClause ( type ) {
switch ( type ) {
case 'links' :
return ' AND url IS NOT NULL'
case 'discussions' :
return ' AND url IS NULL AND bio = false AND "pollCost" IS NULL'
case 'polls' :
return ' AND "pollCost" IS NOT NULL'
case 'bios' :
return ' AND bio = true'
2023-01-26 16:11:55 +00:00
case 'bounties' :
return ' AND bounty IS NOT NULL'
2022-12-01 22:22:13 +00:00
default :
return ''
}
}
2023-05-07 01:25:00 +00:00
// this grabs all the stuff we need to display the item list and only
2023-05-08 20:06:42 +00:00
// hits the db once ... orderBy needs to be duplicated on the outer query because
// joining does not preserve the order of the inner query
async function itemQueryWithMeta ( { me , models , query , orderBy = '' } , ... args ) {
2023-05-07 01:25:00 +00:00
if ( ! me ) {
return await models . $queryRaw ( `
SELECT "Item" . * , to _json ( users . * ) as user
FROM (
$ { query }
) "Item"
2023-05-08 20:06:42 +00:00
JOIN users ON "Item" . "userId" = users . id
$ { orderBy } ` , ...args)
2023-05-07 01:25:00 +00:00
} else {
return await models . $queryRaw ( `
SELECT "Item" . * , to _json ( users . * ) as user , COALESCE ( "ItemAct" . "meMsats" , 0 ) as "meMsats" ,
2023-06-01 00:44:06 +00:00
COALESCE ( "ItemAct" . "meDontLike" , false ) as "meDontLike" , "Bookmark" . "itemId" IS NOT NULL AS "meBookmark" ,
"ThreadSubscription" . "itemId" IS NOT NULL AS "meSubscription"
2023-05-07 01:25:00 +00:00
FROM (
$ { query }
) "Item"
JOIN users ON "Item" . "userId" = users . id
LEFT JOIN "Bookmark" ON "Bookmark" . "itemId" = "Item" . id AND "Bookmark" . "userId" = $ { me . id }
2023-06-01 00:44:06 +00:00
LEFT JOIN "ThreadSubscription" ON "ThreadSubscription" . "itemId" = "Item" . id AND "ThreadSubscription" . "userId" = $ { me . id }
2023-05-07 01:25:00 +00:00
LEFT JOIN LATERAL (
SELECT "itemId" , sum ( "ItemAct" . msats ) FILTER ( WHERE act = 'FEE' OR act = 'TIP' ) AS "meMsats" ,
bool _or ( act = 'DONT_LIKE_THIS' ) AS "meDontLike"
FROM "ItemAct"
WHERE "ItemAct" . "userId" = $ { me . id }
AND "ItemAct" . "itemId" = "Item" . id
GROUP BY "ItemAct" . "itemId"
2023-05-08 20:06:42 +00:00
) "ItemAct" ON true
$ { orderBy } ` , ...args)
2023-05-07 01:25:00 +00:00
}
2023-05-06 23:17:47 +00:00
}
2023-05-23 14:21:04 +00:00
const subClause = ( sub , num , table , solo ) => {
return sub ? ` ${ solo ? 'WHERE' : 'AND' } ${ table ? ` " ${ table } ". ` : '' } "subName" = $ ${ num } ` : ''
2023-05-01 20:58:30 +00:00
}
2021-04-12 18:05:09 +00:00
export default {
Query : {
2022-08-10 15:06:31 +00:00
itemRepetition : async ( parent , { parentId } , { me , models } ) => {
if ( ! me ) return 0
// how many of the parents starting at parentId belong to me
2022-09-01 21:06:11 +00:00
const [ { item _spam : count } ] = await models . $queryRaw ( ` SELECT item_spam( $ 1, $ 2, ' ${ ITEM _SPAM _INTERVAL } ') ` ,
Number ( parentId ) , Number ( me . id ) )
2022-08-10 15:06:31 +00:00
return count
} ,
2023-06-12 22:15:20 +00:00
topItems : async ( parent , { sub , cursor , sort , when } , { me , models } ) => {
2022-10-25 21:35:32 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-06-12 22:15:20 +00:00
const subArr = sub ? [ sub ] : [ ]
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
AND "pinId" IS NULL AND "deletedAt" IS NULL
2023-06-12 22:15:20 +00:00
$ { subClause ( sub , 3 ) }
2023-05-08 20:06:42 +00:00
$ { topClause ( when ) }
$ { await filterClause ( me , models ) }
$ { await topOrderClause ( sort , me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderClause ( sort , me , models )
2023-06-12 22:15:20 +00:00
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-10-25 21:35:32 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2023-06-12 22:15:20 +00:00
topComments : async ( parent , { sub , cursor , sort , when } , { me , models } ) => {
2022-10-25 21:35:32 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-06-12 22:15:20 +00:00
const subArr = sub ? [ sub ] : [ ]
2023-05-08 20:06:42 +00:00
const comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
2023-06-12 22:15:20 +00:00
JOIN "Item" root ON "Item" . "rootId" = root . id
WHERE "Item" . "parentId" IS NOT NULL
AND "Item" . created _at <= $1 AND "Item" . "deletedAt" IS NULL
$ { subClause ( sub , 3 , 'root' ) }
2023-05-08 20:06:42 +00:00
$ { topClause ( when ) }
$ { await filterClause ( me , models ) }
$ { await topOrderClause ( sort , me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderClause ( sort , me , models )
2023-06-12 22:15:20 +00:00
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-10-25 21:35:32 +00:00
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
2022-12-01 22:22:13 +00:00
items : async ( parent , { sub , sort , type , cursor , name , within } , { me , models } ) => {
2021-06-22 17:47:49 +00:00
const decodedCursor = decodeCursor ( cursor )
2022-02-17 17:23:43 +00:00
let items ; let user ; let pins ; let subFull
2022-02-26 16:41:30 +00:00
const activeOrMine = ( ) => {
2022-06-02 23:25:21 +00:00
return me ? ` AND (status <> 'STOPPED' OR "userId" = ${ me . id } ) ` : ' AND status <> \'STOPPED\' '
2022-02-17 17:23:43 +00:00
}
2021-10-26 20:49:37 +00:00
2023-05-05 18:06:53 +00:00
// HACK we want to optionally include the subName in the query
// but the query planner doesn't like unused parameters
const subArr = sub ? [ sub ] : [ ]
2021-06-24 23:56:01 +00:00
switch ( sort ) {
case 'user' :
2021-10-26 20:49:37 +00:00
if ( ! name ) {
throw new UserInputError ( 'must supply name' , { argumentName : 'name' } )
}
user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NULL AND created _at <= $2
AND "pinId" IS NULL
$ { activeOrMine ( ) }
$ { await filterClause ( me , models ) }
ORDER BY created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , user . id , decodedCursor . time , decodedCursor . offset )
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
case 'recent' :
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
$ { subClause ( sub , 3 ) }
$ { activeOrMine ( ) }
$ { await filterClause ( me , models ) }
$ { recentClause ( type ) }
ORDER BY created _at DESC
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-02-17 17:23:43 +00:00
break
case 'top' :
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
AND "pinId" IS NULL AND "deletedAt" IS NULL
$ { topClause ( within ) }
$ { await filterClause ( me , models ) }
$ { await topOrderByWeightedSats ( me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderByWeightedSats ( me , models )
} , decodedCursor . time , decodedCursor . offset )
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
default :
// sub so we know the default ranking
if ( sub ) {
subFull = await models . sub . findUnique ( { where : { name : sub } } )
}
switch ( subFull ? . rankingType ) {
case 'AUCTION' :
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
2023-05-08 22:32:37 +00:00
$ { SELECT } ,
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
THEN 0 ELSE 1 END AS group _rank ,
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
THEN rank ( ) OVER ( ORDER BY "maxBid" DESC , created _at ASC )
ELSE rank ( ) OVER ( ORDER BY created _at DESC ) END AS rank
2023-05-08 20:06:42 +00:00
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
AND "pinId" IS NULL
$ { subClause ( sub , 3 ) }
2023-05-08 22:32:37 +00:00
AND status IN ( 'ACTIVE' , 'NOSATS' )
ORDER BY group _rank , rank
2023-05-08 20:06:42 +00:00
OFFSET $2
2023-05-08 22:32:37 +00:00
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY group_rank, rank'
2023-05-08 20:06:42 +00:00
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-02-17 17:23:43 +00:00
break
default :
2023-05-23 14:21:04 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT } , rank
2023-05-06 23:17:47 +00:00
FROM "Item"
2023-05-23 14:21:04 +00:00
$ { await joinSatRankView ( me , models ) }
$ { subClause ( sub , 2 , 'Item' , true ) }
2023-05-24 07:14:46 +00:00
ORDER BY rank ASC
2023-05-23 14:21:04 +00:00
OFFSET $1
2023-05-08 20:06:42 +00:00
LIMIT $ { LIMIT } ` ,
2023-05-24 07:14:46 +00:00
orderBy : 'ORDER BY rank ASC'
2023-05-23 14:21:04 +00:00
} , decodedCursor . offset , ... subArr )
2022-02-17 17:23:43 +00:00
if ( decodedCursor . offset === 0 ) {
// get pins for the page and return those separately
2023-05-08 20:06:42 +00:00
pins = await itemQueryWithMeta ( {
me ,
models ,
query : `
SELECT rank _filter . *
FROM (
$ { SELECT } ,
rank ( ) OVER (
PARTITION BY "pinId"
ORDER BY created _at DESC
)
FROM "Item"
WHERE "pinId" IS NOT NULL
$ { subClause ( sub , 1 ) }
) rank _filter WHERE RANK = 1 `
} , ... subArr )
2022-02-17 17:23:43 +00:00
}
break
}
break
2021-06-24 23:56:01 +00:00
}
2021-06-22 17:47:49 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
2022-01-07 16:32:31 +00:00
items ,
pins
2021-06-22 17:47:49 +00:00
}
2021-04-24 21:05:07 +00:00
} ,
2022-09-21 19:57:36 +00:00
allItems : async ( parent , { cursor } , { me , models } ) => {
2022-01-25 19:34:51 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-01-25 19:34:51 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-22 18:44:50 +00:00
outlawedItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const notMine = ( ) => {
return me ? ` AND "userId" <> ${ me . id } ` : ''
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . "weightedVotes" - "Item" . "weightedDownVotes" <= - $ { ITEM _FILTER _THRESHOLD }
$ { notMine ( ) }
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-09-22 18:44:50 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-22 20:42:04 +00:00
borderlandItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const notMine = ( ) => {
return me ? ` AND "userId" <> ${ me . id } ` : ''
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . "weightedVotes" - "Item" . "weightedDownVotes" < 0
AND "Item" . "weightedVotes" - "Item" . "weightedDownVotes" > - $ { ITEM _FILTER _THRESHOLD }
$ { notMine ( ) }
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-09-22 20:42:04 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-27 21:19:15 +00:00
freebieItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . freebie
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-09-27 21:19:15 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2023-05-07 01:25:00 +00:00
getBountiesByUserName : async ( parent , { name , cursor , limit } , { me , models } ) => {
2023-01-26 16:11:55 +00:00
const decodedCursor = decodeCursor ( cursor )
const user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'user not found' , {
argumentName : 'name'
} )
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1
AND "bounty" IS NOT NULL
ORDER BY created _at DESC
OFFSET $2
LIMIT $3 ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , user . id , decodedCursor . offset , limit || LIMIT )
2023-01-26 16:11:55 +00:00
return {
cursor : items . length === ( limit || LIMIT ) ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2023-05-01 20:58:30 +00:00
moreFlatComments : async ( parent , { sub , cursor , name , sort , within } , { me , models } ) => {
2021-06-24 23:56:01 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-05-05 18:06:53 +00:00
// HACK we want to optionally include the subName in the query
// but the query planner doesn't like unused parameters
const subArr = sub ? [ sub ] : [ ]
2021-08-18 22:20:33 +00:00
2021-12-16 23:05:31 +00:00
let comments , user
switch ( sort ) {
2022-08-18 22:05:58 +00:00
case 'recent' :
2023-05-08 20:06:42 +00:00
comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
JOIN "Item" root ON "Item" . "rootId" = root . id
WHERE "Item" . "parentId" IS NOT NULL AND "Item" . created _at <= $1
$ { subClause ( sub , 3 , 'root' ) }
$ { await filterClause ( me , models ) }
ORDER BY "Item" . created _at DESC
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-08-18 22:05:58 +00:00
break
2021-12-16 23:05:31 +00:00
case 'user' :
if ( ! name ) {
throw new UserInputError ( 'must supply name' , { argumentName : 'name' } )
}
2021-10-26 20:49:37 +00:00
2021-12-16 23:05:31 +00:00
user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2021-08-18 22:20:33 +00:00
2023-05-08 20:06:42 +00:00
comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NOT NULL
AND created _at <= $2
$ { await filterClause ( me , models ) }
ORDER BY created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , user . id , decodedCursor . time , decodedCursor . offset )
2021-12-16 23:05:31 +00:00
break
case 'top' :
2023-05-08 20:06:42 +00:00
comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . "parentId" IS NOT NULL AND "Item" . "deletedAt" IS NULL
AND "Item" . created _at <= $1
$ { topClause ( within ) }
$ { await filterClause ( me , models ) }
$ { await topOrderByWeightedSats ( me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderByWeightedSats ( me , models )
} , decodedCursor . time , decodedCursor . offset )
2021-12-16 23:05:31 +00:00
break
default :
throw new UserInputError ( 'invalid sort type' , { argumentName : 'sort' } )
}
2021-08-18 22:20:33 +00:00
2021-06-24 23:56:01 +00:00
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
2023-02-16 22:23:59 +00:00
moreBookmarks : async ( parent , { cursor , name } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT } , "Bookmark" . created _at as "bookmarkCreatedAt"
FROM "Item"
JOIN "Bookmark" ON "Bookmark" . "itemId" = "Item" . "id" AND "Bookmark" . "userId" = $1
AND "Bookmark" . created _at <= $2
ORDER BY "Bookmark" . created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "bookmarkCreatedAt" DESC'
} , user . id , decodedCursor . time , decodedCursor . offset )
2023-02-16 22:23:59 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2021-09-23 17:42:00 +00:00
item : getItem ,
2023-01-12 18:05:47 +00:00
pageTitleAndUnshorted : async ( parent , { url } , { models } ) => {
const res = { }
2021-08-22 15:25:17 +00:00
try {
const response = await fetch ( ensureProtocol ( url ) , { redirect : 'follow' } )
const html = await response . text ( )
const doc = domino . createWindow ( html ) . document
const metadata = getMetadata ( doc , url , { title : metadataRuleSets . title } )
2023-01-12 18:05:47 +00:00
res . title = metadata ? . title
} catch { }
try {
const unshorted = await uu ( ) . expand ( url )
if ( unshorted ) {
res . unshorted = unshorted
}
} catch { }
return res
2021-10-28 20:49:51 +00:00
} ,
2023-05-07 01:25:00 +00:00
dupes : async ( parent , { url } , { me , models } ) => {
2021-10-28 20:49:51 +00:00
const urlObj = new URL ( ensureProtocol ( url ) )
2023-06-01 00:49:28 +00:00
let uri = urlObj . hostname + '(:[0-9]+)?' + urlObj . pathname
2021-12-20 22:26:22 +00:00
uri = uri . endsWith ( '/' ) ? uri . slice ( 0 , - 1 ) : uri
2023-01-11 22:20:14 +00:00
const parseResult = parse ( urlObj . hostname )
if ( parseResult ? . subdomain ? . length ) {
const { subdomain } = parseResult
uri = uri . replace ( subdomain , '(%)?' )
} else {
uri = ` (%.)? ${ uri } `
}
let similar = ` (http(s)?://)? ${ uri } /? `
2022-03-10 21:44:46 +00:00
const whitelist = [ 'news.ycombinator.com/item' , 'bitcointalk.org/index.php' ]
2023-01-11 22:04:50 +00:00
const youtube = [ 'www.youtube.com' , 'youtube.com' , 'm.youtube.com' , 'youtu.be' ]
2021-12-20 22:26:22 +00:00
if ( whitelist . includes ( uri ) ) {
similar += ` \\ ${ urlObj . search } `
2022-03-10 21:44:46 +00:00
} else if ( youtube . includes ( urlObj . hostname ) ) {
// extract id and create both links
const matches = url . match ( /(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)/i )
2023-01-24 14:37:33 +00:00
similar = ` (http(s)?://)?((www.|m.)?youtube.com/(watch \\ ?v=|v/|live/) ${ matches ? . groups ? . id } |youtu.be/ ${ matches ? . groups ? . id } )(( \\ ?|&|#)%)? `
2021-12-20 22:26:22 +00:00
} else {
2022-05-18 18:21:24 +00:00
similar += '((\\?|#)%)?'
2021-12-20 22:26:22 +00:00
}
2023-05-08 20:06:42 +00:00
return await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE LOWER ( url ) SIMILAR TO LOWER ( $1 )
ORDER BY created _at DESC
LIMIT 3 `
} , similar )
2021-12-21 21:29:42 +00:00
} ,
2022-09-21 19:57:36 +00:00
comments : async ( parent , { id , sort } , { me , models } ) => {
return comments ( me , models , id , sort )
2022-01-26 15:35:14 +00:00
} ,
2022-09-29 20:42:33 +00:00
auctionPosition : async ( parent , { id , sub , bid } , { models , me } ) => {
2022-10-05 18:55:30 +00:00
const createdAt = id ? ( await getItem ( parent , { id } , { models , me } ) ) . createdAt : new Date ( )
let where
2022-09-29 20:42:33 +00:00
if ( bid > 0 ) {
2022-10-05 18:55:30 +00:00
// if there's a bid
// it's ACTIVE and has a larger bid than ours, or has an equal bid and is older
// count items: (bid > ours.bid OR (bid = ours.bid AND create_at < ours.created_at)) AND status = 'ACTIVE'
where = {
status : 'ACTIVE' ,
OR : [
{ maxBid : { gt : bid } } ,
{ maxBid : bid , createdAt : { lt : createdAt } }
]
}
2022-09-29 20:42:33 +00:00
} else {
2022-10-05 18:55:30 +00:00
// else
// it's an active with a bid gt ours, or its newer than ours and not STOPPED
// count items: ((bid > ours.bid AND status = 'ACTIVE') OR (created_at > ours.created_at AND status <> 'STOPPED'))
where = {
OR : [
{ maxBid : { gt : 0 } , status : 'ACTIVE' } ,
{ createdAt : { gt : createdAt } , status : { not : 'STOPPED' } }
]
}
2022-09-29 20:42:33 +00:00
}
2022-10-05 18:55:30 +00:00
where . subName = sub
2022-02-17 17:23:43 +00:00
if ( id ) {
2022-10-05 18:55:30 +00:00
where . id = { not : Number ( id ) }
2022-02-17 17:23:43 +00:00
}
2022-10-05 18:55:30 +00:00
return await models . item . count ( { where } ) + 1
2021-04-12 18:05:09 +00:00
}
} ,
Mutation : {
2023-02-16 22:23:59 +00:00
bookmarkItem : async ( parent , { id } , { me , models } ) => {
const data = { itemId : Number ( id ) , userId : me . id }
const old = await models . bookmark . findUnique ( { where : { userId _itemId : data } } )
if ( old ) {
await models . bookmark . delete ( { where : { userId _itemId : data } } )
} else await models . bookmark . create ( { data } )
return { id }
} ,
2023-06-01 00:44:06 +00:00
subscribeItem : async ( parent , { id } , { me , models } ) => {
const data = { itemId : Number ( id ) , userId : me . id }
const old = await models . threadSubscription . findUnique ( { where : { userId _itemId : data } } )
if ( old ) {
await models . threadSubscription . delete ( { where : { userId _itemId : data } } )
} else await models . threadSubscription . create ( { data } )
return { id }
} ,
2023-01-12 23:53:09 +00:00
deleteItem : async ( parent , { id } , { me , models } ) => {
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
const data = { deletedAt : new Date ( ) }
if ( old . text ) {
data . text = '*deleted by author*'
}
if ( old . title ) {
data . title = 'deleted by author'
}
if ( old . url ) {
data . url = null
}
if ( old . pollCost ) {
data . pollCost = null
}
return await models . item . update ( { where : { id : Number ( id ) } , data } )
} ,
2022-04-18 22:10:26 +00:00
upsertLink : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
data . url = ensureProtocol ( data . url )
2022-11-15 23:51:00 +00:00
data . url = removeTracking ( data . url )
2021-08-11 20:13:10 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate ( linkSchema , data , models )
2022-04-18 22:10:26 +00:00
if ( id ) {
2022-08-18 18:15:24 +00:00
return await updateItem ( parent , { id , data } , { me , models } )
2022-04-18 22:10:26 +00:00
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2022-04-18 22:10:26 +00:00
upsertDiscussion : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
2021-08-11 20:13:10 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate ( discussionSchema , data , models )
2022-04-18 22:10:26 +00:00
if ( id ) {
2022-08-18 18:15:24 +00:00
return await updateItem ( parent , { id , data } , { me , models } )
2022-04-18 22:10:26 +00:00
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2023-01-26 16:11:55 +00:00
upsertBounty : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
2023-02-08 19:38:04 +00:00
await ssValidate ( bountySchema , data , models )
2023-01-26 16:11:55 +00:00
if ( id ) {
return await updateItem ( parent , { id , data } , { me , models } )
} else {
return await createItem ( parent , data , { me , models } )
}
} ,
2023-02-08 19:38:04 +00:00
upsertPoll : async ( parent , { id , ... data } , { me , models } ) => {
2023-05-01 20:58:30 +00:00
const { sub , forward , boost , title , text , options } = data
2022-07-30 13:25:46 +00:00
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
2023-02-08 19:38:04 +00:00
const optionCount = id
? await models . pollOption . count ( {
where : {
itemId : Number ( id )
}
} )
: 0
await ssValidate ( pollSchema , data , models , optionCount )
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
if ( id ) {
2023-02-08 19:38:04 +00:00
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
2022-08-18 18:15:24 +00:00
}
const [ item ] = await serialize ( models ,
2023-05-01 20:58:30 +00:00
models . $queryRaw ( ` ${ SELECT } FROM update_poll( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7) AS "Item" ` ,
sub || 'bitcoin' , Number ( id ) , title , text , Number ( boost || 0 ) , options , Number ( fwdUser ? . id ) ) )
2022-08-18 18:15:24 +00:00
2023-02-08 19:38:04 +00:00
await createMentions ( item , models )
item . comments = [ ]
2022-07-30 13:25:46 +00:00
return item
} else {
const [ item ] = await serialize ( models ,
2023-05-01 20:58:30 +00:00
models . $queryRaw ( ` ${ SELECT } FROM create_poll( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, ' ${ ITEM _SPAM _INTERVAL } ') AS "Item" ` ,
sub || 'bitcoin' , title , text , 1 , Number ( boost || 0 ) , Number ( me . id ) , options , Number ( fwdUser ? . id ) ) )
2022-08-18 18:15:24 +00:00
await createMentions ( item , models )
2022-07-30 13:25:46 +00:00
item . comments = [ ]
return item
}
} ,
2023-02-08 19:38:04 +00:00
upsertJob : async ( parent , { id , ... data } , { me , models } ) => {
2022-02-17 17:23:43 +00:00
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in to create job' )
}
2023-02-08 19:38:04 +00:00
const { sub , title , company , location , remote , text , url , maxBid , status , logo } = data
2022-02-17 17:23:43 +00:00
const fullSub = await models . sub . findUnique ( { where : { name : sub } } )
if ( ! fullSub ) {
throw new UserInputError ( 'not a valid sub' , { argumentName : 'sub' } )
}
2023-02-08 19:38:04 +00:00
await ssValidate ( jobSchema , data , models )
const loc = location . toLowerCase ( ) === 'remote' ? undefined : location
2022-02-17 17:23:43 +00:00
2022-09-29 20:42:33 +00:00
let item
2022-02-17 17:23:43 +00:00
if ( id ) {
2022-04-18 22:10:26 +00:00
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
2022-09-29 20:42:33 +00:00
( [ item ] = await serialize ( models ,
models . $queryRaw (
` ${ SELECT } FROM update_job( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, $ 10) AS "Item" ` ,
2023-02-08 19:38:04 +00:00
Number ( id ) , title , url , text , Number ( maxBid ) , company , loc , remote , Number ( logo ) , status ) ) )
2022-09-29 20:42:33 +00:00
} else {
( [ item ] = await serialize ( models ,
models . $queryRaw (
` ${ SELECT } FROM create_job( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9) AS "Item" ` ,
2023-02-08 19:38:04 +00:00
title , url , text , Number ( me . id ) , Number ( maxBid ) , company , loc , remote , Number ( logo ) ) ) )
2022-02-17 17:23:43 +00:00
}
2022-09-29 20:42:33 +00:00
await createMentions ( item , models )
return item
2022-02-17 17:23:43 +00:00
} ,
2023-02-08 19:38:04 +00:00
createComment : async ( parent , data , { me , models } ) => {
await ssValidate ( commentSchema , data )
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
const item = await createItem ( parent , data , { me , models } )
const parents = await models . $queryRaw (
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2' ,
Number ( item . parentId ) , Number ( me . id ) )
Promise . allSettled (
parents . map ( ( { userId } ) => sendUserNotification ( userId , {
title : 'you have a new reply' ,
body : data . text ,
2023-07-06 15:00:38 +00:00
item ,
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
tag : 'REPLY'
} ) )
)
return item
2021-04-26 21:55:15 +00:00
} ,
2023-02-08 19:38:04 +00:00
updateComment : async ( parent , { id , ... data } , { me , models } ) => {
await ssValidate ( commentSchema , data )
return await updateItem ( parent , { id , data } , { me , models } )
2021-08-10 22:59:06 +00:00
} ,
2022-07-30 13:25:46 +00:00
pollVote : async ( parent , { id } , { me , models } ) => {
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
await serialize ( models ,
models . $queryRaw ( ` ${ SELECT } FROM poll_vote( $ 1, $ 2) AS "Item" ` ,
Number ( id ) , Number ( me . id ) ) )
return id
} ,
2022-01-20 23:04:12 +00:00
act : async ( parent , { id , sats } , { me , models } ) => {
2021-04-26 21:55:15 +00:00
// need to make sure we are logged in
if ( ! me ) {
2021-05-11 20:29:44 +00:00
throw new AuthenticationError ( 'you must be logged in' )
2021-04-26 21:55:15 +00:00
}
2023-02-08 19:38:04 +00:00
await ssValidate ( amountSchema , { amount : sats } )
2021-04-27 21:30:58 +00:00
2022-01-20 23:04:12 +00:00
// disallow self tips
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 AND "userId" = $2 ` , Number(id), me.id)
if ( item ) {
2023-06-19 18:21:55 +00:00
throw new UserInputError ( 'cannot zap your self' )
2021-09-10 21:13:52 +00:00
}
2022-01-20 23:04:12 +00:00
const [ { item _act : vote } ] = await serialize ( models , models . $queryRaw ` SELECT item_act( ${ Number ( id ) } , ${ me . id } , 'TIP', ${ Number ( sats ) } ) ` )
2021-09-10 21:13:52 +00:00
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
const updatedItem = await models . item . findUnique ( { where : { id : Number ( id ) } } )
const title = ` your ${ updatedItem . title ? 'post' : 'reply' } ${ updatedItem . fwdUser ? 'forwarded' : 'stacked' } ${ Math . floor ( Number ( updatedItem . msats ) / 1000 ) } sats ${ updatedItem . fwdUser ? ` to @ ${ updatedItem . fwdUser . name } ` : '' } `
sendUserNotification ( updatedItem . userId , {
title ,
body : updatedItem . title ? updatedItem . title : updatedItem . text ,
2023-07-06 15:00:38 +00:00
item : updatedItem ,
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
tag : ` TIP- ${ updatedItem . id } `
2023-07-04 22:19:59 +00:00
} ) . catch ( console . error )
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
2021-09-10 21:13:52 +00:00
return {
2022-01-20 23:04:12 +00:00
vote ,
sats
2021-09-10 21:13:52 +00:00
}
2022-09-21 19:57:36 +00:00
} ,
dontLikeThis : async ( parent , { id } , { me , models } ) => {
// need to make sure we are logged in
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
// disallow self down votes
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 AND "userId" = $2 ` , Number(id), me.id)
if ( item ) {
throw new UserInputError ( 'cannot downvote your self' )
}
await serialize ( models , models . $queryRaw ` SELECT item_act( ${ Number ( id ) } , ${ me . id } , 'DONT_LIKE_THIS', ${ DONT _LIKE _THIS _COST } ) ` )
return true
2021-04-12 18:05:09 +00:00
}
} ,
Item : {
2022-11-15 20:51:55 +00:00
sats : async ( item , args , { models } ) => {
return msatsToSats ( item . msats )
} ,
commentSats : async ( item , args , { models } ) => {
return msatsToSats ( item . commentMsats )
} ,
2022-09-29 20:42:33 +00:00
isJob : async ( item , args , { models } ) => {
return item . subName === 'jobs'
} ,
2022-02-17 17:23:43 +00:00
sub : async ( item , args , { models } ) => {
if ( ! item . subName ) {
return null
}
return await models . sub . findUnique ( { where : { name : item . subName } } )
} ,
2022-01-07 16:32:31 +00:00
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
} ,
2022-01-13 19:05:43 +00:00
prior : async ( item , args , { models } ) => {
if ( ! item . pinId ) {
return null
}
const prior = await models . item . findFirst ( {
where : {
pinId : item . pinId ,
createdAt : {
lt : item . createdAt
}
} ,
orderBy : {
createdAt : 'desc'
}
} )
if ( ! prior ) {
return null
}
return prior . id
} ,
2022-07-30 13:25:46 +00:00
poll : async ( item , args , { models , me } ) => {
if ( ! item . pollCost ) {
return null
}
const options = await models . $queryRaw `
SELECT "PollOption" . id , option , count ( "PollVote" . "userId" ) as count ,
coalesce ( bool _or ( "PollVote" . "userId" = $ { me ? . id } ) , 'f' ) as "meVoted"
FROM "PollOption"
LEFT JOIN "PollVote" on "PollVote" . "pollOptionId" = "PollOption" . id
WHERE "PollOption" . "itemId" = $ { item . id }
GROUP BY "PollOption" . id
ORDER BY "PollOption" . id ASC
`
const poll = { }
poll . options = options
poll . meVoted = options . some ( o => o . meVoted )
poll . count = options . reduce ( ( t , o ) => t + o . count , 0 )
return poll
} ,
2023-05-06 23:17:47 +00:00
user : async ( item , args , { models } ) => {
if ( item . user ) {
return item . user
}
return await models . user . findUnique ( { where : { id : item . userId } } )
} ,
2022-04-19 18:32:39 +00:00
fwdUser : async ( item , args , { models } ) => {
if ( ! item . fwdUserId ) {
return null
}
return await models . user . findUnique ( { where : { id : item . fwdUserId } } )
} ,
2022-09-21 19:57:36 +00:00
comments : async ( item , args , { me , models } ) => {
2022-03-31 18:05:11 +00:00
if ( item . comments ) {
return item . comments
}
2023-05-07 01:25:00 +00:00
return comments ( me , models , item . id , item . pinId ? 'recent' : 'hot' )
2022-03-04 18:05:16 +00:00
} ,
2022-10-28 15:58:31 +00:00
wvotes : async ( item ) => {
return item . weightedVotes - item . weightedDownVotes
} ,
2021-12-05 17:37:55 +00:00
meSats : async ( item , args , { me , models } ) => {
2021-09-10 21:13:52 +00:00
if ( ! me ) return 0
2023-05-07 01:25:00 +00:00
if ( typeof item . meMsats === 'number' ) return msatsToSats ( item . meMsats )
2021-09-10 21:13:52 +00:00
2022-11-15 20:51:55 +00:00
const { sum : { msats } } = await models . itemAct . aggregate ( {
2021-09-10 21:13:52 +00:00
sum : {
2022-11-15 20:51:55 +00:00
msats : true
2021-09-10 21:13:52 +00:00
} ,
where : {
2022-01-27 19:18:48 +00:00
itemId : Number ( item . id ) ,
2021-09-10 21:13:52 +00:00
userId : me . id ,
2021-12-05 17:37:55 +00:00
OR : [
{
act : 'TIP'
} ,
{
2022-11-23 18:12:09 +00:00
act : 'FEE'
2021-12-05 17:37:55 +00:00
}
]
2021-09-10 21:13:52 +00:00
}
} )
2022-11-15 20:51:55 +00:00
return ( msats && msatsToSats ( msats ) ) || 0
2021-09-10 21:13:52 +00:00
} ,
2022-09-21 19:57:36 +00:00
meDontLike : async ( item , args , { me , models } ) => {
if ( ! me ) return false
2023-05-07 01:25:00 +00:00
if ( typeof item . meDontLike === 'boolean' ) return item . meDontLike
2022-09-21 19:57:36 +00:00
const dontLike = await models . itemAct . findFirst ( {
where : {
itemId : Number ( item . id ) ,
userId : me . id ,
act : 'DONT_LIKE_THIS'
}
} )
return ! ! dontLike
} ,
2023-02-16 22:23:59 +00:00
meBookmark : async ( item , args , { me , models } ) => {
if ( ! me ) return false
2023-05-07 01:25:00 +00:00
if ( typeof item . meBookmark === 'boolean' ) return item . meBookmark
2023-02-16 22:23:59 +00:00
const bookmark = await models . bookmark . findUnique ( {
where : {
userId _itemId : {
itemId : Number ( item . id ) ,
userId : me . id
}
}
} )
return ! ! bookmark
} ,
2023-06-01 00:44:06 +00:00
meSubscription : async ( item , args , { me , models } ) => {
if ( ! me ) return false
if ( typeof item . meSubscription === 'boolean' ) return item . meSubscription
const subscription = await models . threadSubscription . findUnique ( {
where : {
userId _itemId : {
itemId : Number ( item . id ) ,
userId : me . id
}
}
} )
return ! ! subscription
} ,
2022-09-22 18:44:50 +00:00
outlawed : async ( item , args , { me , models } ) => {
if ( me && Number ( item . userId ) === Number ( me . id ) ) {
return false
}
return item . weightedVotes - item . weightedDownVotes <= - ITEM _FILTER _THRESHOLD
} ,
2021-12-05 17:37:55 +00:00
mine : async ( item , args , { me , models } ) => {
return me ? . id === item . userId
} ,
2021-07-08 00:15:27 +00:00
root : async ( item , args , { models } ) => {
2023-01-26 19:37:51 +00:00
if ( ! item . rootId ) {
2021-07-08 00:15:27 +00:00
return null
}
2023-02-04 00:08:08 +00:00
if ( item . root ) {
return item . root
}
2023-01-26 19:09:57 +00:00
return await models . item . findUnique ( { where : { id : item . rootId } } )
2021-07-08 00:15:27 +00:00
} ,
parent : async ( item , args , { models } ) => {
if ( ! item . parentId ) {
return null
}
return await models . item . findUnique ( { where : { id : item . parentId } } )
2023-01-22 20:17:50 +00:00
} ,
parentOtsHash : async ( item , args , { models } ) => {
if ( ! item . parentId ) {
return null
}
const parent = await models . item . findUnique ( { where : { id : item . parentId } } )
return parent . otsHash
2021-04-26 21:55:15 +00:00
}
}
}
2021-08-18 22:20:33 +00:00
const namePattern = /\B@[\w_]+/gi
2021-09-23 17:42:00 +00:00
export const createMentions = async ( item , models ) => {
2021-08-18 22:20:33 +00:00
// if we miss a mention, in the rare circumstance there's some kind of
// failure, it's not a big deal so we don't do it transactionally
// ideally, we probably would
if ( ! item . text ) {
return
}
try {
2021-08-19 19:53:11 +00:00
const mentions = item . text . match ( namePattern ) ? . map ( m => m . slice ( 1 ) )
if ( mentions ? . length > 0 ) {
2021-08-18 22:20:33 +00:00
const users = await models . user . findMany ( {
where : {
name : { in : mentions }
}
} )
users . forEach ( async user => {
const data = {
itemId : item . id ,
userId : user . id
}
await models . mention . upsert ( {
where : {
itemId _userId : data
} ,
update : data ,
create : data
} )
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
sendUserNotification ( user . id , {
title : 'you were mentioned' ,
body : item . text ,
2023-07-06 15:00:38 +00:00
item ,
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
tag : 'MENTION'
2023-07-04 22:19:59 +00:00
} ) . catch ( console . error )
2021-08-18 22:20:33 +00:00
} )
}
} catch ( e ) {
console . log ( 'mention failure' , e )
}
}
2023-05-01 20:58:30 +00:00
export const updateItem = async ( parent , { id , data : { sub , title , url , text , boost , forward , bounty , parentId } } , { me , models } ) => {
2022-04-18 22:10:26 +00:00
// update iff this item belongs to me
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
2022-08-18 18:15:24 +00:00
// if it's not the FAQ, not their bio, and older than 10 minutes
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-10-23 15:43:39 +00:00
if ( ! [ 349 , 76894 , 78763 , 81862 ] . includes ( old . id ) && user . bioId !== id && Date . now ( ) > new Date ( old . createdAt ) . getTime ( ) + 10 * 60000 ) {
2022-04-18 22:10:26 +00:00
throw new UserInputError ( 'item can no longer be editted' )
}
2022-08-18 18:15:24 +00:00
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
2022-08-10 15:06:31 +00:00
}
2022-08-27 02:57:41 +00:00
if ( ! old . parentId && title . length > MAX _TITLE _LENGTH ) {
2022-08-26 23:31:51 +00:00
throw new UserInputError ( 'title too long' )
}
2022-08-18 18:15:24 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
const [ item ] = await serialize ( models ,
models . $queryRaw (
2023-05-01 20:58:30 +00:00
` ${ SELECT } FROM update_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8) AS "Item" ` ,
old . parentId ? null : sub || 'bitcoin' , Number ( id ) , title , url , text ,
Number ( boost || 0 ) , bounty ? Number ( bounty ) : null , Number ( fwdUser ? . id ) ) )
2021-08-18 22:20:33 +00:00
await createMentions ( item , models )
return item
}
2023-05-01 20:58:30 +00:00
const createItem = async ( parent , { sub , title , url , text , boost , forward , bounty , parentId } , { me , models } ) => {
2021-04-26 21:55:15 +00:00
if ( ! me ) {
2021-05-11 20:29:44 +00:00
throw new AuthenticationError ( 'you must be logged in' )
2021-04-26 21:55:15 +00:00
}
2022-03-09 19:44:50 +00:00
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
2021-09-11 21:52:19 +00:00
}
2022-08-27 01:27:57 +00:00
if ( ! parentId && title . length > MAX _TITLE _LENGTH ) {
2022-08-26 23:31:51 +00:00
throw new UserInputError ( 'title too long' )
}
2022-04-19 18:32:39 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
2023-01-26 16:11:55 +00:00
const [ item ] = await serialize (
models ,
2022-08-10 15:06:31 +00:00
models . $queryRaw (
2023-05-01 20:58:30 +00:00
` ${ SELECT } FROM create_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, ' ${ ITEM _SPAM _INTERVAL } ') AS "Item" ` ,
parentId ? null : sub || 'bitcoin' ,
2023-01-26 16:11:55 +00:00
title ,
url ,
text ,
Number ( boost || 0 ) ,
bounty ? Number ( bounty ) : null ,
Number ( parentId ) ,
Number ( me . id ) ,
Number ( fwdUser ? . id ) ) )
2021-08-18 22:20:33 +00:00
await createMentions ( item , models )
2021-05-20 01:09:32 +00:00
item . comments = [ ]
return item
2021-04-26 21:55:15 +00:00
}
// we have to do our own query because ltree is unsupported
2021-09-23 17:42:00 +00:00
export const SELECT =
2023-05-08 20:06:42 +00:00
` SELECT "Item".id, "Item".created_at, "Item".created_at as "createdAt", "Item".updated_at,
"Item" . updated _at as "updatedAt" , "Item" . title , "Item" . text , "Item" . url , "Item" . "bounty" ,
"Item" . "userId" , "Item" . "fwdUserId" , "Item" . "parentId" , "Item" . "pinId" , "Item" . "maxBid" ,
"Item" . "rootId" , "Item" . upvotes , "Item" . company , "Item" . location , "Item" . remote , "Item" . "deletedAt" ,
"Item" . "subName" , "Item" . status , "Item" . "uploadId" , "Item" . "pollCost" , "Item" . boost , "Item" . msats ,
"Item" . ncomments , "Item" . "commentMsats" , "Item" . "lastCommentAt" , "Item" . "weightedVotes" ,
"Item" . "weightedDownVotes" , "Item" . freebie , "Item" . "otsHash" , "Item" . "bountyPaidTo" ,
2023-05-09 18:52:35 +00:00
ltree2text ( "Item" . "path" ) AS "path" , "Item" . "weightedComments" `
2021-04-27 21:30:58 +00:00
2022-09-21 19:57:36 +00:00
async function topOrderByWeightedSats ( me , models ) {
return ` ORDER BY ${ await orderByNumerator ( me , models ) } DESC NULLS LAST, "Item".id DESC `
}