From 822d5110a893d2e606c3383770886e4993c3bb35 Mon Sep 17 00:00:00 2001 From: daniyal Date: Tue, 3 Sep 2024 16:47:47 +0500 Subject: [PATCH 01/13] fix: in setting page display admin tab to only admin users --- src/pages/settings.tsx | 55 +++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index a86a6eb..3783d3f 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -14,6 +14,8 @@ import '../styles/settings.css' import '../styles/styles.css' import '../styles/write.css' import { copyTextToClipboard } from '../utils' +import { MetadataController } from '../controllers' +import { useEffect, useState } from 'react' export const SettingsPage = () => { const location = useLocation() @@ -47,8 +49,21 @@ export const SettingsPage = () => { const SettingTabs = () => { const location = useLocation() + const [isAdmin, setIsAdmin] = useState(false) const userState = useAppSelector((state) => state.user) + useEffect(() => { + MetadataController.getInstance().then((controller) => { + if (userState.auth && userState.user?.npub) { + setIsAdmin( + controller.adminNpubs.includes(userState.user.npub as string) + ) + } else { + setIsAdmin(false) + } + }) + }, [userState]) + const handleSignOut = () => { logout() } @@ -120,26 +135,28 @@ const SettingTabs = () => { Preference - - - - - Admin - + + + + Admin + + )} {userState.auth && From 77d849e3ab2a79ca8d386ed2c0bb70d2e85eba48 Mon Sep 17 00:00:00 2001 From: freakoverse Date: Tue, 3 Sep 2024 15:15:04 +0000 Subject: [PATCH 02/13] added placeholder gamename for a mod card --- src/components/ModCard.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx index 86b7070..4edfa8b 100644 --- a/src/components/ModCard.tsx +++ b/src/components/ModCard.tsx @@ -36,6 +36,9 @@ export const ModCard = ({

{title}

{summary}

+
+

Game name

+
From 1fde36bc5cb25771c82f971f1062531f5d7c1e88 Mon Sep 17 00:00:00 2001 From: freakoverse Date: Tue, 3 Sep 2024 15:16:58 +0000 Subject: [PATCH 03/13] new class --- src/styles/cardMod.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/styles/cardMod.css b/src/styles/cardMod.css index 4bd8600..49702e1 100644 --- a/src/styles/cardMod.css +++ b/src/styles/cardMod.css @@ -112,6 +112,20 @@ line-height: 1.5; } +.cMMBodyGame { + border-radius: 5px; + padding: 5px 10px; + flex-direction: row; + justify-content: start; + align-items: center; + font-size: 14px; + background: rgba(255,255,255,0.05); + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + -webkit-line-clamp: 1; +} + .cMMFootReactions { display: flex; flex-direction: row; From a661b3f78179574134b578d40d6f0c76b75ba4ed Mon Sep 17 00:00:00 2001 From: freakoverse Date: Tue, 3 Sep 2024 17:56:33 +0000 Subject: [PATCH 04/13] added game name to game mod slide --- src/pages/home.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/home.tsx b/src/pages/home.tsx index dc642cd..f3e4622 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -178,6 +178,10 @@ const SlideContent = ({ naddr }: SlideContentProps) => { {mod.summary}

+

+ Game name +
+

Date: Tue, 3 Sep 2024 17:57:32 +0000 Subject: [PATCH 05/13] Update src/styles/SimpleSlider.css --- src/styles/SimpleSlider.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/styles/SimpleSlider.css b/src/styles/SimpleSlider.css index 60bf918..806c7a2 100644 --- a/src/styles/SimpleSlider.css +++ b/src/styles/SimpleSlider.css @@ -274,6 +274,12 @@ } } +.IBMSMSCWSInfoText.IBMSMSCWSInfoText2 { + -webkit-line-clamp: 1; + border-top: solid 1px rgba(255,255,255,0.1); + padding: 10px 0 0 5px; +} + .swiper-pagination { display: none; bottom: -10px !important; From b12887cdf566ce532ad3f99338ecd5445f23a2c2 Mon Sep 17 00:00:00 2001 From: freakoverse Date: Tue, 3 Sep 2024 18:00:37 +0000 Subject: [PATCH 06/13] Update src/styles/SimpleSlider.css --- src/styles/SimpleSlider.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/SimpleSlider.css b/src/styles/SimpleSlider.css index 806c7a2..38b2ac0 100644 --- a/src/styles/SimpleSlider.css +++ b/src/styles/SimpleSlider.css @@ -278,6 +278,7 @@ -webkit-line-clamp: 1; border-top: solid 1px rgba(255,255,255,0.1); padding: 10px 0 0 5px; + flex-grow: 0; } .swiper-pagination { From a85314f0a78ee4e79905e7483689516f9c034b9a Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 4 Sep 2024 11:28:15 +0500 Subject: [PATCH 07/13] chore(refactor): reduce code duplication in zap --- src/components/ProfileSection.tsx | 217 ++------------------- src/components/Zap.tsx | 236 ++++++++++++++++++++++- src/layout/header.tsx | 310 ++++++++---------------------- src/pages/mod.tsx | 239 ++--------------------- 4 files changed, 341 insertions(+), 661 deletions(-) diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 8dbc1c9..3051b1f 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -1,30 +1,22 @@ -import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools' +import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { QRCodeSVG } from 'qrcode.react' -import { useCallback, useState } from 'react' +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { MetadataController, RelayController, - UserRelaysType, - ZapController + UserRelaysType } from '../controllers' import { useAppSelector, useDidMount } from '../hooks' +import { getProfilePageRoute } from '../routes' import '../styles/author.css' import '../styles/innerPage.css' import '../styles/socialPosts.css' -import { PaymentRequest, UserProfile } from '../types' -import { - copyTextToClipboard, - formatNumber, - log, - LogType, - unformatNumber, - now -} from '../utils' +import { UserProfile } from '../types' +import { copyTextToClipboard, log, LogType, now } from '../utils' import { LoadingSpinner } from './LoadingSpinner' -import { ZapButtons, ZapPresets, ZapQR } from './Zap' -import { getProfilePageRoute } from '../routes' -import { useNavigate } from 'react-router-dom' +import { ZapPopUp } from './Zap' type Props = { pubkey: string @@ -259,7 +251,7 @@ const QRButtonWithPopUp = ({ pubkey }: QRButtonWithPopUpProps) => {
{isOpen && ( -
+
@@ -307,122 +299,6 @@ type ZapButtonWithPopUpProps = { const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => { const [isOpen, setIsOpen] = useState(false) - const [amount, setAmount] = useState(0) - const [message, setMessage] = useState('') - - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - - const [paymentRequest, setPaymentRequest] = useState() - - const userState = useAppSelector((state) => state.user) - - const handleClose = useCallback(() => { - setPaymentRequest(undefined) - setIsLoading(false) - setIsOpen(false) - }, []) - - const handleQRExpiry = useCallback(() => { - setPaymentRequest(undefined) - }, []) - - const handleAmountChange = (event: React.ChangeEvent) => { - const unformattedValue = unformatNumber(event.target.value) - setAmount(unformattedValue) - } - - const generatePaymentRequest = - useCallback(async (): Promise => { - let userHexKey: string - - setIsLoading(true) - setLoadingSpinnerDesc('Getting user pubkey') - - if (userState.auth && userState.user?.pubkey) { - userHexKey = userState.user.pubkey as string - } else { - userHexKey = (await window.nostr?.getPublicKey()) as string - } - - if (!userHexKey) { - setIsLoading(false) - toast.error('Could not get pubkey') - return null - } - - setLoadingSpinnerDesc('Getting admin metadata') - const metadataController = await MetadataController.getInstance() - - const authorMetadata = await metadataController.findMetadata(pubkey) - - if (!authorMetadata?.lud16) { - setIsLoading(false) - toast.error('Lighting address (lud16) is missing in admin metadata!') - return null - } - - if (!authorMetadata?.pubkey) { - setIsLoading(false) - toast.error('pubkey is missing in admin metadata!') - return null - } - - const zapController = ZapController.getInstance() - - setLoadingSpinnerDesc('Creating zap request') - return await zapController - .getLightningPaymentRequest( - authorMetadata.lud16, - amount, - authorMetadata.pubkey as string, - userHexKey, - message - ) - .catch((err) => { - toast.error(err.message || err) - return null - }) - .finally(() => { - setIsLoading(false) - }) - }, [amount, message, userState, pubkey]) - - const handleSend = useCallback(async () => { - const pr = await generatePaymentRequest() - - if (!pr) return - - setIsLoading(true) - setLoadingSpinnerDesc('Sending payment!') - - const zapController = ZapController.getInstance() - - if (await zapController.isWeblnProviderExists()) { - await zapController - .sendPayment(pr.pr) - .then(() => { - toast.success(`Successfully sent ${amount} sats!`) - handleClose() - }) - .catch((err) => { - toast.error(err.message || err) - }) - } else { - toast.warn('Webln is not present. Use QR code to send zap.') - setPaymentRequest(pr) - } - - setIsLoading(false) - }, [amount, handleClose, generatePaymentRequest]) - - const handleGenerateQRCode = async () => { - const pr = await generatePaymentRequest() - - if (!pr) return - - setPaymentRequest(pr) - } return ( <> @@ -442,77 +318,12 @@ const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
{isOpen && ( -
-
-
-
-
-
-

Tip/Zap

-
-
- - - -
-
-
-
-
-
- - -
-
- -
-
-
- - setMessage(e.target.value)} - /> -
- - {paymentRequest && ( - - )} -
-
-
-
-
-
+ setIsOpen(false)} + /> )} - {isLoading && } ) } diff --git a/src/components/Zap.tsx b/src/components/Zap.tsx index 2f441c5..8ef64ad 100644 --- a/src/components/Zap.tsx +++ b/src/components/Zap.tsx @@ -1,12 +1,20 @@ import { QRCodeSVG } from 'qrcode.react' -import React, { Dispatch, SetStateAction, useMemo } from 'react' +import React, { + Dispatch, + ReactNode, + SetStateAction, + useCallback, + useMemo, + useState +} from 'react' import Countdown, { CountdownRenderProps } from 'react-countdown' import { toast } from 'react-toastify' -import { ZapController } from '../controllers' -import { useDidMount } from '../hooks' +import { MetadataController, ZapController } from '../controllers' +import { useAppSelector, useDidMount } from '../hooks' import '../styles/popup.css' import { PaymentRequest } from '../types' -import { copyTextToClipboard } from '../utils' +import { copyTextToClipboard, formatNumber, unformatNumber } from '../utils' +import { LoadingSpinner } from './LoadingSpinner' type PresetAmountProps = { label: string @@ -194,3 +202,223 @@ const Timer = React.memo(({ onTimerExpired }: TimerProps) => {
) }) + +type ZapPopUpProps = { + title: string + labelDescriptionMain?: ReactNode + receiver: string + eventId?: string + aTag?: string + notCloseAfterZap?: boolean + lastNode?: ReactNode + handleClose: () => void +} + +export const ZapPopUp = ({ + title, + labelDescriptionMain, + receiver, + eventId, + aTag, + lastNode, + notCloseAfterZap, + handleClose +}: ZapPopUpProps) => { + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + const [amount, setAmount] = useState(0) + const [message, setMessage] = useState('') + const [paymentRequest, setPaymentRequest] = useState() + + const userState = useAppSelector((state) => state.user) + + const handleAmountChange = (event: React.ChangeEvent) => { + const unformattedValue = unformatNumber(event.target.value) + setAmount(unformattedValue) + } + + const generatePaymentRequest = + useCallback(async (): Promise => { + let userHexKey: string + + setIsLoading(true) + setLoadingSpinnerDesc('Getting user pubkey') + + if (userState.auth && userState.user?.pubkey) { + userHexKey = userState.user.pubkey as string + } else { + userHexKey = (await window.nostr?.getPublicKey()) as string + } + + if (!userHexKey) { + setIsLoading(false) + toast.error('Could not get pubkey') + return null + } + + setLoadingSpinnerDesc('finding receiver metadata') + const metadataController = await MetadataController.getInstance() + + const receiverMetadata = await metadataController.findMetadata(receiver) + + if (!receiverMetadata?.lud16) { + setIsLoading(false) + toast.error('Lighting address (lud16) is missing in receiver metadata!') + return null + } + + if (!receiverMetadata?.pubkey) { + setIsLoading(false) + toast.error('pubkey is missing in receiver metadata!') + return null + } + + const zapController = ZapController.getInstance() + + setLoadingSpinnerDesc('Creating zap request') + return await zapController + .getLightningPaymentRequest( + receiverMetadata.lud16, + amount, + receiverMetadata.pubkey as string, + userHexKey, + message, + eventId, + aTag + ) + .catch((err) => { + toast.error(err.message || err) + return null + }) + .finally(() => { + setIsLoading(false) + }) + }, [amount, message, userState, receiver, eventId, aTag]) + + const handleGenerateQRCode = async () => { + const pr = await generatePaymentRequest() + + if (!pr) return + + setPaymentRequest(pr) + } + + const handleSend = useCallback(async () => { + const pr = await generatePaymentRequest() + + if (!pr) return + + setIsLoading(true) + setLoadingSpinnerDesc('Sending payment!') + + const zapController = ZapController.getInstance() + + if (await zapController.isWeblnProviderExists()) { + await zapController + .sendPayment(pr.pr) + .then(() => { + toast.success(`Successfully sent ${amount} sats!`) + if (!notCloseAfterZap) { + handleClose() + } + }) + .catch((err) => { + toast.error(err.message || err) + }) + } else { + toast.warn('Webln is not present. Use QR code to send zap.') + setPaymentRequest(pr) + } + + setIsLoading(false) + }, [amount, notCloseAfterZap, handleClose, generatePaymentRequest]) + + const handleQRExpiry = useCallback(() => { + setPaymentRequest(undefined) + }, []) + + const handleQRClose = useCallback(() => { + setPaymentRequest(undefined) + setIsLoading(false) + if (!notCloseAfterZap) { + handleClose() + } + }, [notCloseAfterZap, handleClose]) + + return ( + <> + {isLoading && } +
+
+
+
+
+
+

{title}

+
+
+ + + +
+
+
+
+
+
+ {labelDescriptionMain} + + +
+
+ +
+
+
+ + setMessage(e.target.value)} + /> +
+ + {paymentRequest && ( + + )} + {lastNode} +
+
+
+
+
+
+ + ) +} diff --git a/src/layout/header.tsx b/src/layout/header.tsx index 8b1599b..27a157f 100644 --- a/src/layout/header.tsx +++ b/src/layout/header.tsx @@ -2,21 +2,18 @@ import { init as initNostrLogin, launch as launchNostrLoginDialog } from 'nostr-login' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { Link } from 'react-router-dom' -import { toast } from 'react-toastify' import { Banner } from '../components/Banner' -import { LoadingSpinner } from '../components/LoadingSpinner' -import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap' -import { MetadataController, ZapController } from '../controllers' -import { useAppDispatch, useAppSelector } from '../hooks' +import { ZapPopUp } from '../components/Zap' +import { MetadataController } from '../controllers' +import { useAppDispatch, useAppSelector, useDidMount } from '../hooks' import { appRoutes } from '../routes' import { setAuth, setUser } from '../store/reducers/user' import mainStyles from '../styles//main.module.scss' import navStyles from '../styles/nav.module.scss' import '../styles/popup.css' -import { PaymentRequest } from '../types' -import { formatNumber, npubToHex, unformatNumber } from '../utils' +import { npubToHex } from '../utils' export const Header = () => { const dispatch = useAppDispatch() @@ -173,7 +170,9 @@ export const Header = () => {
-
+
{ Blog
-
@@ -235,124 +251,13 @@ export const Header = () => { } const TipButtonWithDialog = React.memo(() => { + const [adminNpub, setAdminNpub] = useState(null) const [isOpen, setIsOpen] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - - const [amount, setAmount] = useState(0) - const [message, setMessage] = useState('') - - const [paymentRequest, setPaymentRequest] = useState() - - const userState = useAppSelector((state) => state.user) - - const handleClose = useCallback(() => { - setPaymentRequest(undefined) - setIsLoading(false) - setIsOpen(false) - }, []) - - const handleQRExpiry = useCallback(() => { - setPaymentRequest(undefined) - }, []) - - const handleAmountChange = (event: React.ChangeEvent) => { - const unformattedValue = unformatNumber(event.target.value) - setAmount(unformattedValue) - } - - const generatePaymentRequest = - useCallback(async (): Promise => { - let userHexKey: string - - setIsLoading(true) - setLoadingSpinnerDesc('Getting user pubkey') - - if (userState.auth && userState.user?.pubkey) { - userHexKey = userState.user.pubkey as string - } else { - userHexKey = (await window.nostr?.getPublicKey()) as string - } - - if (!userHexKey) { - setIsLoading(false) - toast.error('Could not get pubkey') - return null - } - - setLoadingSpinnerDesc('Getting admin metadata') - const metadataController = await MetadataController.getInstance() - - const adminMetadata = await metadataController.findAdminMetadata() - - if (!adminMetadata?.lud16) { - setIsLoading(false) - toast.error('Lighting address (lud16) is missing in admin metadata!') - return null - } - - if (!adminMetadata?.pubkey) { - setIsLoading(false) - toast.error('pubkey is missing in admin metadata!') - return null - } - - const zapController = ZapController.getInstance() - - setLoadingSpinnerDesc('Creating zap request') - return await zapController - .getLightningPaymentRequest( - adminMetadata.lud16, - amount, - adminMetadata.pubkey as string, - userHexKey, - message - ) - .catch((err) => { - toast.error(err.message || err) - return null - }) - .finally(() => { - setIsLoading(false) - }) - }, [amount, message, userState]) - - const handleSend = useCallback(async () => { - const pr = await generatePaymentRequest() - - if (!pr) return - - setIsLoading(true) - setLoadingSpinnerDesc('Sending payment!') - - const zapController = ZapController.getInstance() - - if (await zapController.isWeblnProviderExists()) { - await zapController - .sendPayment(pr.pr) - .then(() => { - toast.success(`Successfully sent ${amount} sats!`) - handleClose() - }) - .catch((err) => { - toast.error(err.message || err) - }) - } else { - toast.warn('Webln is not present. Use QR code to send zap.') - setPaymentRequest(pr) - } - - setIsLoading(false) - }, [amount, handleClose, generatePaymentRequest]) - - const handleGenerateQRCode = async () => { - const pr = await generatePaymentRequest() - - if (!pr) return - - setPaymentRequest(pr) - } + useDidMount(async () => { + const metadataController = await MetadataController.getInstance() + setAdminNpub(metadataController.adminNpubs[0]) + }) return ( <> @@ -371,91 +276,38 @@ const TipButtonWithDialog = React.memo(() => { Tip - {isOpen && ( -
-
-
-
-
-
-

Tip/Zap DEG Mods

-
-
- - - -
-
-
-
-
-
-

- If you don't want the development and maintenance of DEG - Mods to stop, then a tip helps! -

- - -
-
- -
-
-
- - setMessage(e.target.value)} - /> -
- - {paymentRequest && ( - - )} -
-

- DEG Mod's Silent Payment Bitcoin Address (Be careful. Learn more):
- sp1qq205tj23sq3z6qjxt5ts5ps8gdwcrkwypej3h2z2hdclmaptl25xxqjfqhc2de4gaxprgm0yqwfr737swpvvmrph9ctkeyk60knz6xpjhqumafrd -

-
-
-
-
+ {isOpen && adminNpub && ( + setIsOpen(false)} + labelDescriptionMain={ +

+ If you don't want the development and maintenance of DEG Mods to + stop, then a tip helps! +

+ } + lastNode={ +
+

+ DEG Mod's Silent Payment Bitcoin Address (Be careful.{' '} + + Learn more + + ): +
+ + sp1qq205tj23sq3z6qjxt5ts5ps8gdwcrkwypej3h2z2hdclmaptl25xxqjfqhc2de4gaxprgm0yqwfr737swpvvmrph9ctkeyk60knz6xpjhqumafrd + +

-
-
+ } + /> )} - {isLoading && } ) }) diff --git a/src/pages/mod.tsx b/src/pages/mod.tsx index d65544c..9efc2de 100644 --- a/src/pages/mod.tsx +++ b/src/pages/mod.tsx @@ -2,21 +2,15 @@ import Link from '@tiptap/extension-link' import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { formatDate } from 'date-fns' +import FsLightbox from 'fslightbox-react' import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useRef, - useState -} from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { BlogCard } from '../components/BlogCard' import { LoadingSpinner } from '../components/LoadingSpinner' import { ProfileSection } from '../components/ProfileSection' -import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap' +import { ZapButtons, ZapPopUp, ZapPresets, ZapQR } from '../components/Zap' import { MetadataController, RelayController, @@ -48,10 +42,9 @@ import { now, npubToHex, sendDMUsingRandomKey, - unformatNumber, - signAndPublish + signAndPublish, + unformatNumber } from '../utils' -import FsLightbox from 'fslightbox-react' export const ModPage = () => { const { naddr } = useParams() @@ -1638,221 +1631,17 @@ const Zap = ({ modDetails }: ZapProps) => {
- {isOpen && } - - ) -} - -type ZapModalProps = { - modDetails: ModDetails - handleClose: Dispatch> -} - -const ZapModal = ({ modDetails, handleClose }: ZapModalProps) => { - return ( -
-
-
-
-
-
-

Tip/Zap

-
-
handleClose(false)} - > - - - -
-
-
-
- - -
-
-
-
-
-
- ) -} - -type ZapModProps = { - modDetails: ModDetails -} - -const ZapMod = ({ modDetails }: ZapModProps) => { - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - - const [amount, setAmount] = useState(0) - const [message, setMessage] = useState('') - - const [paymentRequest, setPaymentRequest] = useState() - - const userState = useAppSelector((state) => state.user) - - const handleAmountChange = (event: React.ChangeEvent) => { - const unformattedValue = unformatNumber(event.target.value) - setAmount(unformattedValue) - } - - const handleClose = useCallback(() => { - setPaymentRequest(undefined) - setIsLoading(false) - }, []) - - const handleQRExpiry = useCallback(() => { - setPaymentRequest(undefined) - }, []) - - const generatePaymentRequest = - useCallback(async (): Promise => { - let userHexKey: string - - setIsLoading(true) - setLoadingSpinnerDesc('Getting user pubkey') - - if (userState.auth && userState.user?.pubkey) { - userHexKey = userState.user.pubkey as string - } else { - userHexKey = (await window.nostr?.getPublicKey()) as string - } - - if (!userHexKey) { - setIsLoading(false) - toast.error('Could not get pubkey') - return null - } - - setLoadingSpinnerDesc('Getting admin metadata') - const metadataController = await MetadataController.getInstance() - - const authorMetadata = await metadataController.findMetadata( - modDetails.author - ) - - if (!authorMetadata?.lud16) { - setIsLoading(false) - toast.error('Lighting address (lud16) is missing in author metadata!') - return null - } - - if (!authorMetadata?.pubkey) { - setIsLoading(false) - toast.error('pubkey is missing in author metadata!') - return null - } - - const zapController = ZapController.getInstance() - - setLoadingSpinnerDesc('Creating zap request') - return await zapController - .getLightningPaymentRequest( - authorMetadata.lud16, - amount, - authorMetadata.pubkey as string, - userHexKey, - message, - modDetails.id, - modDetails.aTag - ) - .catch((err) => { - toast.error(err.message || err) - return null - }) - .finally(() => { - setIsLoading(false) - }) - }, [amount, message, userState, modDetails]) - - const handleSend = useCallback(async () => { - const pr = await generatePaymentRequest() - - if (!pr) return - - setIsLoading(true) - setLoadingSpinnerDesc('Sending payment!') - - const zapController = ZapController.getInstance() - - if (await zapController.isWeblnProviderExists()) { - await zapController - .sendPayment(pr.pr) - .then(() => { - toast.success(`Successfully sent ${amount} sats!`) - handleClose() - }) - .catch((err) => { - toast.error(err.message || err) - }) - } else { - toast.warn('Webln is not present. Use QR code to send zap.') - setPaymentRequest(pr) - } - - setIsLoading(false) - }, [amount, handleClose, generatePaymentRequest]) - - const handleGenerateQRCode = async () => { - const pr = await generatePaymentRequest() - - if (!pr) return - - setPaymentRequest(pr) - } - - return ( - <> -
-
- - -
-
- -
-
-
- - setMessage(e.target.value)} - /> -
- - {paymentRequest && ( - setIsOpen(false)} + lastNode={} + notCloseAfterZap /> )} - {isLoading && } ) } From 2dd2992810a3f951bdd986f0344f55cdb01572c6 Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 5 Sep 2024 12:39:37 +0500 Subject: [PATCH 08/13] feat: implemented the logic for handling reactions on mods --- src/constants.ts | 15 +++ src/controllers/relay.ts | 145 +++++++++++++++------- src/pages/mod.tsx | 252 ++++++++++++++++++++++++++++++++------- 3 files changed, 322 insertions(+), 90 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index f0e7bff..a611b6c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -36,3 +36,18 @@ export const LANDING_PAGE_DATA = { } ] } +// we use this object to check is a user has reacted positively or negatively to a post +// reactions are kind 7 events and their content is either emoji icon or emoji shortcode +// Extend the following object as per need to include more emojis and shortcodes +// NOTE: In following object emojis and shortcode array are not interlinked. +// Both of these arrays can have separate items +export const REACTIONS = { + positive: { + emojis: ['+', '❤️'], + shortCodes: [':star_struck:', ':red_heart:'] + }, + negative: { + emojis: ['-', '🥵'], + shortCodes: [':hot_face:', ':woozy_face:', ':face_with_thermometer:'] + } +} diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts index 2e362ca..75528c2 100644 --- a/src/controllers/relay.ts +++ b/src/controllers/relay.ts @@ -62,62 +62,77 @@ export class RelayController { /** * Publishes an event to multiple relays. * - * This method connects to the application relay and a set of write relays - * obtained from the `MetadataController`. It then publishes the event to - * all connected relays and returns a list of relays where the event was successfully published. + * This method establishes a connection to the application relay specified by + * an environment variable and a set of relays obtained from the + * `MetadataController`. It attempts to publish the event to all connected + * relays and returns a list of URLs of relays where the event was successfully + * published. + * + * If the process of finding relays or publishing the event takes too long, + * it handles the timeout to prevent blocking the operation. * * @param event - The event to be published. - * @returns A promise that resolves to an array of URLs of relays where the event was published, - * or an empty array if no relays were connected or the event could not be published. + * @param userHexKey - The user's hexadecimal public key, used to retrieve their relays. + * If not provided, the event's public key will be used. + * @param userRelaysType - The type of relays to be retrieved (e.g., write relays). + * Defaults to `UserRelaysType.Write`. + * @returns A promise that resolves to an array of URLs of relays where the event + * was published, or an empty array if no relays were connected or the + * event could not be published. */ - publish = async (event: Event): Promise => { - // Connect to the application relay specified by environment variable + publish = async ( + event: Event, + userHexKey?: string, + userRelaysType?: UserRelaysType + ): Promise => { + // Connect to the application relay specified by an environment variable const appRelayPromise = this.connectRelay(import.meta.env.VITE_APP_RELAY) - // todo: window.nostr.getRelays() is not implemented yet in nostr-login, implement the logic once its done + // TODO: Implement logic to retrieve relays using `window.nostr.getRelays()` once it becomes available in nostr-login. + // Retrieve an instance of MetadataController to find user relays const metadataController = await MetadataController.getInstance() - // Retrieve the list of write relays for the event's public key - // Use a timeout to handle cases where retrieving write relays takes too long - const writeRelaysPromise = metadataController.findUserRelays( - event.pubkey, - UserRelaysType.Write + // Retrieve the list of relays for the specified user's public key + // A timeout is used to prevent long waits if the relay retrieval is delayed + const relaysPromise = metadataController.findUserRelays( + userHexKey || event.pubkey, + userRelaysType || UserRelaysType.Write ) log(this.debug, LogType.Info, `ℹ Finding user's write relays`) - // Use Promise.race to either get the write relay URLs or timeout - const writeRelayUrls = await Promise.race([ - writeRelaysPromise, - timeout() // This is a custom timeout function that rejects the promise after a specified time + // Use Promise.race to either get the relay URLs or handle the timeout + const relayUrls = await Promise.race([ + relaysPromise, + timeout() // Custom timeout function that rejects after a specified time ]).catch((err) => { log(this.debug, LogType.Error, err) return [] as string[] // Return an empty array if an error occurs }) - // push admin relay urls obtained from metadata controller to writeRelayUrls list + // Add admin relay URLs from the metadata controller to the list of relay URLs metadataController.adminRelays.forEach((url) => { - writeRelayUrls.push(url) + relayUrls.push(url) }) - // Connect to all write relays obtained from MetadataController - const relayPromises = writeRelayUrls.map((relayUrl) => + // Attempt to connect to all write relays obtained from MetadataController + const relayPromises = relayUrls.map((relayUrl) => this.connectRelay(relayUrl) ) - // Wait for all relay connections to settle (either fulfilled or rejected) + // Wait for all relay connection attempts to settle (either fulfilled or rejected) await Promise.allSettled([appRelayPromise, ...relayPromises]) - // Check if any relays are connected; if not, log an error and return null + // If no relays are connected, log an error and return an empty array if (this.connectedRelays.length === 0) { log(this.debug, LogType.Error, 'No relay is connected!') return [] } - const publishedOnRelays: string[] = [] // List to track which relays successfully published the event + const publishedOnRelays: string[] = [] // Track relays where the event was successfully published - // Create a promise for publishing the event to each connected relay + // Create promises to publish the event to each connected relay const publishPromises = this.connectedRelays.map((relay) => { log( this.debug, @@ -128,7 +143,7 @@ export class RelayController { return Promise.race([ relay.publish(event), // Publish the event to the relay - timeout(30000) // Set a timeout to handle cases where publishing takes too long + timeout(30000) // Set a timeout to handle slow publishing operations ]) .then((res) => { log( @@ -137,7 +152,7 @@ export class RelayController { `⬆️ nostr (${relay.url}): Publish result:`, res ) - publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays + publishedOnRelays.push(relay.url) // Add successful relay URL to the list }) .catch((err) => { log( @@ -153,16 +168,15 @@ export class RelayController { await Promise.allSettled(publishPromises) if (publishedOnRelays.length > 0) { - // if the event was successfully published to relays then check if it contains the `aTag` - // if so, then cache the event - + // If the event was successfully published to any relays, check if it contains an `aTag` + // If the `aTag` is present, cache the event locally const aTag = event.tags.find((item) => item[0] === 'a') if (aTag && aTag[1]) { this.events.set(aTag[1], event) } } - // Return the list of relay URLs where the event was published + // Return the list of relay URLs where the event was successfully published return publishedOnRelays } @@ -378,28 +392,19 @@ export class RelayController { } /** - * Fetches an event from the user's relays based on a specified filter. - * The function first retrieves the user's relays, and then fetches the event using the provided filter. + * Asynchronously retrieves multiple events from the user's relays based on a specified filter. + * The function first retrieves the user's relays, and then fetches the events using the provided filter. * * @param filter - The event filter to use when fetching the event (e.g., kinds, authors). * @param hexKey - The hexadecimal representation of the user's public key. * @param userRelaysType - The type of relays to search (e.g., write, read). - * @returns A promise that resolves to the fetched event or null if the operation fails. + * @returns A promise that resolves with an array of events. */ - fetchEventFromUserRelays = async ( + fetchEventsFromUserRelays = async ( filter: Filter, hexKey: string, userRelaysType: UserRelaysType - ) => { - // first check if event is present in cached map then return that - // otherwise query relays - if (filter['#a']) { - const aTag = filter['#a'][0] - const cachedEvent = this.events.get(aTag) - - if (cachedEvent) return cachedEvent - } - + ): Promise => { // Get an instance of the MetadataController, which manages user metadata and relays const metadataController = await MetadataController.getInstance() @@ -423,7 +428,55 @@ export class RelayController { }) // Fetch the event from the user's relays using the provided filter and relay URLs - return this.fetchEvent(filter, relayUrls) + return this.fetchEvents(filter, relayUrls) + } + + /** + * Fetches an event from the user's relays based on a specified filter. + * The function first retrieves the user's relays, and then fetches the event using the provided filter. + * + * @param filter - The event filter to use when fetching the event (e.g., kinds, authors). + * @param hexKey - The hexadecimal representation of the user's public key. + * @param userRelaysType - The type of relays to search (e.g., write, read). + * @returns A promise that resolves to the fetched event or null if the operation fails. + */ + fetchEventFromUserRelays = async ( + filter: Filter, + hexKey: string, + userRelaysType: UserRelaysType + ): Promise => { + // first check if event is present in cached map then return that + // otherwise query relays + if (filter['#a']) { + const aTag = filter['#a'][0] + const cachedEvent = this.events.get(aTag) + + if (cachedEvent) return cachedEvent + } + + const events = await this.fetchEventsFromUserRelays( + filter, + hexKey, + userRelaysType + ) + // Sort events by creation date in descending order + events.sort((a, b) => b.created_at - a.created_at) + + if (events.length > 0) { + const event = events[0] + + // if the aTag was specified in filter then cache the fetched event before returning + if (filter['#a']) { + const aTag = filter['#a'][0] + this.events.set(aTag, event) + } + + // return the event + return event + } + + // return null if event array is empty + return null } getTotalZapAmount = async ( diff --git a/src/pages/mod.tsx b/src/pages/mod.tsx index 9efc2de..1bdf6f9 100644 --- a/src/pages/mod.tsx +++ b/src/pages/mod.tsx @@ -3,8 +3,8 @@ import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { formatDate } from 'date-fns' import FsLightbox from 'fslightbox-react' -import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' -import { useCallback, useEffect, useRef, useState } from 'react' +import { Filter, kinds, nip19, UnsignedEvent, Event } from 'nostr-tools' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { BlogCard } from '../components/BlogCard' @@ -45,6 +45,7 @@ import { signAndPublish, unformatNumber } from '../utils' +import { REACTIONS } from '../constants' export const ModPage = () => { const { naddr } = useParams() @@ -1042,48 +1043,7 @@ const Interactions = ({ modDetails }: InteractionsProps) => {
-
-
- - - -
-

