diff --git a/assets/share-experience.png b/assets/share-experience.png new file mode 100644 index 0000000..3b9e991 Binary files /dev/null and b/assets/share-experience.png differ diff --git a/package.json b/package.json index fb49892..135d4d0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "expo-blur": "~14.0.3", "expo-build-properties": "~0.13.3", "expo-camera": "~16.0.18", + "expo-clipboard": "~7.0.1", "expo-constants": "~17.0.8", "expo-dev-client": "~5.0.20", "expo-device": "~7.0.3", diff --git a/src/components/ShareBtn/ShareBtn.js b/src/components/ShareBtn/ShareBtn.js index fc80028..3d15295 100644 --- a/src/components/ShareBtn/ShareBtn.js +++ b/src/components/ShareBtn/ShareBtn.js @@ -1,13 +1,22 @@ -import React from "react"; +import React, { useCallback } from "react"; import { BlurView } from "expo-blur"; import { Pressable } from "react-native"; import { Entypo } from "@expo/vector-icons"; +import { openShareSheet } from "../../utils/shareSheet"; export default function ShareBtn({ style, onPress = null }) { + const handlePress = useCallback(() => { + if (typeof onPress === "function") { + onPress(); + return; + } + + openShareSheet(); + }, [onPress]); + return ( { + if (typeof onPress === "function") { + onPress(); + return; + } + + openShareSheet(); + }, [onPress]); + return ( { @@ -51,7 +51,7 @@ export default function ShareBtnWeb({ style }) { fontFamily: FONT_FAMILY.InterMedium, }} > - Partager l'expérience + Partagez l'expérience { - const { - title = "Partager", - message = "Choisis une application pour partager", - text = "Découvre ce playback sur MusicLand", - url = "", - } = payload || {}; + const heading = payload?.heading ?? musiclandShareHeading; + const url = payload?.url ?? musiclandShareUrl; + const shareTitle = payload?.shareTitle ?? heading; + const shareMessage = payload?.shareMessage ?? musiclandShareMessage; + const linkLabel = payload?.linkLabel ?? url; + const sectionTitle = payload?.sectionTitle ?? "Partager sur..."; - const onClose = () => { + const [copied, setCopied] = useState(false); + const copyTimeout = useRef(null); + + const closeSheet = useCallback(() => { SheetManager.hide("Share"); - }; + }, []); const shareText = useMemo(() => { - return [text, url].filter(Boolean).join(" "); - }, [text, url]); + return [shareMessage, url].filter(Boolean).join(" "); + }, [shareMessage, url]); - const openScheme = async () => { - try { - const supported = await Linking.canOpenURL(url); - if (supported) { - await Linking.openURL(url); - } else { - // fallback vers partage natif - shareLink({ url, title: "Partager", message: shareText }); + const encodedUrl = useMemo(() => encodeURIComponent(url), [url]); + const encodedShareText = useMemo( + () => encodeURIComponent(shareText), + [shareText] + ); + + useEffect(() => { + return () => { + if (copyTimeout.current) { + clearTimeout(copyTimeout.current); } - } catch (e) { - shareLink({ url, title: "Partager", message: shareText }); - } finally { - onClose(); + }; + }, []); + + const handleCopy = useCallback(async () => { + try { + if (copyTimeout.current) { + clearTimeout(copyTimeout.current); + } + + if (isWeb && typeof navigator !== "undefined" && navigator.clipboard) { + await navigator.clipboard.writeText(url); + } else { + const Clipboard = await import("expo-clipboard"); + if (typeof Clipboard?.setStringAsync === "function") { + await Clipboard.setStringAsync(url); + } else if (typeof Clipboard?.default?.setStringAsync === "function") { + await Clipboard.default.setStringAsync(url); + } + } + + setCopied(true); + copyTimeout.current = setTimeout(() => { + setCopied(false); + }, 2000); + } catch (error) { + console.error("share.copy.failed", error); } - }; + }, [url]); + + const handleSystemShare = useCallback(async () => { + if (Platform.OS === "web" && typeof navigator !== "undefined") { + try { + if (typeof navigator.share === "function") { + await navigator.share({ + url, + text: shareText, + title: shareTitle, + }); + return; + } + } catch (error) { + console.error("share.system.web", error); + } + } + + try { + await Share.share( + Platform.select({ + ios: url + ? { url, message: shareText, title: shareTitle } + : { message: shareText, title: shareTitle }, + android: { message: shareText, title: shareTitle }, + default: { message: shareText, title: shareTitle }, + }) + ); + } catch (error) { + console.error("share.system.native", error); + } + }, [shareText, shareTitle, url]); + + const handleShareOption = useCallback( + async (option) => { + const target = option?.target; + + if (!target) { + await handleSystemShare(); + closeSheet(); + return; + } + + try { + if (Platform.OS === "web") { + window.open(target, "_blank", "noopener,noreferrer"); + closeSheet(); + return; + } + + const supported = await Linking.canOpenURL(target); + if (supported) { + await Linking.openURL(target); + closeSheet(); + return; + } + } catch (error) { + console.error(`share.option.error.${option?.key || "unknown"}`, error); + } + + await handleSystemShare(); + closeSheet(); + }, + [closeSheet, handleSystemShare] + ); + + const onSystemShare = useCallback(async () => { + await handleSystemShare(); + closeSheet(); + }, [closeSheet, handleSystemShare]); + + const shareOptions = useMemo(() => { + if (Array.isArray(payload?.options) && payload.options.length) { + return payload.options; + } + + return [ + { + key: "facebook", + label: "Partager sur Facebook", + Icon: (props) => , + target: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, + }, + { + key: "instagram", + label: "Partager sur Instagram", + Icon: (props) => , + target: `https://www.instagram.com/?url=${encodedUrl}`, + }, + { + key: "x", + label: "Partager sur X", + Icon: (props) => , + target: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedShareText}`, + }, + { + key: "email", + label: "Partager par e-mail", + Icon: (props) => , + target: `mailto:?subject=${encodeURIComponent( + shareTitle + )}&body=${encodedShareText}`, + }, + { + key: "qr", + label: "Partager par QR-code", + Icon: (props) => ( + + ), + target: `https://api.qrserver.com/v1/create-qr-code/?size=512x512&data=${encodedUrl}`, + }, + ]; + }, [encodedShareText, encodedUrl, payload?.options, shareTitle]); return ( - - - - {title} - - - {message} - + + + {heading} + + + {linkLabel} + + + + + + + + + + + {copied ? ( + Lien copie + ) : null} - - { - openScheme(); - }} - style={{ alignItems: "center", gap: 8, flex: 1 }} - > - - - partager - - - - partager - - + + {sectionTitle} + + {shareOptions.map((option) => { + const Icon = option?.Icon ?? Feather; + return ( + handleShareOption(option)} + style={styles.optionRow} + > + + + + {option?.label} + + ); + })} + ); }; +const styles = StyleSheet.create({ + container: { + gap: 24, + }, + card: { + backgroundColor: Palette.glass, + borderRadius: 24, + borderWidth: 1, + borderColor: "rgba(255, 255, 255, 0.08)", + paddingHorizontal: 20, + paddingVertical: 18, + gap: 16, + }, + heading: { + fontSize: 20, + color: Palette.white, + textAlign: "center", + fontFamily: FONT_FAMILY.HelveticaNeueMedium, + }, + linkRow: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255, 255, 255, 0.06)", + borderRadius: 16, + paddingHorizontal: 14, + paddingVertical: 10, + gap: 12, + }, + linkText: { + flex: 1, + color: Palette.white, + fontSize: 15, + fontFamily: FONT_FAMILY.InterMedium, + }, + linkActions: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + iconButton: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: "rgba(255, 255, 255, 0.08)", + alignItems: "center", + justifyContent: "center", + }, + copiedText: { + fontSize: 12, + textAlign: "center", + color: Palette.white, + opacity: 0.8, + fontFamily: FONT_FAMILY.InterMedium, + }, + sectionHeading: { + fontSize: 18, + color: Palette.white, + fontFamily: FONT_FAMILY.HelveticaNeueMedium, + textAlign: "center", + }, + optionsList: { + gap: 12, + }, + optionRow: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255, 255, 255, 0.04)", + borderRadius: 18, + paddingHorizontal: 14, + paddingVertical: 14, + gap: 14, + }, + optionIconWrapper: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: "rgba(255, 255, 255, 0.08)", + alignItems: "center", + justifyContent: "center", + }, + optionLabel: { + flex: 1, + fontSize: 15, + color: Palette.white, + fontFamily: FONT_FAMILY.InterMedium, + }, +}); + export default ShareModal; + diff --git a/src/data/index.js b/src/data/index.js index b1d1a14..d351ef6 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -13,3 +13,7 @@ export const firebaseDashboardUrl = export const appleAppStoreUrl = "https://apps.apple.com/app"; export const algoliaAppUrl = "https://dashboard.algolia.com/apps"; +export const musiclandShareUrl = "https://musicland.ai"; +export const musiclandShareHeading = "Partagez l'expérience MusicLand"; +export const musiclandShareMessage = + "Invite tes proches a decouvrir MusicLand et cree des experiences musicales ensemble."; diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index f661e2c..c181d59 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -1,21 +1,21 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Platform, StyleSheet, View } from "react-native"; -import Page from "../../layouts/Page"; import { background } from "../../assets"; -import { gutters, Palette } from "../../styles"; -import { navigate } from "../../navigation/NavigationService"; -import { Routes } from "../../navigation"; -import { useUser } from "../../providers/UserDataProvider"; -import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown"; +import BorderGradientButton from "../../components/BorderGradientButton"; import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel"; import GradientButton from "../../components/GradientButton"; -import BorderGradientButton from "../../components/BorderGradientButton"; -import { isWeb } from "../../hooks/useLayoutType.js"; +import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown"; import ShareBtn from "../../components/ShareBtn/ShareBtn"; +import { isWeb } from "../../hooks/useLayoutType.js"; +import Page from "../../layouts/Page"; +import { Routes } from "../../navigation"; +import { navigate } from "../../navigation/NavigationService"; +import { useUser } from "../../providers/UserDataProvider"; +import { gutters, Palette } from "../../styles"; import { + findFirstUnlockedStageIndex, getCreationStageStates, getStageAction, - findFirstUnlockedStageIndex, } from "../../utils/projectStages"; const Home = () => { @@ -29,7 +29,7 @@ const Home = () => { const projects = useMemo( () => (Array.isArray(userProjects) ? userProjects : []), - [userProjects], + [userProjects] ); const currentProject = useMemo(() => { @@ -75,7 +75,7 @@ const Home = () => { const stageStates = useMemo( () => getCreationStageStates(currentProject), - [currentProject], + [currentProject] ); const firstUnlockedIndex = useMemo(() => { @@ -139,7 +139,7 @@ const Home = () => { const songwriterAction = useMemo( () => getStageAction("songwriter", null), - [], + [] ); const handleStartNew = useCallback(() => { diff --git a/src/screens/Library/components/SearchResultsList.js b/src/screens/Library/components/SearchResultsList.js index 257811f..80980c4 100644 --- a/src/screens/Library/components/SearchResultsList.js +++ b/src/screens/Library/components/SearchResultsList.js @@ -1,6 +1,6 @@ +import { Image as ExpoImage } from "expo-image"; import React, { useState } from "react"; import { Pressable, ScrollView, Text, View } from "react-native"; -import { Image as ExpoImage } from "expo-image"; import { img } from "../../../assets"; import MoreMenu from "../../../components/MoreMenu"; import { Routes } from "../../../navigation"; @@ -85,12 +85,12 @@ const SearchResultsList = ({ setShowMenu( (previous) => !previous || - positionTop?.top !== (menuPosition?.top ?? null), + positionTop?.top !== (menuPosition?.top ?? null) ); }} /> ))} - {(filteredProjects.length >= 6 || projectsLoading) && ( + {/* {(filteredProjects.length >= 6 || projectsLoading) && ( - )} + )} */} )} {filteredProjects.length === 0 && !projectsLoading && ( @@ -163,7 +163,7 @@ const SearchResultsList = ({ ))} - {(filteredUsers.length >= 5 || usersLoading) && ( + {/* {(filteredUsers.length >= 5 || usersLoading) && ( - )} + )} */} )} {filteredUsers.length === 0 && !usersLoading && ( diff --git a/src/utils/Sheet.js b/src/utils/Sheet.js index 1ac8bf8..0782789 100644 --- a/src/utils/Sheet.js +++ b/src/utils/Sheet.js @@ -5,6 +5,7 @@ import DeleteAccountModal from "../components/modal/DeleteAccountModal"; import DeletePlaybackModal from "../components/modal/DeletePlaybackModal"; import DeleteAudioModal from "../components/modal/DeleteAudioModal"; import PlaylistModal from "../components/modal/PlaylistModal"; +import ShareModal from "../components/modal/ShareModal"; registerSheet("Delete", DeleteModal); registerSheet("ProfileSettings", ProfileSettingsModal); @@ -12,5 +13,6 @@ registerSheet("DeleteAccount", DeleteAccountModal); registerSheet("DeletePlayback", DeletePlaybackModal); registerSheet("DeleteAudio", DeleteAudioModal); registerSheet("Playlist", PlaylistModal); +registerSheet("Share", ShareModal); export {}; diff --git a/src/utils/shareSheet.js b/src/utils/shareSheet.js new file mode 100644 index 0000000..5095f55 --- /dev/null +++ b/src/utils/shareSheet.js @@ -0,0 +1,24 @@ +import { SheetManager } from "react-native-actions-sheet"; + +import { + musiclandShareHeading, + musiclandShareMessage, + musiclandShareUrl, +} from "../data"; + +const defaultPayload = { + heading: musiclandShareHeading, + url: musiclandShareUrl, + shareTitle: "MusicLand", + shareMessage: musiclandShareMessage, +}; + +export const openShareSheet = (payload = {}) => { + SheetManager.show("Share", { + payload: { + ...defaultPayload, + ...payload, + }, + }); +}; + diff --git a/yarn.lock b/yarn.lock index bc1fc81..a277b00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5063,6 +5063,11 @@ expo-camera@~16.0.18: dependencies: invariant "^2.2.4" +expo-clipboard@~7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/expo-clipboard/-/expo-clipboard-7.0.1.tgz#31d61270e77a37d2a6b7ae9abf79e060497ef43b" + integrity sha512-rqYk0+WoqitPcPKxmMxSpLonX1E5Ije3LBYfnYMbH3xU5Gr8EAH9QnOWOi4BgahUPvcot6nbFEnx+DqARrmxKQ== + expo-constants@~17.0.5, expo-constants@~17.0.8: version "17.0.8" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.0.8.tgz#d7a21ec6f1f4834ea25aa645be20292ef99c0b81"