From fc17b09e7ad21dc0f90b1215b94f7c2efddcf88f Mon Sep 17 00:00:00 2001 From: leon-morival Date: Tue, 30 Sep 2025 11:24:00 +0200 Subject: [PATCH] playbacks web --- cors.json | 8 + metro.config.js | 7 + src/config/initialGlobalState.js | 3 +- src/layouts/AppLayout.js | 12 +- src/navigation/BottomTab.js | 2 +- src/screens/{ => Playbacks}/Playbacks.js | 22 +- src/screens/Playbacks/Playbacks.web.js | 1029 ++++++++++++++++++++++ 7 files changed, 1068 insertions(+), 15 deletions(-) create mode 100644 cors.json rename src/screens/{ => Playbacks}/Playbacks.js (96%) create mode 100644 src/screens/Playbacks/Playbacks.web.js diff --git a/cors.json b/cors.json new file mode 100644 index 0000000..86a2ba4 --- /dev/null +++ b/cors.json @@ -0,0 +1,8 @@ +[ + { + "origin": ["http://localhost:8081", "https://musicland-one.vercel.app/"], + "method": ["GET", "HEAD", "PUT", "POST", "DELETE"], + "responseHeader": ["Content-Type", "Authorization"], + "maxAgeSeconds": 3600 + } +] diff --git a/metro.config.js b/metro.config.js index 10e63de..dd2180d 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,6 +1,13 @@ const { getDefaultConfig } = require("@expo/metro-config"); const config = getDefaultConfig(__dirname); + +// Vérifier que .js est bien présent +if (!config.resolver.sourceExts.includes("js")) { + config.resolver.sourceExts.push("js"); +} + +// Ajouter ce dont tu as besoin config.resolver.sourceExts.push("cjs", "mjs"); module.exports = config; diff --git a/src/config/initialGlobalState.js b/src/config/initialGlobalState.js index 2b59ec1..62a8275 100644 --- a/src/config/initialGlobalState.js +++ b/src/config/initialGlobalState.js @@ -1,4 +1,4 @@ -import { Palette } from '../styles'; +import { Palette } from "../styles"; export default { currentUID: null, @@ -6,6 +6,7 @@ export default { currentUserRoles: [], webBackgroundImg: null, + webLayoutMode: "default", _config: { colors: { diff --git a/src/layouts/AppLayout.js b/src/layouts/AppLayout.js index 99cea82..a04586a 100644 --- a/src/layouts/AppLayout.js +++ b/src/layouts/AppLayout.js @@ -6,11 +6,13 @@ import useLayoutType from "../hooks/useLayoutType"; import { background as backgrounds } from "../assets"; import { Style } from "../styles"; +// eslint-disable-next-line react/display-name export default ({ currentUID = null, children }) => { const { isWeb = false } = useLayoutType(); // Web: full-screen background with a centered content area (30% width) const [webBackgroundImg] = useGlobal("webBackgroundImg"); + const [webLayoutMode] = useGlobal("webLayoutMode"); if (isWeb) { return ( @@ -39,9 +41,15 @@ export default ({ currentUID = null, children }) => { {/* Centered content column (phone-like width) */} { const { currentUID, followUser, unfollowUser } = useUser() || {}; diff --git a/src/screens/Playbacks/Playbacks.web.js b/src/screens/Playbacks/Playbacks.web.js new file mode 100644 index 0000000..3e752ef --- /dev/null +++ b/src/screens/Playbacks/Playbacks.web.js @@ -0,0 +1,1029 @@ +import { useIsFocused, useRoute } from "@react-navigation/native"; +import { useAudioPlayer } from "expo-audio"; +import { BlurView } from "expo-blur"; +import { Image as ExpoImage } from "expo-image"; +import { VideoView, useVideoPlayer } from "expo-video"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Image, + Platform, + Pressable, + ScrollView, + Share, + StyleSheet, + Text, + TextInput, + View, + useWindowDimensions, +} from "react-native"; +import Carousel from "react-native-reanimated-carousel"; +import { responsiveHeight } from "react-native-responsive-dimensions"; +import { setGlobal } from "reactn"; +import { icons, img } from "../../assets"; +import KaraokeLyrics from "../../components/KaraokeLyrics"; +import { + arrayRemove, + arrayUnion, + increment, + projectsRef, + serverTimestamp, + usersRef, +} from "../../config/firebase"; +import useDataFromRef from "../../hooks/useDataFromRef"; +import { Routes } from "../../navigation"; +import { navigate } from "../../navigation/NavigationService"; +import { useUser } from "../../providers/UserDataProvider"; +import { Palette } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; +import { size } from "../../styles/Style"; + +const formatRelativeTime = (date) => { + try { + const d = date instanceof Date ? date : date?.toDate?.() || null; + if (!d) return ""; + const diff = Math.max(0, Date.now() - d.getTime()); + const sec = Math.floor(diff / 1000); + if (sec < 60) return `${sec}s`; + const min = Math.floor(sec / 60); + if (min < 60) return `${min}min`; + const h = Math.floor(min / 60); + if (h < 24) return `${h}h`; + const dA = Math.floor(h / 24); + return `${dA}j`; + } catch (_e) { + return ""; + } +}; + +const CommentsPanel = ({ + projectId, + description, + commentsCount, + onCommentAdded, + inputRef, +}) => { + const { currentUID, currentUserData } = useUser() || {}; + const [text, setText] = useState(""); + const scrollRef = useRef(null); + + useEffect(() => { + setText(""); + try { + scrollRef.current?.scrollTo({ y: 0, animated: false }); + } catch (_e) {} + }, [projectId]); + + const commentsRef = useMemo(() => { + try { + return projectId + ? projectsRef + .doc(projectId) + .collection("comments") + .orderBy("createdAt", "desc") + : null; + } catch (_e) { + return null; + } + }, [projectId]); + + const { + data: comments = [], + setData: setComments, + loadMore, + } = useDataFromRef({ + ref: commentsRef, + simpleRef: false, + listener: false, + condition: !!commentsRef, + refreshArray: [projectId], + usePagination: true, + batchSize: 20, + }); + + const handleScroll = useCallback( + ({ nativeEvent }) => { + try { + const { layoutMeasurement, contentOffset, contentSize } = + nativeEvent || {}; + if ( + layoutMeasurement?.height + contentOffset?.y >= + (contentSize?.height || 0) - 120 + ) { + loadMore?.(); + } + } catch (_e) {} + }, + [loadMore] + ); + + const onSend = useCallback(async () => { + const value = (text || "").trim(); + if (!value || !projectId || !currentUID) return; + try { + setText(""); + const docRef = await projectsRef + .doc(projectId) + .collection("comments") + .add({ + userId: currentUID, + userName: currentUserData?.userName || "", + profilePicture: currentUserData?.profilePictureURL || "", + text: value, + createdAt: serverTimestamp(), + }); + try { + await projectsRef + .doc(projectId) + .set({ commentsCount: increment(1) }, { merge: true }); + } catch (_e) {} + const optimistic = { + id: docRef?.id || Math.random().toString(36).slice(2), + userId: currentUID, + userName: currentUserData?.userName || "", + profilePicture: currentUserData?.profilePictureURL || "", + text: value, + createdAt: new Date(), + }; + setComments((prev = []) => [ + optimistic, + ...prev.filter((x) => x?.id !== optimistic.id), + ]); + onCommentAdded?.(); + requestAnimationFrame(() => { + try { + scrollRef.current?.scrollTo({ y: 0, animated: true }); + } catch (_e) {} + }); + } catch (_e) {} + }, [ + currentUID, + currentUserData, + onCommentAdded, + projectId, + setComments, + text, + ]); + + const canComment = !!currentUID; + const descriptionText = (description || "").trim(); + + return ( + + + Description + + + {descriptionText || "Description chanson..."} + + + + {`Commentaires${commentsCount ? ` (${commentsCount})` : ""}`} + + {Array.isArray(comments) && comments.length > 0 ? ( + comments.map((c, index) => ( + 0 && styles.commentRowSpacing]} + > + {c?.profilePicture ? ( + + ) : ( + + )} + + + + {c?.userId === currentUID ? "vous" : c?.userName || ""} + + + • {formatRelativeTime(c?.createdAt)} + + + {c?.text} + + + )) + ) : ( + + Aucun commentaire pour le moment + + )} + + + + [ + styles.sendButton, + (!canComment || !text.trim()) && styles.sendButtonDisabled, + pressed && canComment && styles.sendButtonPressed, + ]} + > + + + + + ); +}; + +const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { + const { width: viewportWidth } = useWindowDimensions(); + const [layoutWidth, setLayoutWidth] = useState(viewportWidth); + const commentInputRef = useRef(null); + const { currentUID, followUser, unfollowUser } = useUser() || {}; + const videoUrl = item?.playbackUrl || null; + const audioSource = useMemo(() => { + const fromSong = item?.songUrl ? { uri: item.songUrl } : null; + return fromSong; + }, [item]); + + const hasExternalAudio = !!audioSource; + + const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : []; + const [isLiked, setIsLiked] = useState( + currentUID ? initialLikedBy.includes(currentUID) : false + ); + const [likesCount, setLikesCount] = useState(initialLikedBy.length); + + const computedCommentsCount = useMemo( + () => Number(item?.commentsCount || 0), + [item?.commentsCount] + ); + const [commentsCount, setCommentsCount] = useState(computedCommentsCount); + useEffect(() => { + setCommentsCount(computedCommentsCount); + }, [computedCommentsCount]); + + const [owner, setOwner] = useState( + item?.userId && userCache?.current?.get(item.userId) + ? userCache.current.get(item.userId) + : null + ); + useEffect(() => { + let cancelled = false; + const run = async () => { + try { + const uid = item?.userId; + if (!uid || !userCache) return; + const cached = userCache.current.get(uid); + if (cached) { + if (!cancelled) setOwner(cached); + return; + } + const user = await getUserByUid?.(uid); + if (!cancelled && user) { + userCache.current.set(uid, user); + setOwner(user); + } + } catch (_e) {} + }; + run(); + return () => { + cancelled = true; + }; + }, [item?.userId, userCache, getUserByUid]); + + useEffect(() => { + const uid = item?.userId; + if (!uid) return; + const unsub = usersRef.doc(uid).onSnapshot( + (doc) => { + if (doc?.exists) { + const data = { id: doc.id, ...doc.data() }; + setOwner(data); + try { + userCache?.current?.set(uid, data); + } catch (_e) {} + } + }, + () => {} + ); + return () => unsub?.(); + }, [item?.userId, userCache]); + + const [isFollowing, setIsFollowing] = useState(false); + useEffect(() => { + const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []; + setIsFollowing(currentUID ? list.includes(currentUID) : false); + }, [owner?.followedBy, currentUID]); + + useEffect(() => { + const lb = Array.isArray(item?.likedBy) ? item.likedBy : []; + setLikesCount(lb.length); + setIsLiked(currentUID ? lb.includes(currentUID) : false); + }, [item?.likedBy, currentUID]); + + const audioPlayer = useAudioPlayer(audioSource || undefined); + const videoPlayer = useVideoPlayer(videoUrl || null, (p) => { + p.loop = false; + p.muted = true; + p.timeUpdateEventInterval = 0.2; + }); + + useEffect(() => { + const toggle = async () => { + try { + if (isActive) { + try { + if (audioPlayer && hasExternalAudio) await audioPlayer.seekTo?.(0); + } catch (_e) {} + try { + if (videoPlayer) videoPlayer.currentTime = 0; + } catch (_e) {} + + try { + if (videoPlayer) videoPlayer.play(); + } catch (_e) {} + try { + if (audioPlayer && hasExternalAudio) audioPlayer.play?.(); + } catch (_e) {} + } else { + if (audioPlayer?.playing) await audioPlayer.pause?.(); + if (videoPlayer?.playing) videoPlayer.pause(); + } + } catch (_e) {} + }; + toggle(); + }, [isActive, audioPlayer, videoPlayer, hasExternalAudio]); + + useEffect(() => { + if (!isActive || !hasExternalAudio) return; + const t = setTimeout(() => { + try { + const a = audioPlayer?.currentTime || 0; + const v = videoPlayer?.currentTime || 0; + if (Math.abs(a - v) > 0.2 && videoPlayer) { + videoPlayer.currentTime = Math.max(0, a); + } + } catch (_e) {} + }, 300); + return () => clearTimeout(t); + }, [isActive, hasExternalAudio, audioPlayer, videoPlayer]); + + useEffect(() => { + return () => { + try { + if (audioPlayer?.playing) audioPlayer.pause?.(); + if (videoPlayer?.playing) videoPlayer.pause(); + } catch (_e) {} + }; + }, [audioPlayer, videoPlayer]); + + const alignedWords = useMemo(() => { + const idx = Number(item?.songIndex) || 0; + const ts = item?.musicTimestamps?.[idx]; + + const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []; + return arr.map((w) => ({ + word: String(w?.word ?? ""), + startS: Number(w?.startS ?? 0), + endS: Number(w?.endS ?? 0), + })); + }, [item?.musicTimestamps, item?.songIndex]); + + const [currentTimeS, setCurrentTimeS] = useState(0); + useEffect(() => { + if (!isActive) return; + const id = setInterval(() => { + try { + const t = hasExternalAudio + ? Number(audioPlayer?.currentTime || 0) + : Number(videoPlayer?.currentTime || 0); + setCurrentTimeS(t); + } catch (_e) {} + }, 250); + return () => clearInterval(id); + }, [isActive, hasExternalAudio, audioPlayer, videoPlayer]); + + const descriptionText = + item?.description || item?.title || "Description chanson"; + + const handleLayout = useCallback((event) => { + const nextWidth = event?.nativeEvent?.layout?.width; + if (typeof nextWidth === "number" && nextWidth > 0) { + setLayoutWidth(nextWidth); + } + }, []); + + const widePaddingThreshold = 900; + const horizontalPadding = layoutWidth >= widePaddingThreshold ? 64 : 32; + const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0); + const gapBetweenColumns = 40; + const minVideoWidth = 340; + const maxVideoWidth = 620; + const minCommentsWidth = 260; + const maxCommentsWidth = 420; + + let sideBySide = innerWidth >= 620; + let videoWidth = sideBySide + ? Math.min(maxVideoWidth, Math.max(innerWidth * 0.56, minVideoWidth)) + : Math.min(innerWidth, maxVideoWidth); + let commentsWidth = sideBySide + ? innerWidth - videoWidth - gapBetweenColumns + : innerWidth; + + if (sideBySide && commentsWidth > maxCommentsWidth) { + videoWidth = Math.min( + maxVideoWidth, + videoWidth + (commentsWidth - maxCommentsWidth) + ); + commentsWidth = maxCommentsWidth; + } + + if (sideBySide && commentsWidth < minCommentsWidth) { + const deficit = minCommentsWidth - commentsWidth; + if (videoWidth - deficit >= minVideoWidth) { + videoWidth -= deficit; + commentsWidth = minCommentsWidth; + } else { + sideBySide = false; + videoWidth = Math.min(innerWidth, maxVideoWidth); + commentsWidth = innerWidth; + } + } + + const baseInnerWidth = innerWidth > 0 ? innerWidth : viewportWidth; + const safeVideoWidth = Math.max( + minVideoWidth, + Math.min( + Number.isFinite(videoWidth) ? videoWidth : baseInnerWidth, + maxVideoWidth + ) + ); + const videoCardWidth = sideBySide + ? safeVideoWidth + : Math.min(Math.max(baseInnerWidth * 0.88, minVideoWidth), maxVideoWidth); + const safeCommentsWidth = sideBySide + ? Math.max( + minCommentsWidth, + Math.min( + Number.isFinite(commentsWidth) ? commentsWidth : baseInnerWidth, + maxCommentsWidth + ) + ) + : 0; + + return ( + + + + + {!!videoUrl ? ( + + ) : ( + + )} + + + { + navigate(Routes.SingerProfile, { userId: item?.userId }); + }} + > + {owner?.profilePictureURL ? ( + + ) : ( + + )} + + {owner?.id && currentUID && owner.id !== currentUID && ( + { + try { + const next = !isFollowing; + setIsFollowing(next); + setOwner((prev) => { + const fb = Array.isArray(prev?.followedBy) + ? prev.followedBy + : []; + const newFb = next + ? Array.from(new Set([...fb, currentUID])) + : fb.filter((x) => x !== currentUID); + return prev ? { ...prev, followedBy: newFb } : prev; + }); + if (next) await followUser?.(owner.id); + else await unfollowUser?.(owner.id); + } catch (_e) { + setIsFollowing((v) => !v); + } + }} + > + + + {isFollowing ? "Ne plus suivre" : "Suivre"} + + + + )} + + { + try { + if (!currentUID || !item?.id) return; + const nextLiked = !isLiked; + setIsLiked(nextLiked); + setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1))); + + const ref = projectsRef.doc(item.id); + await ref.set( + { + likedBy: nextLiked + ? arrayUnion(currentUID) + : arrayRemove(currentUID), + }, + { merge: true } + ); + } catch (_e) { + setIsLiked((v) => !v); + setLikesCount((c) => + isLiked ? c + 1 : Math.max(0, c - 1) + ); + } + }} + style={[styles.actionButton, styles.actionSpacing]} + > + + {!!likesCount && ( + {likesCount} + )} + + commentInputRef.current?.focus?.()} + style={[styles.actionButton, styles.actionSpacing]} + > + + {!!commentsCount && ( + {commentsCount} + )} + + { + try { + const url = item?.playbackUrl || ""; + const title = item?.title || "Partager"; + const base = item?.title + ? `Découvre « ${item.title} » sur MusicLand` + : `Découvre ce playback sur MusicLand`; + const message = url ? `${base}\n${url}` : base; + + await Share.share( + Platform.select({ + ios: url ? { url, message, title } : { message, title }, + default: { message, title }, + }) + ); + } catch (_e) {} + }} + style={[styles.actionButton, styles.actionSpacing]} + > + + + + + + {alignedWords?.length > 0 ? ( + + ) : ( + {descriptionText} + )} + + + + + + + + setCommentsCount((c) => Math.max(0, Number(c || 0) + 1)) + } + inputRef={commentInputRef} + /> + + + ); +}; + +const Playbacks = () => { + const { width: windowWidth, height: windowHeight } = useWindowDimensions(); + const [activeIndex, setActiveIndex] = useState(0); + const carouselRef = useRef(null); + const route = useRoute(); + const focusProjectId = + route?.params?.projectId || route?.params?.focusId || null; + const userCache = useRef(new Map()); + const { getUserByUid } = useUser() || {}; + const isFocused = useIsFocused(); + const { data: playbacks = [], loadMore } = useDataFromRef({ + ref: projectsRef.where("playbackUrl", "!=", null), + simpleRef: false, + listener: false, + usePagination: true, + batchSize: 6, + }); + + useEffect(() => { + if (Platform.OS === "web") { + setGlobal({ webLayoutMode: "playbacks-wide" }); + return () => setGlobal({ webLayoutMode: "default" }); + } + return undefined; + }, []); + + const onSnap = useCallback( + (index) => { + setActiveIndex(index); + if (index >= (playbacks?.length || 0) - 6) { + loadMore?.(); + } + }, + [playbacks?.length, loadMore] + ); + + const triedLoadMoreRef = useRef(0); + useEffect(() => { + if (!focusProjectId) return; + const idx = playbacks.findIndex((p) => p?.id === focusProjectId); + if (idx >= 0) { + setActiveIndex(idx); + setTimeout(() => { + try { + carouselRef.current?.scrollTo?.({ index: idx, animated: false }); + } catch (_e) {} + }, 50); + } else if (loadMore && triedLoadMoreRef.current < 6) { + triedLoadMoreRef.current += 1; + loadMore(); + } + }, [focusProjectId, playbacks, loadMore]); + + return ( + + ( + + )} + /> + + ); +}; + +export default Playbacks; + +const styles = StyleSheet.create({ + screen: { + flex: 1, + // backgroundColor: Palette.darkPurple, + }, + itemContainerBase: { + width: "100%", + // backgroundColor: Palette.darkPurple, + paddingVertical: 36, + alignItems: "center", + }, + itemContainerRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + }, + itemContainerCompact: { + flexDirection: "column", + alignItems: "center", + justifyContent: "flex-start", + }, + videoColumn: { + alignItems: "center", + justifyContent: "center", + }, + videoSurface: { + alignSelf: "center", + width: "100%", + maxWidth: 620, + aspectRatio: 9 / 16, + borderRadius: 32, + overflow: "hidden", + backgroundColor: "#07040D", + shadowColor: "#000", + shadowOpacity: 0.45, + shadowRadius: 30, + shadowOffset: { width: 0, height: 20 }, + elevation: 12, + }, + videoContainer: { + flex: 1, + position: "relative", + }, + actionStack: { + position: "absolute", + right: 18, + bottom: 120, + alignItems: "center", + }, + ownerBlock: { + alignItems: "center", + marginBottom: 26, + }, + ownerAvatarButton: { + marginBottom: 16, + }, + ownerAvatar: { + ...size({ size: 56 }), + borderRadius: 100, + }, + followPressable: { + alignSelf: "center", + }, + followButton: { + paddingVertical: 8, + paddingHorizontal: 18, + borderRadius: 14, + borderWidth: 1, + borderColor: Palette.white, + backgroundColor: "#FFFFFF20", + overflow: "hidden", + }, + followButtonText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterMedium, + }, + actionButton: { + alignItems: "center", + justifyContent: "center", + }, + actionSpacing: { + marginTop: 24, + }, + actionIcon: { + ...size({ size: 30 }), + }, + actionLabel: { + marginTop: 6, + fontSize: 13, + color: Palette.white, + fontFamily: FONT_FAMILY.InterMedium, + textAlign: "center", + }, + lyricsContainer: { + position: "absolute", + left: 24, + right: 24, + bottom: 32, + }, + lyricsCard: { + paddingHorizontal: 18, + paddingVertical: 12, + borderRadius: 22, + backgroundColor: Palette.glass, + overflow: "hidden", + }, + lyricsText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + }, + commentsColumn: { + alignSelf: "stretch", + }, + commentsColumnCompact: { + width: "100%", + maxWidth: 640, + marginTop: 32, + }, + commentsWrapper: { + flex: 1, + borderRadius: 28, + overflow: "hidden", + padding: 24, + backgroundColor: Palette.glass, + minHeight: 0, + }, + commentsContent: { + paddingBottom: 24, + }, + sectionTitle: { + fontSize: 18, + color: Palette.white, + fontFamily: FONT_FAMILY.InterSemiBold, + marginBottom: 12, + }, + sectionSpacing: { + marginTop: 24, + }, + descriptionCard: { + borderRadius: 18, + backgroundColor: "#FFFFFF14", + paddingHorizontal: 18, + paddingVertical: 14, + }, + descriptionText: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + lineHeight: 20, + }, + commentRow: { + flexDirection: "row", + alignItems: "flex-start", + }, + commentRowSpacing: { + marginTop: 16, + }, + commentAvatar: { + ...size({ size: 48 }), + borderRadius: 100, + marginRight: 12, + }, + commentAvatarFallback: { + ...size({ size: 48 }), + borderRadius: 100, + backgroundColor: "#FFFFFF33", + marginRight: 12, + }, + commentBubble: { + flex: 1, + backgroundColor: "#FFFFFF18", + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 12, + }, + commentHeader: { + flexDirection: "row", + alignItems: "center", + marginBottom: 4, + }, + commentAuthor: { + fontSize: 13, + color: Palette.white, + fontFamily: FONT_FAMILY.InterMedium, + }, + commentMeta: { + fontSize: 12, + color: "#FFFFFFA0", + fontFamily: FONT_FAMILY.InterRegular, + marginLeft: 8, + }, + commentBody: { + fontSize: 14, + color: Palette.white, + fontFamily: FONT_FAMILY.InterRegular, + lineHeight: 20, + }, + emptyState: { + fontSize: 14, + color: "#FFFFFFBB", + fontFamily: FONT_FAMILY.InterRegular, + }, + commentInputContainer: { + marginTop: 20, + flexDirection: "row", + alignItems: "center", + backgroundColor: "#FFFFFF14", + borderRadius: 18, + paddingHorizontal: 18, + paddingVertical: 10, + }, + commentInput: { + flex: 1, + color: Palette.white, + fontSize: 14, + fontFamily: FONT_FAMILY.InterRegular, + minHeight: 36, + marginRight: 12, + }, + sendButton: { + ...size({ size: 38 }), + borderRadius: 19, + alignItems: "center", + justifyContent: "center", + backgroundColor: Palette.primary, + }, + sendButtonPressed: { + transform: [{ scale: 0.95 }], + }, + sendButtonDisabled: { + backgroundColor: "#FFFFFF33", + }, + sendIcon: { + width: 22, + height: 22, + tintColor: Palette.white, + }, +});