4.2k

-
-
-
-
-
-
- - - -
-

69

-
-
-
-
+
) @@ -1819,3 +1779,207 @@ const ZapSite = () => { ) } + +type ReactionsProps = { + modDetails: ModDetails +} + +const Reactions = ({ modDetails }: ReactionsProps) => { + const [isReactionInProgress, setIsReactionInProgress] = useState(false) + const [isDataLoaded, setIsDataLoaded] = useState(false) + const [reactionEvents, setReactionEvents] = useState([]) + + const userState = useAppSelector((state) => state.user) + + useDidMount(() => { + const filter: Filter = { + kinds: [kinds.Reaction], + '#a': [modDetails.aTag] + } + + RelayController.getInstance() + .fetchEventsFromUserRelays(filter, modDetails.author, UserRelaysType.Read) + .then((events) => { + setReactionEvents(events) + }) + .finally(() => { + setIsDataLoaded(true) + }) + }) + + const checkHasPositiveReaction = () => { + return ( + !!userState.auth && + reactionEvents.some( + (event) => + event.pubkey === userState.user?.pubkey && + (REACTIONS.positive.emojis.includes(event.content) || + REACTIONS.positive.shortCodes.includes(event.content)) + ) + ) + } + + const checkHasNegativeReaction = () => { + return ( + !!userState.auth && + reactionEvents.some( + (event) => + event.pubkey === userState.user?.pubkey && + (REACTIONS.negative.emojis.includes(event.content) || + REACTIONS.negative.shortCodes.includes(event.content)) + ) + ) + } + + const getPubkey = async () => { + let hexPubkey: string + + if (userState.auth && userState.user?.pubkey) { + hexPubkey = userState.user.pubkey as string + } else { + hexPubkey = (await window.nostr?.getPublicKey()) as string + } + + if (!hexPubkey) { + toast.error('Could not get pubkey') + return null + } + + return hexPubkey + } + + const handleReaction = async (isPositive?: boolean) => { + if ( + !isDataLoaded || + checkHasPositiveReaction() || + checkHasNegativeReaction() + ) + return + + // Check if the voting process is already in progress + if (isReactionInProgress) return + + // Set the flag to indicate that the voting process has started + setIsReactionInProgress(true) + + try { + const pubkey = await getPubkey() + if (!pubkey) return + + const unsignedEvent: UnsignedEvent = { + kind: kinds.Reaction, + created_at: now(), + content: isPositive ? '+' : '-', + pubkey, + tags: [ + ['e', modDetails.id], + ['p', modDetails.author], + ['a', modDetails.aTag] + ] + } + + const signedEvent = await window.nostr + ?.signEvent(unsignedEvent) + .then((event) => event as Event) + .catch((err) => { + toast.error('Failed to sign the event!') + log(true, LogType.Error, 'Failed to sign the event!', err) + return null + }) + + if (!signedEvent) return + + const publishedOnRelays = await RelayController.getInstance().publish( + signedEvent as Event, + modDetails.author, + UserRelaysType.Read + ) + + if (publishedOnRelays.length === 0) { + toast.error('Failed to publish reaction event on any relay') + return + } + + setReactionEvents((prev) => [...prev, signedEvent]) + } finally { + setIsReactionInProgress(false) + } + } + + const { likesCount, disLikesCount } = useMemo(() => { + let positiveCount = 0 + let negativeCount = 0 + reactionEvents.forEach((event) => { + if ( + REACTIONS.positive.emojis.includes(event.content) || + REACTIONS.positive.shortCodes.includes(event.content) + ) { + positiveCount++ + } else if ( + REACTIONS.negative.emojis.includes(event.content) || + REACTIONS.negative.shortCodes.includes(event.content) + ) { + negativeCount++ + } + }) + + return { + likesCount: abbreviateNumber(positiveCount), + disLikesCount: abbreviateNumber(negativeCount) + } + }, [reactionEvents]) + + const hasReactedPositively = checkHasPositiveReaction() + const hasReactedNegatively = checkHasNegativeReaction() + + return ( + <> +
handleReaction(true)} + > +
+ + + +
+

{likesCount}

+
+
+
+
+
handleReaction()} + > +
+ + + +
+

