From 1ee56ba91ae185993fcb60d742f19a9529e85321 Mon Sep 17 00:00:00 2001
From: enes <enes@nostrdev.com>
Date: Tue, 3 Dec 2024 15:09:40 +0100
Subject: [PATCH 1/2] feat: generic alert popup and nsfw popup confirmation

---
 src/components/AlertPopup.tsx                | 72 ++++++++++++++++++++
 src/components/Filters/BlogsFilter.tsx       | 23 +------
 src/components/Filters/ModsFilter.tsx        | 16 +----
 src/components/Filters/NsfwFilterOptions.tsx | 64 +++++++++++++++++
 src/components/NsfwAlertPopup.tsx            | 36 ++++++++++
 src/components/ReportPopup.tsx               |  4 +-
 src/hooks/index.ts                           |  1 +
 src/hooks/useSessionStorage.tsx              | 71 +++++++++++++++++++
 src/pages/blogs/index.tsx                    | 27 ++------
 src/pages/settings/preference.tsx            | 29 +++++++-
 src/types/index.ts                           |  1 +
 src/types/popup.ts                           |  9 +++
 src/utils/index.ts                           |  1 +
 src/utils/sessionStorage.ts                  | 32 +++++++++
 14 files changed, 329 insertions(+), 57 deletions(-)
 create mode 100644 src/components/AlertPopup.tsx
 create mode 100644 src/components/Filters/NsfwFilterOptions.tsx
 create mode 100644 src/components/NsfwAlertPopup.tsx
 create mode 100644 src/hooks/useSessionStorage.tsx
 create mode 100644 src/types/popup.ts
 create mode 100644 src/utils/sessionStorage.ts

diff --git a/src/components/AlertPopup.tsx b/src/components/AlertPopup.tsx
new file mode 100644
index 0000000..e9334ee
--- /dev/null
+++ b/src/components/AlertPopup.tsx
@@ -0,0 +1,72 @@
+import { createPortal } from 'react-dom'
+import { AlertPopupProps } from 'types'
+
+export const AlertPopup = ({
+  header,
+  label,
+  handleConfirm,
+  handleClose
+}: AlertPopupProps) => {
+  return createPortal(
+    <div className='popUpMain'>
+      <div className='ContainerMain'>
+        <div className='popUpMainCardWrapper'>
+          <div className='popUpMainCard popUpMainCardQR'>
+            <div className='popUpMainCardTop'>
+              <div className='popUpMainCardTopInfo'>
+                <h3>{header}</h3>
+              </div>
+              <div className='popUpMainCardTopClose' onClick={handleClose}>
+                <svg
+                  xmlns='http://www.w3.org/2000/svg'
+                  viewBox='-96 0 512 512'
+                  width='1em'
+                  height='1em'
+                  fill='currentColor'
+                  style={{ zIndex: 1 }}
+                >
+                  <path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
+                </svg>
+              </div>
+            </div>
+            <div className='pUMCB_Zaps'>
+              <div className='pUMCB_ZapsInside'>
+                <div className='inputLabelWrapperMain'>
+                  <label
+                    className='form-label labelMain'
+                    style={{ fontWeight: 'bold' }}
+                  >
+                    {label}
+                  </label>
+                </div>
+                <div
+                  style={{
+                    display: 'flex',
+                    width: '100%',
+                    gap: '10px'
+                  }}
+                >
+                  <button
+                    className='btn btnMain btnMainPopup'
+                    type='button'
+                    onPointerDown={() => handleConfirm(true)}
+                  >
+                    Yes
+                  </button>
+                  <button
+                    className='btn btnMain btnMainPopup'
+                    type='button'
+                    onPointerDown={() => handleConfirm(false)}
+                  >
+                    No
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>,
+    document.body
+  )
+}
diff --git a/src/components/Filters/BlogsFilter.tsx b/src/components/Filters/BlogsFilter.tsx
index 71b4098..5efc7d2 100644
--- a/src/components/Filters/BlogsFilter.tsx
+++ b/src/components/Filters/BlogsFilter.tsx
@@ -1,16 +1,11 @@
 import { useAppSelector, useLocalStorage } from 'hooks'
 import React from 'react'
