Release 202506151318
This commit is contained in:
48
web/package-lock.json
generated
48
web/package-lock.json
generated
@@ -2532,9 +2532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2603,9 +2603,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -7196,9 +7196,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9325,9 +9325,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9399,9 +9399,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9476,9 +9476,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/brace-expansion": {
|
"node_modules/eslint/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10379,9 +10379,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11671,9 +11671,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jake/node_modules/brace-expansion": {
|
"node_modules/jake/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userAgent` on the `PushSubscription` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PushSubscription" DROP COLUMN "userAgent";
|
||||||
@@ -687,8 +687,6 @@ model PushSubscription {
|
|||||||
p256dh String
|
p256dh String
|
||||||
/// Authentication secret
|
/// Authentication secret
|
||||||
auth String
|
auth String
|
||||||
/// To identify different devices
|
|
||||||
userAgent String?
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ActionError } from 'astro:actions'
|
||||||
import { z } from 'astro:content'
|
import { z } from 'astro:content'
|
||||||
|
|
||||||
import { defineProtectedAction } from '../lib/defineProtectedAction'
|
import { defineProtectedAction } from '../lib/defineProtectedAction'
|
||||||
@@ -32,7 +33,6 @@ export const notificationActions = {
|
|||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
p256dhKey: z.string(),
|
p256dhKey: z.string(),
|
||||||
authKey: z.string(),
|
authKey: z.string(),
|
||||||
userAgent: z.string().optional(),
|
|
||||||
}),
|
}),
|
||||||
handler: async (input, context) => {
|
handler: async (input, context) => {
|
||||||
await prisma.pushSubscription.upsert({
|
await prisma.pushSubscription.upsert({
|
||||||
@@ -43,14 +43,12 @@ export const notificationActions = {
|
|||||||
update: {
|
update: {
|
||||||
p256dh: input.p256dhKey,
|
p256dh: input.p256dhKey,
|
||||||
auth: input.authKey,
|
auth: input.authKey,
|
||||||
userAgent: input.userAgent,
|
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId: context.locals.user.id,
|
userId: context.locals.user.id,
|
||||||
endpoint: input.endpoint,
|
endpoint: input.endpoint,
|
||||||
p256dh: input.p256dhKey,
|
p256dh: input.p256dhKey,
|
||||||
auth: input.authKey,
|
auth: input.authKey,
|
||||||
userAgent: input.userAgent,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -58,7 +56,7 @@ export const notificationActions = {
|
|||||||
|
|
||||||
unsubscribe: defineProtectedAction({
|
unsubscribe: defineProtectedAction({
|
||||||
accept: 'json',
|
accept: 'json',
|
||||||
permissions: 'user',
|
permissions: 'guest',
|
||||||
input: z.object({
|
input: z.object({
|
||||||
endpoint: z.string().optional(),
|
endpoint: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
@@ -66,16 +64,21 @@ export const notificationActions = {
|
|||||||
if (input.endpoint) {
|
if (input.endpoint) {
|
||||||
await prisma.pushSubscription.deleteMany({
|
await prisma.pushSubscription.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId: context.locals.user.id,
|
userId: context.locals.user?.id ?? undefined,
|
||||||
endpoint: input.endpoint,
|
endpoint: input.endpoint,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else if (context.locals.user) {
|
||||||
await prisma.pushSubscription.deleteMany({
|
await prisma.pushSubscription.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId: context.locals.user.id,
|
userId: context.locals.user.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Endpoint is required when user is not logged in.',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -107,12 +107,8 @@ const ogImageUrl = makeOgImageUrl(ogImage, Astro.url)
|
|||||||
|
|
||||||
<DynamicFavicon />
|
<DynamicFavicon />
|
||||||
|
|
||||||
<!-- Components -->
|
<ClientRouter />
|
||||||
{
|
|
||||||
!Astro.url.pathname.startsWith('/admin') && (
|
|
||||||
<ClientRouter />
|
|
||||||
) /* Disable to prevent bugs in important admin forms */
|
|
||||||
}
|
|
||||||
<LoadingIndicator color="green" />
|
<LoadingIndicator color="green" />
|
||||||
<TailwindJsPluggin />
|
<TailwindJsPluggin />
|
||||||
{htmx && <HtmxScript />}
|
{htmx && <HtmxScript />}
|
||||||
|
|||||||
@@ -71,12 +71,13 @@ const [dbComments, pendingCommentsCount, activeRatingComment] = await Astro.loca
|
|||||||
'Failed to fetch comments',
|
'Failed to fetch comments',
|
||||||
async () =>
|
async () =>
|
||||||
await prisma.comment.findMany(
|
await prisma.comment.findMany(
|
||||||
makeCommentsNestedQuery({
|
await makeCommentsNestedQuery({
|
||||||
depth: MAX_COMMENT_DEPTH,
|
depth: MAX_COMMENT_DEPTH,
|
||||||
user,
|
user,
|
||||||
showPending: params.showPending,
|
showPending: params.showPending,
|
||||||
serviceId: service.id,
|
serviceId: service.id,
|
||||||
sort: params.sort,
|
sort: params.sort,
|
||||||
|
highlightedCommentId: params.comment,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type Props = HTMLAttributes<'div'> & {
|
|||||||
pushSubscriptions: Prisma.PushSubscriptionGetPayload<{
|
pushSubscriptions: Prisma.PushSubscriptionGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
endpoint: true
|
endpoint: true
|
||||||
userAgent: true
|
|
||||||
}
|
}
|
||||||
}>[]
|
}>[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ if (!Astro.locals.user) return
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
|
// NOTE: Disable sse: events when user is not logged in
|
||||||
|
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||||
|
stopServerEventsListener()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data as string)
|
const data = JSON.parse(event.data as string)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { registerSW } from 'virtual:pwa-register'
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import { unsubscribeFromPushNotifications } from '../lib/client/clientPushNotifications'
|
||||||
|
|
||||||
const NO_AUTO_RELOAD_ROUTES = ['/account/welcome', '/500', '/404'] as const satisfies `/${string}`[]
|
const NO_AUTO_RELOAD_ROUTES = ['/account/welcome', '/500', '/404'] as const satisfies `/${string}`[]
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
|
|
||||||
function shouldSkipAutoReload() {
|
function shouldSkipAutoReload() {
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
const isErrorPage = document.querySelector('[data-is-error-page]') !== null
|
const isErrorPage = document.body.hasAttribute('data-is-error-page')
|
||||||
|
|
||||||
return isErrorPage || NO_AUTO_RELOAD_ROUTES.some((route) => currentPath === route)
|
return isErrorPage || NO_AUTO_RELOAD_ROUTES.some((route) => currentPath === route)
|
||||||
}
|
}
|
||||||
@@ -48,4 +49,11 @@
|
|||||||
window.addEventListener('beforeinstallprompt', (event) => {
|
window.addEventListener('beforeinstallprompt', (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('astro:page-load', async () => {
|
||||||
|
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||||
|
await unsubscribeFromPushNotifications()
|
||||||
|
window.__SW_REGISTRATION__?.unregister()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
import { differenceInDays } from 'date-fns'
|
|
||||||
|
|
||||||
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
import { verificationStepStatusesByValue } from '../constants/verificationStepStatus'
|
import { verificationStepStatusesByValue } from '../constants/verificationStepStatus'
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
|
import { formatDaysAgo } from '../lib/timeAgo'
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
@@ -27,15 +27,6 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { service } = Astro.props
|
const { service } = Astro.props
|
||||||
|
|
||||||
function formatApprovedAt(approvedAt: Date | null) {
|
|
||||||
if (!approvedAt) return 'less than 15 days ago'
|
|
||||||
|
|
||||||
const days = differenceInDays(new Date(), approvedAt)
|
|
||||||
if (days === 0) return 'today'
|
|
||||||
if (days === 1) return 'yesterday'
|
|
||||||
return `${days.toLocaleString()} days ago`
|
|
||||||
}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -73,7 +64,7 @@ function formatApprovedAt(approvedAt: Date | null) {
|
|||||||
) : service.isRecentlyApproved ? (
|
) : service.isRecentlyApproved ? (
|
||||||
<div class="mb-3 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400">
|
<div class="mb-3 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400">
|
||||||
This service was approved
|
This service was approved
|
||||||
{formatApprovedAt(service.approvedAt)}
|
{service.approvedAt ? formatDaysAgo(service.approvedAt) : 'less than 15 days ago'}
|
||||||
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
||||||
caution.
|
caution.
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ const announcement = await Astro.locals.banners.try(
|
|||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class={cn('bg-night-700 text-day-300 flex min-h-dvh flex-col *:shrink-0', classNames?.body)}
|
class={cn('bg-night-700 text-day-300 flex min-h-dvh flex-col *:shrink-0', classNames?.body)}
|
||||||
data-is-error-page={isErrorPage}
|
data-is-error-page={isErrorPage ? '' : undefined}
|
||||||
|
data-is-logged-in={Astro.locals.user !== null ? '' : undefined}
|
||||||
>
|
>
|
||||||
{announcement && <AnnouncementBanner announcement={announcement} transition:name="header-announcement" />}
|
{announcement && <AnnouncementBanner announcement={announcement} transition:name="header-announcement" />}
|
||||||
<Header
|
<Header
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { kycLevels } from '../constants/kycLevels'
|
|||||||
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
||||||
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
|
|
||||||
import { formatDateShort } from './timeAgo'
|
import { formatDaysAgo } from './timeAgo'
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
links: [],
|
links: [],
|
||||||
customize: (service) => ({
|
customize: (service) => ({
|
||||||
show: service.isRecentlyApproved,
|
show: service.isRecentlyApproved,
|
||||||
description: `Approved on KYCnot.me ${formatDateShort(service.approvedAt ?? service.createdAt)}. Proceed with caution.`,
|
description: `Approved on KYCnot.me less than 15 days ago${service.approvedAt ? ` (${formatDaysAgo(service.approvedAt)})` : ''}. Proceed with caution.`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ export function supportsBrowserNotifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isBrowserNotificationsEnabled() {
|
export function isBrowserNotificationsEnabled() {
|
||||||
return (
|
const browserNotificationsEnabled = typedLocalStorage.browserNotificationsEnabled.get()
|
||||||
supportsBrowserNotifications() &&
|
if (!browserNotificationsEnabled) return false
|
||||||
Notification.permission === 'granted' &&
|
|
||||||
typedLocalStorage.browserNotificationsEnabled.get()
|
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||||
)
|
typedLocalStorage.browserNotificationsEnabled.set(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportsBrowserNotifications() && Notification.permission === 'granted'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enableBrowserNotifications(): Promise<SafeResult> {
|
export async function enableBrowserNotifications(): Promise<SafeResult> {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { actions } from 'astro:actions'
|
|||||||
|
|
||||||
type ServerSubscription = {
|
type ServerSubscription = {
|
||||||
endpoint: string
|
endpoint: string
|
||||||
userAgent: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SafeResult =
|
export type SafeResult =
|
||||||
@@ -45,7 +44,6 @@ export async function subscribeToPushNotifications(vapidPublicKey: string): Prom
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
p256dhKey: p256dh ? btoa(String.fromCharCode(...new Uint8Array(p256dh))) : '',
|
p256dhKey: p256dh ? btoa(String.fromCharCode(...new Uint8Array(p256dh))) : '',
|
||||||
authKey: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
|
authKey: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
|
||||||
} satisfies ActionInput<typeof actions.notification.webPush.subscribe>),
|
} satisfies ActionInput<typeof actions.notification.webPush.subscribe>),
|
||||||
@@ -131,13 +129,7 @@ export async function isCurrentDeviceSubscribed(serverSubscriptions: ServerSubsc
|
|||||||
const currentSubscription = await getCurrentSubscription()
|
const currentSubscription = await getCurrentSubscription()
|
||||||
if (!currentSubscription || serverSubscriptions.length === 0) return false
|
if (!currentSubscription || serverSubscriptions.length === 0) return false
|
||||||
|
|
||||||
const currentEndpoint = currentSubscription.endpoint
|
return serverSubscriptions.some((sub) => sub.endpoint === currentSubscription.endpoint)
|
||||||
const currentUserAgent = navigator.userAgent
|
|
||||||
|
|
||||||
return serverSubscriptions.some(
|
|
||||||
(sub) =>
|
|
||||||
sub.endpoint === currentEndpoint && (sub.userAgent === currentUserAgent || sub.userAgent === null)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlB64ToUint8Array(base64String: string) {
|
function urlB64ToUint8Array(base64String: string) {
|
||||||
@@ -183,5 +175,5 @@ export function parsePushSubscriptions(subscriptionsAsString: string | undefined
|
|||||||
function isServerSubscription(subscription: unknown): subscription is ServerSubscription {
|
function isServerSubscription(subscription: unknown): subscription is ServerSubscription {
|
||||||
if (typeof subscription !== 'object' || subscription === null) return false
|
if (typeof subscription !== 'object' || subscription === null) return false
|
||||||
const s = subscription as Record<string, unknown>
|
const s = subscription as Record<string, unknown>
|
||||||
return typeof s.endpoint === 'string' && (typeof s.userAgent === 'string' || s.userAgent === null)
|
return typeof s.endpoint === 'string'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { prisma } from './prisma'
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
export const MAX_COMMENT_DEPTH = 12
|
export const MAX_COMMENT_DEPTH = 12
|
||||||
@@ -75,12 +77,13 @@ export type CommentWithRepliesPopulated = CommentWithReplies<{
|
|||||||
export const commentSortSchema = z.enum(['newest', 'upvotes', 'status']).default('newest')
|
export const commentSortSchema = z.enum(['newest', 'upvotes', 'status']).default('newest')
|
||||||
export type CommentSortOption = z.infer<typeof commentSortSchema>
|
export type CommentSortOption = z.infer<typeof commentSortSchema>
|
||||||
|
|
||||||
export function makeCommentsNestedQuery({
|
export async function makeCommentsNestedQuery({
|
||||||
depth = 0,
|
depth = 0,
|
||||||
user,
|
user,
|
||||||
showPending,
|
showPending,
|
||||||
serviceId,
|
serviceId,
|
||||||
sort,
|
sort,
|
||||||
|
highlightedCommentId,
|
||||||
}: {
|
}: {
|
||||||
depth?: number
|
depth?: number
|
||||||
user: Prisma.UserGetPayload<{
|
user: Prisma.UserGetPayload<{
|
||||||
@@ -91,6 +94,7 @@ export function makeCommentsNestedQuery({
|
|||||||
showPending?: boolean
|
showPending?: boolean
|
||||||
serviceId: number
|
serviceId: number
|
||||||
sort: CommentSortOption
|
sort: CommentSortOption
|
||||||
|
highlightedCommentId?: number | null
|
||||||
}) {
|
}) {
|
||||||
const orderByClause: Prisma.CommentOrderByWithRelationInput[] = []
|
const orderByClause: Prisma.CommentOrderByWithRelationInput[] = []
|
||||||
|
|
||||||
@@ -108,6 +112,8 @@ export function makeCommentsNestedQuery({
|
|||||||
}
|
}
|
||||||
orderByClause.unshift({ suspicious: 'asc' }) // Always put suspicious comments last within a sort group
|
orderByClause.unshift({ suspicious: 'asc' }) // Always put suspicious comments last within a sort group
|
||||||
|
|
||||||
|
const highlightedBranchIds = highlightedCommentId ? await findAllParentIds(highlightedCommentId, depth) : []
|
||||||
|
|
||||||
const baseQuery = {
|
const baseQuery = {
|
||||||
...commentReplyQuery,
|
...commentReplyQuery,
|
||||||
orderBy: orderByClause,
|
orderBy: orderByClause,
|
||||||
@@ -121,6 +127,9 @@ export function makeCommentsNestedQuery({
|
|||||||
: ({
|
: ({
|
||||||
status: { in: ['APPROVED', 'VERIFIED'] },
|
status: { in: ['APPROVED', 'VERIFIED'] },
|
||||||
} as const satisfies Prisma.CommentWhereInput),
|
} as const satisfies Prisma.CommentWhereInput),
|
||||||
|
...(highlightedBranchIds.length > 0
|
||||||
|
? [{ id: { in: highlightedBranchIds } } as const satisfies Prisma.CommentWhereInput]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
parentId: null,
|
parentId: null,
|
||||||
serviceId,
|
serviceId,
|
||||||
@@ -161,6 +170,47 @@ export function makeRepliesQuery<T extends Prisma.CommentFindManyArgs>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAllParentIds(commentId: number, depth: number) {
|
||||||
|
const commentwithManyParents = await prisma.comment.findFirst({
|
||||||
|
where: { id: commentId },
|
||||||
|
select: makeParentQuerySelect(depth),
|
||||||
|
})
|
||||||
|
|
||||||
|
return extractParentIds(commentwithManyParents, [commentId])
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParentQueryRecursive = {
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
parent: false | { select: ParentQueryRecursive }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeParentQuerySelect(depth: number): ParentQueryRecursive {
|
||||||
|
return {
|
||||||
|
parent: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
parent: depth <= 0 ? false : { select: makeParentQuerySelect(depth - 1) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Prisma.CommentSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractParentIds(
|
||||||
|
comment: Prisma.CommentGetPayload<{ select: ParentQueryRecursive }> | null,
|
||||||
|
acc: number[] = []
|
||||||
|
) {
|
||||||
|
if (!comment?.parent?.id) return acc
|
||||||
|
|
||||||
|
return extractParentIds(comment.parent as Prisma.CommentGetPayload<{ select: ParentQueryRecursive }>, [
|
||||||
|
...acc,
|
||||||
|
comment.parent.id,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
export function makeCommentUrl({
|
export function makeCommentUrl({
|
||||||
serviceSlug,
|
serviceSlug,
|
||||||
commentId,
|
commentId,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function stopImpersonating(context: Pick<APIContext, 'cookies' | 'l
|
|||||||
const sessionId = context.cookies.get(IMPERSONATION_SESSION_COOKIE)?.value
|
const sessionId = context.cookies.get(IMPERSONATION_SESSION_COOKIE)?.value
|
||||||
await redisImpersonationSessions.delete(sessionId)
|
await redisImpersonationSessions.delete(sessionId)
|
||||||
context.cookies.delete(IMPERSONATION_SESSION_COOKIE)
|
context.cookies.delete(IMPERSONATION_SESSION_COOKIE)
|
||||||
context.locals.user = context.locals.actualUser
|
context.locals.user = context.locals.actualUser ?? context.locals.user
|
||||||
context.locals.actualUser = null
|
context.locals.actualUser = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { addDays, format, isBefore, isToday, isYesterday } from 'date-fns'
|
import { addDays, differenceInDays, format, isBefore, isToday, isYesterday } from 'date-fns'
|
||||||
import TimeAgo from 'javascript-time-ago'
|
import TimeAgo from 'javascript-time-ago'
|
||||||
import en from 'javascript-time-ago/locale/en'
|
import en from 'javascript-time-ago/locale/en'
|
||||||
|
|
||||||
@@ -47,3 +47,10 @@ export function formatDateShort(
|
|||||||
|
|
||||||
return transformCase(text, caseType)
|
return transformCase(text, caseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDaysAgo(approvedAt: Date) {
|
||||||
|
const days = differenceInDays(new Date(), approvedAt)
|
||||||
|
if (days === 0) return 'today'
|
||||||
|
if (days === 1) return 'yesterday'
|
||||||
|
return `${days.toLocaleString()} days ago`
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export async function removeUserSessionIdCookie(cookies: AstroCookies) {
|
|||||||
cookies.delete(COOKIE_NAME, { path: '/' })
|
cookies.delete(COOKIE_NAME, { path: '/' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(context: Pick<APIContext, 'cookies' | 'locals'>) {
|
export async function logout(context: Pick<APIContext, 'cookies' | 'locals' | 'request' | 'url'>) {
|
||||||
await stopImpersonating(context)
|
await stopImpersonating(context)
|
||||||
|
|
||||||
await removeUserSessionIdCookie(context.cookies)
|
await removeUserSessionIdCookie(context.cookies)
|
||||||
|
|||||||
@@ -692,12 +692,12 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
|
|||||||
|
|
||||||
<div class="flex justify-end space-x-3">
|
<div class="flex justify-end space-x-3">
|
||||||
<Button
|
<Button
|
||||||
as="button"
|
|
||||||
color="gray"
|
color="gray"
|
||||||
variant="faded"
|
variant="faded"
|
||||||
size="sm"
|
size="sm"
|
||||||
label="Cancel"
|
label="Cancel"
|
||||||
onclick={`document.getElementById('edit-form-${index}').classList.toggle('hidden')`}
|
data-cancel-button
|
||||||
|
data-cancel-form-id={`edit-form-${index}`}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
as="button"
|
as="button"
|
||||||
@@ -721,3 +721,22 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// Optional script for cancel buttons in attribute forms. //
|
||||||
|
// Hides the edit form when cancel button is clicked. //
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>('[data-cancel-button]').forEach((button) => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formId = button.getAttribute('data-cancel-form-id')
|
||||||
|
if (!formId) throw new Error('Form ID not found')
|
||||||
|
const form = document.getElementById(formId)
|
||||||
|
if (!form) throw new Error('Form not found')
|
||||||
|
form.classList.add('hidden')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ const [dbNotifications, notificationPreferences, totalNotifications, pushSubscri
|
|||||||
where: { userId: user.id },
|
where: { userId: user.id },
|
||||||
select: {
|
select: {
|
||||||
endpoint: true,
|
endpoint: true,
|
||||||
userAgent: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
|
|||||||
Reference in New Issue
Block a user