Files
musicland/src/components/modal/ShareModal.js
T

637 lines
18 KiB
JavaScript

import { Feather, FontAwesome, MaterialCommunityIcons } from '@expo/vector-icons'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
Linking,
Platform,
Pressable,
Share,
StyleSheet,
Text,
View,
findNodeHandle,
} from 'react-native'
import { SheetManager } from 'react-native-actions-sheet'
import { BlurView } from 'expo-blur'
import { setGlobal } from 'reactn'
import { musiclandShareHeading, musiclandShareMessage, musiclandShareUrl } from '../../data'
import { isWeb } from '../../hooks/useLayoutType'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import AppActionSheet from '../AppActionSheet'
const ShareModal = ({ payload }) => {
const heading = payload?.heading ?? musiclandShareHeading
const url = payload?.url ?? musiclandShareUrl
const shareTitle = payload?.shareTitle ?? heading
const shareMessage = payload?.shareMessage ?? musiclandShareMessage
const copyUrl = payload?.copyUrl ?? payload?.qrUrl ?? payload?.url ?? musiclandShareUrl
const linkLabel = payload?.linkLabel ?? copyUrl
const sectionTitle = payload?.sectionTitle ?? 'Partager sur...'
const qrTitle = payload?.qrTitle ?? musiclandShareHeading
const qrUrl = payload?.qrUrl ?? musiclandShareUrl
const [copied, setCopied] = useState(false)
const copyTimeout = useRef(null)
const shareQrTimeoutRef = useRef(null)
const modalRef = useRef(null)
const [webVisible, setWebVisible] = useState(true)
const blurProps = Platform.OS === 'android' ? { experimentalBlurMethod: 'dimezisBlurView' } : {}
const closeSheet = useCallback(() => {
if (isWeb) {
setWebVisible(false)
}
try {
const maybePromise = SheetManager.hide('Share')
return Promise.resolve(maybePromise)
} catch (error) {
console.error('share.sheet.close', error)
return Promise.resolve()
}
}, [])
useEffect(() => {
if (isWeb) {
setWebVisible(true)
}
}, [payload])
const shareText = useMemo(() => {
return [shareMessage, url].filter(Boolean).join(' ')
}, [shareMessage, url])
const encodedUrl = useMemo(() => encodeURIComponent(url), [url])
const encodedShareText = useMemo(() => encodeURIComponent(shareText), [shareText])
useEffect(() => {
return () => {
if (copyTimeout.current) {
clearTimeout(copyTimeout.current)
}
}
}, [])
useEffect(() => {
if (!isWeb || typeof document === 'undefined' || !webVisible) {
return
}
const handlePointerDown = (event) => {
const node = modalRef.current
if (!node) {
return
}
const domNode = findNodeHandle(node)
if (domNode && typeof domNode.contains === 'function') {
if (domNode.contains(event.target)) {
return
}
}
if (domNode === event.target) {
return
}
closeSheet()
}
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
closeSheet()
}
}
document.addEventListener('pointerdown', handlePointerDown)
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('pointerdown', handlePointerDown)
document.removeEventListener('keydown', handleKeyDown)
}
}, [closeSheet, webVisible])
const handleCopy = useCallback(async () => {
try {
if (copyTimeout.current) {
clearTimeout(copyTimeout.current)
}
if (isWeb && typeof navigator !== 'undefined' && navigator.clipboard) {
await navigator.clipboard.writeText(copyUrl)
} else {
const Clipboard = await import('expo-clipboard')
if (typeof Clipboard?.setStringAsync === 'function') {
await Clipboard.setStringAsync(copyUrl)
} else if (typeof Clipboard?.default?.setStringAsync === 'function') {
await Clipboard.default.setStringAsync(copyUrl)
}
}
setCopied(true)
copyTimeout.current = setTimeout(() => {
setCopied(false)
}, 2000)
} catch (error) {
console.error('share.copy.failed', error)
}
}, [copyUrl])
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) => {
if (option?.type === 'qr') {
console.log('share.modal.handleShareOption', {
key: option?.key,
platform: Platform.OS,
})
if (shareQrTimeoutRef.current) {
clearTimeout(shareQrTimeoutRef.current)
}
const openQrModal = () => {
console.log('share.modal.openQrModal', {
platform: Platform.OS,
url: qrUrl,
title: qrTitle,
})
shareQrTimeoutRef.current = null
setGlobal({
shareQrModal: {
visible: true,
url: qrUrl,
title: qrTitle,
},
})
}
if (Platform.OS === 'web') {
console.log('share.modal.scheduleQr', { platform: Platform.OS })
const maybePromise = closeSheet()
shareQrTimeoutRef.current = setTimeout(openQrModal, 80)
if (maybePromise && typeof maybePromise.catch === 'function') {
maybePromise.catch((error) => {
console.error('share.modal.closeSheet.error', error)
})
}
} else {
await closeSheet()
openQrModal()
}
return
}
const target = option?.target
if (!target) {
await handleSystemShare()
await closeSheet()
return
}
try {
if (Platform.OS === 'web') {
window.open(target, '_blank', 'noopener,noreferrer')
await closeSheet()
return
}
const supported = await Linking.canOpenURL(target)
if (supported) {
await Linking.openURL(target)
await closeSheet()
return
}
} catch (error) {
console.error(`share.option.error.${option?.key || 'unknown'}`, error)
}
await handleSystemShare()
await closeSheet()
},
[closeSheet, handleSystemShare, qrTitle, qrUrl]
)
const onSystemShare = useCallback(async () => {
await handleSystemShare()
await 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',
type: 'qr',
label: 'Partager par QR-code',
Icon: (props) => <MaterialCommunityIcons name="qrcode-scan" {...props} />,
},
]
}, [encodedShareText, encodedUrl, payload?.options, shareTitle])
const mobileContent = (
<View style={styles.container}>
<BlurView style={[styles.card, styles.blurCard]} tint="dark" intensity={40} {...blurProps}>
<Text style={styles.heading}>{heading}</Text>
<View style={styles.linkRow}>
<Text numberOfLines={1} style={styles.linkText}>
{linkLabel}
</Text>
<View style={styles.linkActions}>
<Pressable hitSlop={8} onPress={handleCopy} style={styles.iconButton}>
<Feather 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 copié</Text> : null}
</BlurView>
<BlurView intensity={40} tint="dark" style={styles.card} {...blurProps}>
<Text style={styles.sectionHeading}>{sectionTitle}</Text>
<View style={styles.optionsList}>
{shareOptions.map((option, index) => {
const Icon = option?.Icon ?? Feather
return (
<BlurView
key={option?.key || index}
style={styles.optionBlur}
intensity={100}
{...blurProps}
>
<Pressable onPress={() => handleShareOption(option)} style={styles.optionRow}>
<View style={styles.optionIconWrapper}>
<Icon size={18} color={Palette.white} />
</View>
<Text style={styles.optionLabel}>{option?.label}</Text>
</Pressable>
</BlurView>
)
})}
</View>
</BlurView>
</View>
)
if (isWeb && !webVisible) {
return null
}
return (
<AppActionSheet id="Share" webModal={isWeb} onClose={closeSheet}>
{isWeb ? (
<View ref={modalRef} collapsable={false} style={styles.webContainer}>
<View style={styles.webHeader}>
<Text style={styles.webHeading}>{heading}</Text>
<Pressable onPress={closeSheet} hitSlop={12} style={styles.webCloseButton}>
<Feather name="x" size={16} color={Palette.white} />
</Pressable>
</View>
<Text style={styles.webSubheading}>{shareMessage}</Text>
<View style={styles.webContent}>
<View style={styles.webLinkCard}>
<Text style={styles.webLinkLabel}>{linkLabel}</Text>
<View style={styles.webLinkActions}>
<Pressable
onPress={handleCopy}
style={[
styles.webActionButton,
styles.webPrimaryButton,
copied && styles.webActionButtonActive,
]}
>
<Feather name={copied ? 'check' : 'copy'} size={16} color={Palette.white} />
<Text style={styles.webActionLabel}>
{copied ? 'Lien copié' : 'Copier le lien'}
</Text>
</Pressable>
{/* <Pressable
onPress={onSystemShare}
style={[styles.webActionButton, styles.webSecondaryButton]}
>
<Feather name="share-2" size={16} color={Palette.white} />
<Text style={styles.webActionLabel}>Partager</Text>
</Pressable> */}
</View>
</View>
<View style={styles.webOptionsCard}>
<Text style={styles.webSectionHeading}>{sectionTitle}</Text>
<View style={styles.webOptionsGrid}>
{shareOptions.map((option, index) => {
const Icon = option?.Icon ?? Feather
return (
<Pressable
key={option?.key || index}
onPress={() => handleShareOption(option)}
style={styles.webOptionRow}
>
<View style={styles.webOptionIcon}>
<Icon size={18} color={Palette.white} />
</View>
<Text style={styles.webOptionLabel}>{option?.label}</Text>
<Feather name="chevron-right" size={16} color="rgba(255, 255, 255, 0.6)" />
</Pressable>
)
})}
</View>
</View>
</View>
</View>
) : (
mobileContent
)}
</AppActionSheet>
)
}
const styles = StyleSheet.create({
container: {
gap: 24,
},
card: {
backgroundColor: Palette.glass,
overflow: 'hidden',
borderRadius: 24,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
paddingHorizontal: 20,
paddingVertical: 18,
gap: 16,
},
blurCard: {
overflow: 'hidden',
position: 'relative',
},
closeButton: {
position: 'absolute',
top: 16,
right: 16,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.12)',
zIndex: 2,
},
heading: {
fontSize: 16,
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,
},
optionBlur: {
borderRadius: 18,
overflow: 'hidden',
},
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,
},
webContainer: {
gap: 24,
},
webHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
},
webHeading: {
flex: 1,
fontSize: 24,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
},
webCloseButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
alignItems: 'center',
justifyContent: 'center',
},
webSubheading: {
fontSize: 14,
color: 'rgba(255, 255, 255, 0.65)',
fontFamily: FONT_FAMILY.InterMedium,
},
webContent: {
gap: 20,
},
webLinkCard: {
backgroundColor: 'rgba(255, 255, 255, 0.06)',
borderRadius: 20,
paddingHorizontal: 20,
paddingVertical: 18,
gap: 16,
},
webLinkLabel: {
color: Palette.white,
fontSize: 15,
fontFamily: FONT_FAMILY.InterMedium,
overflow: 'hidden',
},
webLinkActions: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
},
webActionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
paddingHorizontal: 18,
paddingVertical: 12,
borderRadius: 16,
},
webPrimaryButton: {
backgroundColor: Palette.primary,
},
webSecondaryButton: {
backgroundColor: 'rgba(255, 255, 255, 0.08)',
},
webActionButtonActive: {
backgroundColor: Palette.darkRadioactivGreen,
},
webActionLabel: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
},
webOptionsCard: {
backgroundColor: 'rgba(255, 255, 255, 0.06)',
borderRadius: 20,
paddingHorizontal: 20,
paddingVertical: 22,
gap: 16,
},
webSectionHeading: {
fontSize: 18,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
},
webOptionsGrid: {
gap: 12,
},
webOptionRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
backgroundColor: 'rgba(255, 255, 255, 0.04)',
borderRadius: 16,
paddingHorizontal: 18,
paddingVertical: 14,
},
webOptionIcon: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
marginRight: 4,
},
webOptionLabel: {
flex: 1,
fontSize: 15,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
},
})
export default ShareModal