Release 2025-05-21-MXjT

This commit is contained in:
pluja
2025-05-21 14:31:33 +00:00
parent 845aa1185c
commit ed86f863e3
12 changed files with 305 additions and 149 deletions

View File

@@ -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;

View File

@@ -135,6 +135,7 @@ enum NotificationType {
SUGGESTION_MESSAGE SUGGESTION_MESSAGE
SUGGESTION_STATUS_CHANGE SUGGESTION_STATUS_CHANGE
// KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. // 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. /// Marked as spammer, promoted to admin, etc.
ACCOUNT_STATUS_CHANGE ACCOUNT_STATUS_CHANGE
EVENT_CREATED EVENT_CREATED
@@ -207,6 +208,8 @@ model Notification {
aboutCommentStatusChange CommentStatusChange? aboutCommentStatusChange CommentStatusChange?
aboutServiceVerificationStatusChange ServiceVerificationStatusChange? aboutServiceVerificationStatusChange ServiceVerificationStatusChange?
aboutSuggestionStatusChange ServiceSuggestionStatusChange? aboutSuggestionStatusChange ServiceSuggestionStatusChange?
aboutKarmaTransaction KarmaTransaction? @relation(fields: [aboutKarmaTransactionId], references: [id])
aboutKarmaTransactionId Int?
@@index([userId]) @@index([userId])
@@index([read]) @@index([read])
@@ -229,6 +232,7 @@ model NotificationPreferences {
enableOnMyCommentStatusChange Boolean @default(true) enableOnMyCommentStatusChange Boolean @default(true)
enableAutowatchMyComments Boolean @default(true) enableAutowatchMyComments Boolean @default(true)
enableNotifyPendingRepliesOnWatch Boolean @default(false) enableNotifyPendingRepliesOnWatch Boolean @default(false)
karmaNotificationThreshold Int @default(10)
onEventCreatedForServices Service[] @relation("onEventCreatedForServices") onEventCreatedForServices Service[] @relation("onEventCreatedForServices")
onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices") onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices")
@@ -522,6 +526,7 @@ model KarmaTransaction {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull) grantedBy User? @relation("KarmaGrantedBy", fields: [grantedByUserId], references: [id], onDelete: SetNull)
grantedByUserId Int? grantedByUserId Int?
Notification Notification[]
@@index([createdAt]) @@index([createdAt])
@@index([userId]) @@index([userId])

View File

@@ -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();

View File

@@ -31,6 +31,7 @@ export const notificationActions = {
enableOnMyCommentStatusChange: z.coerce.boolean().optional(), enableOnMyCommentStatusChange: z.coerce.boolean().optional(),
enableAutowatchMyComments: z.coerce.boolean().optional(), enableAutowatchMyComments: z.coerce.boolean().optional(),
enableNotifyPendingRepliesOnWatch: z.coerce.boolean().optional(), enableNotifyPendingRepliesOnWatch: z.coerce.boolean().optional(),
karmaNotificationThreshold: z.coerce.number().int().min(1).optional(),
}), }),
handler: async (input, context) => { handler: async (input, context) => {
await prisma.notificationPreferences.upsert({ await prisma.notificationPreferences.upsert({
@@ -39,12 +40,14 @@ export const notificationActions = {
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange, enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
enableAutowatchMyComments: input.enableAutowatchMyComments, enableAutowatchMyComments: input.enableAutowatchMyComments,
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch, enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
karmaNotificationThreshold: input.karmaNotificationThreshold,
}, },
create: { create: {
userId: context.locals.user.id, userId: context.locals.user.id,
enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange, enableOnMyCommentStatusChange: input.enableOnMyCommentStatusChange,
enableAutowatchMyComments: input.enableAutowatchMyComments, enableAutowatchMyComments: input.enableAutowatchMyComments,
enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch, enableNotifyPendingRepliesOnWatch: input.enableNotifyPendingRepliesOnWatch,
karmaNotificationThreshold: input.karmaNotificationThreshold,
}, },
}) })
}, },

