+ job control
+ {item.status === 'NOSATS' &&
+ your promotion ran out of sats. fund your wallet or reduce bid to continue promoting your job}
+
+
)
}
diff --git a/components/notifications.js b/components/notifications.js
index c55da53f..4b4fd57c 100644
--- a/components/notifications.js
+++ b/components/notifications.js
@@ -74,8 +74,14 @@ function Notification ({ n }) {
- you stacked {n.earnedSats} sats {timeSince(new Date(n.sortTime))}
+ you stacked {n.earnedSats} sats in rewards{timeSince(new Date(n.sortTime))}
+ {n.sources &&
+
+ {n.sources.posts > 0 && {n.sources.posts} sats for top posts}
+ {n.sources.comments > 0 && {n.sources.posts > 0 && ' \\ '}{n.sources.comments} sats for top comments}
+ {n.sources.tips > 0 && {(n.sources.comments > 0 || n.sources.posts > 0) && ' \\ '}{n.sources.tips} sats for tipping top content early}
+
}
SN distributes the sats it earns back to its best users daily. These sats come from jobs, boost, and posting fees.
@@ -99,13 +105,15 @@ function Notification ({ n }) {
you were mentioned in
}
{n.__typename === 'JobChanged' &&
-
- {n.item.status === 'NOSATS'
- ? 'your job ran out of sats'
- : 'your job is active again'}
+
+ {n.item.status === 'ACTIVE'
+ ? 'your job is active again'
+ : (n.item.status === 'NOSATS'
+ ? 'your job promotion ran out of sats'
+ : 'your job has been stopped')}
}
diff --git a/prisma/migrations/20220913173806_earn_columns/migration.sql b/prisma/migrations/20220913173806_earn_columns/migration.sql
new file mode 100644
index 00000000..27074470
--- /dev/null
+++ b/prisma/migrations/20220913173806_earn_columns/migration.sql
@@ -0,0 +1,10 @@
+-- CreateEnum
+CREATE TYPE "EarnType" AS ENUM ('POST', 'COMMENT', 'TIP_COMMENT', 'TIP_POST');
+
+-- AlterTable
+ALTER TABLE "Earn" ADD COLUMN "rank" INTEGER,
+ADD COLUMN "type" "EarnType",
+ADD COLUMN "typeId" INTEGER;
+
+-- CreateIndex
+CREATE INDEX "Earn.created_at_userId_index" ON "Earn"("created_at", "userId");
diff --git a/prisma/migrations/20220913173826_earn_function/migration.sql b/prisma/migrations/20220913173826_earn_function/migration.sql
new file mode 100644
index 00000000..7aacc4b6
--- /dev/null
+++ b/prisma/migrations/20220913173826_earn_function/migration.sql
@@ -0,0 +1,16 @@
+CREATE OR REPLACE FUNCTION earn(user_id INTEGER, earn_msats INTEGER, created_at TIMESTAMP(3),
+ type "EarnType", type_id INTEGER, rank INTEGER)
+RETURNS void AS $$
+DECLARE
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+ -- insert into earn
+ INSERT INTO "Earn" (msats, "userId", created_at, type, "typeId", rank)
+ VALUES (earn_msats, user_id, created_at, type, type_id, rank);
+
+ -- give the user the sats
+ UPDATE users
+ SET msats = msats + earn_msats, "stackedMsats" = "stackedMsats" + earn_msats
+ WHERE id = user_id;
+END;
+$$ LANGUAGE plpgsql;
\ No newline at end of file
diff --git a/prisma/migrations/20220920152500_downvotes/migration.sql b/prisma/migrations/20220920152500_downvotes/migration.sql
new file mode 100644
index 00000000..7a08679b
--- /dev/null
+++ b/prisma/migrations/20220920152500_downvotes/migration.sql
@@ -0,0 +1,8 @@
+-- AlterEnum
+ALTER TYPE "ItemActType" ADD VALUE 'DONT_LIKE_THIS';
+
+-- AlterTable
+ALTER TABLE "Item" ADD COLUMN "weightedDownVotes" DOUBLE PRECISION NOT NULL DEFAULT 0;
+
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "wildWestMode" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/20220920195257_dont_like_this/migration.sql b/prisma/migrations/20220920195257_dont_like_this/migration.sql
new file mode 100644
index 00000000..d6c4912f
--- /dev/null
+++ b/prisma/migrations/20220920195257_dont_like_this/migration.sql
@@ -0,0 +1,74 @@
+-- modify it to take DONT_LIKE_THIS
+CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER)
+RETURNS INTEGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_sats INTEGER;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ SELECT (msats / 1000) INTO user_sats FROM users WHERE id = user_id;
+ IF act_sats > user_sats THEN
+ RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
+ END IF;
+
+ -- deduct sats from actor
+ UPDATE users SET msats = msats - (act_sats * 1000) WHERE id = user_id;
+
+ IF act = 'VOTE' OR act = 'TIP' THEN
+ -- add sats to actee's balance and stacked count
+ UPDATE users
+ SET msats = msats + (act_sats * 1000), "stackedMsats" = "stackedMsats" + (act_sats * 1000)
+ WHERE id = (SELECT COALESCE("fwdUserId", "userId") FROM "Item" WHERE id = item_id);
+
+ -- if they have already voted, this is a tip
+ IF EXISTS (SELECT 1 FROM "ItemAct" WHERE "itemId" = item_id AND "userId" = user_id AND "ItemAct".act = 'VOTE') THEN
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (act_sats, item_id, user_id, 'TIP', now_utc(), now_utc());
+ ELSE
+ -- else this is a vote with a possible extra tip
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (1, item_id, user_id, 'VOTE', now_utc(), now_utc());
+ act_sats := act_sats - 1;
+
+ -- if we have sats left after vote, leave them as a tip
+ IF act_sats > 0 THEN
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (act_sats, item_id, user_id, 'TIP', now_utc(), now_utc());
+ END IF;
+
+ RETURN 1;
+ END IF;
+ ELSE -- BOOST, POLL, DONT_LIKE_THIS
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (act_sats, item_id, user_id, act, now_utc(), now_utc());
+ END IF;
+
+ RETURN 0;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION weighted_downvotes_after_act() RETURNS TRIGGER AS $$
+DECLARE
+ user_trust DOUBLE PRECISION;
+BEGIN
+ -- grab user's trust who is upvoting
+ SELECT trust INTO user_trust FROM users WHERE id = NEW."userId";
+ -- update item
+ UPDATE "Item"
+ SET "weightedDownVotes" = "weightedDownVotes" + user_trust
+ WHERE id = NEW."itemId" AND "userId" <> NEW."userId";
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS weighted_downvotes_after_act ON "ItemAct";
+CREATE TRIGGER weighted_downvotes_after_act
+ AFTER INSERT ON "ItemAct"
+ FOR EACH ROW
+ WHEN (NEW.act = 'DONT_LIKE_THIS')
+ EXECUTE PROCEDURE weighted_downvotes_after_act();
+
+ALTER TABLE "Item" ADD CONSTRAINT "weighted_votes_positive" CHECK ("weightedVotes" >= 0) NOT VALID;
+ALTER TABLE "Item" ADD CONSTRAINT "weighted_down_votes_positive" CHECK ("weightedDownVotes" >= 0) NOT VALID;
\ No newline at end of file
diff --git a/prisma/migrations/20220922210703_outlaw/migration.sql b/prisma/migrations/20220922210703_outlaw/migration.sql
new file mode 100644
index 00000000..98903f5d
--- /dev/null
+++ b/prisma/migrations/20220922210703_outlaw/migration.sql
@@ -0,0 +1,64 @@
+CREATE OR REPLACE FUNCTION create_item(
+ title TEXT, url TEXT, text TEXT, boost INTEGER,
+ parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
+ has_img_link BOOLEAN, spam_within INTERVAL)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_msats INTEGER;
+ cost INTEGER;
+ free_posts INTEGER;
+ free_comments INTEGER;
+ freebie BOOLEAN;
+ item "Item";
+ med_votes INTEGER;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ SELECT msats, "freePosts", "freeComments"
+ INTO user_msats, free_posts, free_comments
+ FROM users WHERE id = user_id;
+
+ freebie := (parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0);
+ cost := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within)) * CASE WHEN has_img_link THEN 10 ELSE 1 END;
+
+ IF NOT freebie AND cost > user_msats THEN
+ RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
+ END IF;
+
+ -- get this user's median item score
+ SELECT COALESCE(percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0) INTO med_votes FROM "Item" WHERE "userId" = user_id;
+
+ -- if their median votes are positive, start at 0
+ -- if the median votes are negative, start their post with that many down votes
+ -- basically: if their median post is bad, presume this post is too
+ IF med_votes >= 0 THEN
+ med_votes := 0;
+ ELSE
+ med_votes := ABS(med_votes);
+ END IF;
+
+ INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", "paidImgLink", "weightedDownVotes", created_at, updated_at)
+ VALUES (title, url, text, user_id, parent_id, fwd_user_id, has_img_link, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
+
+ IF freebie THEN
+ IF parent_id IS NULL THEN
+ UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
+ ELSE
+ UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
+ END IF;
+ ELSE
+ UPDATE users SET msats = msats - cost WHERE id = user_id;
+
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (cost / 1000, item.id, user_id, 'VOTE', now_utc(), now_utc());
+ END IF;
+
+ IF boost > 0 THEN
+ PERFORM item_act(item.id, user_id, 'BOOST', boost);
+ END IF;
+
+ RETURN item;
+END;
+$$;
\ No newline at end of file
diff --git a/prisma/migrations/20220923153826_outlaw_float/migration.sql b/prisma/migrations/20220923153826_outlaw_float/migration.sql
new file mode 100644
index 00000000..69777cdc
--- /dev/null
+++ b/prisma/migrations/20220923153826_outlaw_float/migration.sql
@@ -0,0 +1,64 @@
+CREATE OR REPLACE FUNCTION create_item(
+ title TEXT, url TEXT, text TEXT, boost INTEGER,
+ parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
+ has_img_link BOOLEAN, spam_within INTERVAL)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_msats INTEGER;
+ cost INTEGER;
+ free_posts INTEGER;
+ free_comments INTEGER;
+ freebie BOOLEAN;
+ item "Item";
+ med_votes FLOAT;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ SELECT msats, "freePosts", "freeComments"
+ INTO user_msats, free_posts, free_comments
+ FROM users WHERE id = user_id;
+
+ freebie := (parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0);
+ cost := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within)) * CASE WHEN has_img_link THEN 10 ELSE 1 END;
+
+ IF NOT freebie AND cost > user_msats THEN
+ RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
+ END IF;
+
+ -- get this user's median item score
+ SELECT COALESCE(percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0) INTO med_votes FROM "Item" WHERE "userId" = user_id;
+
+ -- if their median votes are positive, start at 0
+ -- if the median votes are negative, start their post with that many down votes
+ -- basically: if their median post is bad, presume this post is too
+ IF med_votes >= 0 THEN
+ med_votes := 0;
+ ELSE
+ med_votes := ABS(med_votes);
+ END IF;
+
+ INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", "paidImgLink", "weightedDownVotes", created_at, updated_at)
+ VALUES (title, url, text, user_id, parent_id, fwd_user_id, has_img_link, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
+
+ IF freebie THEN
+ IF parent_id IS NULL THEN
+ UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
+ ELSE
+ UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
+ END IF;
+ ELSE
+ UPDATE users SET msats = msats - cost WHERE id = user_id;
+
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (cost / 1000, item.id, user_id, 'VOTE', now_utc(), now_utc());
+ END IF;
+
+ IF boost > 0 THEN
+ PERFORM item_act(item.id, user_id, 'BOOST', boost);
+ END IF;
+
+ RETURN item;
+END;
+$$;
\ No newline at end of file
diff --git a/prisma/migrations/20220926201629_freebies/migration.sql b/prisma/migrations/20220926201629_freebies/migration.sql
new file mode 100644
index 00000000..7e8139aa
--- /dev/null
+++ b/prisma/migrations/20220926201629_freebies/migration.sql
@@ -0,0 +1,9 @@
+-- AlterTable
+ALTER TABLE "Item"
+ADD COLUMN "bio" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "freebie" BOOLEAN NOT NULL DEFAULT false;
+
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "greeterMode" BOOLEAN NOT NULL DEFAULT false,
+ALTER COLUMN "freeComments" SET DEFAULT 5,
+ALTER COLUMN "freePosts" SET DEFAULT 2;
\ No newline at end of file
diff --git a/prisma/migrations/20220926204325_item_bio/migration.sql b/prisma/migrations/20220926204325_item_bio/migration.sql
new file mode 100644
index 00000000..0dac0f3d
--- /dev/null
+++ b/prisma/migrations/20220926204325_item_bio/migration.sql
@@ -0,0 +1,172 @@
+DROP FUNCTION IF EXISTS create_bio(title TEXT, text TEXT, user_id INTEGER, has_img_link BOOLEAN);
+
+-- when creating bio, set bio flag so they won't appear on first page
+CREATE OR REPLACE FUNCTION create_bio(title TEXT, text TEXT, user_id INTEGER)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ item "Item";
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ SELECT * INTO item FROM create_item(title, NULL, text, 0, NULL, user_id, NULL, '0');
+
+ UPDATE "Item" SET bio = true WHERE id = item.id;
+ UPDATE users SET "bioId" = item.id WHERE id = user_id;
+
+ RETURN item;
+END;
+$$;
+
+DROP FUNCTION IF EXISTS create_item(
+ title TEXT, url TEXT, text TEXT, boost INTEGER,
+ parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
+ has_img_link BOOLEAN, spam_within INTERVAL);
+
+-- when creating free item, set freebie flag so can be optionally viewed
+CREATE OR REPLACE FUNCTION create_item(
+ title TEXT, url TEXT, text TEXT, boost INTEGER,
+ parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
+ spam_within INTERVAL)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_msats INTEGER;
+ cost INTEGER;
+ free_posts INTEGER;
+ free_comments INTEGER;
+ freebie BOOLEAN;
+ item "Item";
+ med_votes FLOAT;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ SELECT msats, "freePosts", "freeComments"
+ INTO user_msats, free_posts, free_comments
+ FROM users WHERE id = user_id;
+
+ cost := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within));
+ freebie := (cost <= 1000) AND ((parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0));
+
+ IF NOT freebie AND cost > user_msats THEN
+ RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
+ END IF;
+
+ -- get this user's median item score
+ SELECT COALESCE(percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0) INTO med_votes FROM "Item" WHERE "userId" = user_id;
+
+ -- if their median votes are positive, start at 0
+ -- if the median votes are negative, start their post with that many down votes
+ -- basically: if their median post is bad, presume this post is too
+ IF med_votes >= 0 THEN
+ med_votes := 0;
+ ELSE
+ med_votes := ABS(med_votes);
+ END IF;
+
+ INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", freebie, "weightedDownVotes", created_at, updated_at)
+ VALUES (title, url, text, user_id, parent_id, fwd_user_id, freebie, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
+
+ IF freebie THEN
+ IF parent_id IS NULL THEN
+ UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
+ ELSE
+ UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
+ END IF;
+ ELSE
+ UPDATE users SET msats = msats - cost WHERE id = user_id;
+
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (cost / 1000, item.id, user_id, 'VOTE', now_utc(), now_utc());
+ END IF;
+
+ IF boost > 0 THEN
+ PERFORM item_act(item.id, user_id, 'BOOST', boost);
+ END IF;
+
+ RETURN item;
+END;
+$$;
+
+DROP FUNCTION IF EXISTS update_item(item_id INTEGER,
+ item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
+ fwd_user_id INTEGER, has_img_link BOOLEAN);
+
+CREATE OR REPLACE FUNCTION update_item(item_id INTEGER,
+ item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
+ fwd_user_id INTEGER)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_msats INTEGER;
+ item "Item";
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ UPDATE "Item" set title = item_title, url = item_url, text = item_text, "fwdUserId" = fwd_user_id
+ WHERE id = item_id
+ RETURNING * INTO item;
+
+ IF boost > 0 THEN
+ PERFORM item_act(item.id, item."userId", 'BOOST', boost);
+ END IF;
+
+ RETURN item;
+END;
+$$;
+
+DROP FUNCTION IF EXISTS create_poll(
+ title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
+ options TEXT[], fwd_user_id INTEGER, has_img_link BOOLEAN, spam_within INTERVAL);
+
+CREATE OR REPLACE FUNCTION create_poll(
+ title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
+ options TEXT[], fwd_user_id INTEGER, spam_within INTERVAL)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ item "Item";
+ option TEXT;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ item := create_item(title, null, text, boost, null, user_id, fwd_user_id, spam_within);
+
+ UPDATE "Item" set "pollCost" = poll_cost where id = item.id;
+ FOREACH option IN ARRAY options LOOP
+ INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
+ END LOOP;
+
+ RETURN item;
+END;
+$$;
+
+DROP FUNCTION IF EXISTS update_poll(
+ id INTEGER, title TEXT, text TEXT, boost INTEGER,
+ options TEXT[], fwd_user_id INTEGER, has_img_link BOOLEAN);
+
+CREATE OR REPLACE FUNCTION update_poll(
+ id INTEGER, title TEXT, text TEXT, boost INTEGER,
+ options TEXT[], fwd_user_id INTEGER)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ item "Item";
+ option TEXT;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ item := update_item(id, title, null, text, boost, fwd_user_id);
+
+ FOREACH option IN ARRAY options LOOP
+ INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
+ END LOOP;
+
+ RETURN item;
+END;
+$$;
\ No newline at end of file
diff --git a/prisma/migrations/20220927214007_reserve_names/migration.sql b/prisma/migrations/20220927214007_reserve_names/migration.sql
new file mode 100644
index 00000000..e28da3b0
--- /dev/null
+++ b/prisma/migrations/20220927214007_reserve_names/migration.sql
@@ -0,0 +1,4 @@
+INSERT INTO "users" ("name") VALUES
+('freebie'),
+('borderland'),
+('outlawed');
\ No newline at end of file
diff --git a/prisma/migrations/20220929183848_job_funcs/migration.sql b/prisma/migrations/20220929183848_job_funcs/migration.sql
new file mode 100644
index 00000000..040cbedf
--- /dev/null
+++ b/prisma/migrations/20220929183848_job_funcs/migration.sql
@@ -0,0 +1,101 @@
+-- charge the user for the auction item
+CREATE OR REPLACE FUNCTION run_auction(item_id INTEGER) RETURNS void AS $$
+ DECLARE
+ bid INTEGER;
+ user_id INTEGER;
+ user_msats INTEGER;
+ item_status "Status";
+ status_updated_at timestamp(3);
+ BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ -- extract data we need
+ SELECT "maxBid" * 1000, "userId", status, "statusUpdatedAt" INTO bid, user_id, item_status, status_updated_at FROM "Item" WHERE id = item_id;
+ SELECT msats INTO user_msats FROM users WHERE id = user_id;
+
+ -- 0 bid items expire after 30 days unless updated
+ IF bid = 0 THEN
+ IF item_status <> 'STOPPED' AND status_updated_at < now_utc() - INTERVAL '30 days' THEN
+ UPDATE "Item" SET status = 'STOPPED', "statusUpdatedAt" = now_utc() WHERE id = item_id;
+ END IF;
+ RETURN;
+ END IF;
+
+ -- check if user wallet has enough sats
+ IF bid > user_msats THEN
+ -- if not, set status = NOSATS and statusUpdatedAt to now_utc if not already set
+ IF item_status <> 'NOSATS' THEN
+ UPDATE "Item" SET status = 'NOSATS', "statusUpdatedAt" = now_utc() WHERE id = item_id;
+ END IF;
+ ELSE
+ -- if so, deduct from user
+ UPDATE users SET msats = msats - bid WHERE id = user_id;
+
+ -- create an item act
+ INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
+ VALUES (bid / 1000, item_id, user_id, 'STREAM', now_utc(), now_utc());
+
+ -- update item status = ACTIVE and statusUpdatedAt = now_utc if NOSATS
+ IF item_status = 'NOSATS' THEN
+ UPDATE "Item" SET status = 'ACTIVE', "statusUpdatedAt" = now_utc() WHERE id = item_id;
+ END IF;
+ END IF;
+ END;
+$$ LANGUAGE plpgsql;
+
+-- when creating free item, set freebie flag so can be optionally viewed
+CREATE OR REPLACE FUNCTION create_job(
+ title TEXT, url TEXT, text TEXT, user_id INTEGER, job_bid INTEGER, job_company TEXT,
+ job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER)
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ item "Item";
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+ -- create item
+ SELECT * INTO item FROM create_item(title, url, text, 0, NULL, user_id, NULL, '0');
+
+ -- update by adding additional fields
+ UPDATE "Item"
+ SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id, "subName" = 'jobs'
+ WHERE id = item.id RETURNING * INTO item;
+
+ -- run_auction
+ EXECUTE run_auction(item.id);
+
+ RETURN item;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_job(item_id INTEGER,
+ item_title TEXT, item_url TEXT, item_text TEXT, job_bid INTEGER, job_company TEXT,
+ job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER, job_status "Status")
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_msats INTEGER;
+ item "Item";
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+ -- update item
+ SELECT * INTO item FROM update_item(item_id, item_title, item_url, item_text, 0, NULL);
+
+ IF item.status <> job_status THEN
+ UPDATE "Item"
+ SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id, status = job_status, "statusUpdatedAt" = now_utc()
+ WHERE id = item.id RETURNING * INTO item;
+ ELSE
+ UPDATE "Item"
+ SET "maxBid" = job_bid, company = job_company, location = job_location, remote = job_remote, "uploadId" = job_upload_id
+ WHERE id = item.id RETURNING * INTO item;
+ END IF;
+
+ -- run_auction
+ EXECUTE run_auction(item.id);
+
+ RETURN item;
+END;
+$$;
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 75114ba1..8b4eeba8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -32,8 +32,8 @@ model User {
bioId Int?
msats Int @default(0)
stackedMsats Int @default(0)
- freeComments Int @default(0)
- freePosts Int @default(0)
+ freeComments Int @default(5)
+ freePosts Int @default(2)
checkedNotesAt DateTime?
tipDefault Int @default(10)
fiatCurrency String @default("USD")
@@ -60,6 +60,10 @@ model User {
// privacy settings
hideInvoiceDesc Boolean @default(false)
+ // content settings
+ wildWestMode Boolean @default(false)
+ greeterMode Boolean @default(false)
+
Earn Earn[]
Upload Upload[] @relation(name: "Uploads")
PollVote PollVote[]
@@ -89,6 +93,13 @@ model Upload {
@@index([userId])
}
+enum EarnType {
+ POST
+ COMMENT
+ TIP_COMMENT
+ TIP_POST
+}
+
model Earn {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
@@ -98,8 +109,13 @@ model Earn {
user User @relation(fields: [userId], references: [id])
userId Int
+ type EarnType?
+ typeId Int?
+ rank Int?
+
@@index([createdAt])
@@index([userId])
+ @@index([createdAt, userId])
}
model LnAuth {
@@ -171,9 +187,14 @@ model Item {
upload Upload?
paidImgLink Boolean @default(false)
+ // is free post or bio
+ freebie Boolean @default(false)
+ bio Boolean @default(false)
+
// denormalized self stats
- weightedVotes Float @default(0)
- sats Int @default(0)
+ weightedVotes Float @default(0)
+ weightedDownVotes Float @default(0)
+ sats Int @default(0)
// denormalized comment stats
ncomments Int @default(0)
@@ -285,6 +306,7 @@ enum ItemActType {
TIP
STREAM
POLL
+ DONT_LIKE_THIS
}
model ItemAct {
diff --git a/styles/globals.scss b/styles/globals.scss
index 4fcff96a..3d65842f 100644
--- a/styles/globals.scss
+++ b/styles/globals.scss
@@ -217,7 +217,7 @@ a:hover {
background-color: var(--theme-inputBg);
border: 1px solid var(--theme-borderColor);
max-width: 90vw;
- overflow: scroll;
+ overflow: auto;
}
.dropdown-item {
diff --git a/svgs/cloud-fill.svg b/svgs/cloud-fill.svg
new file mode 100644
index 00000000..ba229a29
--- /dev/null
+++ b/svgs/cloud-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgs/error-warning-fill.svg b/svgs/error-warning-fill.svg
new file mode 100644
index 00000000..a0e4ce1a
--- /dev/null
+++ b/svgs/error-warning-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgs/flag-2-fill.svg b/svgs/flag-2-fill.svg
new file mode 100644
index 00000000..db4089ec
--- /dev/null
+++ b/svgs/flag-2-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgs/flag-fill.svg b/svgs/flag-fill.svg
new file mode 100644
index 00000000..cfc536a6
--- /dev/null
+++ b/svgs/flag-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgs/more-fill.svg b/svgs/more-fill.svg
new file mode 100644
index 00000000..087b4440
--- /dev/null
+++ b/svgs/more-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgs/more-line.svg b/svgs/more-line.svg
new file mode 100644
index 00000000..aafdf470
--- /dev/null
+++ b/svgs/more-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/worker/earn.js b/worker/earn.js
index 871a95f0..95e9895a 100644
--- a/worker/earn.js
+++ b/worker/earn.js
@@ -2,8 +2,7 @@ const serialize = require('../api/resolvers/serial')
const ITEM_EACH_REWARD = 3.0
const UPVOTE_EACH_REWARD = 6.0
-const TOP_ITEMS = 21
-const EARLY_MULTIPLIER_MAX = 100.0
+const TOP_PERCENTILE = 21
// TODO: use a weekly trust measure or make trust decay
function earn ({ models }) {
@@ -11,7 +10,7 @@ function earn ({ models }) {
console.log('running', name)
// compute how much sn earned today
- const [{ sum }] = await models.$queryRaw`
+ let [{ sum }] = await models.$queryRaw`
SELECT sum("ItemAct".sats)
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
@@ -19,10 +18,13 @@ function earn ({ models }) {
OR ("ItemAct".act IN ('VOTE','POLL') AND "Item"."userId" = "ItemAct"."userId"))
AND "ItemAct".created_at > now_utc() - INTERVAL '1 day'`
+ // convert to msats
+ sum = sum * 1000
+
/*
How earnings work:
- 1/3: top 21 posts over last 36 hours, scored on a relative basis
- 1/3: top 21 comments over last 36 hours, scored on a relative basis
+ 1/3: top 21% posts over last 36 hours, scored on a relative basis
+ 1/3: top 21% comments over last 36 hours, scored on a relative basis
1/3: top upvoters of top posts/comments, scored on:
- their trust
- how much they tipped
@@ -30,20 +32,28 @@ function earn ({ models }) {
- how the post/comment scored
*/
- // get earners { id, earnings }
+ if (sum <= 0) {
+ console.log('done', name, 'no earning')
+ return
+ }
+
+ // get earners { userId, id, type, rank, proportion }
const earners = await models.$queryRaw(`
WITH item_ratios AS (
- SELECT *,
- "weightedVotes"/coalesce(NULLIF(sum("weightedVotes") OVER (PARTITION BY "parentId" IS NULL),0), ${TOP_ITEMS}) AS ratio
- FROM (
- SELECT *,
- ROW_NUMBER() OVER (PARTITION BY "parentId" IS NULL ORDER BY "weightedVotes" desc) AS r
- FROM
- "Item"
- WHERE created_at >= now_utc() - interval '36 hours'
- ) x
- WHERE x.r <= ${TOP_ITEMS}
- ),
+ SELECT *,
+ CASE WHEN "parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type,
+ CASE WHEN "weightedVotes" > 0 THEN "weightedVotes"/(sum("weightedVotes") OVER (PARTITION BY "parentId" IS NULL)) ELSE 0 END AS ratio
+ FROM (
+ SELECT *,
+ NTILE(100) OVER (PARTITION BY "parentId" IS NULL ORDER BY "weightedVotes" desc) AS percentile,
+ ROW_NUMBER() OVER (PARTITION BY "parentId" IS NULL ORDER BY "weightedVotes" desc) AS rank
+ FROM
+ "Item"
+ WHERE created_at >= now_utc() - interval '36 hours'
+ AND "weightedVotes" > 0
+ ) x
+ WHERE x.percentile <= ${TOP_PERCENTILE}
+ ),
upvoters AS (
SELECT "ItemAct"."userId", item_ratios.id, item_ratios.ratio, item_ratios."parentId",
sum("ItemAct".sats) as tipped, min("ItemAct".created_at) as acted_at
@@ -54,36 +64,47 @@ function earn ({ models }) {
GROUP BY "ItemAct"."userId", item_ratios.id, item_ratios.ratio, item_ratios."parentId"
),
upvoter_ratios AS (
- SELECT "userId", sum(early_multiplier*tipped_ratio*ratio*users.trust) as upvoting_score,
- "parentId" IS NULL as "isPost"
+ SELECT "userId", sum(early_multiplier*tipped_ratio*ratio*users.trust) as upvoter_ratio,
+ "parentId" IS NULL as "isPost", CASE WHEN "parentId" IS NULL THEN 'TIP_POST' ELSE 'TIP_COMMENT' END as type
FROM (
SELECT *,
- ${EARLY_MULTIPLIER_MAX}/(ROW_NUMBER() OVER (partition by id order by acted_at asc)) AS early_multiplier,
+ 1/(ROW_NUMBER() OVER (partition by id order by acted_at asc)) AS early_multiplier,
tipped::float/(sum(tipped) OVER (partition by id)) tipped_ratio
FROM upvoters
) u
JOIN users on "userId" = users.id
GROUP BY "userId", "parentId" IS NULL
)
- SELECT "userId" as id, FLOOR(sum(proportion)*${sum}*1000) as earnings
- FROM (
- SELECT "userId",
- upvoting_score/(sum(upvoting_score) OVER (PARTITION BY "isPost"))/${UPVOTE_EACH_REWARD} as proportion
- FROM upvoter_ratios
- UNION ALL
- SELECT "userId", ratio/${ITEM_EACH_REWARD} as proportion
- FROM item_ratios
- ) a
- GROUP BY "userId"
- HAVING FLOOR(sum(proportion)*${sum}) >= 1`)
+ SELECT "userId", NULL as id, type, ROW_NUMBER() OVER (PARTITION BY "isPost" ORDER BY upvoter_ratio DESC) as rank,
+ upvoter_ratio/(sum(upvoter_ratio) OVER (PARTITION BY "isPost"))/${UPVOTE_EACH_REWARD} as proportion
+ FROM upvoter_ratios
+ WHERE upvoter_ratio > 0
+ UNION ALL
+ SELECT "userId", id, type, rank, ratio/${ITEM_EACH_REWARD} as proportion
+ FROM item_ratios`)
+
+ // in order to group earnings for users we use the same createdAt time for
+ // all earnings
+ const now = new Date(new Date().getTime())
+
+ // this is just a sanity check because it seems like a good idea
+ let total = 0
// for each earner, serialize earnings
// we do this for each earner because we don't need to serialize
// all earner updates together
earners.forEach(async earner => {
- if (earner.earnings > 0) {
+ const earnings = Math.floor(earner.proportion * sum)
+ total += earnings
+ if (total > sum) {
+ console.log('total exceeds sum', name)
+ return
+ }
+
+ if (earnings > 0) {
await serialize(models,
- models.$executeRaw`SELECT earn(${earner.id}, ${earner.earnings})`)
+ models.$executeRaw`SELECT earn(${earner.userId}, ${earnings},
+ ${now}, ${earner.type}, ${earner.id}, ${earner.rank})`)
}
})
diff --git a/worker/trust.js b/worker/trust.js
index eb2d0e07..a10f9ba4 100644
--- a/worker/trust.js
+++ b/worker/trust.js
@@ -12,6 +12,7 @@ function trust ({ boss, models }) {
// only explore a path up to this depth from start
const MAX_DEPTH = 6
const MAX_TRUST = 0.9
+const MIN_SUCCESS = 5
// https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
const Z_CONFIDENCE = 2.326347874041 // 98% confidence
@@ -162,39 +163,70 @@ function trustGivenGraph (graph, start) {
// return graph
// }
-// upvote confidence graph
+// old upvote confidence graph
+// async function getGraph (models) {
+// const [{ graph }] = await models.$queryRaw`
+// select json_object_agg(id, hops) as graph
+// from (
+// select id, json_agg(json_build_object('node', oid, 'trust', trust)) as hops
+// from (
+// select s.id, s.oid, confidence(s.shared, count(*), ${Z_CONFIDENCE}) as trust
+// from (
+// select a."userId" as id, b."userId" as oid, count(*) as shared
+// from "ItemAct" b
+// join users bu on bu.id = b."userId"
+// join "ItemAct" a on b."itemId" = a."itemId"
+// join users au on au.id = a."userId"
+// join "Item" on "Item".id = b."itemId"
+// where b.act = 'VOTE'
+// and a.act = 'VOTE'
+// and "Item"."parentId" is null
+// and "Item"."userId" <> b."userId"
+// and "Item"."userId" <> a."userId"
+// and b."userId" <> a."userId"
+// and "Item".created_at >= au.created_at and "Item".created_at >= bu.created_at
+// group by b."userId", a."userId") s
+// join users u on s.id = u.id
+// join users ou on s.oid = ou.id
+// join "ItemAct" on "ItemAct"."userId" = s.oid
+// join "Item" on "Item".id = "ItemAct"."itemId"
+// where "ItemAct".act = 'VOTE' and "Item"."parentId" is null
+// and "Item"."userId" <> s.oid and "Item"."userId" <> s.id
+// and "Item".created_at >= u.created_at and "Item".created_at >= ou.created_at
+// group by s.id, s.oid, s.shared
+// ) a
+// group by id
+// ) b`
+// return graph
+// }
+
async function getGraph (models) {
const [{ graph }] = await models.$queryRaw`
- select json_object_agg(id, hops) as graph
- from (
- select id, json_agg(json_build_object('node', oid, 'trust', trust)) as hops
- from (
- select s.id, s.oid, confidence(s.shared, count(*), ${Z_CONFIDENCE}) as trust
- from (
- select a."userId" as id, b."userId" as oid, count(*) as shared
- from "ItemAct" b
- join users bu on bu.id = b."userId"
- join "ItemAct" a on b."itemId" = a."itemId"
- join users au on au.id = a."userId"
- join "Item" on "Item".id = b."itemId"
- where b.act = 'VOTE'
- and a.act = 'VOTE'
- and "Item"."parentId" is null
- and "Item"."userId" <> b."userId"
- and "Item"."userId" <> a."userId"
- and b."userId" <> a."userId"
- and "Item".created_at >= au.created_at and "Item".created_at >= bu.created_at
- group by b."userId", a."userId") s
- join users u on s.id = u.id
- join users ou on s.oid = ou.id
- join "ItemAct" on "ItemAct"."userId" = s.oid
- join "Item" on "Item".id = "ItemAct"."itemId"
- where "ItemAct".act = 'VOTE' and "Item"."parentId" is null
- and "Item"."userId" <> s.oid and "Item"."userId" <> s.id
- and "Item".created_at >= u.created_at and "Item".created_at >= ou.created_at
- group by s.id, s.oid, s.shared
+ SELECT json_object_agg(id, hops) AS graph
+ FROM (
+ SELECT id, json_agg(json_build_object('node', oid, 'trust', trust)) AS hops
+ FROM (
+ WITH user_votes AS (
+ SELECT "ItemAct"."userId" AS user_id, users.name AS name, "ItemAct"."itemId" AS item_id, "ItemAct".created_at AS act_at,
+ users.created_at AS user_at, "Item".created_at AS item_at, count(*) OVER (partition by "ItemAct"."userId") AS user_vote_count
+ FROM "ItemAct"
+ JOIN "Item" ON "Item".id = "ItemAct"."itemId" AND "ItemAct".act = 'VOTE' AND "Item"."parentId" IS NULL
+ JOIN users ON "ItemAct"."userId" = users.id
+ ),
+ user_pair AS (
+ SELECT a.user_id AS a_id, a.name AS a_name, b.user_id AS b_id, b.name AS b_name,
+ count(*) FILTER(WHERE a.act_at > b.act_at) AS before,
+ count(*) FILTER(WHERE b.act_at > a.act_at) AS after,
+ CASE WHEN b.user_at > a.user_at THEN b.user_vote_count ELSE a.user_vote_count END AS total
+ FROM user_votes a
+ JOIN user_votes b ON a.item_id = b.item_id
+ GROUP BY a.user_id, a.name, a.user_at, a.user_vote_count, b.user_id, b.name, b.user_at, b.user_vote_count
+ )
+ SELECT a_id AS id, a_name, b_id AS oid, b_name, confidence(before, total - after, ${Z_CONFIDENCE}) AS trust, before, after, total
+ FROM user_pair
+ WHERE before >= ${MIN_SUCCESS}
) a
- group by id
+ GROUP BY a.id
) b`
return graph
}