diff --git a/pyworker/README.md b/pyworker/README.md index bba9c6e..6eb989b 100644 --- a/pyworker/README.md +++ b/pyworker/README.md @@ -96,7 +96,7 @@ Tasks will run according to their configured cron schedules. ### Force Triggers Task - Maintains database triggers by forcing them to run under certain conditions -- Currently handles updating the "isRecentlyListed" flag for services after 15 days +- Currently handles updating the "isRecentlyApproved" flag for services after 15 days - Scheduled via `CRON_FORCE-TRIGGERS_TASK` ### Service Score Recalculation Task diff --git a/pyworker/pyworker/cli.py b/pyworker/pyworker/cli.py index 54177e8..a8ca5af 100644 --- a/pyworker/pyworker/cli.py +++ b/pyworker/pyworker/cli.py @@ -89,6 +89,11 @@ def parse_args(args: List[str]) -> argparse.Namespace: score_recalc_parser.add_argument( "--service-id", type=int, help="Specific service ID to process (optional)" ) + score_recalc_parser.add_argument( + "--all", + action="store_true", + help="Recalculate scores for all services (ignores --service-id)", + ) return parser.parse_args(args) @@ -295,12 +300,15 @@ def run_force_triggers_task() -> int: close_db_pool() -def run_service_score_recalc_task(service_id: Optional[int] = None) -> int: +def run_service_score_recalc_task( + service_id: Optional[int] = None, all_services: bool = False +) -> int: """ Run the service score recalculation task. Args: service_id: Optional specific service ID to process. + all_services: Whether to recalculate scores for all services. Returns: Exit code. @@ -310,7 +318,34 @@ def run_service_score_recalc_task(service_id: Optional[int] = None) -> int: try: # Initialize task and use as context manager with ServiceScoreRecalculationTask() as task: # type: ignore - result = task.run(service_id) # type: ignore + if all_services: + queued = task.recalculate_all_services() # type: ignore + if not queued: + logger.warning( + "Failed to queue recalculation jobs for all services" + ) + + # Continuously process queued jobs in batches until none remain + while True: + _ = task.run() # type: ignore + + # Check if there are still unprocessed jobs + remaining = 0 + if task.conn: + with task.conn.cursor() as cursor: + cursor.execute( + 'SELECT COUNT(*) FROM "ServiceScoreRecalculationJob" WHERE "processedAt" IS NULL' + ) + remaining = cursor.fetchone()[0] + + if remaining == 0: + break + + result = True # All jobs processed successfully + + else: + result = task.run(service_id) # type: ignore + if result: logger.info("Successfully recalculated service scores") else: @@ -419,7 +454,9 @@ def main() -> int: elif args.task == "force-triggers": return run_force_triggers_task() elif args.task == "service-score-recalc": - return run_service_score_recalc_task(args.service_id) + return run_service_score_recalc_task( + args.service_id, getattr(args, "all", False) + ) elif args.task: logger.error(f"Unknown task: {args.task}") return 1 diff --git a/pyworker/pyworker/tasks/force_triggers.py b/pyworker/pyworker/tasks/force_triggers.py index 60dfefd..56179ad 100644 --- a/pyworker/pyworker/tasks/force_triggers.py +++ b/pyworker/pyworker/tasks/force_triggers.py @@ -9,7 +9,7 @@ class ForceTriggersTask(Task): Force triggers to run under certain conditions. """ - RECENT_LISTED_INTERVAL_DAYS = 15 + RECENT_APPROVED_INTERVAL_DAYS = 15 def __init__(self): super().__init__("force_triggers") @@ -24,10 +24,10 @@ class ForceTriggersTask(Task): update_query = f""" UPDATE "Service" - SET "isRecentlyListed" = FALSE, "updatedAt" = NOW() - WHERE "isRecentlyListed" = TRUE - AND "listedAt" IS NOT NULL - AND "listedAt" < NOW() - INTERVAL '{self.RECENT_LISTED_INTERVAL_DAYS} days' + SET "isRecentlyApproved" = FALSE, "updatedAt" = NOW() + WHERE "isRecentlyApproved" = TRUE + AND "approvedAt" IS NOT NULL + AND "approvedAt" < NOW() - INTERVAL '{self.RECENT_APPROVED_INTERVAL_DAYS} days' """ try: with self.conn.cursor() as cursor: diff --git a/pyworker/pyworker/tasks/service_score_recalc.py b/pyworker/pyworker/tasks/service_score_recalc.py index abb8304..1c4deb3 100644 --- a/pyworker/pyworker/tasks/service_score_recalc.py +++ b/pyworker/pyworker/tasks/service_score_recalc.py @@ -205,8 +205,7 @@ class ServiceScoreRecalculationTask(Task): cursor.execute( """ SELECT id - FROM "Service" - WHERE "isActive" = TRUE + FROM "Service" """ ) services = cursor.fetchall() diff --git a/web/prisma/migrations/20250614101030_service_timestamps/migration.sql b/web/prisma/migrations/20250614101030_service_timestamps/migration.sql new file mode 100644 index 0000000..8ae6c1a --- /dev/null +++ b/web/prisma/migrations/20250614101030_service_timestamps/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Service" ADD COLUMN "approvedAt" TIMESTAMP(3), +ADD COLUMN "spamAt" TIMESTAMP(3); diff --git a/web/prisma/migrations/20250614110959_rename_recently_listed/migration.sql b/web/prisma/migrations/20250614110959_rename_recently_listed/migration.sql new file mode 100644 index 0000000..567315d --- /dev/null +++ b/web/prisma/migrations/20250614110959_rename_recently_listed/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `isRecentlyListed` on the `Service` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Service" DROP COLUMN "isRecentlyListed", +ADD COLUMN "isRecentlyApproved" BOOLEAN NOT NULL DEFAULT false; + +-- CreateIndex +CREATE INDEX "Service_approvedAt_idx" ON "Service"("approvedAt"); + +-- CreateIndex +CREATE INDEX "Service_verifiedAt_idx" ON "Service"("verifiedAt"); + +-- CreateIndex +CREATE INDEX "Service_spamAt_idx" ON "Service"("spamAt"); + +-- CreateIndex +CREATE INDEX "Service_serviceVisibility_idx" ON "Service"("serviceVisibility"); diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index e5cfe19..403b2c6 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -353,8 +353,6 @@ model Service { privacyScore Int @default(0) trustScore Int @default(0) /// Computed via trigger. Do not update through prisma. - isRecentlyListed Boolean @default(false) - /// Computed via trigger. Do not update through prisma. averageUserRating Float? serviceVisibility ServiceVisibility @default(PUBLIC) serviceInfoBanner ServiceInfoBanner @default(NONE) @@ -363,8 +361,6 @@ model Service { verificationSummary String? verificationRequests ServiceVerificationRequest[] verificationProofMd String? - /// Computed via trigger when the service status is VERIFICATION_SUCCESS. Do not update through prisma. - verifiedAt DateTime? /// [UserSentiment] userSentiment Json? userSentimentAt DateTime? @@ -380,7 +376,16 @@ model Service { tosReviewAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + /// Computed via trigger when the visibility is PUBLIC or (ARCHIVED and listedAt was null). Do not update through prisma. listedAt DateTime? + /// Computed via trigger when the verification status is APPROVED. Do not update through prisma. + approvedAt DateTime? + /// Computed via trigger when the verification status is VERIFICATION_SUCCESS. Do not update through prisma. + verifiedAt DateTime? + /// Computed via trigger when the verification status is VERIFICATION_FAILED. Do not update through prisma. + spamAt DateTime? + /// Computed via trigger. Do not update through prisma. + isRecentlyApproved Boolean @default(false) comments Comment[] events Event[] contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod") @@ -396,6 +401,9 @@ model Service { affiliatedUsers ServiceUser[] @relation("ServiceUsers") @@index([listedAt]) + @@index([approvedAt]) + @@index([verifiedAt]) + @@index([spamAt]) @@index([overallScore]) @@index([privacyScore]) @@index([trustScore]) @@ -407,6 +415,7 @@ model Service { @@index([updatedAt]) @@index([slug]) @@index([previousSlugs]) + @@index([serviceVisibility]) } model ServiceContactMethod { diff --git a/web/prisma/seed.ts b/web/prisma/seed.ts index 84d5853..3e28589 100755 --- a/web/prisma/seed.ts +++ b/web/prisma/seed.ts @@ -13,14 +13,15 @@ import { PrismaClient, ServiceSuggestionStatus, ServiceUserRole, - VerificationStatus, type Prisma, type User, type ServiceVisibility, ServiceSuggestionType, KycLevelClarification, VerificationStepStatus, + type VerificationStatus, } from '@prisma/client' +import { differenceInDays, isPast } from 'date-fns' import { omit, uniqBy } from 'lodash-es' import { generateUsername } from 'unique-username-generator' @@ -614,6 +615,14 @@ const generateFakeService = (users: User[]) => { const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), { probability: 0.8, }) + const serviceVisibility = faker.helpers.weightedArrayElement([ + { weight: 80, value: 'PUBLIC' }, + { weight: 10, value: 'UNLISTED' }, + { weight: 5, value: 'HIDDEN' }, + { weight: 5, value: 'ARCHIVED' }, + ]) + const approvedAt = + status === 'APPROVED' || status === 'VERIFICATION_SUCCESS' ? faker.date.recent({ days: 30 }) : null return { name, @@ -629,12 +638,7 @@ const generateFakeService = (users: User[]) => { overallScore: 0, privacyScore: 0, trustScore: 0, - serviceVisibility: faker.helpers.weightedArrayElement([ - { weight: 80, value: 'PUBLIC' }, - { weight: 10, value: 'UNLISTED' }, - { weight: 5, value: 'HIDDEN' }, - { weight: 5, value: 'ARCHIVED' }, - ]), + serviceVisibility, verificationStatus: status, verificationSummary: status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null, @@ -677,8 +681,14 @@ const generateFakeService = (users: User[]) => { { count: { min: 0, max: 2 } } ), imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`, - listedAt: faker.date.past(), - verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null, + listedAt: + serviceVisibility === 'PUBLIC' || serviceVisibility === 'ARCHIVED' + ? faker.date.recent({ days: 30 }) + : null, + verifiedAt: status === 'VERIFICATION_SUCCESS' ? faker.date.recent({ days: 30 }) : null, + spamAt: status === 'VERIFICATION_FAILED' ? faker.date.recent({ days: 30 }) : null, + approvedAt, + isRecentlyApproved: !!approvedAt && isPast(approvedAt) && differenceInDays(new Date(), approvedAt) < 15, tosReview, tosReviewAt: tosReview ? faker.date.recent() @@ -908,7 +918,7 @@ const generateFakeServiceContactMethod = (serviceId: number) => { value: `https://linkedin.com/in/${faker.helpers.slugify(faker.person.fullName())}`, }, { - label: faker.lorem.word({ length: 2 }), + label: 'Custom label', value: `https://bitcointalk.org/index.php?topic=${faker.number.int({ min: 1, max: 1000000 }).toString()}.0`, }, { @@ -918,7 +928,7 @@ const generateFakeServiceContactMethod = (serviceId: number) => { value: faker.internet.url(), }, { - label: faker.lorem.word({ length: 2 }), + label: 'Custom label', value: faker.internet.url(), }, { @@ -1307,7 +1317,7 @@ async function main() { const service = await prisma.service.create({ data: { ...serviceData, - verificationStatus: VerificationStatus.COMMUNITY_CONTRIBUTED, + verificationStatus: 'COMMUNITY_CONTRIBUTED', categories: { connect: randomCategories.map((cat) => ({ id: cat.id })), }, diff --git a/web/prisma/triggers/02_service_score.sql b/web/prisma/triggers/02_service_score.sql index 5738e47..1cbd7d2 100644 --- a/web/prisma/triggers/02_service_score.sql +++ b/web/prisma/triggers/02_service_score.sql @@ -1,7 +1,7 @@ -- This script defines PostgreSQL functions and triggers for managing service scores: -- 1. Automatically calculates and updates privacy, trust, and overall scores -- for services when services or their attributes change. --- 2. Updates the isRecentlyListed flag for services listed within the last 15 days. +-- 2. Updates the isRecentlyApproved flag for services approved within the last 15 days. -- 3. Queues asynchronous score recalculation in "ServiceScoreRecalculationJob" -- when an "Attribute" definition (e.g., points) is updated, ensuring -- efficient handling of widespread score updates. @@ -25,12 +25,8 @@ DECLARE privacy_score INT := 0; kyc_factor INT; clarification_factor INT := 0; - onion_factor INT := 0; - i2p_factor INT := 0; + onion_or_i2p_factor INT := 0; monero_factor INT := 0; - open_source_factor INT := 0; - p2p_factor INT := 0; - decentralized_factor INT := 0; attributes_score INT := 0; BEGIN -- Get service data @@ -57,20 +53,12 @@ BEGIN FROM "Service" WHERE "id" = service_id; - -- Check for onion URLs + -- Check for onion or i2p URLs IF EXISTS ( SELECT 1 FROM "Service" - WHERE "id" = service_id AND array_length("onionUrls", 1) > 0 + WHERE "id" = service_id AND (array_length("onionUrls", 1) > 0 OR array_length("i2pUrls", 1) > 0) ) THEN - onion_factor := 5; - END IF; - - -- Check for i2p URLs - IF EXISTS ( - SELECT 1 FROM "Service" - WHERE "id" = service_id AND array_length("i2pUrls", 1) > 0 - ) THEN - i2p_factor := 5; + onion_or_i2p_factor := 5; END IF; -- Check for Monero acceptance @@ -86,10 +74,10 @@ BEGIN INTO attributes_score FROM "ServiceAttribute" sa JOIN "Attribute" a ON sa."attributeId" = a."id" - WHERE sa."serviceId" = service_id AND a."category" = 'PRIVACY'; + WHERE sa."serviceId" = service_id; -- Calculate final privacy score (base 100) - privacy_score := 50 + kyc_factor + clarification_factor + onion_factor + i2p_factor + monero_factor + open_source_factor + p2p_factor + decentralized_factor + attributes_score; + privacy_score := 50 + kyc_factor + clarification_factor + onion_or_i2p_factor + monero_factor + attributes_score; -- Ensure the score is in reasonable bounds (0-100) privacy_score := GREATEST(0, LEAST(100, privacy_score)); @@ -105,7 +93,7 @@ DECLARE trust_score INT := 0; verification_factor INT; attributes_score INT := 0; - recently_listed_factor INT := 0; + recently_approved_factor INT := 0; tos_penalty_factor INT := 0; BEGIN -- Get verification status factor @@ -126,26 +114,26 @@ BEGIN INTO attributes_score FROM "ServiceAttribute" sa JOIN "Attribute" a ON sa."attributeId" = a.id - WHERE sa."serviceId" = service_id AND a.category = 'TRUST'; + WHERE sa."serviceId" = service_id; - -- Apply penalty if service was listed within the last 15 days + -- Apply penalty if service was approved within the last 15 days IF EXISTS ( SELECT 1 FROM "Service" WHERE id = service_id - AND "listedAt" IS NOT NULL + AND "approvedAt" IS NOT NULL AND "verificationStatus" = 'APPROVED' - AND (NOW() - "listedAt") <= INTERVAL '15 days' + AND (NOW() - "approvedAt") <= INTERVAL '15 days' ) THEN - recently_listed_factor := -10; - -- Update the isRecentlyListed flag to true + recently_approved_factor := -10; + -- Update the isRecentlyApproved flag to true UPDATE "Service" - SET "isRecentlyListed" = TRUE + SET "isRecentlyApproved" = TRUE WHERE id = service_id; ELSE - -- Update the isRecentlyListed flag to false + -- Update the isRecentlyApproved flag to false UPDATE "Service" - SET "isRecentlyListed" = FALSE + SET "isRecentlyApproved" = FALSE WHERE id = service_id; END IF; @@ -161,7 +149,7 @@ BEGIN END IF; -- Calculate final trust score (base 100) - trust_score := 50 + verification_factor + attributes_score + recently_listed_factor + tos_penalty_factor; + trust_score := 50 + verification_factor + attributes_score + recently_approved_factor + tos_penalty_factor; -- Ensure the score is in reasonable bounds (0-100) trust_score := GREATEST(0, LEAST(100, trust_score)); @@ -176,7 +164,7 @@ RETURNS INT AS $$ DECLARE overall_score INT; BEGIN - overall_score := CAST(ROUND(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0) AS INT); + overall_score := CAST(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0 AS INT); RETURN GREATEST(0, LEAST(10, overall_score)); END; $$ LANGUAGE plpgsql; diff --git a/web/prisma/triggers/04_service_verification_status.sql b/web/prisma/triggers/04_service_verification_status.sql index c22e900..fb373ad 100644 --- a/web/prisma/triggers/04_service_verification_status.sql +++ b/web/prisma/triggers/04_service_verification_status.sql @@ -1,48 +1,60 @@ --- This script manages the `listedAt`, `verifiedAt`, and `isRecentlyListed` timestamps --- for services based on changes to their `verificationStatus`. It ensures these timestamps --- are set or cleared appropriately when a service's verification status is updated. - -CREATE OR REPLACE FUNCTION manage_service_timestamps() +CREATE OR REPLACE FUNCTION manage_service_visibility_timestamps() RETURNS TRIGGER AS $$ BEGIN - -- Manage listedAt timestamp - IF NEW."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN - -- Set listedAt only on the first time status becomes APPROVED or VERIFICATION_SUCCESS + IF NEW."serviceVisibility" = 'PUBLIC' OR NEW."serviceVisibility" = 'ARCHIVED' THEN IF OLD."listedAt" IS NULL THEN NEW."listedAt" := NOW(); - NEW."isRecentlyListed" := TRUE; END IF; - ELSIF OLD."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN - -- Clear listedAt if the status changes FROM APPROVED or VERIFICATION_SUCCESS to something else - -- The trigger's WHEN clause ensures NEW."verificationStatus" is different. + ELSE NEW."listedAt" := NULL; - NEW."isRecentlyListed" := FALSE; - END IF; - - -- Manage verifiedAt timestamp - IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN - -- Set verifiedAt when status changes TO VERIFICATION_SUCCESS - NEW."verifiedAt" := NOW(); - NEW."isRecentlyListed" := FALSE; - ELSIF OLD."verificationStatus" = 'VERIFICATION_SUCCESS' THEN - -- Clear verifiedAt when status changes FROM VERIFICATION_SUCCESS - -- The trigger's WHEN clause ensures NEW."verificationStatus" is different. - NEW."verifiedAt" := NULL; - NEW."isRecentlyListed" := FALSE; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; --- Drop the old trigger first if it exists under the old name +CREATE OR REPLACE FUNCTION manage_service_verification_timestamps() +RETURNS TRIGGER AS $$ +BEGIN + IF (NEW."verificationStatus" = 'APPROVED' OR NEW."verificationStatus" = 'VERIFICATION_SUCCESS') THEN + IF OLD."approvedAt" IS NULL THEN + NEW."approvedAt" := NOW(); + NEW."isRecentlyApproved" := TRUE; + END IF; + ELSE + NEW."approvedAt" := NULL; + NEW."isRecentlyApproved" := FALSE; + END IF; + + IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN + NEW."verifiedAt" := NOW(); + ELSE + NEW."verifiedAt" := NULL; + END IF; + + IF NEW."verificationStatus" = 'VERIFICATION_FAILED' THEN + NEW."spamAt" := NOW(); + ELSE + NEW."spamAt" := NULL; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Drop the old triggers TODO: remove this some day DROP TRIGGER IF EXISTS trigger_set_service_listed_at ON "Service"; --- Drop the trigger if it exists under the new name DROP TRIGGER IF EXISTS trigger_manage_service_timestamps ON "Service"; -CREATE TRIGGER trigger_manage_service_timestamps +DROP TRIGGER IF EXISTS trigger_manage_service_visibility_timestamps ON "Service"; +DROP TRIGGER IF EXISTS trigger_manage_service_verification_timestamps ON "Service"; + +CREATE TRIGGER trigger_manage_service_visibility_timestamps +BEFORE UPDATE OF "serviceVisibility" ON "Service" +FOR EACH ROW +EXECUTE FUNCTION manage_service_visibility_timestamps(); + +CREATE TRIGGER trigger_manage_service_verification_timestamps BEFORE UPDATE OF "verificationStatus" ON "Service" FOR EACH ROW --- Only execute the function if the verificationStatus value has actually changed -WHEN (OLD."verificationStatus" IS DISTINCT FROM NEW."verificationStatus") -EXECUTE FUNCTION manage_service_timestamps(); +EXECUTE FUNCTION manage_service_verification_timestamps(); diff --git a/web/src/actions/api/service.ts b/web/src/actions/api/service.ts index 7942197..83c11d2 100644 --- a/web/src/actions/api/service.ts +++ b/web/src/actions/api/service.ts @@ -65,13 +65,13 @@ export const apiServiceActions = { tosUrls: true, referral: true, listedAt: true, + approvedAt: true, verifiedAt: true, serviceVisibility: true, } as const satisfies Prisma.ServiceSelect let service = await prisma.service.findFirst({ where: { - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] }, OR: [ @@ -92,7 +92,6 @@ export const apiServiceActions = { if (!service && input.slug) { service = await prisma.service.findFirst({ where: { - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] }, previousSlugs: { has: input.slug }, @@ -105,9 +104,7 @@ export const apiServiceActions = { !service || (service.serviceVisibility !== 'PUBLIC' && service.serviceVisibility !== 'ARCHIVED' && - service.serviceVisibility !== 'UNLISTED') || - !service.listedAt || - service.listedAt > new Date() + service.serviceVisibility !== 'UNLISTED') ) { throw new ActionError({ code: 'NOT_FOUND', @@ -130,6 +127,7 @@ export const apiServiceActions = { 'description', ]), verifiedAt: service.verifiedAt, + approvedAt: service.approvedAt, kycLevel: service.kycLevel, kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']), kycLevelClarification: service.kycLevelClarification, diff --git a/web/src/actions/serviceSuggestion.ts b/web/src/actions/serviceSuggestion.ts index 7566613..f93014e 100644 --- a/web/src/actions/serviceSuggestion.ts +++ b/web/src/actions/serviceSuggestion.ts @@ -36,7 +36,6 @@ const findPossibleDuplicates = async (input: { name: string }) => { id: { in: matches.map(({ id }) => id), }, - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'], }, @@ -252,7 +251,6 @@ export const serviceSuggestionActions = { overallScore: 0, privacyScore: 0, trustScore: 0, - listedAt: new Date(), serviceVisibility: 'UNLISTED', categories: { connect: input.categories.map((id) => ({ id })), diff --git a/web/src/components/BaseHead.astro b/web/src/components/BaseHead.astro index d1b1ef8..0daf884 100644 --- a/web/src/components/BaseHead.astro +++ b/web/src/components/BaseHead.astro @@ -6,7 +6,7 @@ import { pwaAssetsHead } from 'virtual:pwa-assets/head' import { pwaInfo } from 'virtual:pwa-info' import { isNotArray } from '../lib/arrays' -import { DEPLOYMENT_MODE } from '../lib/envVariables' +import { DEPLOYMENT_MODE } from '../lib/client/envVariables' import DevToolsMessageScript from './DevToolsMessageScript.astro' import DynamicFavicon from './DynamicFavicon.astro' diff --git a/web/src/components/CommentItem.astro b/web/src/components/CommentItem.astro index b18c7f8..7749452 100644 --- a/web/src/components/CommentItem.astro +++ b/web/src/components/CommentItem.astro @@ -15,6 +15,7 @@ import { } from '../lib/commentsWithReplies' import { computeKarmaUnlocks } from '../lib/karmaUnlocks' import { formatDateShort } from '../lib/timeAgo' +import { urlDomain } from '../lib/urls' import BadgeSmall from './BadgeSmall.astro' import CommentModeration from './CommentModeration.astro' @@ -170,7 +171,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin: comment.author.admin || comment.author.moderator ? `KYCnot.me ${comment.author.admin ? 'Admin' : 'Moderator'}${comment.author.verifiedLink ? '. ' : ''}` : '' - }${comment.author.verifiedLink ? `Related to ${comment.author.verifiedLink}` : ''}`} + }${comment.author.verifiedLink ? `Related to ${urlDomain(comment.author.verifiedLink)}` : ''}`} > diff --git a/web/src/components/DynamicFavicon.astro b/web/src/components/DynamicFavicon.astro index ffd4376..7005003 100644 --- a/web/src/components/DynamicFavicon.astro +++ b/web/src/components/DynamicFavicon.astro @@ -1,5 +1,5 @@ --- -import { DEPLOYMENT_MODE } from '../lib/envVariables' +import { DEPLOYMENT_MODE } from '../lib/client/envVariables' import { prisma } from '../lib/prisma' const user = Astro.locals.user diff --git a/web/src/components/Header.astro b/web/src/components/Header.astro index 4c8ed62..6e747ea 100644 --- a/web/src/components/Header.astro +++ b/web/src/components/Header.astro @@ -3,8 +3,8 @@ import { Icon } from 'astro-icon/components' import { sample } from 'lodash-es' import { splashTexts } from '../constants/splashTexts' +import { DEPLOYMENT_MODE } from '../lib/client/envVariables' import { cn } from '../lib/cn' -import { DEPLOYMENT_MODE } from '../lib/envVariables' import { makeLoginUrl, makeUnimpersonateUrl } from '../lib/redirectUrls' import AdminOnly from './AdminOnly.astro' diff --git a/web/src/components/InputCardGroup.astro b/web/src/components/InputCardGroup.astro index 6072249..041f35e 100644 --- a/web/src/components/InputCardGroup.astro +++ b/web/src/components/InputCardGroup.astro @@ -20,7 +20,6 @@ type Props = Omit< iconClass?: string description?: MarkdownString disabled?: boolean - noTransitionPersist?: boolean }[] disabled?: boolean selectedValue?: Multiple extends true ? string[] : string @@ -70,7 +69,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0 )} > import { isBrowserNotificationsEnabled, showBrowserNotification } from '../lib/client/browserNotifications' - import { makeNotificationOptions } from '../lib/notificationOptions' + import { + makeBrowserNotificationOptions, + makeBrowserNotificationTitle, + } from '../lib/client/notificationOptions' document.addEventListener('sse:new-notification', (event) => { if (isBrowserNotificationsEnabled()) { const payload = event.detail const notification = showBrowserNotification( - payload.title, - makeNotificationOptions(payload, { removeActions: true }) + makeBrowserNotificationTitle(payload.title), + makeBrowserNotificationOptions(payload, { removeActions: true }) ) // Handle notification click diff --git a/web/src/components/VerificationWarningBanner.astro b/web/src/components/VerificationWarningBanner.astro index ddd4464..43cb522 100644 --- a/web/src/components/VerificationWarningBanner.astro +++ b/web/src/components/VerificationWarningBanner.astro @@ -1,24 +1,21 @@ --- import { Icon } from 'astro-icon/components' -import { differenceInDays, isPast } from 'date-fns' +import { differenceInDays } from 'date-fns' import { verificationStatusesByValue } from '../constants/verificationStatus' import { verificationStepStatusesByValue } from '../constants/verificationStepStatus' import { cn } from '../lib/cn' -import TimeFormatted from './TimeFormatted.astro' - import type { Prisma } from '@prisma/client' -const RECENTLY_ADDED_DAYS = 7 - type Props = { service: Prisma.ServiceGetPayload<{ select: { verificationStatus: true verificationProofMd: true verificationSummary: true - listedAt: true + approvedAt: true + isRecentlyApproved: true createdAt: true verificationSteps: { select: { @@ -31,8 +28,14 @@ type Props = { const { service } = Astro.props -const listedDate = service.listedAt ?? service.createdAt -const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), listedDate) < RECENTLY_ADDED_DAYS +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` +} --- { @@ -67,10 +70,10 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list - ) : wasRecentlyAdded ? ( + ) : service.isRecentlyApproved ? (
- This service was {service.listedAt === null ? 'added ' : 'listed '}{' '} - + This service was approved + {formatApprovedAt(service.approvedAt)} {service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with caution. Learn more diff --git a/web/src/lib/attributes.ts b/web/src/lib/attributes.ts index cb9dc57..9ab7d06 100644 --- a/web/src/lib/attributes.ts +++ b/web/src/lib/attributes.ts @@ -35,8 +35,8 @@ type NonDbAttributeFull = NonDbAttribute & { select: { verificationStatus: true serviceVisibility: true - isRecentlyListed: true - listedAt: true + isRecentlyApproved: true + approvedAt: true createdAt: true tosReviewAt: true tosReview: true @@ -189,17 +189,17 @@ export const nonDbAttributes: NonDbAttributeFull[] = [ }), }, { - slug: 'recently-listed', - title: 'Recently listed', + slug: 'recently-approved', + title: 'Recently approved', type: 'WARNING', category: 'TRUST', - description: 'Listed on KYCnot.me less than 15 days ago. Proceed with caution.', + description: 'Approved on KYCnot.me less than 15 days ago. Proceed with caution.', privacyPoints: 0, trustPoints: -5, links: [], customize: (service) => ({ - show: service.isRecentlyListed, - description: `Listed on KYCnot.me ${formatDateShort(service.listedAt ?? service.createdAt)}. Proceed with caution.`, + show: service.isRecentlyApproved, + description: `Approved on KYCnot.me ${formatDateShort(service.approvedAt ?? service.createdAt)}. Proceed with caution.`, }), }, { @@ -217,41 +217,22 @@ export const nonDbAttributes: NonDbAttributeFull[] = [ }), }, { - slug: 'has-onion-urls', - title: 'Has Onion URLs', + slug: 'has-onion-or-i2p-urls', + title: 'Has Onion or I2P URLs', type: 'GOOD', category: 'PRIVACY', - description: 'Onion (Tor) URLs enhance privacy and anonymity.', + description: 'Onion (Tor) and I2P URLs enhance privacy and anonymity.', privacyPoints: 5, trustPoints: 0, links: [ { - url: '/?onion=true', + url: '/?networks=onion&networks=i2p', label: 'Search with this', icon: 'ri:search-line', }, ], customize: (service) => ({ - show: service.onionUrls.length > 0, - }), - }, - { - slug: 'has-i2p-urls', - title: 'Has I2P URLs', - type: 'GOOD', - category: 'PRIVACY', - description: 'I2P URLs enhance privacy and anonymity.', - privacyPoints: 5, - trustPoints: 0, - links: [ - { - url: '/?i2p=true', - label: 'Search with this', - icon: 'ri:search-line', - }, - ], - customize: (service) => ({ - show: service.i2pUrls.length > 0, + show: service.onionUrls.length > 0 || service.i2pUrls.length > 0, }), }, { diff --git a/web/src/lib/client/envVariables.ts b/web/src/lib/client/envVariables.ts new file mode 100644 index 0000000..4421aee --- /dev/null +++ b/web/src/lib/client/envVariables.ts @@ -0,0 +1,7 @@ +export const DEPLOYMENT_MODE = import.meta.env.PROD + ? import.meta.env.MODE === 'development' || + import.meta.env.MODE === 'staging' || + import.meta.env.MODE === 'production' + ? import.meta.env.MODE + : 'development' + : 'development' diff --git a/web/src/lib/notificationOptions.ts b/web/src/lib/client/notificationOptions.ts similarity index 68% rename from web/src/lib/notificationOptions.ts rename to web/src/lib/client/notificationOptions.ts index 438154e..23d8905 100644 --- a/web/src/lib/notificationOptions.ts +++ b/web/src/lib/client/notificationOptions.ts @@ -1,4 +1,6 @@ -import type { NotificationData, NotificationPayload } from './serverEventsTypes' +import { DEPLOYMENT_MODE } from './envVariables' + +import type { NotificationData, NotificationPayload } from '../serverEventsTypes' export type CustomNotificationOptions = NotificationOptions & { actions?: { action: string; title: string; icon?: string }[] @@ -6,14 +8,24 @@ export type CustomNotificationOptions = NotificationOptions & { data: NotificationData } -export function makeNotificationOptions( +export function makeBrowserNotificationTitle(title?: string | null) { + const prefix = DEPLOYMENT_MODE === 'development' ? '[DEV] ' : DEPLOYMENT_MODE === 'staging' ? '[PRE] ' : '' + return `${prefix}${title ?? 'New Notification'}` +} + +export function makeBrowserNotificationOptions( payload: NotificationPayload | null, options: { removeActions?: boolean } = {} ) { const defaultOptions: CustomNotificationOptions = { body: 'You have a new notification', lang: 'en-US', - icon: '/favicon.svg', + icon: + DEPLOYMENT_MODE === 'development' + ? '/favicon-dev.svg' + : DEPLOYMENT_MODE === 'staging' + ? '/favicon-stage.svg' + : '/favicon.svg', badge: '/notification-icon.svg', requireInteraction: false, silent: false, diff --git a/web/src/lib/envVariables.ts b/web/src/lib/envVariables.ts deleted file mode 100644 index 36d7f48..0000000 --- a/web/src/lib/envVariables.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from 'astro/zod' - -const schema = z.enum(['development', 'staging', 'production']) - -export const DEPLOYMENT_MODE = schema.parse(import.meta.env.PROD ? import.meta.env.MODE : 'development') diff --git a/web/src/lib/feeds.ts b/web/src/lib/feeds.ts index d38bdda..185b005 100644 --- a/web/src/lib/feeds.ts +++ b/web/src/lib/feeds.ts @@ -39,7 +39,6 @@ export async function getService(slug: string | undefined): Promise< const service = (await prisma.service.findFirst({ where: { - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] }, slug, }, @@ -47,7 +46,6 @@ export async function getService(slug: string | undefined): Promise< })) ?? (await prisma.service.findFirst({ where: { - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] }, previousSlugs: { has: slug }, }, @@ -175,7 +173,6 @@ export async function getEvents(): Promise< where: { visible: true, service: { - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] }, }, }, diff --git a/web/src/lib/userSecretToken.ts b/web/src/lib/userSecretToken.ts index fa648a1..4a1a183 100644 --- a/web/src/lib/userSecretToken.ts +++ b/web/src/lib/userSecretToken.ts @@ -10,7 +10,7 @@ import { } from '../constants/characters' import { getRandom, typedJoin } from './arrays' -import { DEPLOYMENT_MODE } from './envVariables' +import { DEPLOYMENT_MODE } from './client/envVariables' import { transformCase } from './strings' const DIGEST = 'sha512' diff --git a/web/src/pages/500.astro b/web/src/pages/500.astro index aa13367..271359c 100644 --- a/web/src/pages/500.astro +++ b/web/src/pages/500.astro @@ -5,7 +5,7 @@ import { LOGS_UI_URL } from 'astro:env/server' import { SUPPORT_EMAIL } from '../constants/project' import BaseLayout from '../layouts/BaseLayout.astro' -import { DEPLOYMENT_MODE } from '../lib/envVariables' +import { DEPLOYMENT_MODE } from '../lib/client/envVariables' import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters' type Props = { diff --git a/web/src/pages/about.mdx b/web/src/pages/about.mdx index 85fe5df..0a4946c 100644 --- a/web/src/pages/about.mdx +++ b/web/src/pages/about.mdx @@ -165,7 +165,7 @@ The trust score represents how reliable and trustworthy a service is, based on o ##### Overall Score -The overall score is calculated as `(privacy * 0.6) + (trust * 0.4)` and provides a combined measure of privacy and trust. +The overall score is calculated as `(privacy * 0.6) + (trust * 0.4)` truncated. This provides a combined measure of privacy and trust. #### Terms of Service Reviews diff --git a/web/src/pages/account/index.astro b/web/src/pages/account/index.astro index 4d1188f..bfc9845 100644 --- a/web/src/pages/account/index.astro +++ b/web/src/pages/account/index.astro @@ -22,6 +22,7 @@ import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks' import { prisma } from '../../lib/prisma' import { makeLoginUrl } from '../../lib/redirectUrls' import { formatDateShort } from '../../lib/timeAgo' +import { urlDomain } from '../../lib/urls' const userId = Astro.locals.user?.id if (!userId) { @@ -423,7 +424,7 @@ if (!user) return Astro.rewrite('/404') rel="noopener noreferrer" class="text-blue-400 hover:underline" > - {user.verifiedLink} + {urlDomain(user.verifiedLink)}
@@ -857,12 +858,7 @@ if (!user) return Astro.rewrite('/404') - + {statusInfo.label} diff --git a/web/src/pages/admin/announcements/index.astro b/web/src/pages/admin/announcements/index.astro index d33dc4e..643611a 100644 --- a/web/src/pages/admin/announcements/index.astro +++ b/web/src/pages/admin/announcements/index.astro @@ -271,7 +271,6 @@ if (toggleResult?.error) { label: type.label, value: type.value, icon: type.icon, - noTransitionPersist: false, }))} cardSize="sm" required @@ -306,8 +305,8 @@ if (toggleResult?.error) { label="Status" error={createInputErrors.isActive} options={[ - { label: 'Active', value: 'true', noTransitionPersist: true }, - { label: 'Inactive', value: 'false', noTransitionPersist: true }, + { label: 'Active', value: 'true' }, + { label: 'Inactive', value: 'false' }, ]} selectedValue={newAnnouncement.isActive ? 'true' : 'false'} cardSize="sm" @@ -628,7 +627,6 @@ if (toggleResult?.error) { label: type.label, value: type.value, icon: type.icon, - noTransitionPersist: true, }))} cardSize="sm" required @@ -661,8 +659,8 @@ if (toggleResult?.error) { name="isActive" label="Status" options={[ - { label: 'Active', value: 'true', noTransitionPersist: true }, - { label: 'Inactive', value: 'false', noTransitionPersist: true }, + { label: 'Active', value: 'true' }, + { label: 'Inactive', value: 'false' }, ]} selectedValue={announcement.isActive ? 'true' : 'false'} cardSize="sm" diff --git a/web/src/pages/admin/services/[slug]/edit.astro b/web/src/pages/admin/services/[slug]/edit.astro index d99d44d..77f6483 100644 --- a/web/src/pages/admin/services/[slug]/edit.astro +++ b/web/src/pages/admin/services/[slug]/edit.astro @@ -35,7 +35,7 @@ import { verificationStepStatuses, } from '../../../../constants/verificationStepStatus' import BaseLayout from '../../../../layouts/BaseLayout.astro' -import { DEPLOYMENT_MODE } from '../../../../lib/envVariables' +import { DEPLOYMENT_MODE } from '../../../../lib/client/envVariables' import { listFiles } from '../../../../lib/fileStorage' import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo' import { pluralize } from '../../../../lib/pluralize' @@ -441,7 +441,6 @@ const apiCalls = await Astro.locals.banners.try( value: kycLevel.id.toString(), icon: kycLevel.icon, description: kycLevel.description, - noTransitionPersist: true, }))} selectedValue={service.kycLevel.toString()} iconSize="md" @@ -458,7 +457,6 @@ const apiCalls = await Astro.locals.banners.try( value: clarification.value, icon: clarification.icon, description: clarification.description, - noTransitionPersist: true, }))} selectedValue={service.kycLevelClarification} iconSize="sm" @@ -475,7 +473,6 @@ const apiCalls = await Astro.locals.banners.try( icon: status.icon, iconClass: status.classNames.icon, description: status.description, - noTransitionPersist: true, }))} selectedValue={service.verificationStatus} error={serviceInputErrors.verificationStatus} @@ -491,7 +488,6 @@ const apiCalls = await Astro.locals.banners.try( label: currency.name, value: currency.id, icon: currency.icon, - noTransitionPersist: true, }))} selectedValue={service.acceptedCurrencies} error={serviceInputErrors.acceptedCurrencies} @@ -532,7 +528,6 @@ const apiCalls = await Astro.locals.banners.try( icon: visibility.icon, iconClass: visibility.iconClass, description: visibility.description, - noTransitionPersist: true, }))} selectedValue={service.serviceVisibility} error={serviceInputErrors.serviceVisibility} diff --git a/web/src/pages/admin/users/[username].astro b/web/src/pages/admin/users/[username].astro index 3d194e7..904a675 100644 --- a/web/src/pages/admin/users/[username].astro +++ b/web/src/pages/admin/users/[username].astro @@ -230,26 +230,22 @@ if (!user) return Astro.rewrite('/404') label: 'Admin', value: 'admin', icon: 'ri:shield-star-fill', - noTransitionPersist: true, }, { label: 'Moderator', value: 'moderator', icon: 'ri:graduation-cap-fill', - noTransitionPersist: true, }, { label: 'Spammer', value: 'spammer', icon: 'ri:alert-fill', - noTransitionPersist: true, }, { label: 'Verified', value: 'verified', icon: 'ri:verified-badge-fill', disabled: true, - noTransitionPersist: true, }, ]} selectedValue={[ @@ -434,7 +430,6 @@ if (!user) return Astro.rewrite('/404') label: role.label, value: role.value, icon: role.icon, - noTransitionPersist: true, }))} required cardSize="sm" diff --git a/web/src/pages/docs/api.mdx b/web/src/pages/docs/api.mdx index e3cb49b..606cc82 100644 --- a/web/src/pages/docs/api.mdx +++ b/web/src/pages/docs/api.mdx @@ -52,6 +52,7 @@ type ServiceResponse = { description: string } verifiedAt: Date | null + approvedAt: Date | null kycLevel: 0 | 1 | 2 | 3 | 4 kycLevelInfo: { value: 0 | 1 | 2 | 3 | 4 @@ -145,7 +146,8 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \ "labelShort": "Verified", "description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee." }, - "verifiedAt": "2025-01-20T07:12:29.393Z", + "verifiedAt": "2025-06-14T11:02:39.294Z", + "approvedAt": "2025-05-31T19:09:18.043Z", "kycLevel": 0, "kycLevelInfo": { "value": 0, @@ -163,7 +165,7 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \ "slug": "exchange" } ], - "listedAt": "2025-05-31T19:09:18.043Z", + "listedAt": "2025-04-20T07:12:29.393Z", "serviceUrls": [ "https://example.com", "http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion" diff --git a/web/src/pages/events.astro b/web/src/pages/events.astro index 4598628..5551daa 100644 --- a/web/src/pages/events.astro +++ b/web/src/pages/events.astro @@ -45,7 +45,6 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([ async () => prisma.service.findMany({ where: { - listedAt: { lte: new Date() }, serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] }, }, select: { @@ -72,7 +71,6 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([ }, service: { slug: params.service ?? undefined, - listedAt: params.service ? undefined : { lte: new Date() }, serviceVisibility: { in: params.service ? ['PUBLIC', 'ARCHIVED', 'UNLISTED'] : ['PUBLIC', 'ARCHIVED'], }, diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index f4e7ded..4d93926 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -219,7 +219,6 @@ const servicesQMatch = filters.q ? await findServicesBySimilarity(filters.q) : n const where = { id: servicesQMatch ? { in: servicesQMatch.map(({ id }) => id) } : undefined, - listedAt: { lte: new Date() }, categories: filters.categories.length ? { some: { slug: { in: filters.categories } } } : undefined, verificationStatus: { in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification, @@ -317,7 +316,6 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] = services: { where: { serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] }, - listedAt: { lte: new Date() }, }, }, }, diff --git a/web/src/pages/service/[slug].astro b/web/src/pages/service/[slug].astro index 18f0700..70353dc 100644 --- a/web/src/pages/service/[slug].astro +++ b/web/src/pages/service/[slug].astro @@ -70,7 +70,6 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany( where: { slug, serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] }, - listedAt: { lte: new Date() }, }, select: { id: true, @@ -93,6 +92,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany( referral: true, imageUrl: true, listedAt: true, + approvedAt: true, createdAt: true, acceptedCurrencies: true, tosReview: true, @@ -100,7 +100,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany( userSentiment: true, userSentimentAt: true, averageUserRating: true, - isRecentlyListed: true, + isRecentlyApproved: true, contactMethods: { select: { value: true, @@ -230,7 +230,6 @@ if (!service) { where: { previousSlugs: { has: slug }, serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] }, - listedAt: { lte: new Date() }, }, select: { slug: true }, }) @@ -1005,15 +1004,23 @@ const activeEventToShow = attribute.typeInfo.classNames.text )} > - - {attribute.title} + {attribute.title} + <> + {weights.map((w) => ( + + {formatNumber(w.value, w.formatOptions)} + + ))} + @@ -1080,19 +1087,16 @@ const activeEventToShow = baseScoreType.classNames.text )} > - - {baseScoreType.label} - +50 + {baseScoreType.label} + +50 + +50 ) }

- Overall = 60% Privacy + 40% Trust (Rounded) + Overall = 60% Privacy + 40% Trust (Truncated)

@@ -975,12 +973,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
- + {statusInfo.label} diff --git a/web/src/sw.ts b/web/src/sw.ts index cfca338..cdf1b79 100644 --- a/web/src/sw.ts +++ b/web/src/sw.ts @@ -5,7 +5,10 @@ import { clientsClaim } from 'workbox-core' import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching' -import { makeNotificationOptions } from './lib/notificationOptions' +import { + makeBrowserNotificationOptions, + makeBrowserNotificationTitle, +} from './lib/client/notificationOptions' import type { NotificationData, NotificationPayload } from './lib/serverEventsTypes' @@ -59,8 +62,8 @@ async function handleNotificationClick(url: string) { async function showPushNotification(payload: NotificationPayload | null) { await self.registration.showNotification( - payload?.title ?? 'New Notification', - makeNotificationOptions(payload) + makeBrowserNotificationTitle(payload?.title), + makeBrowserNotificationOptions(payload) ) }