diff --git a/src/components/Inputs.tsx b/src/components/Inputs.tsx
index 38d48af..95b4f48 100644
--- a/src/components/Inputs.tsx
+++ b/src/components/Inputs.tsx
@@ -7,7 +7,7 @@ import '../styles/styles.css'
import '../styles/tiptap.scss'
interface InputFieldProps {
- label: string
+ label: string | React.ReactElement
description?: string
type?: 'text' | 'textarea' | 'richtext'
placeholder: string
diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx
index c54b300..108d33f 100644
--- a/src/components/ModCard.tsx
+++ b/src/components/ModCard.tsx
@@ -57,6 +57,11 @@ export const ModCard = React.memo((props: ModDetails) => {
{props.title}
diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx
index 7411c47..ea54912 100644
--- a/src/components/ModForm.tsx
+++ b/src/components/ModForm.tsx
@@ -29,6 +29,7 @@ import {
import { CheckboxField, InputError, InputField } from './Inputs'
import { LoadingSpinner } from './LoadingSpinner'
import { NDKEvent } from '@nostr-dev-kit/ndk'
+import { OriginalAuthor } from './OriginalAuthor'
interface FormErrors {
game?: string
@@ -40,6 +41,8 @@ interface FormErrors {
screenshotsUrls?: string[]
tags?: string
downloadUrls?: string[]
+ author?: string
+ originalAuthor?: string
}
interface GameOption {
@@ -198,36 +201,42 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
const aTag =
formState.aTag || `${kinds.ClassifiedListing}:${hexPubkey}:${uuid}`
+ const tags = [
+ ['d', uuid],
+ ['a', aTag],
+ ['r', formState.rTag],
+ ['t', T_TAG_VALUE],
+ [
+ 'published_at',
+ existingModData
+ ? existingModData.published_at.toString()
+ : currentTimeStamp.toString()
+ ],
+ ['game', formState.game],
+ ['title', formState.title],
+ ['featuredImageUrl', formState.featuredImageUrl],
+ ['summary', formState.summary],
+ ['nsfw', formState.nsfw.toString()],
+ ['repost', formState.repost.toString()],
+ ['screenshotsUrls', ...formState.screenshotsUrls],
+ ['tags', ...formState.tags.split(',')],
+ [
+ 'downloadUrls',
+ ...formState.downloadUrls.map((downloadUrl) =>
+ JSON.stringify(downloadUrl)
+ )
+ ]
+ ]
+ if (formState.repost && formState.originalAuthor) {
+ tags.push(['originalAuthor', formState.originalAuthor])
+ }
+
const unsignedEvent: UnsignedEvent = {
kind: kinds.ClassifiedListing,
created_at: currentTimeStamp,
pubkey: hexPubkey,
content: formState.body,
- tags: [
- ['d', uuid],
- ['a', aTag],
- ['r', formState.rTag],
- ['t', T_TAG_VALUE],
- [
- 'published_at',
- existingModData
- ? existingModData.published_at.toString()
- : currentTimeStamp.toString()
- ],
- ['game', formState.game],
- ['title', formState.title],
- ['featuredImageUrl', formState.featuredImageUrl],
- ['summary', formState.summary],
- ['nsfw', formState.nsfw.toString()],
- ['screenshotsUrls', ...formState.screenshotsUrls],
- ['tags', ...formState.tags.split(',')],
- [
- 'downloadUrls',
- ...formState.downloadUrls.map((downloadUrl) =>
- JSON.stringify(downloadUrl)
- )
- ]
- ]
+ tags
}
const signedEvent = await window.nostr
@@ -318,6 +327,13 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
}
}
+ if (
+ formState.repost &&
+ (!formState.originalAuthor || formState.originalAuthor === '')
+ ) {
+ errors.originalAuthor = 'Original author field can not be empty'
+ }
+
if (formState.tags === '') {
errors.tags = 'Tags field can not be empty'
}
@@ -397,6 +413,31 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
handleChange={handleCheckboxChange}
type='stylized'
/>
+
+ {formState.repost && (
+ <>
+
+ Created by:{' '}
+ {}
+
+ }
+ type='text'
+ placeholder="Original author's name, npub or nprofile"
+ name='originalAuthor'
+ value={formState.originalAuthor || ''}
+ error={formErrors.originalAuthor || ''}
+ onChange={handleInputChange}
+ />
+ >
+ )}
diff --git a/src/components/OriginalAuthor.tsx b/src/components/OriginalAuthor.tsx
new file mode 100644
index 0000000..60dc4dc
--- /dev/null
+++ b/src/components/OriginalAuthor.tsx
@@ -0,0 +1,48 @@
+import { nip19 } from 'nostr-tools'
+import { appRoutes, getProfilePageRoute } from 'routes'
+import { npubToHex } from 'utils'
+import { ProfileLink } from './ProfileLink'
+
+interface OriginalAuthorProps {
+ value: string
+ fallback?: boolean
+}
+
+export const OriginalAuthor = ({
+ value,
+ fallback = false
+}: OriginalAuthorProps) => {
+ let profilePubkey
+ let displayName = '[name not set up]'
+
+ // Try to decode/encode depending on what we send to link
+ let profileRoute = appRoutes.home
+ try {
+ if (value.startsWith('nprofile1')) {
+ const decoded = nip19.decode(value as `nprofile1${string}`)
+ profileRoute = getProfilePageRoute(value)
+ profilePubkey = decoded?.data.pubkey
+ } else if (value.startsWith('npub1')) {
+ profilePubkey = npubToHex(value)
+ const nprofile = profilePubkey
+ ? nip19.nprofileEncode({
+ pubkey: profilePubkey
+ })
+ : undefined
+
+ if (nprofile) {
+ profileRoute = getProfilePageRoute(nprofile)
+ }
+ } else {
+ displayName = value
+ }
+ } catch (error) {
+ console.error('Failed to create profile link:', error)
+ displayName = value
+ }
+
+ if (profileRoute && profilePubkey)
+ return
+
+ return fallback ? displayName : null
+}
diff --git a/src/components/ProfileLink.tsx b/src/components/ProfileLink.tsx
new file mode 100644
index 0000000..0fcdfca
--- /dev/null
+++ b/src/components/ProfileLink.tsx
@@ -0,0 +1,15 @@
+import { useProfile } from 'hooks/useProfile'
+import { Link } from 'react-router-dom'
+
+interface ProfileLinkProps {
+ pubkey: string
+ profileRoute: string
+}
+
+export const ProfileLink = ({ pubkey, profileRoute }: ProfileLinkProps) => {
+ const profile = useProfile(pubkey)
+ const displayName =
+ profile?.displayName || profile?.name || '[name not set up]'
+
+ return
{displayName}
+}
diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx
index da3cb91..53d2944 100644
--- a/src/pages/mod/index.tsx
+++ b/src/pages/mod/index.tsx
@@ -38,6 +38,7 @@ import { Interactions } from 'components/Internal/Interactions'
import { ReportPopup } from 'components/ReportPopup'
import { Spinner } from 'components/Spinner'
import { RouterLoadingSpinner } from 'components/LoadingSpinner'
+import { OriginalAuthor } from 'components/OriginalAuthor'
const MOD_REPORT_REASONS = [
{ label: 'Actually CP', key: 'actuallyCP' },
@@ -105,6 +106,8 @@ export const ModPage = () => {
screenshotsUrls={mod.screenshotsUrls}
tags={mod.tags}
nsfw={mod.nsfw}
+ repost={mod.repost}
+ originalAuthor={mod.originalAuthor}
/>
{
const postBodyRef = useRef(null)
const viewFullPostBtnRef = useRef(null)
@@ -504,6 +511,17 @@ const Body = ({
)}
+ {repost && (
+
+
+ REPOST. Original Author:{' '}
+ {!!originalAuthor && (
+
+ )}
+
+
+ )}
+
{tags.map((tag, index) => (
{tag}
diff --git a/src/types/mod.ts b/src/types/mod.ts
index bfad46f..bc27e18 100644
--- a/src/types/mod.ts
+++ b/src/types/mod.ts
@@ -28,6 +28,7 @@ export interface ModFormState {
summary: string
nsfw: boolean
repost: boolean
+ originalAuthor?: string
screenshotsUrls: string[]
tags: string
downloadUrls: DownloadUrl[]
diff --git a/src/utils/mod.ts b/src/utils/mod.ts
index 95f82a3..24b59bd 100644
--- a/src/utils/mod.ts
+++ b/src/utils/mod.ts
@@ -40,6 +40,7 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
summary: getFirstTagValue('summary'),
nsfw: getFirstTagValue('nsfw') === 'true',
repost: getFirstTagValue('repost') === 'true',
+ originalAuthor: getFirstTagValue('originalAuthor'),
screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
tags: getTagValue(event, 'tags') || [],
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
@@ -120,6 +121,7 @@ export const initializeFormState = (
summary: existingModData?.summary || '',
nsfw: existingModData?.nsfw || false,
repost: existingModData?.repost || false,
+ originalAuthor: existingModData?.originalAuthor || undefined,
screenshotsUrls: existingModData?.screenshotsUrls || [''],
tags: existingModData?.tags.join(',') || '',
downloadUrls: existingModData?.downloadUrls || [