diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 0000000000..6afc7667c6 --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/splitted-square.svg b/assets/icons/splitted-square.svg new file mode 100644 index 0000000000..6634f661d9 --- /dev/null +++ b/assets/icons/splitted-square.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/package.json b/package.json index 6bda65456a..a59540cd71 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,8 @@ "@types/crypto-js": "^4.2.2", "@types/leaflet": "^1.9.12", "@types/leaflet.markercluster": "^1.5.4", + "@types/markdown-it-emoji": "^3.0.1", + "@types/markdown-it-footnote": "^3.0.4", "@types/papaparse": "^5.3.14", "@types/pluralize": "^0.0.33", "assert": "^2.1.0", @@ -121,6 +123,8 @@ "long": "^5.2.1", "lottie-react-native": "6.5.1", "markdown-it": "^14.1.0", + "markdown-it-emoji": "^3.0.0", + "markdown-it-footnote": "^4.0.0", "merkletreejs": "^0.4.0", "metamask-react": "^2.4.1", "moment": "^2.29.4", @@ -157,6 +161,7 @@ "react-native-reanimated": "^3.6.2", "react-native-reanimated-carousel": "4.0.0-alpha.9", "react-native-reanimated-table": "^0.0.2", + "react-native-render-html": "^6.3.4", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-smooth-slider": "^1.3.6", @@ -197,7 +202,7 @@ "@types/draft-convert": "^2.1.4", "@types/draft-js": "^0.11.9", "@types/html-to-draftjs": "^1.4.0", - "@types/markdown-it": "^13.0.7", + "@types/markdown-it": "^14.1.2", "@types/node": "^20.9.1", "@types/react": "~18.2.45", "@types/react-native-countdown-component": "^2.7.0", diff --git a/packages/components/gradientText/GradientText.tsx b/packages/components/gradientText/GradientText.tsx index 846dbd7f63..88077577fe 100644 --- a/packages/components/gradientText/GradientText.tsx +++ b/packages/components/gradientText/GradientText.tsx @@ -123,6 +123,8 @@ const gradient = (type: GradientType): LinearGradientProps => { return getMapPostTextGradient(PostCategory.Normal); case getMapPostTextGradientType(PostCategory.Article): return getMapPostTextGradient(PostCategory.Article); + case getMapPostTextGradientType(PostCategory.ArticleMarkdown): + return getMapPostTextGradient(PostCategory.Article); case getMapPostTextGradientType(PostCategory.Video): return getMapPostTextGradient(PostCategory.Video); case getMapPostTextGradientType(PostCategory.Picture): diff --git a/packages/components/gradientText/GradientText.web.tsx b/packages/components/gradientText/GradientText.web.tsx index e7ddd31819..362623ad4a 100644 --- a/packages/components/gradientText/GradientText.web.tsx +++ b/packages/components/gradientText/GradientText.web.tsx @@ -48,6 +48,8 @@ const gradient = (type: GradientType) => { return getMapPostTextGradientString(PostCategory.Normal); case getMapPostTextGradientType(PostCategory.Article): return getMapPostTextGradientString(PostCategory.Article); + case getMapPostTextGradientType(PostCategory.ArticleMarkdown): + return getMapPostTextGradientString(PostCategory.Article); case getMapPostTextGradientType(PostCategory.Video): return getMapPostTextGradientString(PostCategory.Video); case getMapPostTextGradientType(PostCategory.Picture): diff --git a/packages/components/socialFeed/Map/Map.web.tsx b/packages/components/socialFeed/Map/Map.web.tsx index 9da11a5890..8d2928ffa3 100644 --- a/packages/components/socialFeed/Map/Map.web.tsx +++ b/packages/components/socialFeed/Map/Map.web.tsx @@ -287,6 +287,10 @@ export const Map: FC = ({ + ) : marker.post.category === PostCategory.ArticleMarkdown ? ( + + + ) : marker.post.category === PostCategory.Article ? ( diff --git a/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx b/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx index 0349acc156..161be7549e 100644 --- a/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx +++ b/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx @@ -48,7 +48,9 @@ export const ArticleMapPost: FC<{ return ( - {title} + + {title} + diff --git a/packages/components/socialFeed/NewsFeed/LocationButton.tsx b/packages/components/socialFeed/NewsFeed/LocationButton.tsx index 544e821ba4..e083106f9b 100644 --- a/packages/components/socialFeed/NewsFeed/LocationButton.tsx +++ b/packages/components/socialFeed/NewsFeed/LocationButton.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; -import { ColorValue } from "react-native"; +import { ColorValue, StyleProp, ViewStyle } from "react-native"; +import { MouseEvent } from "react-native/Libraries/Types/CoreEventTypes"; import locationRefinedSVG from "@/assets/icons/location-refined.svg"; import { SVG } from "@/components/SVG"; @@ -9,9 +10,17 @@ export const LocationButton: FC<{ onPress: () => void; color?: ColorValue; stroke?: ColorValue; -}> = ({ onPress, stroke, color }) => { + style?: StyleProp; + onHoverIn?: (event: MouseEvent) => void; + onHoverOut?: (event: MouseEvent) => void; +}> = ({ onPress, stroke, color, style, onHoverIn, onHoverOut }) => { return ( - + = ({ style={cardStyle} refetchFeed={refetch} /> + ) : post.category === PostCategory.ArticleMarkdown ? ( + ) : post.category === PostCategory.Video ? ( ; refetchFeed?: () => Promise; isFlagged?: boolean; -}> = memo(({ post, isPostConsultation, refetchFeed, style, isFlagged }) => { - const navigation = useAppNavigation(); - const [localPost, setLocalPost] = useState(post); - const [viewWidth, setViewWidth] = useState(0); - const { width: windowWidth } = useWindowDimensions(); + disabled?: boolean; +}> = memo( + ({ post, isPostConsultation, refetchFeed, style, isFlagged, disabled }) => { + const navigation = useAppNavigation(); + const [localPost, setLocalPost] = useState(post); + const [viewWidth, setViewWidth] = useState(0); + const { width: windowWidth } = useWindowDimensions(); - const articleCardHeight = windowWidth < SOCIAL_FEED_BREAKPOINT_M ? 214 : 254; - const thumbnailImageWidth = viewWidth / 3; - const borderRadius = - windowWidth < RESPONSIVE_BREAKPOINT_S ? 0 : SOCIAl_CARD_BORDER_RADIUS; + const articleCardHeight = + windowWidth < SOCIAL_FEED_BREAKPOINT_M ? 214 : 254; + const thumbnailImageWidth = viewWidth / 3; + const borderRadius = + windowWidth < RESPONSIVE_BREAKPOINT_S ? 0 : SOCIAl_CARD_BORDER_RADIUS; - const metadata = zodTryParseJSON( - ZodSocialFeedArticleMetadata, - localPost.metadata, - ); - const oldMetadata = zodTryParseJSON( - ZodSocialFeedPostMetadata, - localPost.metadata, - ); - const thumbnailImage = - metadata?.thumbnailImage || - // Old articles doesn't have thumbnailImage, but they have a file with a isCoverImage flag - oldMetadata?.files?.find((file) => file.isCoverImage); - const simplePostMetadata = metadata || oldMetadata; - const message = simplePostMetadata?.message; + const metadata = zodTryParseJSON( + ZodSocialFeedArticleMetadata, + localPost.metadata, + ); + const oldMetadata = zodTryParseJSON( + ZodSocialFeedPostMetadata, + localPost.metadata, + ); + const thumbnailImage = + metadata?.thumbnailImage || + // Old articles doesn't have thumbnailImage, but they have a file with a isCoverImage flag + oldMetadata?.files?.find((file) => file.isCoverImage); + const simplePostMetadata = metadata || oldMetadata; + const message = simplePostMetadata?.message; - const shortDescription = useMemo(() => { - if (metadata?.shortDescription) { - return metadata.shortDescription; - } - if (!message) return ""; - if (isArticleHTMLNeedsTruncate(message, true)) { - const { truncatedHtml } = getTruncatedArticleHTML(message); - const contentState = - createStateFromHTML(truncatedHtml).getCurrentContent(); - return ( - metadata?.shortDescription || - // Old articles doesn't have shortDescription, so we use the start of the html content - contentState.getPlainText() - ); - } - return ""; - }, [message, metadata?.shortDescription]); + const shortDescription = useMemo(() => { + if (metadata?.shortDescription) { + return metadata.shortDescription; + } + if (!message) return ""; + if (isArticleHTMLNeedsTruncate(message, true)) { + const { truncatedHtml } = getTruncatedArticleHTML(message); + const contentState = + createStateFromHTML(truncatedHtml).getCurrentContent(); + return ( + metadata?.shortDescription || + // Old articles doesn't have shortDescription, so we use the start of the html content + contentState.getPlainText() + ); + } + return ""; + }, [message, metadata?.shortDescription]); - useEffect(() => { - setLocalPost(post); - }, [post]); + useEffect(() => { + setLocalPost(post); + }, [post]); - const thumbnailURI = thumbnailImage?.url - ? thumbnailImage.url.includes("://") - ? thumbnailImage.url - : "ipfs://" + thumbnailImage.url // we need this hack because ipfs "urls" in feed are raw CIDs - : defaultThumbnailImage; + const thumbnailURI = thumbnailImage?.url + ? thumbnailImage.url.includes("://") + ? thumbnailImage.url + : "ipfs://" + thumbnailImage.url // we need this hack because ipfs "urls" in feed are raw CIDs + : defaultThumbnailImage; - const title = simplePostMetadata?.title; + const title = simplePostMetadata?.title; - return ( - - - navigation.navigate("FeedPostView", { - id: localPost.id, - }) - } - onLayout={(e) => setViewWidth(e.nativeEvent.layout.width)} - style={[ - { - borderWidth: 1, - borderColor: withAlpha(neutral33, 0.5), - borderRadius, - backgroundColor: neutral00, - width: "100%", - flexDirection: "row", - justifyContent: "space-between", - height: articleCardHeight, - flex: 1, - }, - style, - ]} + return ( + - + navigation.navigate("FeedPostView", { + id: localPost.id, + }) + } + onLayout={(e) => setViewWidth(e.nativeEvent.layout.width)} + style={[ + { + borderWidth: 1, + borderColor: withAlpha(neutral33, 0.5), + borderRadius, + backgroundColor: neutral00, + width: "100%", + flexDirection: "row", + justifyContent: "space-between", + height: articleCardHeight, + flex: 1, + }, + style, + ]} > - - + + + - - - {title?.trim().replace("\n", " ")} - + + + {title?.trim().replace("\n", " ")} + - - - {shortDescription.trim().replace("\n", " ")} - + + + {shortDescription.trim().replace("\n", " ")} + + - - {/*We use a shadow to highlight the footer when it's onto the thumbnail image (mobile)*/} - - {isFlagged ? ( - - ) : ( - - )} - + {/*We use a shadow to highlight the footer when it's onto the thumbnail image (mobile)*/} + + {isFlagged ? ( + + ) : ( + + )} + - - - - ); -}); + + + + ); + }, +); diff --git a/packages/components/socialFeed/SocialCard/cards/SocialArticleMarkdownCard.tsx b/packages/components/socialFeed/SocialCard/cards/SocialArticleMarkdownCard.tsx new file mode 100644 index 0000000000..e0d29b45f2 --- /dev/null +++ b/packages/components/socialFeed/SocialCard/cards/SocialArticleMarkdownCard.tsx @@ -0,0 +1,193 @@ +import { LinearGradient } from "expo-linear-gradient"; +import React, { FC, memo, useEffect, useState } from "react"; +import { StyleProp, useWindowDimensions, View, ViewStyle } from "react-native"; + +import { BrandText } from "../../../BrandText"; +import { OptimizedImage } from "../../../OptimizedImage"; +import { CustomPressable } from "../../../buttons/CustomPressable"; +import { SpacerColumn } from "../../../spacer"; +import { FlaggedCardFooter } from "../FlaggedCardFooter"; +import { SocialCardFooter } from "../SocialCardFooter"; +import { SocialCardHeader } from "../SocialCardHeader"; +import { SocialCardWrapper } from "../SocialCardWrapper"; + +import { Post } from "@/api/feed/v1/feed"; +import defaultThumbnailImage from "@/assets/default-images/default-article-thumbnail.png"; +import { useAppNavigation } from "@/hooks/navigation/useAppNavigation"; +import { zodTryParseJSON } from "@/utils/sanitize"; +import { + ARTICLE_THUMBNAIL_IMAGE_MAX_WIDTH, + SOCIAl_CARD_BORDER_RADIUS, +} from "@/utils/social-feed"; +import { + neutral00, + neutral33, + neutralA3, + withAlpha, +} from "@/utils/style/colors"; +import { fontRegular13, fontRegular15 } from "@/utils/style/fonts"; +import { + layout, + RESPONSIVE_BREAKPOINT_S, + SOCIAL_FEED_BREAKPOINT_M, +} from "@/utils/style/layout"; +import { ZodSocialFeedArticleMarkdownMetadata } from "@/utils/types/feed"; + +const ARTICLE_CARD_PADDING_VERTICAL = layout.spacing_x2; +const ARTICLE_CARD_PADDING_HORIZONTAL = layout.spacing_x2_5; + +// TODO: It's a copy of SocialArticleCard.tsx, just made waiting for a posts UI (and data) refacto. => Merge them in the future + +export const SocialArticleMarkdownCard: FC<{ + post: Post; + isPostConsultation?: boolean; + style?: StyleProp; + refetchFeed?: () => Promise; + isFlagged?: boolean; + disabled?: boolean; +}> = memo( + ({ post, isPostConsultation, refetchFeed, style, isFlagged, disabled }) => { + const navigation = useAppNavigation(); + const [localPost, setLocalPost] = useState(post); + const [viewWidth, setViewWidth] = useState(0); + const { width: windowWidth } = useWindowDimensions(); + + const articleCardHeight = + windowWidth < SOCIAL_FEED_BREAKPOINT_M ? 214 : 254; + const thumbnailImageWidth = viewWidth / 3; + const borderRadius = + windowWidth < RESPONSIVE_BREAKPOINT_S ? 0 : SOCIAl_CARD_BORDER_RADIUS; + + const metadata = zodTryParseJSON( + ZodSocialFeedArticleMarkdownMetadata, + localPost.metadata, + ); + const thumbnailImage = metadata?.thumbnailImage; + const shortDescription = metadata?.shortDescription || ""; + const title = metadata?.title; + + useEffect(() => { + setLocalPost(post); + }, [post]); + + return ( + + + navigation.navigate("FeedPostView", { + id: localPost.id, + }) + } + onLayout={(e) => setViewWidth(e.nativeEvent.layout.width)} + style={[ + { + borderWidth: 1, + borderColor: withAlpha(neutral33, 0.5), + borderRadius, + backgroundColor: neutral00, + width: "100%", + flexDirection: "row", + justifyContent: "space-between", + height: articleCardHeight, + flex: 1, + }, + style, + ]} + > + + + + + + + {title?.trim().replace("\n", " ")} + + + + + {shortDescription.trim().replace("\n", " ")} + + + + + {/*We use a shadow to highlight the footer when it's onto the thumbnail image (mobile)*/} + + {isFlagged ? ( + + ) : ( + + )} + + + + + + ); + }, +); diff --git a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx index 03ac700048..46a7309ba0 100644 --- a/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx +++ b/packages/screens/FeedNewArticle/FeedNewArticleScreen.tsx @@ -1,62 +1,68 @@ import pluralize from "pluralize"; import React, { useEffect, useRef, useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { ScrollView, View } from "react-native"; +import { FormProvider, useForm } from "react-hook-form"; +import { ScrollView, useWindowDimensions, View } from "react-native"; import { useSelector } from "react-redux"; -import priceSVG from "../../../assets/icons/price.svg"; import useSelectedWallet from "../../hooks/useSelectedWallet"; +import penSVG from "@/assets/icons/pen.svg"; +import priceSVG from "@/assets/icons/price.svg"; import { BrandText } from "@/components/BrandText"; import { SVG } from "@/components/SVG"; import { ScreenContainer } from "@/components/ScreenContainer"; import { ScreenTitle } from "@/components/ScreenContainer/ScreenTitle"; import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; import { DAOSelector } from "@/components/dao/DAOSelector"; import { Label, TextInputCustom } from "@/components/inputs/TextInputCustom"; import { FileUploader } from "@/components/inputs/fileUploader"; import { FeedPostingProgressBar } from "@/components/loaders/FeedPostingProgressBar"; -import { RichText } from "@/components/socialFeed/RichText"; -import { PublishValues } from "@/components/socialFeed/RichText/RichText.type"; +import { SocialArticleMarkdownCard } from "@/components/socialFeed/SocialCard/cards/SocialArticleMarkdownCard"; import { MapModal } from "@/components/socialFeed/modals/MapModal/MapModal"; -import { SpacerColumn } from "@/components/spacer"; +import { SpacerColumn, SpacerRow } from "@/components/spacer"; import { useFeedbacks } from "@/context/FeedbacksProvider"; import { useWalletControl } from "@/context/WalletControlProvider"; import { useFeedPosting } from "@/hooks/feed/useFeedPosting"; import { useIpfs } from "@/hooks/useIpfs"; import { useIsMobile } from "@/hooks/useIsMobile"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; import { NetworkFeature } from "@/networks"; +import { ArticleContentEditor } from "@/screens/FeedNewArticle/components/ArticleContentEditor/ArticleContentEditor"; +import { NewArticleLocationButton } from "@/screens/FeedNewArticle/components/NewArticleLocationButton"; import { selectNFTStorageAPI } from "@/store/slices/settings"; import { feedPostingStep, FeedPostingStepId } from "@/utils/feed/posting"; -import { generateArticleMetadata } from "@/utils/feed/queries"; +import { generateArticleMarkdownMetadata } from "@/utils/feed/queries"; import { generateIpfsKey } from "@/utils/ipfs"; import { IMAGE_MIME_TYPES } from "@/utils/mime"; import { ScreenFC, useAppNavigation } from "@/utils/navigation"; -import { - ARTICLE_COVER_IMAGE_MAX_HEIGHT, - ARTICLE_COVER_IMAGE_RATIO, - ARTICLE_THUMBNAIL_IMAGE_MAX_HEIGHT, - ARTICLE_THUMBNAIL_IMAGE_MAX_WIDTH, -} from "@/utils/social-feed"; import { neutral00, neutral11, + neutral33, neutral77, + neutralFF, secondaryColor, } from "@/utils/style/colors"; import { fontSemibold13 } from "@/utils/style/fonts"; -import { layout, screenContentMaxWidth } from "@/utils/style/layout"; +import { + layout, + RESPONSIVE_BREAKPOINT_S, + screenContentMaxWidth, +} from "@/utils/style/layout"; import { CustomLatLngExpression, NewArticleFormValues, PostCategory, + SocialFeedArticleMarkdownMetadata, } from "@/utils/types/feed"; -import { RemoteFileData } from "@/utils/types/files"; - -//TODO: In mobile : Make ActionsContainer accessible (floating button ?) export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { + const { width } = useMaxResolution(); + const { width: windowWidth } = useWindowDimensions(); + const isSmallScreen = windowWidth < RESPONSIVE_BREAKPOINT_S; const isMobile = useIsMobile(); const wallet = useSelectedWallet(); const selectedNetworkId = useSelectedNetworkId(); @@ -66,7 +72,8 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { const { uploadFilesToPinata, ipfsUploadProgress } = useIpfs(); const [isUploadLoading, setIsUploadLoading] = useState(false); const [isProgressBarShown, setIsProgressBarShown] = useState(false); - const postCategory = PostCategory.Article; + const [isMapShown, setIsMapShown] = useState(false); + const postCategory = PostCategory.ArticleMarkdown; const { makePost, isProcessing, @@ -88,7 +95,7 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { message: "", }); navigateBack(); - reset(); + newArticleForm.reset(); }, 1000); }); const forceNetworkFeature = NetworkFeature.SocialFeed; @@ -98,36 +105,36 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { const { setToast } = useFeedbacks(); const navigation = useAppNavigation(); const scrollViewRef = useRef(null); + const [isThumbnailButtonHovered, setThumbnailButtonHovered] = useState(false); const [location, setLocation] = useState(); - const [isMapShown, setIsMapShown] = useState(false); - const { - control, - setValue, - reset, - watch, - formState: { errors }, - } = useForm({ + const cardStyle = isSmallScreen && { + borderRadius: 0, + borderLeftWidth: 0, + borderRightWidth: 0, + }; + const newArticleForm = useForm({ defaultValues: { title: "", message: "", - files: [], - gifs: [], - hashtags: [], - mentions: [], thumbnailImage: undefined, shortDescription: "", }, mode: "onBlur", }); - //TODO: Not handled for now - // const { mutate: openGraphMutate, data: openGraphData } = useOpenGraph(); - const formValues = watch(); + const formValues = newArticleForm.watch(); + const previewMetadata: SocialFeedArticleMarkdownMetadata = { + title: formValues.title, + shortDescription: formValues.shortDescription || "", + thumbnailImage: formValues.thumbnailImage, + message: "", + hashtags: [], + mentions: [], + }; - //TODO: Keep short post formValues when returning to short post const navigateBack = () => navigation.navigate("Feed"); - const onPublish = async (values: PublishValues) => { + const onPublish = async () => { const action = "Publish an Article"; if (!wallet?.address || !wallet.connected) { showConnectWalletModal({ @@ -148,69 +155,41 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { } setIsUploadLoading(true); setIsProgressBarShown(true); - try { - const localFiles = [ - ...(formValues.files || []), - ...values.images, - ...values.audios, - ...values.videos, - ]; - if (formValues.thumbnailImage) localFiles.push(formValues.thumbnailImage); - if (formValues.coverImage) localFiles.push(formValues.coverImage); - - let pinataJWTKey = undefined; - if (localFiles?.length) { - setStep(feedPostingStep(FeedPostingStepId.GENERATING_KEY)); - - pinataJWTKey = - userIPFSKey || (await generateIpfsKey(selectedNetworkId, userId)); - } - - // Upload files to IPFS - let remoteFiles: RemoteFileData[] = []; - if (pinataJWTKey) { - setStep(feedPostingStep(FeedPostingStepId.UPLOADING_FILES)); - - remoteFiles = await uploadFilesToPinata({ - files: localFiles, - pinataJWTKey, - }); - } - // If the user uploaded files, but they are not pinned to IPFS, it returns files with empty url, so this is an error. - if (formValues.files?.length && !remoteFiles.find((file) => file.url)) { - console.error("upload file err : Fail to pin to IPFS"); + try { + // Upload thumbnail to IPFS + const pinataJWTKey = + userIPFSKey || (await generateIpfsKey(selectedNetworkId, userId)); + if (!pinataJWTKey) { + console.error("upload file err : No Pinata JWT"); setToast({ - mode: "normal", - type: "error", title: "File upload failed", - message: "Fail to pin to IPFS, please try to Publish again", + message: "No Pinata JWT", + type: "error", + mode: "normal", }); setIsUploadLoading(false); return; } + setStep(feedPostingStep(FeedPostingStepId.UPLOADING_FILES)); - let message = values.html; - if (remoteFiles.length) { - localFiles?.map((file, index) => { - // Audio are not in the HTML for now - if (remoteFiles[index]?.fileType !== "audio") { - message = message.replace(file.url, remoteFiles[index].url); - } - }); - } + const remoteThumbnail = formValues.thumbnailImage + ? ( + await uploadFilesToPinata({ + files: [formValues.thumbnailImage], + pinataJWTKey, + }) + )[0] + : undefined; - const metadata = generateArticleMetadata({ + const metadata = generateArticleMarkdownMetadata({ ...formValues, - thumbnailImage: remoteFiles.find( - (remoteFile) => remoteFile.isThumbnailImage, - ), - coverImage: remoteFiles.find((remoteFile) => remoteFile.isCoverImage), - gifs: values.gifs, - files: remoteFiles, - mentions: values.mentions, - hashtags: values.hashtags, - message, + thumbnailImage: remoteThumbnail, + gifs: [], + files: [], + mentions: [], + hashtags: [], + message: formValues.message, location, }); @@ -228,12 +207,6 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { } }; - // Scroll to bottom when the loading bar appears - useEffect(() => { - if (step.id !== "UNDEFINED" && isLoading) - scrollViewRef.current?.scrollToEnd(); - }, [step, isLoading]); - // Reset DAOSelector when the user selects another wallet const [daoSelectorKey, setDaoSelectorKey] = useState(0); useEffect(() => { @@ -241,16 +214,6 @@ export const FeedNewArticleScreen: ScreenFC<"FeedNewArticle"> = () => { setDaoSelectorKey((key) => key + 1); }, [wallet]); - // // OpenGraph URL preview - // useEffect(() => { - // addedUrls.forEach(url => { - // openGraphMutate({ - // url, - // }); - // - // }) - // }, [addedUrls]) - return ( = () => { headerChildren={New Article} onBackPress={navigateBack} footerChildren + noMargin noScroll > = () => { alignSelf: "center", }} > - - - - - - - {freePostCount - ? `You have ${freePostCount} free ${pluralize( - "Article", - freePostCount, - )} left` - : `The cost for this Article is ${prettyPublishingFee}`} - - + - - setValue("thumbnailImage", { - isThumbnailImage: true, - ...files[0], - }) - } - mimeTypes={IMAGE_MIME_TYPES} - /> + + + + + {freePostCount + ? `You have ${freePostCount} free ${pluralize( + "Article", + freePostCount, + )} left` + : `The cost for this Article is ${prettyPublishingFee}`} + + - - setValue("coverImage", { - isCoverImage: true, - ...files[0], - }) - } - mimeTypes={IMAGE_MIME_TYPES} - /> + + - - noBrokenCorners - rules={{ required: true }} - height={48} - label="Title" - placeHolder="Type title here" - name="title" - control={control} - variant="labelOutside" - containerStyle={{ marginVertical: layout.spacing_x3 }} - boxMainContainerStyle={{ - backgroundColor: neutral00, - borderRadius: 12, - }} - /> + + + - - noBrokenCorners - rules={{ required: true }} - multiline - label="Short description" - placeHolder="Type short description here" - name="shortDescription" - control={control} - variant="labelOutside" - containerStyle={{ marginBottom: layout.spacing_x3 }} - boxMainContainerStyle={{ - backgroundColor: neutral00, - borderRadius: 12, - }} - /> + {step.id !== "UNDEFINED" && isProgressBarShown && ( + <> + + + + )} - - - - + noBrokenCorners + rules={{ required: true }} + height={48} + label="Preview title" + placeHolder="Type title here" + name="title" + control={newArticleForm.control} + variant="labelOutside" + containerStyle={{ marginVertical: layout.spacing_x3 }} + boxMainContainerStyle={{ + backgroundColor: neutral00, + borderRadius: 12, }} - render={({ field: { onChange, onBlur } }) => ( - - )} /> + + + noBrokenCorners + rules={{ required: true }} + multiline + label="Preview subtitle" + placeHolder="Type short description here" + name="shortDescription" + control={newArticleForm.control} + variant="labelOutside" + containerStyle={{ marginBottom: layout.spacing_x3 }} + boxMainContainerStyle={{ + backgroundColor: neutral00, + borderRadius: 12, + }} + /> + + + - {step.id !== "UNDEFINED" && isProgressBarShown && ( - <> - + + + newArticleForm.setValue("thumbnailImage", { + isThumbnailImage: true, + ...files[0], + }) + } + mimeTypes={IMAGE_MIME_TYPES} + > + {({ onPress }) => ( + setThumbnailButtonHovered(true)} + onHoverOut={() => setThumbnailButtonHovered(false)} + onPress={onPress} + style={{ + position: "absolute", + right: 8, + top: 8, + zIndex: 1, + backgroundColor: neutral00, + borderColor: isThumbnailButtonHovered + ? neutralFF + : neutral33, + borderWidth: 1, + borderRadius: 999, + height: 36, + width: 36, + justifyContent: "center", + alignItems: "center", + }} + > + + + )} + + + - - - )} + + + + + + + {isMapShown && ( diff --git a/packages/screens/FeedNewArticle/components/ArticleContentEditor/ArticleContentEditor.tsx b/packages/screens/FeedNewArticle/components/ArticleContentEditor/ArticleContentEditor.tsx new file mode 100644 index 0000000000..fcaab32ef4 --- /dev/null +++ b/packages/screens/FeedNewArticle/components/ArticleContentEditor/ArticleContentEditor.tsx @@ -0,0 +1,194 @@ +import { FC, useRef, useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; +import { + ScrollView, + TextInput, + TextStyle, + useWindowDimensions, + View, +} from "react-native"; +import RenderHtml from "react-native-render-html"; + +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { Label } from "@/components/inputs/TextInputCustom"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; +import { Toolbar } from "@/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/Toolbar"; +import { + ContentMode, + articleMd as md, + renderHtmlTagStyles, + renderHtmlDomVisitors, +} from "@/utils/feed/markdown"; +import { ARTICLE_MAX_WIDTH } from "@/utils/social-feed"; +import { + neutral00, + neutral33, + neutralA3, + neutralFF, +} from "@/utils/style/colors"; +import { layout, RESPONSIVE_BREAKPOINT_S } from "@/utils/style/layout"; +import { NewArticleFormValues } from "@/utils/types/feed"; + +interface Props { + width: number; +} + +export const ArticleContentEditor: FC = ({ width }) => { + // ========== UI + const { width: windowWidth } = useWindowDimensions(); + const { height } = useMaxResolution(); + const textInputRef = useRef(null); + const [isTextInputHovered, setTextInputHovered] = useState(false); + const borderWidth = 1; + const textInputContainerPadding = layout.spacing_x2 - borderWidth * 2; + const responsiveTextInputContainerPadding = + windowWidth < RESPONSIVE_BREAKPOINT_S ? 0 : textInputContainerPadding; + const toolbarWrapperHeight = 68; + const labelsWrappersHeight = 32; + const editionAndPreviewHeight = + height - toolbarWrapperHeight - textInputContainerPadding * 2; + const textInputMinHeight = + editionAndPreviewHeight - + labelsWrappersHeight - + responsiveTextInputContainerPadding * 2; + + const [textInputHeight, setTextInputHeight] = useState(textInputMinHeight); + const [mode, setMode] = useState("BOTH"); + const [renderHtmlWidth, setRenderHtmlWidth] = useState(0); + + // ========== Form + const { watch, control } = useFormContext(); + const message = watch("message"); + + // ========== Markdown + const html = md.render(message); + + // ========== JSX + return ( + + {/* ==== Toolbar */} + + + + + {/* ==== Edition and preview */} + + {/* ==== Edition */} + {(mode === "BOTH" || mode === "EDITION") && ( + textInputRef.current?.focus()} + onHoverIn={() => setTextInputHovered(true)} + onHoverOut={() => setTextInputHovered(false)} + > + + + + + + name="message" + control={control} + render={({ field }) => { + const { value, onChange } = field as { + value: string; + onChange: (value: string) => void; + }; + return ( + = RESPONSIVE_BREAKPOINT_S && { + borderWidth, + borderColor: isTextInputHovered ? neutralFF : neutral33, + }, + ]} + > + { + // The input grows depending on the content height + setTextInputHeight(e.nativeEvent.contentSize.height); + }} + ref={textInputRef} + /> + + ); + }} + /> + + )} + + {/* ==== Preview */} + {(mode === "BOTH" || mode === "PREVIEW") && ( + + + + + + setRenderHtmlWidth(e.nativeEvent.layout.width)} + > + + + + )} + + + ); +}; diff --git a/packages/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/ModeButtons.tsx b/packages/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/ModeButtons.tsx new file mode 100644 index 0000000000..fed600ead3 --- /dev/null +++ b/packages/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/ModeButtons.tsx @@ -0,0 +1,80 @@ +import { Dispatch, FC, SetStateAction, useState } from "react"; + +import eyeSVG from "@/assets/icons/eye.svg"; +import penSVG from "@/assets/icons/pen.svg"; +import splittedSquareSVG from "@/assets/icons/splitted-square.svg"; +import { SVG } from "@/components/SVG"; +import { CustomPressable } from "@/components/buttons/CustomPressable"; +import { SpacerRow } from "@/components/spacer"; +import { toolbarBackgroundColor } from "@/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/Toolbar"; +import { ContentMode } from "@/utils/feed/markdown"; +import { neutral33, neutralFF } from "@/utils/style/colors"; +import { layout } from "@/utils/style/layout"; + +interface Props { + setMode: Dispatch>; + mode: ContentMode; +} + +export const ModeButtons: FC = ({ setMode, mode }) => { + const [hoveredButton, setHoveredButton] = useState(null); + + return ( + <> + setMode("EDITION")} + style={{ + backgroundColor: + mode === "EDITION" ? neutral33 : toolbarBackgroundColor, + padding: layout.spacing_x0_5, + borderRadius: 6, + borderWidth: 1, + borderColor: + hoveredButton === "EDITION" ? neutralFF : toolbarBackgroundColor, + }} + onHoverIn={() => setHoveredButton("EDITION")} + onHoverOut={() => setHoveredButton(null)} + > + + + + setMode("BOTH")} + style={{ + backgroundColor: mode === "BOTH" ? neutral33 : toolbarBackgroundColor, + padding: layout.spacing_x0_5, + borderRadius: 6, + borderWidth: 1, + borderColor: + hoveredButton === "BOTH" ? neutralFF : toolbarBackgroundColor, + }} + onHoverIn={() => setHoveredButton("BOTH")} + onHoverOut={() => setHoveredButton(null)} + > + + + + setMode("PREVIEW")} + style={{ + backgroundColor: + mode === "PREVIEW" ? neutral33 : toolbarBackgroundColor, + padding: layout.spacing_x0_5, + borderRadius: 6, + borderWidth: 1, + borderColor: + hoveredButton === "PREVIEW" ? neutralFF : toolbarBackgroundColor, + }} + onHoverIn={() => setHoveredButton("PREVIEW")} + onHoverOut={() => setHoveredButton(null)} + > + + + + ); +}; diff --git a/packages/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/Toolbar.tsx b/packages/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/Toolbar.tsx new file mode 100644 index 0000000000..60fd47933e --- /dev/null +++ b/packages/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/Toolbar.tsx @@ -0,0 +1,31 @@ +import { Dispatch, FC, SetStateAction } from "react"; + +import { TertiaryBox } from "@/components/boxes/TertiaryBox"; +import { ModeButtons } from "@/screens/FeedNewArticle/components/ArticleContentEditor/Toolbar/ModeButtons"; +import { ContentMode } from "@/utils/feed/markdown"; +import { neutral17 } from "@/utils/style/colors"; +import { layout } from "@/utils/style/layout"; + +interface Props { + setMode: Dispatch>; + mode: ContentMode; +} + +export const toolbarBackgroundColor = neutral17; + +export const Toolbar: FC = ({ setMode, mode }) => { + return ( + + + + ); +}; diff --git a/packages/screens/FeedNewArticle/components/NewArticleLocationButton.tsx b/packages/screens/FeedNewArticle/components/NewArticleLocationButton.tsx new file mode 100644 index 0000000000..77bf134f96 --- /dev/null +++ b/packages/screens/FeedNewArticle/components/NewArticleLocationButton.tsx @@ -0,0 +1,33 @@ +import React, { Dispatch, FC, SetStateAction, useState } from "react"; + +import { LocationButton } from "@/components/socialFeed/NewsFeed/LocationButton"; +import { neutral33, neutralFF } from "@/utils/style/colors"; +import { CustomLatLngExpression } from "@/utils/types/feed"; + +export const NewArticleLocationButton: FC<{ + location?: CustomLatLngExpression; + setIsMapShown: Dispatch>; +}> = ({ location, setIsMapShown }) => { + const [isHovered, setHovered] = useState(false); + + return ( + <> + setIsMapShown(true)} + onHoverIn={() => setHovered(true)} + onHoverOut={() => setHovered(false)} + stroke={!location ? neutralFF : undefined} + color={!location ? undefined : neutralFF} + style={{ + height: 48, + width: 48, + borderWidth: 1, + borderColor: isHovered ? neutralFF : neutral33, + borderRadius: 6, + alignItems: "center", + justifyContent: "center", + }} + /> + + ); +}; diff --git a/packages/screens/FeedPostView/FeedPostView.tsx b/packages/screens/FeedPostView/FeedPostView.tsx index b4006a2ade..7e55d95fc8 100644 --- a/packages/screens/FeedPostView/FeedPostView.tsx +++ b/packages/screens/FeedPostView/FeedPostView.tsx @@ -10,6 +10,7 @@ import { ScreenContainer } from "@/components/ScreenContainer"; import { ScreenTitle } from "@/components/ScreenContainer/ScreenTitle"; import { usePost } from "@/hooks/feed/usePost"; import { parseNetworkObjectId } from "@/networks"; +import { FeedPostArticleMarkdownView } from "@/screens/FeedPostView/components/FeedPostArticleMarkdownView"; import { convertLegacyPostId } from "@/utils/feed/queries"; import { ScreenFC, useAppNavigation } from "@/utils/navigation"; import { primaryColor } from "@/utils/style/colors"; @@ -76,6 +77,14 @@ export const FeedPostView: ScreenFC<"FeedPostView"> = ({ if (post.category === PostCategory.Video) { return ; + } else if (post.category === PostCategory.ArticleMarkdown) { + return ( + + ); } else if (post.category === PostCategory.Article) { return ( Promise; + isLoadingPost?: boolean; +}> = ({ post, refetchPost, isLoadingPost }) => { + const navigation = useAppNavigation(); + const { width: windowWidth } = useWindowDimensions(); + const { width } = useMaxResolution(); + const isMobile = useIsMobile(); + const [parentOffsetValue, setParentOffsetValue] = useState(0); + + const authorId = post?.authorId; + const authorNSInfo = useNSUserInfo(authorId); + const [, authorAddress] = parseUserId(post?.authorId); + const username = authorNSInfo?.metadata?.tokenId || authorAddress; + + const [localPost, setLocalPost] = useState(post); + const feedInputRef = useRef(null); + const [replyTo, setReplyTo] = useState(); + const aref = useAnimatedRef(); + const [flatListContentOffsetY, setFlatListContentOffsetY] = useState(0); + const [articleOffsetY, setArticleOffsetY] = useState(0); + const [articleWidth, setArticleWidth] = useState(0); + const [renderHtmlWidth, setRenderHtmlWidth] = useState(0); + const isGoingUp = useSharedValue(false); + const isLoadingSharedValue = useSharedValue(true); + const [isCreateModalVisible, setCreateModalVisible] = useState(false); + const { + data: comments, + refetch: refetchComments, + hasNextPage, + fetchNextPage, + isLoading: isLoadingComments, + } = useFetchComments({ + parentId: post.id, + totalCount: post.subPostLength, + enabled: true, + }); + const isNextPageAvailable = useSharedValue(hasNextPage); + + const articleMetadata = zodTryParseJSON( + ZodSocialFeedArticleMarkdownMetadata, + post.metadata, + ); + const message = articleMetadata?.message; + const html = message ? md.render(message) : null; + const title = articleMetadata?.title; + const location = articleMetadata?.location; + + const headerLabel = useMemo(() => { + const authorDisplayName = + authorNSInfo?.metadata?.tokenId || + tinyAddress(authorAddress) || + DEFAULT_USERNAME; + return `Article by ${authorDisplayName}`; + }, [authorNSInfo?.metadata?.tokenId, authorAddress]); + + const onPressReply: OnPressReplyType = (data) => { + feedInputRef.current?.resetForm(); + setReplyTo(data); + feedInputRef.current?.setValue(`@${username} `); + feedInputRef.current?.focusInput(); + }; + + const handleSubmitInProgress = () => { + if (replyTo?.parentId && replyTo.yOffsetValue) + aref.current?.scrollTo(replyTo.yOffsetValue); + else aref.current?.scrollTo(0); + }; + + const scrollHandler = useAnimatedScrollHandler( + { + onScroll: (event) => { + let offsetPadding = 40; + offsetPadding += event.layoutMeasurement.height; + if ( + event.contentOffset.y >= event.contentSize.height - offsetPadding && + isNextPageAvailable.value + ) { + fetchNextPage(); + } + + if (flatListContentOffsetY > event.contentOffset.y) { + isGoingUp.value = true; + } else if (flatListContentOffsetY < event.contentOffset.y) { + isGoingUp.value = false; + } + setFlatListContentOffsetY(event.contentOffset.y); + }, + }, + [post.id], + ); + + useEffect(() => { + isLoadingSharedValue.value = isLoadingPost || isLoadingComments; + }, [isLoadingPost, isLoadingComments, isLoadingSharedValue]); + + useEffect(() => { + if (post.category === PostCategory.Video) + navigation.replace("FeedPostView", { + id: post.id, + }); + }, [post.category, post.id, navigation]); + + useEffect(() => { + // HECK: updated state was not showing up in scrollhander + isNextPageAvailable.value = hasNextPage; + }, [hasNextPage, isNextPageAvailable]); + + if (!articleMetadata || !html) return null; + return ( + {headerLabel}} + onBackPress={() => + post?.parentPostIdentifier + ? navigation.navigate("FeedPostView", { + id: post.id, + }) + : navigation.canGoBack() + ? navigation.goBack() + : navigation.navigate("Feed") + } + footerChildren + noScroll + > + + {/* ScreenContainer has noScroll, so we need to add MobileTitle here */} + {isMobile && } + + { + setArticleOffsetY(height); + setArticleWidth(width); + }} + style={{ + width: "100%", + maxWidth: ARTICLE_MAX_WIDTH + contentPaddingHorizontal * 2, + borderBottomWidth: 1, + borderBottomColor: neutral33, + borderRadius: + windowWidth < RESPONSIVE_BREAKPOINT_S + ? 0 + : SOCIAl_CARD_BORDER_RADIUS, + paddingHorizontal: contentPaddingHorizontal, + paddingBottom: layout.spacing_x2, + }} + > + + {/*========== Article title, author info */} + {!!title && {title}} + + + + + + {/*========== Article content */} + + + setRenderHtmlWidth(e.nativeEvent.layout.width) + } + > + + + + + + {/*========== Actions */} + onPressReply({ username })} + refetchFeed={refetchPost} + setPost={setLocalPost} + /> + + + + {/*========== Refresh button no mobile */} + {!isMobile && ( + + { + refetchComments(); + }} + /> + + )} + + setParentOffsetValue(e.nativeEvent.layout.y)} + style={{ width: "100%" }} + > + + + + + {/*========== Comment input */} + {!isMobile && ( + <> + + { + setReplyTo(undefined); + refetchComments(); + }} + /> + + )} + + + {/*========== Refresh button mobile */} + {flatListContentOffsetY >= articleOffsetY + 66 && !isMobile && ( + + + + )} + + {/*========== Refresh button and Comment button mobile */} + {isMobile && ( + <> + + + setCreateModalVisible(true)} + /> + + { + refetchComments(); + }} + /> + + + + )} + + setCreateModalVisible(false)} + onSubmitSuccess={() => { + setReplyTo(undefined); + refetchComments(); + }} + replyTo={replyTo} + parentId={post.localIdentifier} + /> + + ); +}; + +const contentContainerCStyle: ViewStyle = { + alignItems: "center", + alignSelf: "center", +}; +const floatingActionsCStyle: ViewStyle = { + position: "absolute", + justifyContent: "center", + alignItems: "center", + right: 68, + bottom: 230, +}; diff --git a/packages/screens/Mini/Feed/ArticlesFeedScreen.tsx b/packages/screens/Mini/Feed/ArticlesFeedScreen.tsx index 7212af2b5c..e6798128c2 100644 --- a/packages/screens/Mini/Feed/ArticlesFeedScreen.tsx +++ b/packages/screens/Mini/Feed/ArticlesFeedScreen.tsx @@ -14,7 +14,7 @@ export const ArticlesFeedScreen = () => { const req: Partial = { filter: { networkId: selectedNetworkId, - categories: [PostCategory.Article], + categories: [PostCategory.Article, PostCategory.ArticleMarkdown], user: "", mentions: [], hashtags: [], diff --git a/packages/utils/feed/map.ts b/packages/utils/feed/map.ts index 5e2b9e7d4f..4808f1e29c 100644 --- a/packages/utils/feed/map.ts +++ b/packages/utils/feed/map.ts @@ -187,6 +187,7 @@ export const getMapPostIconSVG = ( case PostCategory.VideoNote: return videoPostSvg; case PostCategory.Article: + case PostCategory.ArticleMarkdown: return articlePostSvg; case PostCategory.Normal: return normalPostSvg; @@ -206,6 +207,7 @@ export const getMapPostIconSVGString = (postCategory: PostCategory) => { case PostCategory.VideoNote: return videoPostSvgString; case PostCategory.Article: + case PostCategory.ArticleMarkdown: return articlePostSvgString; case PostCategory.Normal: return normalPostSvgString; @@ -225,6 +227,7 @@ export const getMapPostIconColorRgba = (postCategory: PostCategory) => { case PostCategory.VideoNote: return "198,171,255,.40"; case PostCategory.Article: + case PostCategory.ArticleMarkdown: return "255,252,207,.40"; case PostCategory.Normal: return "255,178,107,.40"; @@ -244,6 +247,7 @@ export const getMapPostTextGradientType = (postCategory: PostCategory) => { case PostCategory.VideoNote: return "feed-map-video-post"; case PostCategory.Article: + case PostCategory.ArticleMarkdown: return "feed-map-article-post"; case PostCategory.Normal: return "feed-map-normal-post"; @@ -271,6 +275,7 @@ export const getMapPostTextGradient = (postCategory: PostCategory) => { gradientProps.colors = ["#C6ABFF", "#A57AFF"]; break; case PostCategory.Article: + case PostCategory.ArticleMarkdown: gradientProps.colors = ["#FFFC6B", "#E5E13B"]; break; case PostCategory.Normal: @@ -294,6 +299,7 @@ export const getMapPostTextGradientString = (postCategory: PostCategory) => { case PostCategory.VideoNote: return `180deg, #C6ABFF 100%, #A57AFF 100%`; case PostCategory.Article: + case PostCategory.ArticleMarkdown: return `180deg, #FFFC6B 100%, #E5E13B 100%`; case PostCategory.Normal: return `180deg, #FFB26B 100%, #E58C3B 100%`; diff --git a/packages/utils/feed/markdown.ts b/packages/utils/feed/markdown.ts new file mode 100644 index 0000000000..b8943a1968 --- /dev/null +++ b/packages/utils/feed/markdown.ts @@ -0,0 +1,187 @@ +import markdownit from "markdown-it"; +import { full as emoji } from "markdown-it-emoji/dist/index.cjs"; +import footnote_plugin from "markdown-it-footnote"; +import { MixedStyleRecord, Element } from "react-native-render-html"; + +import { + neutral17, + neutral33, + neutral67, + neutralA3, + neutralFF, + primaryColor, +} from "@/utils/style/colors"; + +export type ContentMode = "EDITION" | "BOTH" | "PREVIEW"; + +// The markdownit instance. Used to get the same parameters at Article creation and consultation. +export const articleMd = markdownit({ + linkify: true, + breaks: true, +}) + .use(emoji) + .use(footnote_plugin); + +// DOM modifications on document, texts, or elements from react-native-render-html. +// Because react-native-render-html doesn't allow common CSS selectors, we need to style tags using domVisitors callbacks +export const renderHtmlDomVisitors = { + onElement: (element: Element) => { + // Removing marginBottom from the child p of blockquote + if ( + element.name === "blockquote" && + element.children && + element.children.length > 0 + ) { + const tagChild = element.children.find((child) => child.type === "tag"); + // tagChild is a react-native-render-html Node. It doesn't have attribs, but it has attribs in fact (wtf ?) + if (tagChild && "attribs" in tagChild) { + tagChild.attribs = { + style: "margin-bottom: 0", + }; + } + } + }, +}; + +// HTML tags styles used by RenderHtml from react-native-render-html +type HtmlTagStyle = Record; +const baseTextStyle: HtmlTagStyle = { + color: neutralA3, + fontSize: 14, + letterSpacing: -(14 * 0.04), + lineHeight: 22, + fontFamily: "Exo_500Medium", + fontWeight: "500", +}; +const baseBlockStyle: HtmlTagStyle = { + marginTop: 0, + marginBottom: 16, +}; +const baseCodeStyle: HtmlTagStyle = { + ...baseTextStyle, + fontSize: 13, + letterSpacing: -(13 * 0.04), + backgroundColor: neutral17, + borderRadius: 4, +}; +const baseTableChildrenStyle: HtmlTagStyle = { + borderColor: neutral33, +}; +export const renderHtmlTagStyles: MixedStyleRecord = { + body: { + ...baseTextStyle, + }, + p: { + ...baseBlockStyle, + ...baseTextStyle, + }, + strong: { fontWeight: "700" }, + a: { + color: primaryColor, + textDecorationLine: "none", + }, + hr: { backgroundColor: neutralA3 }, + h1: { + ...baseTextStyle, + color: neutralFF, + fontSize: 28, + letterSpacing: -(28 * 0.02), + lineHeight: 37, + }, + h2: { + ...baseTextStyle, + color: neutralFF, + fontSize: 21, + letterSpacing: -(21 * 0.02), + lineHeight: 28, + }, + h3: { + ...baseTextStyle, + color: neutralFF, + fontSize: 16, + letterSpacing: -(16 * 0.02), + lineHeight: 23, + }, + h4: { + ...baseTextStyle, + color: neutralFF, + lineHeight: 20, + }, + h5: { + ...baseTextStyle, + lineHeight: 20, + }, + h6: { + ...baseTextStyle, + fontSize: 12, + letterSpacing: -(12 * 0.04), + lineHeight: 16, + }, + ul: { + ...baseBlockStyle, + ...baseTextStyle, + lineHeight: 20, + }, + ol: { + ...baseBlockStyle, + ...baseTextStyle, + lineHeight: 20, + }, + + blockquote: { + ...baseBlockStyle, + ...baseTextStyle, + color: neutral67, + lineHeight: 20, + marginLeft: 0, + paddingLeft: 14, + borderLeftWidth: 3, + borderLeftColor: neutral67, + }, + + code: { + ...baseCodeStyle, + marginVertical: 4, + paddingHorizontal: 4, + paddingVertical: 2, + alignSelf: "flex-start", + }, + pre: { + ...baseBlockStyle, + ...baseCodeStyle, + paddingHorizontal: 8, + }, + + table: { + marginBottom: 16, + }, + thead: { + ...baseTableChildrenStyle, + borderTopLeftRadius: 4, + borderTopRightRadius: 4, + borderLeftWidth: 1, + borderTopWidth: 1, + borderRightWidth: 1, + backgroundColor: neutral17, + }, + th: { + ...baseTableChildrenStyle, + padding: 8, + }, + tbody: { + ...baseTableChildrenStyle, + borderBottomLeftRadius: 4, + borderBottomRightRadius: 4, + borderLeftWidth: 1, + borderBottomWidth: 1, + borderRightWidth: 1, + }, + tr: { + ...baseTableChildrenStyle, + }, + td: { + ...baseTableChildrenStyle, + borderTopWidth: 0.5, + padding: 8, + }, +}; diff --git a/packages/utils/feed/queries.ts b/packages/utils/feed/queries.ts index b9f6897f61..feb41f91de 100644 --- a/packages/utils/feed/queries.ts +++ b/packages/utils/feed/queries.ts @@ -14,9 +14,9 @@ import { NewArticleFormValues, NewPostFormValues, PostCategory, - SocialFeedArticleMetadata, + SocialFeedArticleMarkdownMetadata, SocialFeedPostMetadata, - ZodSocialFeedArticleMetadata, + ZodSocialFeedArticleMarkdownMetadata, ZodSocialFeedPostMetadata, } from "../types/feed"; import { RemoteFileData } from "../types/files"; @@ -112,7 +112,7 @@ interface GeneratePostMetadataParams extends Omit { location?: CustomLatLngExpression; } -interface GenerateArticleMetadataParams +interface GenerateArticleMarkdownMetadataParams extends Omit< NewArticleFormValues, "files" | "thumbnailImage" | "coverImage" @@ -147,7 +147,7 @@ export const generatePostMetadata = ({ return m; }; -export const generateArticleMetadata = ({ +export const generateArticleMarkdownMetadata = ({ title, message, files, @@ -159,8 +159,8 @@ export const generateArticleMetadata = ({ coverImage, shortDescription, location, -}: GenerateArticleMetadataParams): SocialFeedArticleMetadata => { - const m = ZodSocialFeedArticleMetadata.parse({ +}: GenerateArticleMarkdownMetadataParams): SocialFeedArticleMarkdownMetadata => { + const m = ZodSocialFeedArticleMarkdownMetadata.parse({ title, message, files, diff --git a/packages/utils/types/feed.ts b/packages/utils/types/feed.ts index b80f2d679b..68c4a026d7 100644 --- a/packages/utils/types/feed.ts +++ b/packages/utils/types/feed.ts @@ -19,6 +19,7 @@ export enum PostCategory { Flagged, MusicAudio, Video, + ArticleMarkdown, } export interface NewArticleFormValues { @@ -106,8 +107,20 @@ export const ZodSocialFeedArticleMetadata = z.object({ mentions: z.array(z.string()), ...zodSocialFeedCommonMetadata.shape, }); -export type SocialFeedArticleMetadata = z.infer< - typeof ZodSocialFeedArticleMetadata + +export const ZodSocialFeedArticleMarkdownMetadata = z.object({ + shortDescription: z.string(), + thumbnailImage: ZodRemoteFileData.optional(), + coverImage: ZodRemoteFileData.optional(), + message: z.string(), + files: MaybeFiles.optional(), + gifs: z.array(z.string()).optional(), + hashtags: z.array(z.string()), + mentions: z.array(z.string()), + ...zodSocialFeedCommonMetadata.shape, +}); +export type SocialFeedArticleMarkdownMetadata = z.infer< + typeof ZodSocialFeedArticleMarkdownMetadata >; export const ZodSocialFeedTrackMetadata = z.object({ diff --git a/yarn.lock b/yarn.lock index 240fc5ed44..2f0530e1f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4832,6 +4832,24 @@ __metadata: languageName: node linkType: hard +"@jsamr/counter-style@npm:^2.0.1": + version: 2.0.2 + resolution: "@jsamr/counter-style@npm:2.0.2" + checksum: 9434d6e52dcbf6a3422137e3397d801aa3b4f3fd780fc5a12c47db171502f281eaa8ae69b953a1d1bdaf4effeac7c674e7dbdd8341157a6f21a087ccb7af5bfe + languageName: node + linkType: hard + +"@jsamr/react-native-li@npm:^2.3.0": + version: 2.3.1 + resolution: "@jsamr/react-native-li@npm:2.3.1" + peerDependencies: + "@jsamr/counter-style": ^1.0.0 || ^2.0.0 + react: "*" + react-native: "*" + checksum: 3465ac894d125261660cc5d779c226560578927354c8c661be9bcdc46438121cd5561079dd76ad82bb9970c0adf753e62726d6d8849b1b66484aa8090701916b + languageName: node + linkType: hard + "@jsdevtools/ono@npm:^7.1.3": version: 7.1.3 resolution: "@jsdevtools/ono@npm:7.1.3" @@ -5049,6 +5067,38 @@ __metadata: languageName: node linkType: hard +"@native-html/css-processor@npm:1.11.0": + version: 1.11.0 + resolution: "@native-html/css-processor@npm:1.11.0" + dependencies: + css-to-react-native: ^3.0.0 + csstype: ^3.0.8 + peerDependencies: + "@types/react": "*" + "@types/react-native": "*" + checksum: 741ff04c6bfb7f004670ed03c230f417266002c59bd0314e066df28044f5d6ce76ff62db85ff801b9e14dee5a048a87b77d2213bc6f869de31f4d93802c54fd0 + languageName: node + linkType: hard + +"@native-html/transient-render-engine@npm:11.2.3": + version: 11.2.3 + resolution: "@native-html/transient-render-engine@npm:11.2.3" + dependencies: + "@native-html/css-processor": 1.11.0 + "@types/ramda": ^0.27.44 + csstype: ^3.0.9 + domelementtype: ^2.2.0 + domhandler: ^4.2.2 + domutils: ^2.8.0 + htmlparser2: ^7.1.2 + ramda: ^0.27.2 + peerDependencies: + "@types/react-native": "*" + react-native: ^* + checksum: 13248216b19c07703fa5ff9942889ea7dc669d6fd9c944d3d5cf2757088c3e66a5b760f194ac0193ddbbb3f4556655fe10c6e4e5a5efd030da8ec1360b08a605 + languageName: node + linkType: hard + "@noble/curves@npm:1.4.2, @noble/curves@npm:~1.4.0": version: 1.4.2 resolution: "@noble/curves@npm:1.4.2" @@ -6785,10 +6835,10 @@ __metadata: languageName: node linkType: hard -"@types/linkify-it@npm:*": - version: 3.0.5 - resolution: "@types/linkify-it@npm:3.0.5" - checksum: fac28f41a6e576282300a459d70ea0d33aab70dbb77c3d09582bb0335bb00d862b6de69585792a4d590aae4173fbab0bf28861e2d90ca7b2b1439b52688e9ff6 +"@types/linkify-it@npm:^5": + version: 5.0.0 + resolution: "@types/linkify-it@npm:5.0.0" + checksum: ec98e03aa883f70153a17a1e6ed9e28b39a604049b485daeddae3a1482ec65cac0817520be6e301d99fd1a934b3950cf0f855655aae6ec27da2bb676ba4a148e languageName: node linkType: hard @@ -6806,20 +6856,38 @@ __metadata: languageName: node linkType: hard -"@types/markdown-it@npm:^13.0.7": - version: 13.0.7 - resolution: "@types/markdown-it@npm:13.0.7" +"@types/markdown-it-emoji@npm:^3.0.1": + version: 3.0.1 + resolution: "@types/markdown-it-emoji@npm:3.0.1" dependencies: - "@types/linkify-it": "*" - "@types/mdurl": "*" - checksum: c9e9af441340eb870a7b90b298f6197aa80b55bee28f179a4f85052333f0cb3d3f2763981359d58cf09024961f013999c1c743c1e52a185ca36576d4403f7eb9 + "@types/markdown-it": ^14 + checksum: cf11b177dca826d7617bc89b8d1ee2a5203bd1a370a62a699e3c6eb0299e7c10c71694d796dedfc05f888834c00e662274c0b2b71c4e73927ac57d189fc6f99c languageName: node linkType: hard -"@types/mdurl@npm:*": - version: 1.0.5 - resolution: "@types/mdurl@npm:1.0.5" - checksum: e8e872e8da8f517a9c748b06cec61c947cb73fd3069e8aeb0926670ec5dfac5d30549b3d0f1634950401633e812f9b7263f2d5dbe7e98fce12bcb2c659aa4b21 +"@types/markdown-it-footnote@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/markdown-it-footnote@npm:3.0.4" + dependencies: + "@types/markdown-it": "*" + checksum: 84d38790e1911eaf94bd3418a8782de1dd2543963f282849fe3fde7089f3ed6c3f5d07defd2ba51ad8d1cf3b32eeddfb21262e230bf8baddce5154f6735ed9d6 + languageName: node + linkType: hard + +"@types/markdown-it@npm:*, @types/markdown-it@npm:^14, @types/markdown-it@npm:^14.1.2": + version: 14.1.2 + resolution: "@types/markdown-it@npm:14.1.2" + dependencies: + "@types/linkify-it": ^5 + "@types/mdurl": ^2 + checksum: ad66e0b377d6af09a155bb65f675d1e2cb27d20a3d407377fe4508eb29cde1e765430b99d5129f89012e2524abb5525d629f7057a59ff9fd0967e1ff645b9ec6 + languageName: node + linkType: hard + +"@types/mdurl@npm:^2": + version: 2.0.0 + resolution: "@types/mdurl@npm:2.0.0" + checksum: 78746e96c655ceed63db06382da466fd52c7e9dc54d60b12973dfdd110cae06b9439c4b90e17bb8d4461109184b3ea9f3e9f96b3e4bf4aa9fe18b6ac35f283c8 languageName: node linkType: hard @@ -6899,6 +6967,15 @@ __metadata: languageName: node linkType: hard +"@types/ramda@npm:^0.27.40, @types/ramda@npm:^0.27.44": + version: 0.27.66 + resolution: "@types/ramda@npm:0.27.66" + dependencies: + ts-toolbelt: ^6.15.1 + checksum: eea577e4a0934849b4103c1452a7c8ddbc9bbf0e2aafb908467212654555145f846a16fe737563b582e8fb5bd6698481ebec1237537e5e662587c47f626e4c92 + languageName: node + linkType: hard + "@types/react-native-countdown-component@npm:^2.7.0": version: 2.7.4 resolution: "@types/react-native-countdown-component@npm:2.7.4" @@ -6982,6 +7059,13 @@ __metadata: languageName: node linkType: hard +"@types/urijs@npm:^1.19.15": + version: 1.19.25 + resolution: "@types/urijs@npm:1.19.25" + checksum: cce3fd2845d5e143f4130134a5f6ff7e02b4dfc05f4d13c7b28a404fd9420bb8a6483a572c0662693bb18c5b3d8f814270aa75f3fd539f32fae22d005e755b5d + languageName: node + linkType: hard + "@types/use-sync-external-store@npm:^0.0.3": version: 0.0.3 resolution: "@types/use-sync-external-store@npm:0.0.3" @@ -8793,6 +8877,13 @@ __metadata: languageName: node linkType: hard +"camelize@npm:^1.0.0": + version: 1.0.1 + resolution: "camelize@npm:1.0.1" + checksum: 91d8611d09af725e422a23993890d22b2b72b4cabf7239651856950c76b4bf53fe0d0da7c5e4db05180e898e4e647220e78c9fbc976113bd96d603d1fcbfcb99 + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001565": version: 1.0.30001579 resolution: "caniuse-lite@npm:1.0.30001579" @@ -8875,6 +8966,20 @@ __metadata: languageName: node linkType: hard +"character-entities-html4@npm:^1.0.0": + version: 1.1.4 + resolution: "character-entities-html4@npm:1.1.4" + checksum: 22536aba07a378a2326420423ceadd65c0121032c527f80e84dfc648381992ed5aa666d7c2b267cd269864b3682d5b0315fc2f03a9e7c017d1a96d24ec292d5f + languageName: node + linkType: hard + +"character-entities-legacy@npm:^1.0.0": + version: 1.1.4 + resolution: "character-entities-legacy@npm:1.1.4" + checksum: fe03a82c154414da3a0c8ab3188e4237ec68006cbcd681cf23c7cfb9502a0e76cd30ab69a2e50857ca10d984d57de3b307680fff5328ccd427f400e559c3a811 + languageName: node + linkType: hard + "chardet@npm:^0.4.0": version: 0.4.2 resolution: "chardet@npm:0.4.2" @@ -9668,6 +9773,13 @@ __metadata: languageName: node linkType: hard +"css-color-keywords@npm:^1.0.0": + version: 1.0.0 + resolution: "css-color-keywords@npm:1.0.0" + checksum: 8f125e3ad477bd03c77b533044bd9e8a6f7c0da52d49bbc0bbe38327b3829d6ba04d368ca49dd9ff3b667d2fc8f1698d891c198bbf8feade1a5501bf5a296408 + languageName: node + linkType: hard + "css-in-js-utils@npm:^3.1.0": version: 3.1.0 resolution: "css-in-js-utils@npm:3.1.0" @@ -9690,6 +9802,17 @@ __metadata: languageName: node linkType: hard +"css-to-react-native@npm:^3.0.0": + version: 3.2.0 + resolution: "css-to-react-native@npm:3.2.0" + dependencies: + camelize: ^1.0.0 + css-color-keywords: ^1.0.0 + postcss-value-parser: ^4.0.2 + checksum: 263be65e805aef02c3f20c064665c998a8c35293e1505dbe6e3054fb186b01a9897ac6cf121f9840e5a9dfe3fb3994f6fcd0af84a865f1df78ba5bf89e77adce + languageName: node + linkType: hard + "css-tree@npm:^1.1.3": version: 1.1.3 resolution: "css-tree@npm:1.1.3" @@ -9736,7 +9859,7 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^3.0.2": +"csstype@npm:^3.0.2, csstype@npm:^3.0.8, csstype@npm:^3.0.9": version: 3.1.3 resolution: "csstype@npm:3.1.3" checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 @@ -10329,6 +10452,17 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:^1.0.1": + version: 1.4.1 + resolution: "dom-serializer@npm:1.4.1" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^4.2.0 + entities: ^2.0.0 + checksum: fbb0b01f87a8a2d18e6e5a388ad0f7ec4a5c05c06d219377da1abc7bb0f674d804f4a8a94e3f71ff15f6cb7dcfc75704a54b261db672b9b3ab03da6b758b0b22 + languageName: node + linkType: hard + "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -10340,13 +10474,22 @@ __metadata: languageName: node linkType: hard -"domelementtype@npm:^2.3.0": +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 languageName: node linkType: hard +"domhandler@npm:^4.2.0, domhandler@npm:^4.2.2": + version: 4.3.1 + resolution: "domhandler@npm:4.3.1" + dependencies: + domelementtype: ^2.2.0 + checksum: 4c665ceed016e1911bf7d1dadc09dc888090b64dee7851cccd2fcf5442747ec39c647bb1cb8c8919f8bbdd0f0c625a6bafeeed4b2d656bbecdbae893f43ffaaa + languageName: node + linkType: hard + "domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": version: 5.0.3 resolution: "domhandler@npm:5.0.3" @@ -10356,6 +10499,17 @@ __metadata: languageName: node linkType: hard +"domutils@npm:^2.8.0": + version: 2.8.0 + resolution: "domutils@npm:2.8.0" + dependencies: + dom-serializer: ^1.0.1 + domelementtype: ^2.2.0 + domhandler: ^4.2.0 + checksum: abf7434315283e9aadc2a24bac0e00eab07ae4313b40cc239f89d84d7315ebdfd2fb1b5bf750a96bc1b4403d7237c7b2ebf60459be394d625ead4ca89b934391 + languageName: node + linkType: hard + "domutils@npm:^3.0.1": version: 3.1.0 resolution: "domutils@npm:3.1.0" @@ -10611,6 +10765,20 @@ __metadata: languageName: node linkType: hard +"entities@npm:^2.0.0": + version: 2.2.0 + resolution: "entities@npm:2.2.0" + checksum: 19010dacaf0912c895ea262b4f6128574f9ccf8d4b3b65c7e8334ad0079b3706376360e28d8843ff50a78aabcb8f08f0a32dbfacdc77e47ed77ca08b713669b3 + languageName: node + linkType: hard + +"entities@npm:^3.0.1": + version: 3.0.1 + resolution: "entities@npm:3.0.1" + checksum: aaf7f12033f0939be91f5161593f853f2da55866db55ccbf72f45430b8977e2b79dbd58c53d0fdd2d00bd7d313b75b0968d09f038df88e308aa97e39f9456572 + languageName: node + linkType: hard + "entities@npm:^4.2.0, entities@npm:^4.4.0, entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -13047,6 +13215,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^7.1.2": + version: 7.2.0 + resolution: "htmlparser2@npm:7.2.0" + dependencies: + domelementtype: ^2.0.1 + domhandler: ^4.2.2 + domutils: ^2.8.0 + entities: ^3.0.1 + checksum: 96563d9965729cfcb3f5f19c26d013c6831b4cb38d79d8c185e9cd669ea6a9ffe8fb9ccc74d29a068c9078aa0e2767053ed6b19aa32723c41550340d0094bea0 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -15176,6 +15356,20 @@ __metadata: languageName: node linkType: hard +"markdown-it-emoji@npm:^3.0.0": + version: 3.0.0 + resolution: "markdown-it-emoji@npm:3.0.0" + checksum: 421290e310285b9ef979e409ea056623489541013ee7307956a3450a06e0de034c585e217ed43a7bf9a6f16102542cb75799b975a861ba01a2db2b7105e16871 + languageName: node + linkType: hard + +"markdown-it-footnote@npm:^4.0.0": + version: 4.0.0 + resolution: "markdown-it-footnote@npm:4.0.0" + checksum: 75543f8c81d7ba9620f5b2bc3fcbb5130ad7b4e3afbec19da3bdf3417dfb885582c66ccb0b39e3847bdf2876a110378095260477084fe0e7c29a887b4404401e + languageName: node + linkType: hard + "markdown-it@npm:^14.1.0": version: 14.1.0 resolution: "markdown-it@npm:14.1.0" @@ -17028,7 +17222,7 @@ __metadata: languageName: node linkType: hard -"postcss-value-parser@npm:^4.2.0": +"postcss-value-parser@npm:^4.0.2, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f @@ -17232,7 +17426,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.5.6, prop-types@npm:^15.5.8, prop-types@npm:^15.7.2, prop-types@npm:^15.8.0, prop-types@npm:^15.8.1": +"prop-types@npm:^15.5.6, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.7.2, prop-types@npm:^15.8.0, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -17465,6 +17659,13 @@ __metadata: languageName: node linkType: hard +"ramda@npm:^0.27.2": + version: 0.27.2 + resolution: "ramda@npm:0.27.2" + checksum: 28d6735dd1eea1a796c56cf6111f3673c6105bbd736e521cdd7826c46a18eeff337c2dba4668f6eed990d539b9961fd6db19aa46ccc1530ba67a396c0a9f580d + languageName: node + linkType: hard + "randexp@npm:0.4.6": version: 0.4.6 resolution: "randexp@npm:0.4.6" @@ -17877,6 +18078,26 @@ __metadata: languageName: node linkType: hard +"react-native-render-html@npm:^6.3.4": + version: 6.3.4 + resolution: "react-native-render-html@npm:6.3.4" + dependencies: + "@jsamr/counter-style": ^2.0.1 + "@jsamr/react-native-li": ^2.3.0 + "@native-html/transient-render-engine": 11.2.3 + "@types/ramda": ^0.27.40 + "@types/urijs": ^1.19.15 + prop-types: ^15.5.7 + ramda: ^0.27.2 + stringify-entities: ^3.1.0 + urijs: ^1.19.6 + peerDependencies: + react: "*" + react-native: "*" + checksum: 9fd0c915664d4d25d23f48b4b33101385f2e497c643664c09b457eb091f90cd1d60f9c2c4bfad1a55403c8037d52de5dcbdebe0b1ebc9e4883d8a3099a23633b + languageName: node + linkType: hard + "react-native-safe-area-context@npm:4.8.2": version: 4.8.2 resolution: "react-native-safe-area-context@npm:4.8.2" @@ -19789,6 +20010,17 @@ __metadata: languageName: node linkType: hard +"stringify-entities@npm:^3.1.0": + version: 3.1.0 + resolution: "stringify-entities@npm:3.1.0" + dependencies: + character-entities-html4: ^1.0.0 + character-entities-legacy: ^1.0.0 + xtend: ^4.0.0 + checksum: 5b6212e2985101ddb8197d999a6c01abb610f2ba6efd6f8f7d7ec763b61cb08b55735b03febdf501c2091f484df16bc82412419ef35ee21135548f6a15881044 + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -20178,7 +20410,9 @@ __metadata: "@types/html-to-draftjs": ^1.4.0 "@types/leaflet": ^1.9.12 "@types/leaflet.markercluster": ^1.5.4 - "@types/markdown-it": ^13.0.7 + "@types/markdown-it": ^14.1.2 + "@types/markdown-it-emoji": ^3.0.1 + "@types/markdown-it-footnote": ^3.0.4 "@types/node": ^20.9.1 "@types/papaparse": ^5.3.14 "@types/pluralize": ^0.0.33 @@ -20240,6 +20474,8 @@ __metadata: long: ^5.2.1 lottie-react-native: 6.5.1 markdown-it: ^14.1.0 + markdown-it-emoji: ^3.0.0 + markdown-it-footnote: ^4.0.0 merkletreejs: ^0.4.0 metamask-react: ^2.4.1 moment: ^2.29.4 @@ -20279,6 +20515,7 @@ __metadata: react-native-reanimated: ^3.6.2 react-native-reanimated-carousel: 4.0.0-alpha.9 react-native-reanimated-table: ^0.0.2 + react-native-render-html: ^6.3.4 react-native-safe-area-context: 4.8.2 react-native-screens: ~3.29.0 react-native-smooth-slider: ^1.3.6 @@ -20570,6 +20807,13 @@ __metadata: languageName: node linkType: hard +"ts-toolbelt@npm:^6.15.1": + version: 6.15.5 + resolution: "ts-toolbelt@npm:6.15.5" + checksum: 24ad00cfd9ce735c76c873a9b1347eac475b94e39ebbdf100c9019dce88dd5f4babed52884cf82bb456a38c28edd0099ab6f704b84b2e5e034852b618472c1f3 + languageName: node + linkType: hard + "ts-unused-exports@npm:^10.0.1": version: 10.0.1 resolution: "ts-unused-exports@npm:10.0.1" @@ -21108,6 +21352,13 @@ __metadata: languageName: node linkType: hard +"urijs@npm:^1.19.6": + version: 1.19.11 + resolution: "urijs@npm:1.19.11" + checksum: f9b95004560754d30fd7dbee44b47414d662dc9863f1cf5632a7c7983648df11d23c0be73b9b4f9554463b61d5b0a520b70df9e1ee963ebb4af02e6da2cc80f3 + languageName: node + linkType: hard + "url-join@npm:4.0.0": version: 4.0.0 resolution: "url-join@npm:4.0.0" @@ -22234,7 +22485,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:~4.0.1": +"xtend@npm:^4.0.0, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a