From ed86f863e35f09df98c616cd0d2e3b96c9dd97a0 Mon Sep 17 00:00:00 2001 From: pluja Date: Wed, 21 May 2025 14:31:33 +0000 Subject: [PATCH] Release 2025-05-21-MXjT --- .../migration.sql | 11 ++ web/prisma/schema.prisma | 5 + .../triggers/11_notifications_karma.sql | 29 ++++ web/src/actions/notifications.ts | 3 + web/src/components/AnnouncementBanner.astro | 2 +- web/src/constants/announcementTypes.ts | 24 +-- web/src/constants/notificationTypes.ts | 10 +- web/src/lib/notifications.ts | 26 +++ web/src/pages/account/index.astro | 157 ++++++++++-------- web/src/pages/notifications.astro | 25 +++ web/src/pages/u/[username].astro | 152 +++++++++-------- web/src/styles/global.css | 10 ++ 12 files changed, 305 insertions(+), 149 deletions(-) create mode 100644 web/prisma/migrations/20250521140743_karma_change_notifications/migration.sql create mode 100644 web/prisma/triggers/11_notifications_karma.sql diff --git a/web/prisma/migrations/20250521140743_karma_change_notifications/migration.sql b/web/prisma/migrations/20250521140743_karma_change_notifications/migration.sql new file mode 100644 index 0000000..afd957c --- /dev/null +++ b/web/prisma/migrations/20250521140743_karma_change_notifications/migration.sql @@ -0,0 +1,11 @@ +-- AlterEnum +ALTER TYPE "NotificationType" ADD VALUE 'KARMA_CHANGE'; + +-- AlterTable +ALTER TABLE "Notification" ADD COLUMN "aboutKarmaTransactionId" INTEGER; + +-- AlterTable +ALTER TABLE "NotificationPreferences" ADD COLUMN "karmaNotificationThreshold" INTEGER NOT NULL DEFAULT 10; + +-- AddForeignKey +ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutKarmaTransactionId_fkey" FOREIGN KEY ("aboutKarmaTransactionId") REFERENCES "KarmaTransaction"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index abeec0d..786989c 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -135,6 +135,7 @@ enum NotificationType { SUGGESTION_MESSAGE SUGGESTION_STATUS_CHANGE // KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. + KARMA_CHANGE /// Marked as spammer, promoted to admin, etc. ACCOUNT_STATUS_CHANGE EVENT_CREATED @@ -207,6 +208,8 @@ model Notification { aboutCommentStatusChange CommentStatusChange? aboutServiceVerificationStatusChange ServiceVerificationStatusChange? aboutSuggestionStatusChange ServiceSuggestionStatusChange? + aboutKarmaTransaction KarmaTransaction? @relation(fields: [aboutKarmaTransactionId], references: [id]) + aboutKarmaTransactionId Int? @@index([userId]) @@index([read]) @@ -229,6 +232,7 @@ model NotificationPreferences { enableOnMyCommentStatusChange Boolean @default(true) enableAutowatchMyComments Boolean @default(true) enableNotifyPendingRepliesOnWatch Boolean @default(false) + karmaNotificationThreshold Int @default(10) onEventCreatedForServices Service[] @relation("onEventCreatedForServices") onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices") @@ -522,6 +526,7 @@ model KarmaTransaction { createdAt DateTime @default(now()) grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull) grantedByUserId Int? + Notification Notification[] @@index([createdAt]) @@index([userId]) diff --git a/web/prisma/triggers/11_notifications_karma.sql b/web/prisma/triggers/11_notifications_karma.sql new file mode 100644 index 0000000..c687217 --- /dev/null +++ b/web/prisma/triggers/11_notifications_karma.sql @@ -0,0 +1,29 @@ +CREATE OR REPLACE FUNCTION trigger_karma_notifications() +RETURNS TRIGGER AS $$ +BEGIN + -- Only create notification if the user has enabled karma notifications + -- and the karma change exceeds their threshold + INSERT INTO "Notification" ("userId", "type", "aboutKarmaTransactionId") + SELECT NEW."userId", 'KARMA_CHANGE', NEW.id + FROM "NotificationPreferences" np + WHERE np."userId" = NEW."userId" + AND ABS(NEW.points) >= COALESCE(np."karmaNotificationThreshold", 10) + AND NOT EXISTS ( + SELECT 1 FROM "Notification" n + WHERE n."userId" = NEW."userId" + AND n."type" = 'KARMA_CHANGE' + AND n."aboutKarmaTransactionId" = NEW.id + ); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Drop the trigger if it exists to ensure a clean setup +DROP TRIGGER IF EXISTS karma_notifications_trigger ON "KarmaTransaction"; + +-- Create the trigger to fire after inserts +CREATE TRIGGER karma_notifications_trigger + AFTER INSERT ON "KarmaTransaction" + FOR EACH ROW + EXECUTE FUNCTION trigger_karma_notifications(); \ No newline at end of file diff --git a/web/src/actions/notifications.ts b/web/src/actions/notifications.ts index 3ae200c..b415f17 100644 --- a/web/src/actions/notifications.ts +++ b/web/src/actions/notifications.ts @@ -31,6 +31,7 @@ export const notificationActions = { enableOnMyCommentStatusChange: z.coerce.boolean().optional(), enableAutowatchMyComments: z.coerce.boolean().optional(), enableNotifyPendingRepliesOnWatch: z.coerce.boolean().optional(), + karmaNotificationThreshold: z.coerce.number().int().min(1).optional(), }), handler: async (input, context) => { await prisma.notificationPreferences.upsert({ @@ -39,12 +40,14 @@ export const notificationActions = { enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange, enableAutowatchMyComments: input.enableAutowatchMyComments, enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch, + karmaNotificationThreshold: input.karmaNotificationThreshold, }, create: { userId: context.locals.user.id, enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange, enableAutowatchMyComments: input.enableAutowatchMyComments, enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch, + karmaNotificationThreshold: input.karmaNotificationThreshold, }, }) }, diff --git a/web/src/components/AnnouncementBanner.astro b/web/src/components/AnnouncementBanner.astro index 66a40df..13eef3c 100644 --- a/web/src/components/AnnouncementBanner.astro +++ b/web/src/components/AnnouncementBanner.astro @@ -70,7 +70,7 @@ const Tag = announcement.link ? 'a' : 'div' diff --git a/web/src/constants/announcementTypes.ts b/web/src/constants/announcementTypes.ts index 28c0fa9..019d6a4 100644 --- a/web/src/constants/announcementTypes.ts +++ b/web/src/constants/announcementTypes.ts @@ -25,12 +25,12 @@ export const { (value): AnnouncementTypeInfo => ({ value, label: value ? transformCase(value.replaceAll('_', ' '), 'title') : String(value), - icon: 'ri:information-fill', + icon: 'ri:question-line', classNames: { - container: 'bg-blue-950', - bg: 'from-blue-400 to-blue-700', - content: '[--gradient-edge:var(--color-blue-100)] [--gradient-center:var(--color-blue-200)]', - icon: 'text-blue-400/80', + container: 'bg-cyan-950', + bg: 'from-cyan-400 to-cyan-700', + content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]', + icon: 'text-cyan-300/80', badge: 'bg-blue-900/30 text-blue-400', }, }), @@ -38,12 +38,12 @@ export const { { value: 'INFO', label: 'Info', - icon: 'ri:information-fill', + icon: 'ri:information-line', classNames: { - container: 'bg-green-950', - bg: 'from-green-400 to-green-700', - content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-lime-200)]', - icon: 'text-green-400/80', + container: 'bg-cyan-950', + bg: 'from-cyan-400 to-cyan-700', + content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]', + icon: 'text-cyan-300/80', badge: 'bg-blue-900/30 text-blue-400', }, }, @@ -54,7 +54,7 @@ export const { classNames: { container: 'bg-yellow-950', bg: 'from-yellow-400 to-yellow-700', - content: '[--gradient-edge:var(--color-yellow-100)] [--gradient-center:var(--color-amber-200)]', + content: '[--gradient-edge:var(--color-lime-100)] [--gradient-center:var(--color-yellow-400)]', icon: 'text-yellow-400/80', badge: 'bg-yellow-900/30 text-yellow-400', }, @@ -66,7 +66,7 @@ export const { classNames: { container: 'bg-red-950', bg: 'from-red-400 to-red-700', - content: '[--gradient-edge:var(--color-red-100)] [--gradient-center:var(--color-rose-200)]', + content: '[--gradient-edge:var(--color-red-100)] [--gradient-center:var(--color-rose-400)]', icon: 'text-red-400/80', badge: 'bg-red-900/30 text-red-400', }, diff --git a/web/src/constants/notificationTypes.ts b/web/src/constants/notificationTypes.ts index bf6e67e..4f41018 100644 --- a/web/src/constants/notificationTypes.ts +++ b/web/src/constants/notificationTypes.ts @@ -46,11 +46,11 @@ export const { icon: 'ri:lightbulb-line', }, // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. - // { - // id: 'KARMA_UNLOCK', - // label: 'Karma unlock', - // icon: 'ri:award-line', - // }, + { + id: 'KARMA_CHANGE', + label: 'Karma recieved', + icon: 'ri:award-line', + }, { id: 'ACCOUNT_STATUS_CHANGE', label: 'Change in account status', diff --git a/web/src/lib/notifications.ts b/web/src/lib/notifications.ts index 8ced794..b9e3ea0 100644 --- a/web/src/lib/notifications.ts +++ b/web/src/lib/notifications.ts @@ -1,6 +1,7 @@ import { accountStatusChangesById } from '../constants/accountStatusChange' import { commentStatusChangesById } from '../constants/commentStatusChange' import { eventTypesById } from '../constants/eventTypes' +import { getKarmaTransactionActionInfo } from '../constants/karmaTransactionActions' import { serviceVerificationStatusChangesById } from '../constants/serviceStatusChange' import { serviceSuggestionStatusChangesById } from '../constants/suggestionStatusChange' @@ -16,6 +17,12 @@ export function makeNotificationTitle( aboutCommentStatusChange: true aboutServiceVerificationStatusChange: true aboutSuggestionStatusChange: true + aboutKarmaTransaction: { + select: { + points: true + action: true + } + } aboutComment: { select: { author: { select: { id: true } } @@ -137,6 +144,13 @@ export function makeNotificationTitle( // case 'KARMA_UNLOCK': { // return 'New karma level unlocked' // } + case 'KARMA_CHANGE': { + if (!notification.aboutKarmaTransaction) return 'Your karma has changed' + const { points, action } = notification.aboutKarmaTransaction + const sign = points > 0 ? '+' : '' + const karmaInfo = getKarmaTransactionActionInfo(action) + return `${sign}${points.toLocaleString()} karma for ${karmaInfo.label}` + } case 'ACCOUNT_STATUS_CHANGE': { if (!notification.aboutAccountStatusChange) return 'Your account status has been updated' const accountStatusChange = accountStatusChangesById[notification.aboutAccountStatusChange] @@ -165,6 +179,11 @@ export function makeNotificationContent( notification: Prisma.NotificationGetPayload<{ select: { type: true + aboutKarmaTransaction: { + select: { + description: true + } + } aboutComment: { select: { content: true @@ -187,6 +206,10 @@ export function makeNotificationContent( switch (notification.type) { // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. // case 'KARMA_UNLOCK': + case 'KARMA_CHANGE': { + if (!notification.aboutKarmaTransaction) return null + return notification.aboutKarmaTransaction.description + } case 'SUGGESTION_STATUS_CHANGE': case 'ACCOUNT_STATUS_CHANGE': case 'SERVICE_VERIFICATION_STATUS_CHANGE': { @@ -280,6 +303,9 @@ export function makeNotificationLink( // case 'KARMA_UNLOCK': { // return `${origin}/account#karma-unlocks` // } + case 'KARMA_CHANGE': { + return `${origin}/account#karma-transactions` + } case 'ACCOUNT_STATUS_CHANGE': { return `${origin}/account#account-status` } diff --git a/web/src/pages/account/index.astro b/web/src/pages/account/index.astro index db7affc..17466de 100644 --- a/web/src/pages/account/index.astro +++ b/web/src/pages/account/index.astro @@ -534,77 +534,38 @@ if (!user) return Astro.rewrite('/404')
-

Positive unlocks

+ + - { - sortBy( - karmaUnlocks.filter((unlock) => unlock.karma >= 0), - 'karma' - ).map((unlock) => ( -
-
- - - -
-

- {unlock.name} -

-

{unlock.karma.toLocaleString()} karma

-
-
-
- {user.karmaUnlocks[unlock.id] ? ( - - Unlocked - - ) : ( - - Locked - - )} -
-
- )) - } -
- -
-

Negative unlocks

- - { - sortBy( - karmaUnlocks.filter((unlock) => unlock.karma < 0), - 'karma' - ) - .reverse() - .map((unlock) => ( + -

- - Negative karma leads to restrictions. Keep interactions positive to - avoid penalties. -

+
+ + + + +
@@ -858,7 +880,10 @@ if (!user) return Astro.rewrite('/404') } -
+

diff --git a/web/src/pages/notifications.astro b/web/src/pages/notifications.astro index 1bbc94e..73bef7a 100644 --- a/web/src/pages/notifications.astro +++ b/web/src/pages/notifications.astro @@ -124,6 +124,13 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast verificationStatus: true, }, }, + aboutKarmaTransaction: { + select: { + points: true, + action: true, + description: true, + }, + }, }, }), [], @@ -136,6 +143,7 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast enableOnMyCommentStatusChange: true, enableAutowatchMyComments: true, enableNotifyPendingRepliesOnWatch: true, + karmaNotificationThreshold: true, }), null, ], @@ -326,6 +334,23 @@ const notifications = dbNotifications.map((notification) => ({ ))} +
+ + + Notify me when my karma changes by at least + +
+ + points +
+
+
diff --git a/web/src/pages/u/[username].astro b/web/src/pages/u/[username].astro index dbf2417..1f5dbea 100644 --- a/web/src/pages/u/[username].astro +++ b/web/src/pages/u/[username].astro @@ -632,77 +632,38 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
-

Positive unlocks

+ + - { - sortBy( - karmaUnlocks.filter((unlock) => unlock.karma >= 0), - 'karma' - ).map((unlock) => ( -
-
- - - -
-

- {unlock.name} -

-

{unlock.karma.toLocaleString()} karma

-
-
-
- {user.karmaUnlocks[unlock.id] ? ( - - Unlocked - - ) : ( - - Locked - - )} -
-
- )) - } -
- -
-

Negative unlocks

- - { - sortBy( - karmaUnlocks.filter((unlock) => unlock.karma < 0), - 'karma' - ) - .reverse() - .map((unlock) => ( + -

- - Negative karma leads to restrictions. Keep interactions positive to - avoid penalties. -

+
+ + + + +

diff --git a/web/src/styles/global.css b/web/src/styles/global.css index fbaa8d3..d1575a2 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -105,6 +105,16 @@ } } +@theme { + --animate-text-gradient: text-gradient 4s linear 0s infinite normal forwards running; + + @keyframes text-gradient { + to { + background-position: -200%; + } + } +} + @theme { --ease-in-sine: cubic-bezier(0.12, 0, 0.39, 0); --ease-out-sine: cubic-bezier(0.61, 1, 0.88, 1);