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 (
-
+
+
+ );
+ },
+);
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