(null);
+
+ // Fetch the consulted post
+ const { post: consultedPost } = usePost(consultedPostId);
+ const consultedPostBaseMetadata =
+ consultedPost &&
+ zodTryParseJSON(zodSocialFeedCommonMetadata, consultedPost.metadata);
+ const consultedPostLocation = consultedPostBaseMetadata?.location;
+
+ // Fetch existing posts that have a location and display them as markers
+ const { data } = useFetchFeedLocation({
+ north: bounds?.getNorth(),
+ south: bounds?.getSouth(),
+ west: bounds?.getWest(),
+ east: bounds?.getEast(),
+ networkId: selectedNetworkId,
+ });
+ const posts = data?.list;
+ const aggregatedPosts = data?.aggregations;
+
+ // Markers
+ const markers: MarkerPopup[] = useMemo(() => {
+ if (!posts) return [];
+ const results: MarkerPopup[] = [];
+ posts.forEach((post, index) => {
+ const metadata = zodTryParseJSON(
+ ZodSocialFeedPostMetadata,
+ post.metadata,
+ );
+ if (!metadata?.location) return;
+ results.push({
+ position: metadata.location,
+ post,
+ isHighlighted: post.id === consultedPostId,
+ });
+ });
+ return results;
+ }, [posts, consultedPostId]);
+
+ // Heatmap
+ const heatPoints = aggregatedPosts
+ ? aggregatedPosts.map((aggregatedPost) => {
+ return [
+ aggregatedPost.lat,
+ aggregatedPost.long,
+ aggregatedPost.totalPoints,
+ ];
+ })
+ : [];
+
+ const borderClass = "icon-border";
+ const borderHighlightedClassFlag = "--highlighted";
+ // Custom map post icon
+ const postIcon = (postCategory: PostCategory, isHighlighted?: boolean) => {
+ const size = 32;
+ const borderWidth = 1;
+ const sizeWithBorders = 32 + borderWidth * 2;
+ return new DivIcon({
+ html: `${getMapPostIconSVGString(postCategory)}
`,
+ className: "",
+ iconSize: [sizeWithBorders, sizeWithBorders],
+ });
+ };
+
+ // Custom cluster icon
+ const clusterIcon = (cluster: any) => {
+ const isHighlighted = cluster
+ .getAllChildMarkers()
+ .some((child: any) =>
+ child.options.icon.options.html.includes("icon-border--highlighted"),
+ );
+ return new DivIcon({
+ html: `${cluster.getChildCount()}
`,
+ className: "custom-marker-cluster",
+ iconSize: point(33, 33, true) as PointExpression,
+ });
+ };
+
+ return (
+
+
+ {/*----Loads and displays tiles on the map*/}
+
+ {/*---- Heatmap displayed when dezoom*/}
+
+ Array.isArray(point) && typeof point[2] === "string"
+ ? parseFloat(point[2])
+ : 0
+ }
+ longitudeExtractor={(point) =>
+ Array.isArray(point) && typeof point[1] === "number" ? point[1] : 0
+ }
+ latitudeExtractor={(point) =>
+ Array.isArray(point) && typeof point[0] === "number" ? point[0] : 0
+ }
+ />
+ {/*---- Existing posts that have a location*/}
+
+ {/* When the user is creating a post*/}
+ {creatingPostLocation && (
+
+ )}
+ {/* Mapping through the markers (Fetched posts) */}
+ {markers?.map((marker, index) => (
+
+ {marker.post.category === PostCategory.Normal ? (
+
+
+
+ ) : marker.post.category === PostCategory.MusicAudio ||
+ marker.post.category === PostCategory.Audio ? (
+
+
+
+ ) : marker.post.category === PostCategory.VideoNote ||
+ marker.post.category === PostCategory.Video ? (
+
+
+
+ ) : marker.post.category === PostCategory.Picture ? (
+
+
+
+ ) : marker.post.category === PostCategory.Article ? (
+
+
+
+ ) : null}
+
+ ))}
+
+
+
+
+ );
+};
diff --git a/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx b/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx
new file mode 100644
index 0000000000..0349acc156
--- /dev/null
+++ b/packages/components/socialFeed/Map/MapPosts/ArticleMapPost.tsx
@@ -0,0 +1,63 @@
+import React, { FC } from "react";
+import { View } from "react-native";
+
+import { Post } from "@/api/feed/v1/feed";
+import { BrandText } from "@/components/BrandText";
+import { Separator } from "@/components/separators/Separator";
+import { MapPostWrapper } from "@/components/socialFeed/Map/MapPosts/MapPostWrapper";
+import {
+ createStateFromHTML,
+ getTruncatedArticleHTML,
+ isArticleHTMLNeedsTruncate,
+} from "@/components/socialFeed/RichText";
+import { SpacerColumn } from "@/components/spacer";
+import { zodTryParseJSON } from "@/utils/sanitize";
+import { neutralFF, withAlpha } from "@/utils/style/colors";
+import { fontSemibold10 } from "@/utils/style/fonts";
+import {
+ ZodSocialFeedArticleMetadata,
+ ZodSocialFeedPostMetadata,
+} from "@/utils/types/feed";
+export const ArticleMapPost: FC<{
+ post: Post;
+}> = ({ post }) => {
+ const metadata = zodTryParseJSON(ZodSocialFeedArticleMetadata, post.metadata);
+ const oldMetadata = zodTryParseJSON(ZodSocialFeedPostMetadata, post.metadata);
+ const metadataToUse = metadata || oldMetadata;
+ const title = metadataToUse?.title || "Article from Social Feed";
+
+ const shortDescription = () => {
+ const message = metadataToUse?.message;
+ 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 "";
+ };
+
+ return (
+
+
+ {title}
+
+
+
+
+
+
+ {shortDescription().trim().replace("\n", " ")}
+
+
+
+ );
+};
diff --git a/packages/components/socialFeed/Map/MapPosts/MapPostWrapper.tsx b/packages/components/socialFeed/Map/MapPosts/MapPostWrapper.tsx
new file mode 100644
index 0000000000..5a8539aae6
--- /dev/null
+++ b/packages/components/socialFeed/Map/MapPosts/MapPostWrapper.tsx
@@ -0,0 +1,73 @@
+import { FC, ReactNode, useState } from "react";
+import { StyleProp, View, ViewStyle } from "react-native";
+
+import { Post } from "@/api/feed/v1/feed";
+import FlexRow from "@/components/FlexRow";
+import { SVG } from "@/components/SVG";
+import { CustomPressable } from "@/components/buttons/CustomPressable";
+import { GradientText } from "@/components/gradientText";
+import { SpacerColumn, SpacerRow } from "@/components/spacer";
+import { useNSUserInfo } from "@/hooks/useNSUserInfo";
+import { parseUserId } from "@/networks";
+import {
+ getMapPostIconSVG,
+ getMapPostTextGradientType,
+} from "@/utils/feed/map";
+import { useAppNavigation } from "@/utils/navigation";
+import { DEFAULT_USERNAME } from "@/utils/social-feed";
+import { fontSemibold10 } from "@/utils/style/fonts";
+import { layout } from "@/utils/style/layout";
+import { tinyAddress } from "@/utils/text";
+
+export const MapPostWrapper: FC<{
+ post: Post;
+ children: ReactNode;
+ style?: StyleProp;
+}> = ({ post, children, style }) => {
+ const navigation = useAppNavigation();
+ const [isHeaderHovered, setHeaderHovered] = useState(false);
+ const [, authorAddress] = parseUserId(post.authorId);
+ const authorNSInfo = useNSUserInfo(post.authorId);
+ const username =
+ authorNSInfo?.metadata?.tokenId ||
+ tinyAddress(authorAddress) ||
+ DEFAULT_USERNAME;
+
+ return (
+
+ navigation.navigate("FeedPostView", { id: post.id })}
+ style={{ opacity: isHeaderHovered ? 0.5 : 1 }}
+ onHoverOut={() => setHeaderHovered(false)}
+ onHoverIn={() => setHeaderHovered(true)}
+ >
+
+
+
+
+
+ @{username}
+
+
+
+
+
+
+ {children}
+
+ );
+};
diff --git a/packages/components/socialFeed/Map/MapPosts/MusicMapPost.tsx b/packages/components/socialFeed/Map/MapPosts/MusicMapPost.tsx
new file mode 100644
index 0000000000..7a66e8c39c
--- /dev/null
+++ b/packages/components/socialFeed/Map/MapPosts/MusicMapPost.tsx
@@ -0,0 +1,89 @@
+import React, { FC, useRef } from "react";
+import { View } from "react-native";
+import { v4 as uuidv4 } from "uuid";
+
+import { Post } from "@/api/feed/v1/feed";
+import { BrandText } from "@/components/BrandText";
+import { MediaPlayerBarRefined } from "@/components/mediaPlayer/MediaPlayerBarRefined";
+import { Separator } from "@/components/separators/Separator";
+import { MapPostWrapper } from "@/components/socialFeed/Map/MapPosts/MapPostWrapper";
+import { SpacerColumn } from "@/components/spacer";
+import { useMediaPlayer } from "@/context/MediaPlayerProvider";
+import { zodTryParseJSON } from "@/utils/sanitize";
+import { errorColor, neutralFF, withAlpha } from "@/utils/style/colors";
+import { fontSemibold10 } from "@/utils/style/fonts";
+import {
+ zodSocialFeedCommonMetadata,
+ ZodSocialFeedPostMetadata,
+ ZodSocialFeedTrackMetadata,
+} from "@/utils/types/feed";
+import { Media } from "@/utils/types/mediaPlayer";
+
+export const MusicMapPost: FC<{
+ post: Post;
+}> = ({ post }) => {
+ const { current: id } = useRef(uuidv4());
+ const { handlePlayPause, playbackStatus, loadAndPlaySoundsQueue, media } =
+ useMediaPlayer();
+ const isInMediaPlayer =
+ !!media && (post.id === media.postId || media.id === id);
+ const musicAudioNotePostMetadata = zodTryParseJSON(
+ ZodSocialFeedTrackMetadata,
+ post.metadata,
+ );
+ const musicPostMetadata = zodTryParseJSON(
+ ZodSocialFeedPostMetadata,
+ post.metadata,
+ );
+ const baseMetadata = zodTryParseJSON(
+ zodSocialFeedCommonMetadata,
+ post.metadata,
+ );
+ const title = baseMetadata?.title || "Music from Social Feed";
+
+ // MusicAudio and Music have different metadata but have the same render on the map, so we handle these 2 cases
+ const mediaToPlay: Media | undefined = musicAudioNotePostMetadata
+ ? {
+ id,
+ fileUrl: musicAudioNotePostMetadata.audioFile.url,
+ duration: musicAudioNotePostMetadata.audioFile.audioMetadata?.duration,
+ postId: post.id,
+ }
+ : musicPostMetadata?.files
+ ? {
+ id,
+ fileUrl: musicPostMetadata.files[0].url,
+ duration: musicPostMetadata.files[0].audioMetadata?.duration,
+ postId: post.id,
+ }
+ : undefined;
+
+ const onPressPlayPause = async () => {
+ if (isInMediaPlayer) await handlePlayPause();
+ else if (mediaToPlay) await loadAndPlaySoundsQueue([mediaToPlay]);
+ };
+
+ return (
+
+
+ {title}
+
+
+
+
+
+ {mediaToPlay ? (
+
+ ) : (
+
+ No media to play
+
+ )}
+
+
+ );
+};
diff --git a/packages/components/socialFeed/Map/MapPosts/NormalMapPost.tsx b/packages/components/socialFeed/Map/MapPosts/NormalMapPost.tsx
new file mode 100644
index 0000000000..452e3246e5
--- /dev/null
+++ b/packages/components/socialFeed/Map/MapPosts/NormalMapPost.tsx
@@ -0,0 +1,37 @@
+import { FC } from "react";
+
+import { Post } from "@/api/feed/v1/feed";
+import { BrandText } from "@/components/BrandText";
+import { MapPostWrapper } from "@/components/socialFeed/Map/MapPosts/MapPostWrapper";
+import { TextRenderer } from "@/components/socialFeed/NewsFeed/TextRenderer/TextRenderer";
+import { HTML_TAG_REGEXP } from "@/utils/regex";
+import { zodTryParseJSON } from "@/utils/sanitize";
+import { errorColor, neutralFF } from "@/utils/style/colors";
+import { fontSemibold10 } from "@/utils/style/fonts";
+import { ZodSocialFeedPostMetadata } from "@/utils/types/feed";
+
+export const NormalMapPost: FC<{
+ post: Post;
+}> = ({ post }) => {
+ const postMetadata = zodTryParseJSON(
+ ZodSocialFeedPostMetadata,
+ post.metadata,
+ );
+
+ return (
+
+ {postMetadata ? (
+
+ ) : (
+
+ Wrong post format
+
+ )}
+
+ );
+};
diff --git a/packages/components/socialFeed/Map/MapPosts/PictureMapPost.tsx b/packages/components/socialFeed/Map/MapPosts/PictureMapPost.tsx
new file mode 100644
index 0000000000..e7818fdb33
--- /dev/null
+++ b/packages/components/socialFeed/Map/MapPosts/PictureMapPost.tsx
@@ -0,0 +1,63 @@
+import React, { FC } from "react";
+
+import { Post } from "@/api/feed/v1/feed";
+import { BrandText } from "@/components/BrandText";
+import { OptimizedImage } from "@/components/OptimizedImage";
+import { SmallCarouselAlt } from "@/components/carousels/SmallCarousel/SmallCarouselAlt";
+import { MapPostWrapper } from "@/components/socialFeed/Map/MapPosts/MapPostWrapper";
+import { GIF_URL_REGEX } from "@/utils/regex";
+import { zodTryParseJSON } from "@/utils/sanitize";
+import { errorColor } from "@/utils/style/colors";
+import { fontSemibold10 } from "@/utils/style/fonts";
+import { ZodSocialFeedPostMetadata } from "@/utils/types/feed";
+
+export const PictureMapPost: FC<{
+ post: Post;
+}> = ({ post }) => {
+ const postMetadata = zodTryParseJSON(
+ ZodSocialFeedPostMetadata,
+ post.metadata,
+ );
+ const imagesFiles = postMetadata?.files?.filter(
+ (file) => file.fileType === "image" || file.fileType === "base64",
+ );
+
+ return (
+
+ {!imagesFiles?.length && !postMetadata?.gifs?.length ? (
+
+ No image found
+
+ ) : (
+
+ !item.url && !GIF_URL_REGEX.test(item) ? (
+
+ Image not found
+
+ ) : (
+
+ )
+ }
+ />
+ )}
+
+ );
+};
diff --git a/packages/components/socialFeed/Map/MapPosts/VideoMapPost.tsx b/packages/components/socialFeed/Map/MapPosts/VideoMapPost.tsx
new file mode 100644
index 0000000000..0543c18f7a
--- /dev/null
+++ b/packages/components/socialFeed/Map/MapPosts/VideoMapPost.tsx
@@ -0,0 +1,166 @@
+import {
+ AVPlaybackStatus,
+ AVPlaybackStatusSuccess,
+ ResizeMode,
+ Video,
+} from "expo-av";
+import React, { FC, useRef, useState } from "react";
+import { View } from "react-native";
+import { v4 as uuidv4 } from "uuid";
+
+import { Post } from "@/api/feed/v1/feed";
+import { BrandText } from "@/components/BrandText";
+import { MediaPlayerBarRefined } from "@/components/mediaPlayer/MediaPlayerBarRefined";
+import { Separator } from "@/components/separators/Separator";
+import { MapPostWrapper } from "@/components/socialFeed/Map/MapPosts/MapPostWrapper";
+import { SpacerColumn } from "@/components/spacer";
+import { useFeedbacks } from "@/context/FeedbacksProvider";
+import { useMediaPlayer } from "@/context/MediaPlayerProvider";
+import { web3ToWeb2URI } from "@/utils/ipfs";
+import { zodTryParseJSON } from "@/utils/sanitize";
+import { errorColor, neutralFF, withAlpha } from "@/utils/style/colors";
+import { fontSemibold10 } from "@/utils/style/fonts";
+import {
+ SocialFeedVideoMetadata,
+ zodSocialFeedCommonMetadata,
+ ZodSocialFeedPostMetadata,
+ ZodSocialFeedVideoMetadata,
+} from "@/utils/types/feed";
+import { Media } from "@/utils/types/mediaPlayer";
+
+export const VideoMapPost: FC<{
+ post: Post;
+}> = ({ post }) => {
+ const { current: id } = useRef(uuidv4());
+ const videoRef = useRef