share modal

This commit is contained in:
2025-10-03 11:32:47 +02:00
parent d03cec164f
commit 8ea7f89c7b
12 changed files with 413 additions and 125 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

+1
View File
@@ -40,6 +40,7 @@
"expo-blur": "~14.0.3", "expo-blur": "~14.0.3",
"expo-build-properties": "~0.13.3", "expo-build-properties": "~0.13.3",
"expo-camera": "~16.0.18", "expo-camera": "~16.0.18",
"expo-clipboard": "~7.0.1",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",
"expo-dev-client": "~5.0.20", "expo-dev-client": "~5.0.20",
"expo-device": "~7.0.3", "expo-device": "~7.0.3",
+12 -3
View File
@@ -1,13 +1,22 @@
import React from "react"; import React, { useCallback } from "react";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Pressable } from "react-native"; import { Pressable } from "react-native";
import { Entypo } from "@expo/vector-icons"; import { Entypo } from "@expo/vector-icons";
import { openShareSheet } from "../../utils/shareSheet";
export default function ShareBtn({ style, onPress = null }) { export default function ShareBtn({ style, onPress = null }) {
const handlePress = useCallback(() => {
if (typeof onPress === "function") {
onPress();
return;
}
openShareSheet();
}, [onPress]);
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={handlePress}
disabled={!onPress}
style={{ style={{
...style, ...style,
}} }}
+12 -3
View File
@@ -1,15 +1,24 @@
import React from "react"; import React, { useCallback } from "react";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Pressable, Text } from "react-native"; import { Pressable, Text } from "react-native";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { Entypo } from "@expo/vector-icons"; import { Entypo } from "@expo/vector-icons";
import { openShareSheet } from "../../utils/shareSheet";
export default function ShareBtn({ style, onPress = null }) { export default function ShareBtn({ style, onPress = null }) {
const handlePress = useCallback(() => {
if (typeof onPress === "function") {
onPress();
return;
}
openShareSheet();
}, [onPress]);
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={handlePress}
disabled={!onPress}
style={{ style={{
...style, ...style,
}} }}
+5 -5
View File
@@ -1,10 +1,10 @@
import React from "react";
import { Alert, Pressable, Platform, Text } from "react-native";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Image } from "expo-image";
import React from "react";
import { Alert, Platform, Pressable, Text } from "react-native";
import { icons } from "../assets";
import { Palette } from "../styles"; import { Palette } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts"; import { FONT_FAMILY } from "../styles/Fonts";
import { icons } from "../assets";
import { Image } from "expo-image";
export default function ShareBtnWeb({ style }) { export default function ShareBtnWeb({ style }) {
const handleShare = () => { const handleShare = () => {
@@ -51,7 +51,7 @@ export default function ShareBtnWeb({ style }) {
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
}} }}
> >
Partager l'expérience Partagez l'expérience
</Text> </Text>
<Image <Image
source={icons.share} source={icons.share}
+324 -90
View File
@@ -1,120 +1,354 @@
import React, { useMemo } from "react"; import React, {
import { Linking, Pressable, Text, View } from "react-native"; useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Linking,
Platform,
Pressable,
Share,
StyleSheet,
Text,
View,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet"; import { SheetManager } from "react-native-actions-sheet";
import { shareLink } from "../../helpers"; import {
Feather,
FontAwesome,
MaterialCommunityIcons,
} from "@expo/vector-icons";
import AppActionSheet from "../AppActionSheet";
import { Palette } from "../../styles"; import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import AppActionSheet from "../AppActionSheet"; import { isWeb } from "../../hooks/useLayoutType";
import {
musiclandShareHeading,
musiclandShareMessage,
musiclandShareUrl,
} from "../../data";
const ShareModal = ({ payload }) => { const ShareModal = ({ payload }) => {
const { const heading = payload?.heading ?? musiclandShareHeading;
title = "Partager", const url = payload?.url ?? musiclandShareUrl;
message = "Choisis une application pour partager", const shareTitle = payload?.shareTitle ?? heading;
text = "Découvre ce playback sur MusicLand", const shareMessage = payload?.shareMessage ?? musiclandShareMessage;
url = "", const linkLabel = payload?.linkLabel ?? url;
} = payload || {}; const sectionTitle = payload?.sectionTitle ?? "Partager sur...";
const onClose = () => { const [copied, setCopied] = useState(false);
const copyTimeout = useRef(null);
const closeSheet = useCallback(() => {
SheetManager.hide("Share"); SheetManager.hide("Share");
}; }, []);
const shareText = useMemo(() => { const shareText = useMemo(() => {
return [text, url].filter(Boolean).join(" "); return [shareMessage, url].filter(Boolean).join(" ");
}, [text, url]); }, [shareMessage, url]);
const openScheme = async () => { const encodedUrl = useMemo(() => encodeURIComponent(url), [url]);
try { const encodedShareText = useMemo(
const supported = await Linking.canOpenURL(url); () => encodeURIComponent(shareText),
if (supported) { [shareText]
await Linking.openURL(url); );
} else {
// fallback vers partage natif useEffect(() => {
shareLink({ url, title: "Partager", message: shareText }); return () => {
} if (copyTimeout.current) {
} catch (e) { clearTimeout(copyTimeout.current);
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) => <FontAwesome name="facebook" {...props} />,
target: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,
},
{
key: "instagram",
label: "Partager sur Instagram",
Icon: (props) => <FontAwesome name="instagram" {...props} />,
target: `https://www.instagram.com/?url=${encodedUrl}`,
},
{
key: "x",
label: "Partager sur X",
Icon: (props) => <FontAwesome name="twitter" {...props} />,
target: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedShareText}`,
},
{
key: "email",
label: "Partager par e-mail",
Icon: (props) => <Feather name="mail" {...props} />,
target: `mailto:?subject=${encodeURIComponent(
shareTitle
)}&body=${encodedShareText}`,
},
{
key: "qr",
label: "Partager par QR-code",
Icon: (props) => (
<MaterialCommunityIcons name="qrcode-scan" {...props} />
),
target: `https://api.qrserver.com/v1/create-qr-code/?size=512x512&data=${encodedUrl}`,
},
];
}, [encodedShareText, encodedUrl, payload?.options, shareTitle]);
return ( return (
<AppActionSheet id="Share"> <AppActionSheet id="Share">
<View style={{ gap: 24 }}> <View style={styles.container}>
<View style={{ gap: 6 }}> <View style={styles.card}>
<Text <Text style={styles.heading}>{heading}</Text>
style={{ <View style={styles.linkRow}>
fontSize: 20, <Text numberOfLines={1} style={styles.linkText}>
color: Palette.white, {linkLabel}
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
textAlign: "center",
}}
>
{title}
</Text> </Text>
<Text <View style={styles.linkActions}>
style={{ <Pressable
fontSize: 14, hitSlop={8}
color: Palette.white, onPress={handleCopy}
opacity: 0.8, style={styles.iconButton}
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
textAlign: "center",
}}
> >
{message} <Feather
</Text> name={copied ? "check" : "copy"}
size={16}
color={Palette.white}
/>
</Pressable>
<Pressable
hitSlop={8}
onPress={onSystemShare}
style={styles.iconButton}
>
<Feather name="share-2" size={16} color={Palette.white} />
</Pressable>
</View>
</View>
{copied ? (
<Text style={styles.copiedText}>Lien copie</Text>
) : null}
</View> </View>
<View <View style={styles.card}>
style={{ <Text style={styles.sectionHeading}>{sectionTitle}</Text>
flexDirection: "row", <View style={styles.optionsList}>
justifyContent: "space-between", {shareOptions.map((option) => {
alignItems: "center", const Icon = option?.Icon ?? Feather;
gap: 16, return (
paddingHorizontal: 12,
}}
>
<Pressable <Pressable
onPress={() => { key={option?.key}
openScheme(); onPress={() => handleShareOption(option)}
}} style={styles.optionRow}
style={{ alignItems: "center", gap: 8, flex: 1 }}
> >
<View <View style={styles.optionIconWrapper}>
style={{ <Icon size={18} color={Palette.white} />
width: 56,
height: 56,
borderRadius: 28,
borderWidth: 1,
borderColor: "#FFFFFF60",
alignItems: "center",
justifyContent: "center",
}}
>
<Text
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 12,
}}
>
partager
</Text>
</View> </View>
<Text <Text style={styles.optionLabel}>{option?.label}</Text>
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 12,
}}
>
partager
</Text>
</Pressable> </Pressable>
);
})}
</View>
</View> </View>
</View> </View>
</AppActionSheet> </AppActionSheet>
); );
}; };
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; export default ShareModal;
+4
View File
@@ -13,3 +13,7 @@ export const firebaseDashboardUrl =
export const appleAppStoreUrl = "https://apps.apple.com/app"; export const appleAppStoreUrl = "https://apps.apple.com/app";
export const algoliaAppUrl = "https://dashboard.algolia.com/apps"; 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.";
+12 -12
View File
@@ -1,21 +1,21 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Platform, StyleSheet, View } from "react-native"; import { Platform, StyleSheet, View } from "react-native";
import Page from "../../layouts/Page";
import { background } from "../../assets"; import { background } from "../../assets";
import { gutters, Palette } from "../../styles"; import BorderGradientButton from "../../components/BorderGradientButton";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import { useUser } from "../../providers/UserDataProvider";
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel"; import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import BorderGradientButton from "../../components/BorderGradientButton"; import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
import { isWeb } from "../../hooks/useLayoutType.js";
import ShareBtn from "../../components/ShareBtn/ShareBtn"; 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 { import {
findFirstUnlockedStageIndex,
getCreationStageStates, getCreationStageStates,
getStageAction, getStageAction,
findFirstUnlockedStageIndex,
} from "../../utils/projectStages"; } from "../../utils/projectStages";
const Home = () => { const Home = () => {
@@ -29,7 +29,7 @@ const Home = () => {
const projects = useMemo( const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []), () => (Array.isArray(userProjects) ? userProjects : []),
[userProjects], [userProjects]
); );
const currentProject = useMemo(() => { const currentProject = useMemo(() => {
@@ -75,7 +75,7 @@ const Home = () => {
const stageStates = useMemo( const stageStates = useMemo(
() => getCreationStageStates(currentProject), () => getCreationStageStates(currentProject),
[currentProject], [currentProject]
); );
const firstUnlockedIndex = useMemo(() => { const firstUnlockedIndex = useMemo(() => {
@@ -139,7 +139,7 @@ const Home = () => {
const songwriterAction = useMemo( const songwriterAction = useMemo(
() => getStageAction("songwriter", null), () => getStageAction("songwriter", null),
[], []
); );
const handleStartNew = useCallback(() => { const handleStartNew = useCallback(() => {
@@ -1,6 +1,6 @@
import { Image as ExpoImage } from "expo-image";
import React, { useState } from "react"; import React, { useState } from "react";
import { Pressable, ScrollView, Text, View } from "react-native"; import { Pressable, ScrollView, Text, View } from "react-native";
import { Image as ExpoImage } from "expo-image";
import { img } from "../../../assets"; import { img } from "../../../assets";
import MoreMenu from "../../../components/MoreMenu"; import MoreMenu from "../../../components/MoreMenu";
import { Routes } from "../../../navigation"; import { Routes } from "../../../navigation";
@@ -85,12 +85,12 @@ const SearchResultsList = ({
setShowMenu( setShowMenu(
(previous) => (previous) =>
!previous || !previous ||
positionTop?.top !== (menuPosition?.top ?? null), positionTop?.top !== (menuPosition?.top ?? null)
); );
}} }}
/> />
))} ))}
{(filteredProjects.length >= 6 || projectsLoading) && ( {/* {(filteredProjects.length >= 6 || projectsLoading) && (
<Pressable <Pressable
onPress={loadMoreProjects} onPress={loadMoreProjects}
style={{ alignSelf: "center", marginTop: 6 }} style={{ alignSelf: "center", marginTop: 6 }}
@@ -106,7 +106,7 @@ const SearchResultsList = ({
{projectsLoading ? "Chargement…" : "Charger plus"} {projectsLoading ? "Chargement…" : "Charger plus"}
</Text> </Text>
</Pressable> </Pressable>
)} )} */}
</View> </View>
)} )}
{filteredProjects.length === 0 && !projectsLoading && ( {filteredProjects.length === 0 && !projectsLoading && (
@@ -163,7 +163,7 @@ const SearchResultsList = ({
</Text> </Text>
</Pressable> </Pressable>
))} ))}
{(filteredUsers.length >= 5 || usersLoading) && ( {/* {(filteredUsers.length >= 5 || usersLoading) && (
<Pressable <Pressable
onPress={loadMoreUsers} onPress={loadMoreUsers}
style={{ alignSelf: "center", marginTop: 6 }} style={{ alignSelf: "center", marginTop: 6 }}
@@ -179,7 +179,7 @@ const SearchResultsList = ({
{usersLoading ? "Chargement…" : "Charger plus"} {usersLoading ? "Chargement…" : "Charger plus"}
</Text> </Text>
</Pressable> </Pressable>
)} )} */}
</View> </View>
)} )}
{filteredUsers.length === 0 && !usersLoading && ( {filteredUsers.length === 0 && !usersLoading && (
+2
View File
@@ -5,6 +5,7 @@ import DeleteAccountModal from "../components/modal/DeleteAccountModal";
import DeletePlaybackModal from "../components/modal/DeletePlaybackModal"; import DeletePlaybackModal from "../components/modal/DeletePlaybackModal";
import DeleteAudioModal from "../components/modal/DeleteAudioModal"; import DeleteAudioModal from "../components/modal/DeleteAudioModal";
import PlaylistModal from "../components/modal/PlaylistModal"; import PlaylistModal from "../components/modal/PlaylistModal";
import ShareModal from "../components/modal/ShareModal";
registerSheet("Delete", DeleteModal); registerSheet("Delete", DeleteModal);
registerSheet("ProfileSettings", ProfileSettingsModal); registerSheet("ProfileSettings", ProfileSettingsModal);
@@ -12,5 +13,6 @@ registerSheet("DeleteAccount", DeleteAccountModal);
registerSheet("DeletePlayback", DeletePlaybackModal); registerSheet("DeletePlayback", DeletePlaybackModal);
registerSheet("DeleteAudio", DeleteAudioModal); registerSheet("DeleteAudio", DeleteAudioModal);
registerSheet("Playlist", PlaylistModal); registerSheet("Playlist", PlaylistModal);
registerSheet("Share", ShareModal);
export {}; export {};
+24
View File
@@ -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,
},
});
};
+5
View File
@@ -5063,6 +5063,11 @@ expo-camera@~16.0.18:
dependencies: dependencies:
invariant "^2.2.4" 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: expo-constants@~17.0.5, expo-constants@~17.0.8:
version "17.0.8" version "17.0.8"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.0.8.tgz#d7a21ec6f1f4834ea25aa645be20292ef99c0b81" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-17.0.8.tgz#d7a21ec6f1f4834ea25aa645be20292ef99c0b81"