View File

@@ -70,7 +70,7 @@ const Tag = announcement.link ? 'a' : 'div'
<Icon name={typeInfo.icon} class={cn('size-5 flex-shrink-0')} /> <Icon name={typeInfo.icon} class={cn('size-5 flex-shrink-0')} />
<span <span
class={cn( class={cn(
'font-title line-clamp-3 bg-[linear-gradient(90deg,var(--gradient-edge,#FFEBF9)_0%,var(--gradient-center,#8a56cc)_50%,var(--gradient-edge,#FFEBF9)_100%)] bg-clip-text text-sm leading-tight text-pretty text-transparent [&_a]:underline', 'font-title animate-text-gradient line-clamp-3 bg-[linear-gradient(90deg,var(--gradient-edge,#FFEBF9)_0%,var(--gradient-center,#8a56cc)_50%,var(--gradient-edge,#FFEBF9)_100%)] bg-size-[200%] bg-clip-text text-sm leading-tight text-pretty text-transparent [&_a]:underline',
typeInfo.classNames.content typeInfo.classNames.content
)} )}
> >

View File

@@ -25,12 +25,12 @@ export const {
(value): AnnouncementTypeInfo<typeof value> => ({ (value): AnnouncementTypeInfo<typeof value> => ({
value, value,
label: value ? transformCase(value.replaceAll('_', ' '), 'title') : String(value), label: value ? transformCase(value.replaceAll('_', ' '), 'title') : String(value),
icon: 'ri:information-fill', icon: 'ri:question-line',
classNames: { classNames: {
container: 'bg-blue-950', container: 'bg-cyan-950',
bg: 'from-blue-400 to-blue-700', bg: 'from-cyan-400 to-cyan-700',
content: '[--gradient-edge:var(--color-blue-100)] [--gradient-center:var(--color-blue-200)]', content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]',
icon: 'text-blue-400/80', icon: 'text-cyan-300/80',
badge: 'bg-blue-900/30 text-blue-400', badge: 'bg-blue-900/30 text-blue-400',
}, },
}), }),
@@ -38,12 +38,12 @@ export const {
{ {
value: 'INFO', value: 'INFO',
label: 'Info', label: 'Info',
icon: 'ri:information-fill', icon: 'ri:information-line',
classNames: { classNames: {
container: 'bg-green-950', container: 'bg-cyan-950',
bg: 'from-green-400 to-green-700', bg: 'from-cyan-400 to-cyan-700',
content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-lime-200)]', content: '[--gradient-edge:var(--color-green-100)] [--gradient-center:var(--color-cyan-400)]',
icon: 'text-green-400/80', icon: 'text-cyan-300/80',
badge: 'bg-blue-900/30 text-blue-400', badge: 'bg-blue-900/30 text-blue-400',
}, },
}, },
@@ -54,7 +54,7 @@ export const {
classNames: { classNames: {
container: 'bg-yellow-950', container: 'bg-yellow-950',
bg: 'from-yellow-400 to-yellow-700', 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', icon: 'text-yellow-400/80',
badge: 'bg-yellow-900/30 text-yellow-400', badge: 'bg-yellow-900/30 text-yellow-400',
}, },
@@ -66,7 +66,7 @@ export const {
classNames: { classNames: {
container: 'bg-red-950', container: 'bg-red-950',
bg: 'from-red-400 to-red-700', 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', icon: 'text-red-400/80',
badge: 'bg-red-900/30 text-red-400', badge: 'bg-red-900/30 text-red-400',
}, },

View File

@@ -46,11 +46,11 @@ export const {
icon: 'ri:lightbulb-line', icon: 'ri:lightbulb-line',
}, },
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
// { {
// id: 'KARMA_UNLOCK', id: 'KARMA_CHANGE',
// label: 'Karma unlock', label: 'Karma recieved',
// icon: 'ri:award-line', icon: 'ri:award-line',
// }, },
{ {
id: 'ACCOUNT_STATUS_CHANGE', id: 'ACCOUNT_STATUS_CHANGE',
label: 'Change in account status', label: 'Change in account status',

View File

@@ -1,6 +1,7 @@
import { accountStatusChangesById } from '../constants/accountStatusChange' import { accountStatusChangesById } from '../constants/accountStatusChange'
import { commentStatusChangesById } from '../constants/commentStatusChange' import { commentStatusChangesById } from '../constants/commentStatusChange'
import { eventTypesById } from '../constants/eventTypes' import { eventTypesById } from '../constants/eventTypes'
import { getKarmaTransactionActionInfo } from '../constants/karmaTransactionActions'
import { serviceVerificationStatusChangesById } from '../constants/serviceStatusChange' import { serviceVerificationStatusChangesById } from '../constants/serviceStatusChange'
import { serviceSuggestionStatusChangesById } from '../constants/suggestionStatusChange' import { serviceSuggestionStatusChangesById } from '../constants/suggestionStatusChange'
@@ -16,6 +17,12 @@ export function makeNotificationTitle(
aboutCommentStatusChange: true aboutCommentStatusChange: true
aboutServiceVerificationStatusChange: true aboutServiceVerificationStatusChange: true
aboutSuggestionStatusChange: true aboutSuggestionStatusChange: true
aboutKarmaTransaction: {
select: {
points: true
action: true
}
}
aboutComment: { aboutComment: {
select: { select: {
author: { select: { id: true } } author: { select: { id: true } }
@@ -137,6 +144,13 @@ export function makeNotificationTitle(
// case 'KARMA_UNLOCK': { // case 'KARMA_UNLOCK': {
// return 'New karma level unlocked' // 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': { case 'ACCOUNT_STATUS_CHANGE': {
if (!notification.aboutAccountStatusChange) return 'Your account status has been updated' if (!notification.aboutAccountStatusChange) return 'Your account status has been updated'
const accountStatusChange = accountStatusChangesById[notification.aboutAccountStatusChange] const accountStatusChange = accountStatusChangesById[notification.aboutAccountStatusChange]
@@ -165,6 +179,11 @@ export function makeNotificationContent(
notification: Prisma.NotificationGetPayload<{ notification: Prisma.NotificationGetPayload<{
select: { select: {
type: true type: true
aboutKarmaTransaction: {
select: {
description: true
}
}
aboutComment: { aboutComment: {
select: { select: {
content: true content: true
@@ -187,6 +206,10 @@ export function makeNotificationContent(
switch (notification.type) { switch (notification.type) {
// TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code. // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
// case 'KARMA_UNLOCK': // case 'KARMA_UNLOCK':
case 'KARMA_CHANGE': {
if (!notification.aboutKarmaTransaction) return null
return notification.aboutKarmaTransaction.description
}
case 'SUGGESTION_STATUS_CHANGE': case 'SUGGESTION_STATUS_CHANGE':
case 'ACCOUNT_STATUS_CHANGE': case 'ACCOUNT_STATUS_CHANGE':
case 'SERVICE_VERIFICATION_STATUS_CHANGE': { case 'SERVICE_VERIFICATION_STATUS_CHANGE': {
@@ -280,6 +303,9 @@ export function makeNotificationLink(
// case 'KARMA_UNLOCK': { // case 'KARMA_UNLOCK': {
// return `${origin}/account#karma-unlocks` // return `${origin}/account#karma-unlocks`
// } // }
case 'KARMA_CHANGE': {
return `${origin}/account#karma-transactions`
}
case 'ACCOUNT_STATUS_CHANGE': { case 'ACCOUNT_STATUS_CHANGE': {
return `${origin}/account#account-status` return `${origin}/account#account-status`
} }

View File

@@ -534,8 +534,16 @@ if (!user) return Astro.rewrite('/404')
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-3"> <div class="space-y-3">
<h3 class="font-title border-day-700/20 text-day-200 border-b pb-2 text-sm">Positive unlocks</h3> <input type="checkbox" id="positive-unlocks-toggle" class="peer sr-only md:hidden" checked />
<label
for="positive-unlocks-toggle"
class="flex cursor-pointer items-center justify-between border-b border-green-500/20 pb-2 md:cursor-default peer-checked:[&_[data-expand-arrow]]:rotate-180"
>
<h3 class="font-title text-day-200 text-sm">Positive unlocks</h3>
<Icon name="ri:arrow-down-s-line" class="text-day-400 size-5 md:hidden" data-expand-arrow />
</label>
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
{ {
sortBy( sortBy(
karmaUnlocks.filter((unlock) => unlock.karma >= 0), karmaUnlocks.filter((unlock) => unlock.karma >= 0),
@@ -555,7 +563,10 @@ if (!user) return Astro.rewrite('/404')
</span> </span>
<div> <div>
<p <p
class={cn('font-medium', user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400')} class={cn(
'font-medium',
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
)}
> >
{unlock.name} {unlock.name}
</p> </p>
@@ -577,10 +588,20 @@ if (!user) return Astro.rewrite('/404')
)) ))
} }
</div> </div>
</div>
<div class="space-y-3"> <div class="space-y-3">
<h3 class="font-title border-b border-red-500/20 pb-2 text-sm text-red-400">Negative unlocks</h3> <input type="checkbox" id="negative-unlocks-toggle" class="peer sr-only md:hidden" />
<label
for="negative-unlocks-toggle"
class="flex cursor-pointer items-center justify-between border-b border-red-500/20 pb-2 md:cursor-default peer-checked:[&_[data-expand-arrow]]:rotate-180"
>
<h3 class="font-title text-sm text-red-400">Negative unlocks</h3>
<Icon name="ri:arrow-down-s-line" class="text-day-400 size-5 md:hidden" data-expand-arrow />
</label>
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
{ {
sortBy( sortBy(
karmaUnlocks.filter((unlock) => unlock.karma < 0), karmaUnlocks.filter((unlock) => unlock.karma < 0),
@@ -634,6 +655,7 @@ if (!user) return Astro.rewrite('/404')
</p> </p>
</div> </div>
</div> </div>
</div>
</section> </section>
<div class="space-y-6"> <div class="space-y-6">
@@ -858,7 +880,10 @@ if (!user) return Astro.rewrite('/404')
} }
</section> </section>
<section class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs"> <section
class="border-night-400 bg-night-400/5 rounded-lg border p-6 shadow-sm backdrop-blur-xs"
id="karma-transactions"
>
<header class="flex items-center justify-between"> <header class="flex items-center justify-between">
<h2 class="font-title text-day-200 mb-4 text-xl font-bold"> <h2 class="font-title text-day-200 mb-4 text-xl font-bold">
<Icon name="ri:exchange-line" class="mr-2 inline-block size-5" /> <Icon name="ri:exchange-line" class="mr-2 inline-block size-5" />

View File

@@ -124,6 +124,13 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast
verificationStatus: true, verificationStatus: true,
}, },
}, },
aboutKarmaTransaction: {
select: {
points: true,
action: true,
description: true,
},
},
}, },
}), }),
[], [],
@@ -136,6 +143,7 @@ const [dbNotifications, notificationPreferences, totalNotifications] = await Ast
enableOnMyCommentStatusChange: true, enableOnMyCommentStatusChange: true,
enableAutowatchMyComments: true, enableAutowatchMyComments: true,
enableNotifyPendingRepliesOnWatch: true, enableNotifyPendingRepliesOnWatch: true,
karmaNotificationThreshold: true,
}), }),
null, null,
], ],
@@ -326,6 +334,23 @@ const notifications = dbNotifications.map((notification) => ({
</label> </label>
))} ))}
<div class="mt-4 flex items-center justify-between rounded-md p-2 transition-colors duration-200 hover:bg-zinc-800">
<span class="flex items-center text-zinc-300">
<Icon name="ri:award-line" class="mr-2 size-5 text-zinc-400" />
Notify me when my karma changes by at least
</span>
<div class="flex items-center gap-2">
<input
type="number"
name="karmaNotificationThreshold"
value={notificationPreferences.karmaNotificationThreshold}
min="1"
class="w-20 rounded border border-zinc-700 bg-zinc-800 px-2 py-1 text-zinc-200 focus:border-blue-600 focus:ring-1 focus:ring-blue-600 focus:outline-none"
/>
<span class="text-zinc-400">points</span>
</div>
</div>
<div class="mt-4 flex justify-end"> <div class="mt-4 flex justify-end">
<Button type="submit" label="Save" icon="ri:save-line" color="success" /> <Button type="submit" label="Save" icon="ri:save-line" color="success" />
</div> </div>

View File

@@ -632,8 +632,16 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-3"> <div class="space-y-3">
<h3 class="font-title border-day-700/20 text-day-200 border-b pb-2 text-sm">Positive unlocks</h3> <input type="checkbox" id="positive-unlocks-toggle" class="peer sr-only md:hidden" checked />
<label
for="positive-unlocks-toggle"
class="flex cursor-pointer items-center justify-between border-b border-green-500/20 pb-2 md:cursor-default peer-checked:[&_[data-expand-arrow]]:rotate-180"
>
<h3 class="font-title text-day-200 text-sm">Positive unlocks</h3>
<Icon name="ri:arrow-down-s-line" class="text-day-400 size-5 md:hidden" data-expand-arrow />
</label>
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
{ {
sortBy( sortBy(
karmaUnlocks.filter((unlock) => unlock.karma >= 0), karmaUnlocks.filter((unlock) => unlock.karma >= 0),
@@ -653,7 +661,10 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
</span> </span>
<div> <div>
<p <p
class={cn('font-medium', user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400')} class={cn(
'font-medium',
user.karmaUnlocks[unlock.id] ? 'text-day-300' : 'text-day-400'
)}
> >
{unlock.name} {unlock.name}
</p> </p>
@@ -675,10 +686,20 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
)) ))
} }
</div> </div>
</div>
<div class="space-y-3"> <div class="space-y-3">
<h3 class="font-title border-b border-red-500/20 pb-2 text-sm text-red-400">Negative unlocks</h3> <input type="checkbox" id="negative-unlocks-toggle" class="peer sr-only md:hidden" />
<label
for="negative-unlocks-toggle"
class="flex cursor-pointer items-center justify-between border-b border-red-500/20 pb-2 md:cursor-default peer-checked:[&_[data-expand-arrow]]:rotate-180"
>
<h3 class="font-title text-sm text-red-400">Negative unlocks</h3>
<Icon name="ri:arrow-down-s-line" class="text-day-400 size-5 md:hidden" data-expand-arrow />
</label>
<div class="mt-3 hidden space-y-3 peer-checked:block md:block">
{ {
sortBy( sortBy(
karmaUnlocks.filter((unlock) => unlock.karma < 0), karmaUnlocks.filter((unlock) => unlock.karma < 0),
@@ -732,6 +753,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
</p> </p>
</div> </div>
</div> </div>
</div>
</section> </section>
<div class="space-y-6"> <div class="space-y-6">

View File

@@ -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 { @theme {
--ease-in-sine: cubic-bezier(0.12, 0, 0.39, 0); --ease-in-sine: cubic-bezier(0.12, 0, 0.39, 0);
--ease-out-sine: cubic-bezier(0.61, 1, 0.88, 1); --ease-out-sine: cubic-bezier(0.61, 1, 0.88, 1);