@@ -690,6 +661,7 @@ const ReportUserPopup = ({
const ProfileTabBlogs = () => {
const { profile, muteLists, nsfwList } =
useLoaderData() as ProfilePageLoaderResult
+ const navigation = useNavigation()
const { fetchEvents } = useNDKContext()
const [filterOptions] = useLocalStorage('filter-blog', DEFAULT_FILTER_OPTIONS)
const [isLoading, setIsLoading] = useState(true)
@@ -704,10 +676,8 @@ const ProfileTabBlogs = () => {
filter['#r'] = [host]
}
- if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
- filter['#nsfw'] = [
- (filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
- ]
+ if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
+ filter['#L'] = ['content-warning']
}
return filter
@@ -725,7 +695,7 @@ const ProfileTabBlogs = () => {
}
fetchEvents(filter)
.then((events) => {
- setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
+ setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT)
})
.finally(() => {
@@ -752,7 +722,7 @@ const ProfileTabBlogs = () => {
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
setPage((prev) => prev + 1)
setBlogs(
- nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((e) => e.naddr)
+ nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((b) => b.naddr)
)
})
.finally(() => setIsLoading(false))
@@ -775,7 +745,7 @@ const ProfileTabBlogs = () => {
.then((events) => {
setHasMore(true)
setPage((prev) => prev - 1)
- setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
+ setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
})
.finally(() => setIsLoading(false))
}
@@ -799,6 +769,11 @@ const ProfileTabBlogs = () => {
})
}
+ // Filter nsfw (Hide_NSFW option)
+ _blogs = _blogs.filter(
+ (b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
+ )
+
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
// Allow "Unmoderated Fully" when author visits own profile
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {
@@ -845,7 +820,9 @@ const ProfileTabBlogs = () => {
return (
<>
- {isLoading && }
+ {(isLoading || navigation.state !== 'idle') && (
+
+ )}
diff --git a/src/pages/profile/loader.ts b/src/pages/profile/loader.ts
index aecb15d..f3ac4c0 100644
--- a/src/pages/profile/loader.ts
+++ b/src/pages/profile/loader.ts
@@ -4,9 +4,10 @@ import { LoaderFunctionArgs, redirect } from 'react-router-dom'
import { appRoutes, getProfilePageRoute } from 'routes'
import { store } from 'store'
import { MuteLists, UserProfile } from 'types'
-import { log, LogType } from 'utils'
+import { log, LogType, npubToHex } from 'utils'
export interface ProfilePageLoaderResult {
+ profilePubkey: string
profile: UserProfile
isBlocked: boolean
isOwnProfile: boolean
@@ -24,10 +25,25 @@ export const profileRouteLoader =
const { nprofile } = params
let profilePubkey: string | undefined
try {
- const value = nprofile
- ? nip19.decode(nprofile as `nprofile1${string}`)
- : undefined
- profilePubkey = value?.data.pubkey
+ // Decode if it starts with nprofile1
+ if (nprofile?.startsWith('nprofile1')) {
+ const value = nprofile
+ ? nip19.decode(nprofile as `nprofile1${string}`)
+ : undefined
+ profilePubkey = value?.data.pubkey
+ } else if (nprofile?.startsWith('npub1')) {
+ // Try to get hex from the npub and encode it to nprofile
+ const value = npubToHex(nprofile)
+ if (value) {
+ return redirect(
+ getProfilePageRoute(
+ nip19.nprofileEncode({
+ pubkey: value
+ })
+ )
+ )
+ }
+ }
} catch (error) {
// Silently ignore and redirect to home or logged in user
log(true, LogType.Error, 'Failed to decode nprofile.', error)
@@ -57,6 +73,7 @@ export const profileRouteLoader =
// Empty result
const result: ProfilePageLoaderResult = {
+ profilePubkey: profilePubkey,
profile: {},
isBlocked: false,
isOwnProfile: false,
diff --git a/src/pages/write/action.tsx b/src/pages/write/action.tsx
index 447a605..61bb7ad 100644
--- a/src/pages/write/action.tsx
+++ b/src/pages/write/action.tsx
@@ -84,22 +84,27 @@ export const writeRouteAction =
.split(',')
.map((t) => ['t', t])
+ const tags = [
+ ['d', uuid],
+ ['a', aTag],
+ ['r', rTag],
+ ['published_at', published_at.toString()],
+ ['title', formSubmit.title!],
+ ['image', formSubmit.image!],
+ ['summary', formSubmit.summary!],
+ ...tTags
+ ]
+
+ // Add NSFW tag, L label namespace standardized tag
+ // https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
+ if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning'])
+
const unsignedEvent: UnsignedEvent = {
kind: kinds.LongFormArticle,
created_at: currentTimeStamp,
pubkey: hexPubkey,
content: content,
- tags: [
- ['d', uuid],
- ['a', aTag],
- ['r', rTag],
- ['published_at', published_at.toString()],
- ['title', formSubmit.title!],
- ['image', formSubmit.image!],
- ['summary', formSubmit.summary!],
- ['nsfw', (formSubmit.nsfw === 'on').toString()],
- ...tTags
- ]
+ tags: tags
}
try {
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 5c14fd0..aec86c6 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -103,13 +103,15 @@ export const routerWithNdkContext = (context: NDKContextType) =>
path: appRoutes.blog,
element: ,
loader: blogRouteLoader(context),
- action: blogRouteAction(context)
+ action: blogRouteAction(context),
+ errorElement:
},
{
path: appRoutes.blogEdit,
element: ,
loader: blogRouteLoader(context),
- action: writeRouteAction(context)
+ action: writeRouteAction(context),
+ errorElement:
},
{
path: appRoutes.blogReport_actionOnly,
diff --git a/src/styles/dotsSpinner.module.scss b/src/styles/dotsSpinner.module.scss
new file mode 100644
index 0000000..772dfa8
--- /dev/null
+++ b/src/styles/dotsSpinner.module.scss
@@ -0,0 +1,18 @@
+.loading::after {
+ content: '.';
+ animation: dots 1.5s steps(4, end) infinite;
+}
+
+@keyframes dots {
+ 0%,
+ 20% {
+ content: '.\00a0\00a0';
+ }
+ 40% {
+ content: '..\00a0';
+ }
+ 60%,
+ 100% {
+ content: '...';
+ }
+}
diff --git a/src/utils/blog.ts b/src/utils/blog.ts
index df11b63..089e058 100644
--- a/src/utils/blog.ts
+++ b/src/utils/blog.ts
@@ -3,22 +3,36 @@ import { BlogCardDetails, BlogDetails } from 'types'
import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr'
import { kinds, nip19 } from 'nostr-tools'
-export const extractBlogDetails = (event: NDKEvent): Partial => ({
- title: getFirstTagValue(event, 'title'),
- content: event.content,
- summary: getFirstTagValue(event, 'summary'),
- image: getFirstTagValue(event, 'image'),
- nsfw: getFirstTagValue(event, 'nsfw') === 'true',
+export const extractBlogDetails = (event: NDKEvent): Partial => {
+ const dTag = getFirstTagValue(event, 'd')
- id: event.id,
- author: event.pubkey,
- published_at: getFirstTagValueAsInt(event, 'published_at'),
- edited_at: event.created_at,
- rTag: getFirstTagValue(event, 'r') || 'N/A',
- dTag: getFirstTagValue(event, 'd'),
- aTag: getFirstTagValue(event, 'a'),
- tTags: getTagValues(event, 't') || []
-})
+ // Check if the aTag exists on the blog
+ let aTag = getFirstTagValue(event, 'a')
+
+ // Create aTag from components if aTag is not included
+ if (typeof aTag === 'undefined' && event.pubkey && dTag) {
+ aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}`
+ }
+
+ return {
+ title: getFirstTagValue(event, 'title'),
+ content: event.content,
+ summary: getFirstTagValue(event, 'summary'),
+ image: getFirstTagValue(event, 'image'),
+ // Check L label namespace for content warning or nsfw (backwards compatibility)
+ nsfw:
+ getFirstTagValue(event, 'L') === 'content-warning' ||
+ getFirstTagValue(event, 'nsfw') === 'true',
+ id: event.id,
+ author: event.pubkey,
+ published_at: getFirstTagValueAsInt(event, 'published_at'),
+ edited_at: event.created_at,
+ rTag: getFirstTagValue(event, 'r') || 'N/A',
+ dTag: dTag,
+ aTag: aTag,
+ tTags: getTagValues(event, 't') || []
+ }
+}
export const extractBlogCardDetails = (
event: NDKEvent