{disLikesCount}

+
+
+
+
+ + ) +} From 98f4666f96fcfab97eff0959e838ce47499bbe19 Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 5 Sep 2024 12:46:14 +0500 Subject: [PATCH 09/13] chore: add game name in mod card and featured slider on landing page --- src/components/ModCard.tsx | 4 +++- src/pages/home.tsx | 4 +++- src/pages/mods.tsx | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/ModCard.tsx b/src/components/ModCard.tsx index 4edfa8b..781d65c 100644 --- a/src/components/ModCard.tsx +++ b/src/components/ModCard.tsx @@ -3,6 +3,7 @@ import { handleModImageError } from '../utils' type ModCardProps = { title: string + gameName: string summary: string imageUrl: string link: string @@ -11,6 +12,7 @@ type ModCardProps = { export const ModCard = ({ title, + gameName, summary, imageUrl, link, @@ -37,7 +39,7 @@ export const ModCard = ({

{title}

{summary}

-

Game name

+

{gameName}

diff --git a/src/pages/home.tsx b/src/pages/home.tsx index f3e4622..243f58f 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -179,7 +179,7 @@ const SlideContent = ({ naddr }: SlideContentProps) => {

- Game name + {mod.game}

@@ -239,6 +239,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => { return ( { { Date: Thu, 5 Sep 2024 13:30:10 +0500 Subject: [PATCH 10/13] chore: display reactions UI after reactionEvents are loaded --- src/pages/mod.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/mod.tsx b/src/pages/mod.tsx index 1bdf6f9..dbe1bbe 100644 --- a/src/pages/mod.tsx +++ b/src/pages/mod.tsx @@ -1932,6 +1932,8 @@ const Reactions = ({ modDetails }: ReactionsProps) => { const hasReactedPositively = checkHasPositiveReaction() const hasReactedNegatively = checkHasNegativeReaction() + if (!isDataLoaded) return null + return ( <>
Date: Thu, 5 Sep 2024 09:38:15 +0000 Subject: [PATCH 11/13] adjusted and added more emojis/shortcodes --- src/constants.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index a611b6c..3b60501 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -36,18 +36,21 @@ export const LANDING_PAGE_DATA = { } ] } -// we use this object to check is a user has reacted positively or negatively to a post +// we use this object to check if a user has reacted positively or negatively to a post // reactions are kind 7 events and their content is either emoji icon or emoji shortcode // Extend the following object as per need to include more emojis and shortcodes // NOTE: In following object emojis and shortcode array are not interlinked. // Both of these arrays can have separate items export const REACTIONS = { positive: { - emojis: ['+', '❤️'], - shortCodes: [':star_struck:', ':red_heart:'] + emojis: ['+', '❤️', '💙', '💖', '💚','⭐', '🚀', '🫂', '🎉', '🥳', '🎊', '👍', '💪', '😎'], + shortCodes: [':red_heart:', ':blue_heart:', 'sparkling_heart', 'green_heart', 'star', 'rocket', 'people_hugging', 'party_popper', + 'tada', 'partying_face', 'confetti_ball', 'thumbs_up', '+1', 'thumbsup', 'thumbup', 'flexed_biceps', 'muscle'] }, negative: { - emojis: ['-', '🥵'], - shortCodes: [':hot_face:', ':woozy_face:', ':face_with_thermometer:'] + emojis: ['-', '💩', '💔', '👎', '😠', '😞', '🤬', '🤢', '🤮', '🖕', '😡', '💢', '😠', '💀'], + shortCodes: ['poop', 'shit', 'poo', 'hankey', 'pile_of_poo', 'broken_heart', 'thumbsdown', 'thumbdown', 'nauseated_face', 'sick', + 'face_vomiting', 'vomiting_face', 'face_with_open_mouth_vomiting', 'middle_finger', 'rage', 'anger', 'anger_symbol', 'angry_face', 'angry', + 'smiling_face_with_sunglasses', 'sunglasses', 'skull', 'skeleton'] } } From 87359a914e4c6aaf6198a0e00be76c707e27e9ab Mon Sep 17 00:00:00 2001 From: daniyal Date: Thu, 5 Sep 2024 15:16:42 +0500 Subject: [PATCH 12/13] fix: add signed event to reactionEvents array even before publishing so that UI can be updated immediately --- src/pages/mod.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/mod.tsx b/src/pages/mod.tsx index dbe1bbe..cd27bf3 100644 --- a/src/pages/mod.tsx +++ b/src/pages/mod.tsx @@ -1882,13 +1882,15 @@ const Reactions = ({ modDetails }: ReactionsProps) => { ?.signEvent(unsignedEvent) .then((event) => event as Event) .catch((err) => { - toast.error('Failed to sign the event!') + toast.error('Failed to sign the reaction event!') log(true, LogType.Error, 'Failed to sign the event!', err) return null }) if (!signedEvent) return + setReactionEvents((prev) => [...prev, signedEvent]) + const publishedOnRelays = await RelayController.getInstance().publish( signedEvent as Event, modDetails.author, @@ -1896,11 +1898,13 @@ const Reactions = ({ modDetails }: ReactionsProps) => { ) if (publishedOnRelays.length === 0) { - toast.error('Failed to publish reaction event on any relay') + log( + true, + LogType.Error, + 'Failed to publish reaction event on any relay' + ) return } - - setReactionEvents((prev) => [...prev, signedEvent]) } finally { setIsReactionInProgress(false) } From 23ad13fa853e148f72cb13a73fa9fa1d6b6b0dba Mon Sep 17 00:00:00 2001 From: freakoverse Date: Thu, 5 Sep 2024 10:29:22 +0000 Subject: [PATCH 13/13] shortcodes "::" fix --- src/constants.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 3b60501..16a3eef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -44,13 +44,13 @@ export const LANDING_PAGE_DATA = { export const REACTIONS = { positive: { emojis: ['+', '❤️', '💙', '💖', '💚','⭐', '🚀', '🫂', '🎉', '🥳', '🎊', '👍', '💪', '😎'], - shortCodes: [':red_heart:', ':blue_heart:', 'sparkling_heart', 'green_heart', 'star', 'rocket', 'people_hugging', 'party_popper', - 'tada', 'partying_face', 'confetti_ball', 'thumbs_up', '+1', 'thumbsup', 'thumbup', 'flexed_biceps', 'muscle'] + shortCodes: [':red_heart:', ':blue_heart:', ':sparkling_heart:', ':green_heart:', ':star:', ':rocket:', ':people_hugging:', ':party_popper:', + ':tada:', ':partying_face:', ':confetti_ball:', ':thumbs_up:', ':+1:', ':thumbsup:', ':thumbup:', ':flexed_biceps:', ':muscle:'] }, negative: { emojis: ['-', '💩', '💔', '👎', '😠', '😞', '🤬', '🤢', '🤮', '🖕', '😡', '💢', '😠', '💀'], - shortCodes: ['poop', 'shit', 'poo', 'hankey', 'pile_of_poo', 'broken_heart', 'thumbsdown', 'thumbdown', 'nauseated_face', 'sick', - 'face_vomiting', 'vomiting_face', 'face_with_open_mouth_vomiting', 'middle_finger', 'rage', 'anger', 'anger_symbol', 'angry_face', 'angry', - 'smiling_face_with_sunglasses', 'sunglasses', 'skull', 'skeleton'] + shortCodes: [':poop:', ':shit:', ':poo:', ':hankey:', ':pile_of_poo:', ':broken_heart:', ':thumbsdown:', ':thumbdown:', ':nauseated_face:', ':sick:', + ':face_vomiting:', ':vomiting_face:', ':face_with_open_mouth_vomiting:', ':middle_finger:', ':rage:', ':anger:', ':anger_symbol:', ':angry_face:', ':angry:', + ':smiling_face_with_sunglasses:', ':sunglasses:', ':skull:', ':skeleton:'] } }