diff --git a/web/astro.config.mjs b/web/astro.config.mjs
index de1a52d..7c50b13 100644
--- a/web/astro.config.mjs
+++ b/web/astro.config.mjs
@@ -107,6 +107,7 @@ export default defineConfig({
'/service/[...slug]/proof': '/service/[...slug]/#verification',
'/attribute/[...slug]': '/attributes',
'/attr/[...slug]': '/attributes',
+ '/service/[...slug]/review': '/service/[...slug]#comments',
// #endregion
},
env: {
diff --git a/web/prisma/seed.ts b/web/prisma/seed.ts
index 74edd41..84d5853 100755
--- a/web/prisma/seed.ts
+++ b/web/prisma/seed.ts
@@ -1143,7 +1143,7 @@ async function main() {
}
let users = await Promise.all(
- Array.from({ length: 10 }, async () => {
+ Array.from({ length: 570 }, async () => {
const { user } = await createAccount()
return user
})
diff --git a/web/src/components/CommentItem.astro b/web/src/components/CommentItem.astro
index 46d23cb..90114c2 100644
--- a/web/src/components/CommentItem.astro
+++ b/web/src/components/CommentItem.astro
@@ -150,7 +150,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
checked={comment.suspicious}
/>
-
diff --git a/web/src/components/InputSelect.astro b/web/src/components/InputSelect.astro
index 6800c9e..e164b58 100644
--- a/web/src/components/InputSelect.astro
+++ b/web/src/components/InputSelect.astro
@@ -14,10 +14,11 @@ type Props = Omit, 'children' | 'inputId' |
value: string
disabled?: boolean
}[]
- selectProps?: Omit, 'name'>
+ selectProps?: Omit, 'name' | 'value'>
+ selectedValue?: string[] | string
}
-const { options, selectProps, ...wrapperProps } = Astro.props
+const { options, selectProps, selectedValue, ...wrapperProps } = Astro.props
const inputId = selectProps?.id ?? Astro.locals.makeId(`input-${wrapperProps.name}`)
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
@@ -39,7 +40,15 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
>
{
options.map((option) => (
-
))
diff --git a/web/src/constants/attributeTypes.ts b/web/src/constants/attributeTypes.ts
index d7b571e..ff06755 100644
--- a/web/src/constants/attributeTypes.ts
+++ b/web/src/constants/attributeTypes.ts
@@ -108,3 +108,19 @@ export const {
},
] as const satisfies AttributeTypeInfo[]
)
+
+export const baseScoreType = {
+ value: 'BASE_SCORE',
+ slug: 'base-score',
+ label: 'Base score',
+ icon: 'ri:information-line',
+ order: 5,
+ classNames: {
+ container: 'bg-night-500',
+ subcontainer: '',
+ text: 'text-day-200',
+ textLight: '',
+ icon: '',
+ button: '',
+ },
+} as const satisfies AttributeTypeInfo
diff --git a/web/src/constants/contactMethods.ts b/web/src/constants/contactMethods.ts
index 0f9eeda..e53c222 100644
--- a/web/src/constants/contactMethods.ts
+++ b/web/src/constants/contactMethods.ts
@@ -106,7 +106,7 @@ export const {
type: 'matrix',
label: 'Matrix',
matcher: /^https?:\/\/(?:www\.)?matrix\.to\/#\/(.+)/,
- formatter: ([, value]) => (value ? `#${value}` : 'Matrix'),
+ formatter: ([, value]) => value ?? 'Matrix',
icon: 'ri:hashtag',
urlType: 'url',
},
@@ -121,7 +121,7 @@ export const {
{
type: 'simplex',
label: 'SimpleX Chat',
- matcher: /^https?:\/\/(?:www\.)?(simplex\.chat)\//,
+ matcher: /^https?:\/\/(?:www\.)?((?:simplex\.chat|smp\d+\.simplex\.im))\//,
formatter: () => 'SimpleX Chat',
icon: 'simplex',
urlType: 'url',
diff --git a/web/src/constants/kycLevels.ts b/web/src/constants/kycLevels.ts
index 5f48e0c..ee68374 100644
--- a/web/src/constants/kycLevels.ts
+++ b/web/src/constants/kycLevels.ts
@@ -2,12 +2,16 @@ import { makeHelpersForOptions } from '../lib/makeHelpersForOptions'
import { parseIntWithFallback } from '../lib/numbers'
import { transformCase } from '../lib/strings'
+import type { AttributeType } from '@prisma/client'
+
type KycLevelInfo = {
id: T
value: number
icon: string
name: string
description: string
+ privacyPoints: number
+ attributeType: AttributeType
}
export const {
@@ -22,6 +26,8 @@ export const {
icon: 'diamond-question',
name: `KYC ${id ? transformCase(id, 'title') : String(id)}`,
description: '',
+ privacyPoints: 0,
+ attributeType: 'INFO',
}),
[
{
@@ -30,6 +36,8 @@ export const {
icon: 'anonymous-mask',
name: 'Guaranteed no KYC',
description: 'Terms explicitly state KYC will never be requested.',
+ privacyPoints: 25,
+ attributeType: 'GOOD',
},
{
id: '1',
@@ -37,6 +45,8 @@ export const {
icon: 'diamond-question',
name: 'No KYC mention',
description: 'No mention of current or future KYC requirements.',
+ privacyPoints: 15,
+ attributeType: 'GOOD',
},
{
id: '2',
@@ -45,6 +55,8 @@ export const {
name: 'KYC on authorities request',
description:
'No routine KYC, but may cooperate with authorities, block funds or implement future KYC requirements.',
+ privacyPoints: -5,
+ attributeType: 'WARNING',
},
{
id: '3',
@@ -52,6 +64,8 @@ export const {
icon: 'gun',
name: 'Shotgun KYC',
description: 'May request KYC and block funds based on automated triggers.',
+ privacyPoints: -15,
+ attributeType: 'WARNING',
},
{
id: '4',
@@ -59,6 +73,8 @@ export const {
icon: 'fingerprint-detailed',
name: 'Mandatory KYC',
description: 'Required for key features and can be required arbitrarily at any time.',
+ privacyPoints: -25,
+ attributeType: 'BAD',
},
] as const satisfies KycLevelInfo<'0' | '1' | '2' | '3' | '4'>[]
)
diff --git a/web/src/lib/attributes.ts b/web/src/lib/attributes.ts
index f966d2f..c59fb09 100644
--- a/web/src/lib/attributes.ts
+++ b/web/src/lib/attributes.ts
@@ -2,6 +2,8 @@ import { orderBy } from 'lodash-es'
import { getAttributeCategoryInfo } from '../constants/attributeCategories'
import { getAttributeTypeInfo } from '../constants/attributeTypes'
+import { getKycLevelClarificationInfo } from '../constants/kycLevelClarifications'
+import { kycLevels } from '../constants/kycLevels'
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
@@ -27,7 +29,7 @@ type NonDbAttribute = Prisma.AttributeGetPayload<{
}[]
}
-export const nonDbAttributes: (NonDbAttribute & {
+type NonDbAttributeFull = NonDbAttribute & {
customize: (
service: Prisma.ServiceGetPayload<{
select: {
@@ -41,12 +43,16 @@ export const nonDbAttributes: (NonDbAttribute & {
onionUrls: true
i2pUrls: true
acceptedCurrencies: true
+ kycLevel: true
+ kycLevelClarification: true
}
}>
) => Partial> & {
show: boolean
}
-})[] = [
+}
+
+export const nonDbAttributes: NonDbAttributeFull[] = [
{
slug: 'verification-verified',
title: 'Verified',
@@ -135,6 +141,31 @@ export const nonDbAttributes: (NonDbAttribute & {
description: `${verificationStatusesByValue.VERIFICATION_FAILED.description} ${READ_MORE_SENTENCE_LINK}\n\nCheck out the [proof](#verification).`,
}),
},
+ ...kycLevels.map((kycLevel) => ({
+ slug: `kyc-level-${kycLevel.id}`,
+ title: kycLevel.name,
+ type: kycLevel.attributeType,
+ category: 'PRIVACY',
+ description: kycLevel.description,
+ privacyPoints: kycLevel.privacyPoints,
+ trustPoints: 0,
+ links: [
+ {
+ url: `/?max-kyc=${kycLevel.id}`,
+ label: 'With this or better',
+ icon: 'ri:search-line',
+ },
+ ],
+ customize: (service) => {
+ const clarification = getKycLevelClarificationInfo(service.kycLevelClarification)
+ return {
+ show: service.kycLevel === kycLevel.value,
+ title: kycLevel.name + (clarification.value !== 'NONE' ? ` (${clarification.label})` : ''),
+ description:
+ kycLevel.description + (clarification.value !== 'NONE' ? ` ${clarification.description}` : ''),
+ }
+ },
+ })),
{
slug: 'archived',
title: serviceVisibilitiesById.ARCHIVED.label,
@@ -261,20 +292,7 @@ export function sortAttributes<
}
export function makeNonDbAttributes(
- service: Prisma.ServiceGetPayload<{
- select: {
- verificationStatus: true
- serviceVisibility: true
- isRecentlyListed: true
- listedAt: true
- createdAt: true
- tosReviewAt: true
- tosReview: true
- onionUrls: true
- i2pUrls: true
- acceptedCurrencies: true
- }
- }>,
+ service: Parameters[0],
{ filter = false }: { filter?: boolean } = {}
) {
const attributes = nonDbAttributes.map(({ customize, ...attribute }) => ({
diff --git a/web/src/pages/about.mdx b/web/src/pages/about.mdx
index ee19be8..1065d51 100644
--- a/web/src/pages/about.mdx
+++ b/web/src/pages/about.mdx
@@ -153,15 +153,6 @@ Scores are calculated **automatically** using clear, fixed rules. We do not chan
The privacy score measures how well a service protects user privacy, using a transparent, rules-based approach:
1. **Base Score:** Every service starts with a neutral score of 50 points.
-1. **KYC Level:** Adjusts the score based on the level of identity verification required:
- - KYC Level 0 (No KYC): **+25 points**
- - KYC Level 1 (Minimal KYC): **+10 points**
- - KYC Level 2 (Moderate KYC): **-5 points**
- - KYC Level 3 (More KYC): **-15 points**
- - KYC Level 4 (Full mandatory KYC): **-25 points**
-1. **Onion URL:** **+5 points** if the service offers at least one Onion (Tor) URL.
-1. **I2P URL:** **+5 points** if the service offers at least one I2P URL.
-1. **Monero Acceptance:** **+5 points** if the service accepts Monero as a payment method.
1. **Privacy Attributes:** The sum of all privacy points from attributes categorized as 'PRIVACY' is added to the score. [See all attributes](/attributes).
1. **Final Score Range:** The final score is always kept between 0 and 100.
@@ -170,13 +161,6 @@ The privacy score measures how well a service protects user privacy, using a tra
The trust score represents how reliable and trustworthy a service is, based on objective, transparent criteria.
1. **Base Score:** Every service begins with a neutral score of 50 points.
-1. **Verification Status:**
- - **Verification Success:** +10 points
- - **Approved:** +5 points
- - **Community Contributed:** 0 points
- - **Verification Failed (SCAM):** -50 points
-1. **Recently Listed:** If a service was listed within the last 15 days and its status is `APPROVED`, a penalty of -10 points is applied to the trust score, and the service is flagged as recently listed.
-1. **Can't Analyze ToS:** If a service's Terms of Service cannot be analyzed by our AI (usually due to captchas, client-side rendering, DDoS protections, or non-text format), a penalty of -3 points is applied to the trust score.
1. **Trust Attributes:** The total trust points from all attributes categorized as 'TRUST' are added to the score. [See all attributes](/attributes).
1. **Final Score Range:** The final score is always kept between 0 and 100.
diff --git a/web/src/pages/admin/service-suggestions/[id].astro b/web/src/pages/admin/service-suggestions/[id].astro
index 4e2b528..6cf3088 100644
--- a/web/src/pages/admin/service-suggestions/[id].astro
+++ b/web/src/pages/admin/service-suggestions/[id].astro
@@ -173,7 +173,7 @@ const typeInfo = getServiceSuggestionTypeInfo(serviceSuggestion.type)
label: status.label,
value: status.value,
}))}
- selectProps={{ value: serviceSuggestion.status }}
+ selectedValue={serviceSuggestion.status}
class="flex-1"
error={serviceSuggestionUpdateInputErrors.status}
/>
diff --git a/web/src/pages/admin/services/[slug]/edit.astro b/web/src/pages/admin/services/[slug]/edit.astro
index f4e7285..d99d44d 100644
--- a/web/src/pages/admin/services/[slug]/edit.astro
+++ b/web/src/pages/admin/services/[slug]/edit.astro
@@ -801,7 +801,8 @@ const apiCalls = await Astro.locals.banners.try(
label: type.label,
value: type.id,
}))}
- selectProps={{ required: true, value: event.type }}
+ selectedValue={event.type}
+ selectProps={{ required: true }}
error={eventUpdateInputErrors.type}
/>
@@ -982,7 +983,7 @@ const apiCalls = await Astro.locals.banners.try(
label: status.label,
value: status.value,
}))}
- selectProps={{ value: step.status }}
+ selectedValue={step.status}
error={verificationStepUpdateInputErrors.status}
/>
diff --git a/web/src/pages/service/[slug].astro b/web/src/pages/service/[slug].astro
index ec04618..7e0ee0a 100644
--- a/web/src/pages/service/[slug].astro
+++ b/web/src/pages/service/[slug].astro
@@ -27,7 +27,7 @@ import Tooltip from '../../components/Tooltip.astro'
import UserBadge from '../../components/UserBadge.astro'
import VerificationWarningBanner from '../../components/VerificationWarningBanner.astro'
import { getAttributeCategoryInfo } from '../../constants/attributeCategories'
-import { getAttributeTypeInfo } from '../../constants/attributeTypes'
+import { baseScoreType, getAttributeTypeInfo } from '../../constants/attributeTypes'
import { formatContactMethod } from '../../constants/contactMethods'
import { currencies, getCurrencyInfo } from '../../constants/currencies'
import { getEventTypeInfo } from '../../constants/eventTypes'
@@ -1052,6 +1052,20 @@ const activeEventToShow =
)
})}
+
+
+ {baseScoreType.label}
+ +50
+
)
}
@@ -1459,6 +1473,7 @@ const activeEventToShow =
Comments