-import {
-  FilterOptions,
-  ModeratedFilter,
-  NSFWFilter,
-  SortBy,
-  WOTFilterOptions
-} from 'types'
+import { FilterOptions, ModeratedFilter, SortBy, WOTFilterOptions } from 'types'
 import { DEFAULT_FILTER_OPTIONS } from 'utils'
 import { Dropdown } from './Dropdown'
 import { Option } from './Option'
 import { Filter } from '.'
+import { NsfwFilterOptions } from './NsfwFilterOptions'
 
 type Props = {
   author?: string | undefined
@@ -115,19 +110,7 @@ export const BlogsFilter = React.memo(
 
         {/* nsfw filter options */}
         <Dropdown label={filterOptions.nsfw}>
-          {Object.values(NSFWFilter).map((item, index) => (
-            <Option
-              key={`nsfwFilterItem-${index}`}
-              onClick={() =>
-                setFilterOptions((prev) => ({
-                  ...prev,
-                  nsfw: item
-                }))
-              }
-            >
-              {item}
-            </Option>
-          ))}
+          <NsfwFilterOptions filterKey={filterKey} />
         </Dropdown>
 
         {/* source filter options */}
diff --git a/src/components/Filters/ModsFilter.tsx b/src/components/Filters/ModsFilter.tsx
index afa43c4..8604db5 100644
--- a/src/components/Filters/ModsFilter.tsx
+++ b/src/components/Filters/ModsFilter.tsx
@@ -5,13 +5,13 @@ import {
   SortBy,
   ModeratedFilter,
   WOTFilterOptions,
-  NSFWFilter,
   RepostFilter
 } from 'types'
 import { DEFAULT_FILTER_OPTIONS } from 'utils'
 import { Filter } from '.'
 import { Dropdown } from './Dropdown'
 import { Option } from './Option'
+import { NsfwFilterOptions } from './NsfwFilterOptions'
 
 type Props = {
   author?: string | undefined
@@ -115,19 +115,7 @@ export const ModFilter = React.memo(
 
         {/* nsfw filter options */}
         <Dropdown label={filterOptions.nsfw}>
-          {Object.values(NSFWFilter).map((item, index) => (
-            <Option
-              key={`nsfwFilterItem-${index}`}
-              onClick={() =>
-                setFilterOptions((prev) => ({
-                  ...prev,
-                  nsfw: item
-                }))
-              }
-            >
-              {item}
-            </Option>
-          ))}
+          <NsfwFilterOptions filterKey={filterKey} />
         </Dropdown>
 
         {/* repost filter options */}
diff --git a/src/components/Filters/NsfwFilterOptions.tsx b/src/components/Filters/NsfwFilterOptions.tsx
new file mode 100644
index 0000000..71392f8
--- /dev/null
+++ b/src/components/Filters/NsfwFilterOptions.tsx
@@ -0,0 +1,64 @@
+import { FilterOptions, NSFWFilter } from 'types'
+import { Option } from './Option'
+import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
+import { useState } from 'react'
+import { useLocalStorage, useSessionStorage } from 'hooks'
+import { DEFAULT_FILTER_OPTIONS } from 'utils'
+
+interface NsfwFilterOptionsProps {
+  filterKey: string
+}
+
+export const NsfwFilterOptions = ({ filterKey }: NsfwFilterOptionsProps) => {
+  const [, setFilterOptions] = useLocalStorage<FilterOptions>(
+    filterKey,
+    DEFAULT_FILTER_OPTIONS
+  )
+  const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(false)
+  const [selectedNsfwOption, setSelectedNsfwOption] = useState<
+    NSFWFilter | undefined
+  >()
+  const [confirmNsfw] = useSessionStorage<boolean>('confirm-nsfw', false)
+  const handleConfirm = (confirm: boolean) => {
+    if (confirm && selectedNsfwOption) {
+      setFilterOptions((prev) => ({
+        ...prev,
+        nsfw: selectedNsfwOption
+      }))
+    }
+  }
+
+  return (
+    <>
+      {Object.values(NSFWFilter).map((item, index) => (
+        <Option
+          key={`nsfwFilterItem-${index}`}
+          onClick={() => {
+            // Trigger NSFW popup
+            if (
+              (item === NSFWFilter.Only_NSFW ||
+                item === NSFWFilter.Show_NSFW) &&
+              !confirmNsfw
+            ) {
+              setSelectedNsfwOption(item)
+              setShowNsfwPopup(true)
+            } else {
+              setFilterOptions((prev) => ({
+                ...prev,
+                nsfw: item
+              }))
+            }
+          }}
+        >
+          {item}
+        </Option>
+      ))}
+      {showNsfwPopup && (
+        <NsfwAlertPopup
+          handleConfirm={handleConfirm}
+          handleClose={() => setShowNsfwPopup(false)}
+        />
+      )}
+    </>
+  )
+}
diff --git a/src/components/NsfwAlertPopup.tsx b/src/components/NsfwAlertPopup.tsx
new file mode 100644
index 0000000..dbb9c60
--- /dev/null
+++ b/src/components/NsfwAlertPopup.tsx
@@ -0,0 +1,36 @@
+import { AlertPopupProps } from 'types'
+import { AlertPopup } from './AlertPopup'
+import { useSessionStorage } from 'hooks'
+
+type NsfwAlertPopup = Omit<AlertPopupProps, 'header' | 'label'>
+
+/**
+ * Triggers when the user wants to switch the filter to see any of the NSFW options
+ * (including preferences)
+ *
+ * Option will be remembered for the session only and will not show the popup again
+ */
+export const NsfwAlertPopup = ({
+  handleConfirm,
+  handleClose
+}: NsfwAlertPopup) => {
+  const [confirmNsfw, setConfirmNsfw] = useSessionStorage<boolean>(
+    'confirm-nsfw',
+    false
+  )
+
+  return (
+    !confirmNsfw && (
+      <AlertPopup
+        header='Confirm'
+        label='Are you above 18 years of age?'
+        handleClose={handleClose}
+        handleConfirm={(confirm: boolean) => {
+          setConfirmNsfw(confirm)
+          handleConfirm(confirm)
+          handleClose()
+        }}
+      />
+    )
+  )
+}
diff --git a/src/components/ReportPopup.tsx b/src/components/ReportPopup.tsx
index d31e4b3..ace813d 100644
--- a/src/components/ReportPopup.tsx
+++ b/src/components/ReportPopup.tsx
@@ -3,12 +3,12 @@ import { CheckboxFieldUncontrolled } from 'components/Inputs'
 import { useEffect } from 'react'
 import { ReportReason } from 'types/report'
 import { LoadingSpinner } from './LoadingSpinner'
+import { PopupProps } from 'types'
 
 type ReportPopupProps = {
   openedAt: number
   reasons: ReportReason[]
-  handleClose: () => void
-}
+} & PopupProps
 
 export const ReportPopup = ({
   openedAt,
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 3daf9f4..c01237e 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -8,3 +8,4 @@ export * from './useReactions'
 export * from './useNDKContext'
 export * from './useScrollDisable'
 export * from './useLocalStorage'
+export * from './useSessionStorage'
diff --git a/src/hooks/useSessionStorage.tsx b/src/hooks/useSessionStorage.tsx
new file mode 100644
index 0000000..8f50422
--- /dev/null
+++ b/src/hooks/useSessionStorage.tsx
@@ -0,0 +1,71 @@
+import React from 'react'
+import {
+  getSessionStorageItem,
+  removeSessionStorageItem,
+  setSessionStorageItem
+} from 'utils'
+
+const useSessionStorageSubscribe = (callback: () => void) => {
+  window.addEventListener('sessionStorage', callback)
+  return () => window.removeEventListener('sessionStorage', callback)
+}
+
+function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
+  if (typeof storedValue === 'object' && storedValue !== null) {
+    return { ...initialValue, ...storedValue }
+  }
+  return storedValue
+}
+
+export function useSessionStorage<T>(
+  key: string,
+  initialValue: T
+): [T, React.Dispatch<React.SetStateAction<T>>] {
+  const getSnapshot = () => {
+    // Get the stored value
+    const storedValue = getSessionStorageItem(key, initialValue)
+
+    // Parse the value
+    const parsedStoredValue = JSON.parse(storedValue)
+
+    // Merge the default and the stored in case some of the required fields are missing
+    return JSON.stringify(
+      mergeWithInitialValue(parsedStoredValue, initialValue)
+    )
+  }
+
+  const data = React.useSyncExternalStore(
+    useSessionStorageSubscribe,
+    getSnapshot
+  )
+
+  const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
+    (v: React.SetStateAction<T>) => {
+      try {
+        const nextState =
+          typeof v === 'function'
+            ? (v as (prevState: T) => T)(JSON.parse(data))
+            : v
+
+        if (nextState === undefined || nextState === null) {
+          removeSessionStorageItem(key)
+        } else {
+          setSessionStorageItem(key, JSON.stringify(nextState))
+        }
+      } catch (e) {
+        console.warn(e)
+      }
+    },
+    [data, key]
+  )
+
+  React.useEffect(() => {
+    // Set session storage only when it's empty
+    const data = window.sessionStorage.getItem(key)
+    if (data === null) {
+      setSessionStorageItem(key, JSON.stringify(initialValue))
+    }
+  }, [key, initialValue])
+
+  return [JSON.parse(data) as T, setState]
+}
diff --git a/src/pages/blogs/index.tsx b/src/pages/blogs/index.tsx
index 2155b39..d4b4fab 100644
--- a/src/pages/blogs/index.tsx
+++ b/src/pages/blogs/index.tsx
@@ -14,17 +14,16 @@ import { LoadingSpinner } from 'components/LoadingSpinner'
 import { Filter } from 'components/Filters'
 import { Dropdown } from 'components/Filters/Dropdown'
 import { Option } from 'components/Filters/Option'
+import { NsfwFilterOptions } from 'components/Filters/NsfwFilterOptions'
 
 export const BlogsPage = () => {
   const navigation = useNavigation()
   const blogs = useLoaderData() as Partial<BlogCardDetails>[] | undefined
-  const [filterOptions, setFilterOptions] = useLocalStorage(
-    'filter-blog-curated',
-    {
-      sort: SortBy.Latest,
-      nsfw: NSFWFilter.Hide_NSFW
-    }
-  )
+  const filterKey = 'filter-blog-curated'
+  const [filterOptions, setFilterOptions] = useLocalStorage(filterKey, {
+    sort: SortBy.Latest,
+    nsfw: NSFWFilter.Hide_NSFW
+  })
 
   // Search
   const searchTermRef = useRef<HTMLInputElement>(null)
@@ -147,19 +146,7 @@ export const BlogsPage = () => {
             </Dropdown>
 
             <Dropdown label={filterOptions.nsfw}>
-              {Object.values(NSFWFilter).map((item, index) => (
-                <Option
-                  key={`nsfwFilterItem-${index}`}
-                  onClick={() =>
-                    setFilterOptions((prev) => ({
-                      ...prev,
-                      nsfw: item
-                    }))
-                  }
-                >
-                  {item}
-                </Option>
-              ))}
+              <NsfwFilterOptions filterKey={filterKey} />
             </Dropdown>
           </Filter>
 
diff --git a/src/pages/settings/preference.tsx b/src/pages/settings/preference.tsx
index 985a509..c2b0d42 100644
--- a/src/pages/settings/preference.tsx
+++ b/src/pages/settings/preference.tsx
@@ -1,6 +1,12 @@
 import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'
 import { LoadingSpinner } from 'components/LoadingSpinner'
-import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks'
+import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
+import {
+  useAppDispatch,
+  useAppSelector,
+  useNDKContext,
+  useSessionStorage
+} from 'hooks'
 import { kinds, UnsignedEvent, Event } from 'nostr-tools'
 import { useEffect, useState } from 'react'
 import { toast } from 'react-toastify'
@@ -19,6 +25,13 @@ export const PreferencesSetting = () => {
   const [wotLevel, setWotLevel] = useState(userWotLevel)
   const [isSaving, setIsSaving] = useState(false)
 
+  const [nsfw, setNsfw] = useState(false)
+  const [confirmNsfw] = useSessionStorage<boolean>('confirm-nsfw', false)
+  const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(false)
+  const handleNsfwConfirm = (confirm: boolean) => {
+    setNsfw(confirm)
+  }
+
   useEffect(() => {
     if (user?.pubkey) {
       const hexPubkey = user.pubkey as string
@@ -191,6 +204,14 @@ export const PreferencesSetting = () => {
                   type='checkbox'
                   className='CheckboxMain'
                   name='NSFWPreference'
+                  checked={nsfw}
+                  onChange={(e) => {
+                    if (e.currentTarget.checked && !confirmNsfw) {
+                      setShowNsfwPopup(true)
+                    } else {
+                      setNsfw(e.currentTarget.checked)
+                    }
+                  }}
                 />
               </div>
             </div>
@@ -238,6 +259,12 @@ export const PreferencesSetting = () => {
                 Save
               </button>
             </div>
+            {showNsfwPopup && (
+              <NsfwAlertPopup
+                handleConfirm={handleNsfwConfirm}
+                handleClose={() => setShowNsfwPopup(false)}
+              />
+            )}
           </div>
         </div>
       </div>
diff --git a/src/types/index.ts b/src/types/index.ts
index 8fe37df..64a1407 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -4,3 +4,4 @@ export * from './nostr'
 export * from './user'
 export * from './zap'
 export * from './blog'
+export * from './popup'
diff --git a/src/types/popup.ts b/src/types/popup.ts
new file mode 100644
index 0000000..b1551cf
--- /dev/null
+++ b/src/types/popup.ts
@@ -0,0 +1,9 @@
+export interface PopupProps {
+  handleClose: () => void
+}
+
+export interface AlertPopupProps extends PopupProps {
+  header: string
+  label: string
+  handleConfirm: (confirm: boolean) => void
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 91fe37b..4e3c1d1 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -4,6 +4,7 @@ export * from './url'
 export * from './utils'
 export * from './zap'
 export * from './localStorage'
+export * from './sessionStorage'
 export * from './consts'
 export * from './blog'
 export * from './curationSets'
diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts
new file mode 100644
index 0000000..e40b0e9
--- /dev/null
+++ b/src/utils/sessionStorage.ts
@@ -0,0 +1,32 @@
+export function getSessionStorageItem<T>(key: string, defaultValue: T): string {
+  try {
+    const data = window.sessionStorage.getItem(key)
+    if (data === null) return JSON.stringify(defaultValue)
+    return data
+  } catch (err) {
+    console.error(`Error while fetching session storage value: `, err)
+    return JSON.stringify(defaultValue)
+  }
+}
+
+export function setSessionStorageItem(key: string, value: string) {
+  try {
+    window.sessionStorage.setItem(key, value)
+    dispatchSessionStorageEvent(key, value)
+  } catch (err) {
+    console.error(`Error while saving session storage value: `, err)
+  }
+}
+
+export function removeSessionStorageItem(key: string) {
+  try {
+    window.sessionStorage.removeItem(key)
+    dispatchSessionStorageEvent(key, null)
+  } catch (err) {
+    console.error(`Error while deleting session storage value: `, err)
+  }
+}
+
+function dispatchSessionStorageEvent(key: string, newValue: string | null) {
+  window.dispatchEvent(new StorageEvent('sessionStorage', { key, newValue }))
+}

From 71f934129c2543cc71811854fd4498a7cb0e82d3 Mon Sep 17 00:00:00 2001
From: enes <enes@nostrdev.com>
Date: Tue, 3 Dec 2024 17:32:55 +0100
Subject: [PATCH 2/2] refactor: use local storage instead of session for nsfw
 preference

---
 src/components/Filters/NsfwFilterOptions.tsx | 4 ++--
 src/components/NsfwAlertPopup.tsx            | 4 ++--
 src/pages/settings/preference.tsx            | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/components/Filters/NsfwFilterOptions.tsx b/src/components/Filters/NsfwFilterOptions.tsx
index 71392f8..05ab906 100644
--- a/src/components/Filters/NsfwFilterOptions.tsx
+++ b/src/components/Filters/NsfwFilterOptions.tsx
@@ -2,7 +2,7 @@ import { FilterOptions, NSFWFilter } from 'types'
 import { Option } from './Option'
 import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
 import { useState } from 'react'
-import { useLocalStorage, useSessionStorage } from 'hooks'
+import { useLocalStorage } from 'hooks'
 import { DEFAULT_FILTER_OPTIONS } from 'utils'
 
 interface NsfwFilterOptionsProps {
@@ -18,7 +18,7 @@ export const NsfwFilterOptions = ({ filterKey }: NsfwFilterOptionsProps) => {
   const [selectedNsfwOption, setSelectedNsfwOption] = useState<
     NSFWFilter | undefined
   >()
-  const [confirmNsfw] = useSessionStorage<boolean>('confirm-nsfw', false)
+  const [confirmNsfw] = useLocalStorage<boolean>('confirm-nsfw', false)
   const handleConfirm = (confirm: boolean) => {
     if (confirm && selectedNsfwOption) {
       setFilterOptions((prev) => ({
diff --git a/src/components/NsfwAlertPopup.tsx b/src/components/NsfwAlertPopup.tsx
index dbb9c60..1685a97 100644
--- a/src/components/NsfwAlertPopup.tsx
+++ b/src/components/NsfwAlertPopup.tsx
@@ -1,6 +1,6 @@
 import { AlertPopupProps } from 'types'
 import { AlertPopup } from './AlertPopup'
-import { useSessionStorage } from 'hooks'
+import { useLocalStorage } from 'hooks'
 
 type NsfwAlertPopup = Omit<AlertPopupProps, 'header' | 'label'>
 
@@ -14,7 +14,7 @@ export const NsfwAlertPopup = ({
   handleConfirm,
   handleClose
 }: NsfwAlertPopup) => {
-  const [confirmNsfw, setConfirmNsfw] = useSessionStorage<boolean>(
+  const [confirmNsfw, setConfirmNsfw] = useLocalStorage<boolean>(
     'confirm-nsfw',
     false
   )
diff --git a/src/pages/settings/preference.tsx b/src/pages/settings/preference.tsx
index c2b0d42..7f6c8e0 100644
--- a/src/pages/settings/preference.tsx
+++ b/src/pages/settings/preference.tsx
@@ -5,7 +5,7 @@ import {
   useAppDispatch,
   useAppSelector,
   useNDKContext,
-  useSessionStorage
+  useLocalStorage
 } from 'hooks'
 import { kinds, UnsignedEvent, Event } from 'nostr-tools'
 import { useEffect, useState } from 'react'
@@ -26,7 +26,7 @@ export const PreferencesSetting = () => {
   const [isSaving, setIsSaving] = useState(false)
 
   const [nsfw, setNsfw] = useState(false)
-  const [confirmNsfw] = useSessionStorage<boolean>('confirm-nsfw', false)
+  const [confirmNsfw] = useLocalStorage<boolean>('confirm-nsfw', false)
   const [showNsfwPopup, setShowNsfwPopup] = useState<boolean>(false)
   const handleNsfwConfirm = (confirm: boolean) => {
     setNsfw(confirm)