feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+49 -50
View File
@@ -1,83 +1,82 @@
import { useIsFocused, useRoute } from "@react-navigation/native";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import Carousel from "react-native-reanimated-carousel";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import PlaybackItem from "./components/PlaybackItem";
import { useIsFocused, useRoute } from '@react-navigation/native'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { View } from 'react-native'
import Carousel from 'react-native-reanimated-carousel'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { projectsRef } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import PlaybackItem from './components/PlaybackItem'
const Playbacks = () => {
const [activeIndex, setActiveIndex] = useState(0);
const userCache = useRef(new Map());
const isFocused = useIsFocused();
const route = useRoute();
const focusProjectId =
route?.params?.projectId || route?.params?.focusId || null;
const [activeIndex, setActiveIndex] = useState(0)
const userCache = useRef(new Map())
const isFocused = useIsFocused()
const route = useRoute()
const focusProjectId = route?.params?.projectId || route?.params?.focusId || null
const { data: rawPlaybacks = [], loadMore } = useDataFromRef({
ref: projectsRef.where("playbackUrl", "!=", null),
ref: projectsRef.where('playbackUrl', '!=', null),
simpleRef: false,
listener: false,
usePagination: true,
batchSize: 6,
});
})
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1;
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId);
}, [focusProjectId, rawPlaybacks]);
if (!focusProjectId) return -1
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [focusProjectId, rawPlaybacks])
const playbacks = useMemo(() => {
if (focusIndex < 0) return rawPlaybacks;
const target = rawPlaybacks[focusIndex];
const before = rawPlaybacks.slice(0, focusIndex);
const after = rawPlaybacks.slice(focusIndex + 1);
return [target, ...after, ...before];
}, [focusIndex, rawPlaybacks]);
if (focusIndex < 0) return rawPlaybacks
const target = rawPlaybacks[focusIndex]
const before = rawPlaybacks.slice(0, focusIndex)
const after = rawPlaybacks.slice(focusIndex + 1)
return [target, ...after, ...before]
}, [focusIndex, rawPlaybacks])
const onSnap = useCallback(
(index) => {
setActiveIndex(index);
setActiveIndex(index)
if (index >= (playbacks?.length || 0) - 6) {
loadMore?.();
loadMore?.()
}
},
[playbacks?.length, loadMore],
);
[playbacks?.length, loadMore]
)
const triedLoadMoreRef = useRef(0);
const focusLoadAttemptsRef = useRef(0);
const hasAppliedFocusRef = useRef(false);
const triedLoadMoreRef = useRef(0)
const focusLoadAttemptsRef = useRef(0)
const hasAppliedFocusRef = useRef(false)
useEffect(() => {
focusLoadAttemptsRef.current = 0;
hasAppliedFocusRef.current = false;
focusLoadAttemptsRef.current = 0
hasAppliedFocusRef.current = false
if (!focusProjectId) {
setActiveIndex(0);
setActiveIndex(0)
}
}, [focusProjectId]);
}, [focusProjectId])
useEffect(() => {
if (loadMore && triedLoadMoreRef.current < 6) {
triedLoadMoreRef.current += 1;
loadMore();
triedLoadMoreRef.current += 1
loadMore()
}
}, [loadMore, playbacks]);
}, [loadMore, playbacks])
useEffect(() => {
if (!focusProjectId) return;
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
hasAppliedFocusRef.current = true;
setActiveIndex(0);
return;
hasAppliedFocusRef.current = true
setActiveIndex(0)
return
}
if (loadMore && focusLoadAttemptsRef.current < 6) {
focusLoadAttemptsRef.current += 1;
loadMore();
focusLoadAttemptsRef.current += 1
loadMore()
}
}, [focusIndex, focusProjectId, loadMore, playbacks]);
}, [focusIndex, focusProjectId, loadMore, playbacks])
return (
<View style={{ flex: 1, backgroundColor: "black" }}>
<View style={{ flex: 1, backgroundColor: 'black' }}>
<Carousel
data={playbacks}
vertical
@@ -95,7 +94,7 @@ const Playbacks = () => {
)}
/>
</View>
);
};
)
}
export default Playbacks;
export default Playbacks
+187 -221
View File
@@ -1,234 +1,217 @@
import { useIsFocused, useRoute } from "@react-navigation/native";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FlatList,
Image,
Pressable,
StyleSheet,
View,
useWindowDimensions,
} from "react-native";
import { icons } from "../../assets";
import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import PlaybackItem from "./components/PlaybackItem.web";
import { useIsFocused, useRoute } from '@react-navigation/native'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FlatList, Image, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native'
import { icons } from '../../assets'
import { projectsRef } from '../../config/firebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import { useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import PlaybackItem from './components/PlaybackItem.web'
const Playbacks = () => {
const { height: windowHeight } = useWindowDimensions();
const [activeIndex, setActiveIndex] = useState(0);
const route = useRoute();
const focusProjectId =
route?.params?.projectId || route?.params?.focusId || null;
const userCache = useRef(new Map());
const { getUserByUid } = useUser() || {};
const isFocused = useIsFocused();
const { height: windowHeight } = useWindowDimensions()
const [activeIndex, setActiveIndex] = useState(0)
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: rawPlaybacks = [],
loadMore,
hasMore,
loading,
} = useDataFromRef({
ref: projectsRef.where("playbackUrl", "!=", null),
ref: projectsRef.where('playbackUrl', '!=', null),
simpleRef: false,
listener: false,
usePagination: true,
batchSize: 6,
});
})
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1;
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId);
}, [focusProjectId, rawPlaybacks]);
if (!focusProjectId) return -1
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [focusProjectId, rawPlaybacks])
const playbacks = useMemo(() => {
if (focusIndex < 0) return rawPlaybacks;
const target = rawPlaybacks[focusIndex];
const before = rawPlaybacks.slice(0, focusIndex);
const after = rawPlaybacks.slice(focusIndex + 1);
return [target, ...after, ...before];
}, [focusIndex, rawPlaybacks]);
if (focusIndex < 0) return rawPlaybacks
const target = rawPlaybacks[focusIndex]
const before = rawPlaybacks.slice(0, focusIndex)
const after = rawPlaybacks.slice(focusIndex + 1)
return [target, ...after, ...before]
}, [focusIndex, rawPlaybacks])
const activePlayback = playbacks?.[activeIndex] || null;
const activeVideoUrl = activePlayback?.playbackUrl || null;
const activeTitle =
typeof activePlayback?.title === "string"
? activePlayback.title.trim()
: "";
const activePlayback = playbacks?.[activeIndex] || null
const activeVideoUrl = activePlayback?.playbackUrl || null
const activeTitle = typeof activePlayback?.title === 'string' ? activePlayback.title.trim() : ''
const activeCreatorName = activePlayback?.userName;
const backgroundVideoRef = useRef(null);
const lastActiveIndexRef = useRef(0);
const backgroundSeekPendingRef = useRef(false);
const listRef = useRef(null);
const activeCreatorName = activePlayback?.userName
const backgroundVideoRef = useRef(null)
const lastActiveIndexRef = useRef(0)
const backgroundSeekPendingRef = useRef(false)
const listRef = useRef(null)
useEffect(() => {
const total = playbacks?.length || 0;
const total = playbacks?.length || 0
if (total <= 0) {
if (activeIndex !== 0) {
lastActiveIndexRef.current = 0;
setActiveIndex(0);
lastActiveIndexRef.current = 0
setActiveIndex(0)
}
return;
return
}
const maxIndex = total - 1;
const maxIndex = total - 1
if (activeIndex > maxIndex) {
lastActiveIndexRef.current = maxIndex;
setActiveIndex(maxIndex);
lastActiveIndexRef.current = maxIndex
setActiveIndex(maxIndex)
}
}, [playbacks?.length, activeIndex]);
}, [playbacks?.length, activeIndex])
const handleActiveIndexChange = useCallback(
(rawIndex) => {
if (Number.isNaN(rawIndex)) return;
const total = playbacks?.length || 0;
const maxIndex = Math.max(0, total - 1);
const clamped = Math.max(0, Math.min(rawIndex, maxIndex));
if (clamped === lastActiveIndexRef.current) return;
lastActiveIndexRef.current = clamped;
setActiveIndex((prev) => (prev === clamped ? prev : clamped));
if (Number.isNaN(rawIndex)) return
const total = playbacks?.length || 0
const maxIndex = Math.max(0, total - 1)
const clamped = Math.max(0, Math.min(rawIndex, maxIndex))
if (clamped === lastActiveIndexRef.current) return
lastActiveIndexRef.current = clamped
setActiveIndex((prev) => (prev === clamped ? prev : clamped))
if (total > 0 && clamped >= Math.max(0, total - 6)) {
loadMore?.();
loadMore?.()
}
},
[playbacks?.length, loadMore],
);
[playbacks?.length, loadMore]
)
const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => {
const bg = backgroundVideoRef.current;
if (!bg) return;
const bg = backgroundVideoRef.current
if (!bg) return
if (typeof currentTime === "number" && Number.isFinite(currentTime)) {
if (typeof currentTime === 'number' && Number.isFinite(currentTime)) {
const applyTime = () => {
backgroundSeekPendingRef.current = false;
const diff = Math.abs((bg.currentTime || 0) - currentTime);
backgroundSeekPendingRef.current = false
const diff = Math.abs((bg.currentTime || 0) - currentTime)
if (diff > 0.25) {
try {
bg.currentTime = currentTime;
bg.currentTime = currentTime
} catch (_e) {}
}
};
}
if (bg.readyState >= 1) applyTime();
if (bg.readyState >= 1) applyTime()
else if (!backgroundSeekPendingRef.current) {
backgroundSeekPendingRef.current = true;
const handler = () => applyTime();
bg.addEventListener("loadeddata", handler, { once: true });
backgroundSeekPendingRef.current = true
const handler = () => applyTime()
bg.addEventListener('loadeddata', handler, { once: true })
}
}
if (isPlaying === true) {
if (bg.paused) {
bg.play().catch(() => {});
bg.play().catch(() => {})
}
} else if (isPlaying === false) {
try {
if (!bg.paused) bg.pause();
if (!bg.paused) bg.pause()
} catch (_e) {}
}
}, []);
}, [])
useEffect(() => {
const bg = backgroundVideoRef.current;
if (!bg) return;
const bg = backgroundVideoRef.current
if (!bg) return
if (!activeVideoUrl) {
backgroundSeekPendingRef.current = false;
backgroundSeekPendingRef.current = false
try {
bg.pause();
bg.pause()
} catch (_e) {}
bg.removeAttribute?.("src");
bg.load?.();
return;
bg.removeAttribute?.('src')
bg.load?.()
return
}
backgroundSeekPendingRef.current = false;
backgroundSeekPendingRef.current = false
const setSrcIfNeeded = () => {
const attrSrc = bg.getAttribute("src");
const attrSrc = bg.getAttribute('src')
if (attrSrc !== activeVideoUrl) {
bg.setAttribute("src", activeVideoUrl);
bg.setAttribute('src', activeVideoUrl)
try {
bg.load();
bg.load()
} catch (_e) {}
}
};
}
const startPlayback = () => {
bg.play().catch(() => {});
};
bg.play().catch(() => {})
}
setSrcIfNeeded();
setSrcIfNeeded()
if (bg.readyState >= 2) {
startPlayback();
return;
startPlayback()
return
}
const handleLoaded = () => {
startPlayback();
};
bg.addEventListener("loadeddata", handleLoaded, { once: true });
startPlayback()
}
bg.addEventListener('loadeddata', handleLoaded, { once: true })
return () => {
bg.removeEventListener("loadeddata", handleLoaded);
};
}, [activeVideoUrl]);
bg.removeEventListener('loadeddata', handleLoaded)
}
}, [activeVideoUrl])
const triedLoadMoreRef = useRef(0);
const hasAppliedFocusRef = useRef(false);
const focusLoadAttemptsRef = useRef(0);
const triedLoadMoreRef = useRef(0)
const hasAppliedFocusRef = useRef(false)
const focusLoadAttemptsRef = useRef(0)
useEffect(() => {
hasAppliedFocusRef.current = false;
focusLoadAttemptsRef.current = 0;
}, [focusProjectId]);
hasAppliedFocusRef.current = false
focusLoadAttemptsRef.current = 0
}, [focusProjectId])
useEffect(() => {
if (!focusProjectId) return;
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
hasAppliedFocusRef.current = true;
lastActiveIndexRef.current = 0;
setActiveIndex(0);
hasAppliedFocusRef.current = true
lastActiveIndexRef.current = 0
setActiveIndex(0)
requestAnimationFrame(() => {
try {
listRef.current?.scrollToOffset?.({ offset: 0, animated: false });
listRef.current?.scrollToOffset?.({ offset: 0, animated: false })
} catch (_e) {}
});
return;
})
return
}
if (loadMore && focusLoadAttemptsRef.current < 6) {
focusLoadAttemptsRef.current += 1;
loadMore();
focusLoadAttemptsRef.current += 1
loadMore()
}
}, [focusIndex, focusProjectId, loadMore, playbacks]);
}, [focusIndex, focusProjectId, loadMore, playbacks])
// === FlatList (one real page per item) ===
// keep active index in sync with scroll
const onMomentumScrollEnd = useCallback(
(e) => {
try {
const y = e?.nativeEvent?.contentOffset?.y || 0;
const idx = Math.round(y / windowHeight);
handleActiveIndexChange(idx);
const y = e?.nativeEvent?.contentOffset?.y || 0
const idx = Math.round(y / windowHeight)
handleActiveIndexChange(idx)
} catch (_e) {}
},
[windowHeight, handleActiveIndexChange],
);
[windowHeight, handleActiveIndexChange]
)
const onScroll = useCallback(
(e) => {
try {
const y = e?.nativeEvent?.contentOffset?.y || 0;
const idx = Math.round(y / windowHeight);
handleActiveIndexChange(idx);
const y = e?.nativeEvent?.contentOffset?.y || 0
const idx = Math.round(y / windowHeight)
handleActiveIndexChange(idx)
} catch (_e) {}
},
[windowHeight, handleActiveIndexChange],
);
[windowHeight, handleActiveIndexChange]
)
const getItemLayout = useCallback(
(_data, index) => ({
@@ -236,90 +219,80 @@ const Playbacks = () => {
offset: windowHeight * index,
index,
}),
[windowHeight],
);
[windowHeight]
)
const total = playbacks.length;
const total = playbacks.length
const indicatorItems = useMemo(() => {
if (total <= 0) return [];
const maxVisible = 7;
let start = 0;
let end = total - 1;
if (total <= 0) return []
const maxVisible = 7
let start = 0
let end = total - 1
if (total > maxVisible) {
const half = Math.floor(maxVisible / 2);
start = activeIndex - half;
end = start + maxVisible - 1;
const half = Math.floor(maxVisible / 2)
start = activeIndex - half
end = start + maxVisible - 1
if (start < 0) {
end += -start;
start = 0;
end += -start
start = 0
}
if (end >= total) {
const overshoot = end - (total - 1);
start = Math.max(0, start - overshoot);
end = total - 1;
const overshoot = end - (total - 1)
start = Math.max(0, start - overshoot)
end = total - 1
}
}
const items = [];
const items = []
for (let i = start; i <= end; i += 1) {
items.push({
key: `indicator-${i}`,
isActive: i === activeIndex,
isPast: i < activeIndex,
});
})
}
return items;
}, [total, activeIndex]);
return items
}, [total, activeIndex])
const canScrollUp = total > 0 && activeIndex > 0;
const atLastLoaded = total > 0 && activeIndex >= total - 1;
const canScrollDown = total > 0 && (!atLastLoaded || hasMore);
const showIndicators = total > 0;
const canScrollUp = total > 0 && activeIndex > 0
const atLastLoaded = total > 0 && activeIndex >= total - 1
const canScrollDown = total > 0 && (!atLastLoaded || hasMore)
const showIndicators = total > 0
const indicatorLabel = useMemo(() => {
if (total <= 0) return null;
if (hasMore || loading) return `${activeIndex + 1}+`;
return `${activeIndex + 1}/${total}`;
}, [total, activeIndex, hasMore, loading]);
if (total <= 0) return null
if (hasMore || loading) return `${activeIndex + 1}+`
return `${activeIndex + 1}/${total}`
}, [total, activeIndex, hasMore, loading])
const scrollToPlayback = useCallback(
(direction) => {
if (!direction || !listRef.current) return;
const totalItems = playbacks.length;
if (totalItems <= 0) return;
const targetIndex = Math.max(
0,
Math.min(activeIndex + direction, totalItems - 1),
);
if (!direction || !listRef.current) return
const totalItems = playbacks.length
if (totalItems <= 0) return
const targetIndex = Math.max(0, Math.min(activeIndex + direction, totalItems - 1))
if (targetIndex === activeIndex) {
if (direction > 0 && hasMore) {
loadMore?.();
loadMore?.()
}
return;
return
}
const targetOffset = targetIndex * windowHeight;
const targetOffset = targetIndex * windowHeight
try {
listRef.current.scrollToOffset({
offset: targetOffset,
animated: true,
});
})
} catch (_e) {
try {
listRef.current.scrollToIndex({
index: targetIndex,
animated: true,
});
})
} catch (__e) {}
}
handleActiveIndexChange(targetIndex);
handleActiveIndexChange(targetIndex)
},
[
activeIndex,
handleActiveIndexChange,
hasMore,
loadMore,
playbacks.length,
windowHeight,
],
);
[activeIndex, handleActiveIndexChange, hasMore, loadMore, playbacks.length, windowHeight]
)
return (
<View style={styles.screen}>
@@ -335,13 +308,13 @@ const Playbacks = () => {
playsInline
preload="auto"
style={{
position: "absolute",
position: 'absolute',
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
filter: "blur(28px) saturate(120%) brightness(55%)",
transform: "scale(1.1)",
width: '100%',
height: '100%',
objectFit: 'cover',
filter: 'blur(28px) saturate(120%) brightness(55%)',
transform: 'scale(1.1)',
}}
/>
<View style={styles.backgroundOverlay} />
@@ -352,7 +325,7 @@ const Playbacks = () => {
data={playbacks}
keyExtractor={(item, index) => String(item?.id || index)}
renderItem={({ item, index }) => (
<View style={{ width: "100%", height: windowHeight }}>
<View style={{ width: '100%', height: windowHeight }}>
<PlaybackItem
item={item}
userCache={userCache}
@@ -398,16 +371,12 @@ const Playbacks = () => {
styles.indicatorDot,
item.isActive && styles.indicatorDotActive,
item.isPast && styles.indicatorDotPast,
index < indicatorItems.length - 1
? styles.indicatorDotSpacing
: null,
index < indicatorItems.length - 1 ? styles.indicatorDotSpacing : null,
]}
/>
))}
{canScrollDown && hasMore && (
<View
style={[styles.indicatorMoreDot, styles.indicatorDotSpacing]}
/>
<View style={[styles.indicatorMoreDot, styles.indicatorDotSpacing]} />
)}
</View>
{/* {indicatorLabel ? (
@@ -422,53 +391,50 @@ const Playbacks = () => {
<Image
source={icons.chevronDown}
resizeMode="contain"
style={[
styles.indicatorArrow,
!canScrollDown && styles.indicatorArrowDisabled,
]}
style={[styles.indicatorArrow, !canScrollDown && styles.indicatorArrowDisabled]}
/>
</Pressable>
</View>
)}
</View>
// </Page>
);
};
)
}
export default Playbacks;
export default Playbacks
const styles = StyleSheet.create({
screen: {
flex: 1,
},
backgroundVideoWrapper: {
height: "100%",
width: "100%",
position: "absolute",
height: '100%',
width: '100%',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
overflow: "hidden",
overflow: 'hidden',
zIndex: -1,
backgroundColor: "#060606",
backgroundColor: '#060606',
},
backgroundOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(2, 2, 2, 0.35)",
backgroundColor: 'rgba(2, 2, 2, 0.35)',
},
topOverlayContainer: {
position: "absolute",
position: 'absolute',
top: 32,
left: 24,
right: 24,
zIndex: 10,
},
topOverlayContent: {
alignSelf: "flex-start",
alignSelf: 'flex-start',
paddingVertical: 12,
paddingHorizontal: 18,
borderRadius: 18,
backgroundColor: "rgba(0,0,0,0.45)",
backgroundColor: 'rgba(0,0,0,0.45)',
gap: 4,
},
topOverlayTitle: {
@@ -483,12 +449,12 @@ const styles = StyleSheet.create({
opacity: 0.9,
},
indicatorContainer: {
position: "absolute",
position: 'absolute',
left: 20,
top: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
gap: 16,
},
indicatorArrow: {
@@ -502,22 +468,22 @@ const styles = StyleSheet.create({
paddingHorizontal: 8,
},
indicatorArrowUp: {
transform: [{ rotate: "180deg" }],
transform: [{ rotate: '180deg' }],
},
indicatorArrowDisabled: {
opacity: 0.25,
},
indicatorDotsWrapper: {
alignItems: "center",
alignItems: 'center',
},
indicatorDot: {
width: 6,
height: 12,
borderRadius: 3,
backgroundColor: "rgba(255,255,255,0.3)",
backgroundColor: 'rgba(255,255,255,0.3)',
},
indicatorDotPast: {
backgroundColor: "rgba(255,255,255,0.55)",
backgroundColor: 'rgba(255,255,255,0.55)',
},
indicatorDotActive: {
backgroundColor: Palette.white,
@@ -537,6 +503,6 @@ const styles = StyleSheet.create({
color: Palette.white,
fontSize: 12,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
});
})
@@ -1,13 +1,7 @@
import { BlurView } from "expo-blur";
import moment from "moment";
import "moment/locale/fr";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { BlurView } from 'expo-blur'
import moment from 'moment'
import 'moment/locale/fr'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
Image,
Platform,
@@ -17,32 +11,28 @@ import {
Text,
TextInput,
View,
} from "react-native";
import { icons } from "../../../assets";
import ProfilePicture from "../../../components/ProfilePicture";
import {
increment,
projectsRef,
serverTimestamp,
} from "../../../config/firebase";
import useDataFromRef from "../../../hooks/useDataFromRef";
import { useUser } from "../../../providers/UserDataProvider";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
import { getArtistDisplayName } from "../../../utils/artistName";
import { ensureAuthenticated } from "../../../utils/authRedirect";
} from 'react-native'
import { icons } from '../../../assets'
import ProfilePicture from '../../../components/ProfilePicture'
import { increment, projectsRef, serverTimestamp } from '../../../config/firebase'
import useDataFromRef from '../../../hooks/useDataFromRef'
import { useUser } from '../../../providers/UserDataProvider'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { size } from '../../../styles/Style'
import { getArtistDisplayName } from '../../../utils/artistName'
import { ensureAuthenticated } from '../../../utils/authRedirect'
moment.locale("fr");
moment.locale('fr')
const formatRelativeTime = (date) => {
try {
const d = date instanceof Date ? date : date?.toDate?.() || null;
return d ? moment(d).fromNow() : "";
const d = date instanceof Date ? date : date?.toDate?.() || null
return d ? moment(d).fromNow() : ''
} catch (_e) {
return "";
return ''
}
};
}
const CommentsPanel = ({
projectId,
@@ -52,37 +42,31 @@ const CommentsPanel = ({
inputRef,
panelHeight,
}) => {
const { currentUID, currentUserData } = useUser() || {};
const { currentUID, currentUserData } = useUser() || {}
const currentUserDisplayName = useMemo(
() => getArtistDisplayName(currentUserData, ""),
() => getArtistDisplayName(currentUserData, ''),
[currentUserData]
);
const [text, setText] = useState("");
const scrollRef = useRef(null);
const requireAuth = useCallback(
() => ensureAuthenticated(currentUID),
[currentUID]
);
)
const [text, setText] = useState('')
const scrollRef = useRef(null)
const requireAuth = useCallback(() => ensureAuthenticated(currentUID), [currentUID])
useEffect(() => {
setText("");
setText('')
try {
scrollRef.current?.scrollTo({ y: 0, animated: false });
scrollRef.current?.scrollTo({ y: 0, animated: false })
} catch (_e) {}
}, [projectId]);
}, [projectId])
const commentsRef = useMemo(() => {
try {
return projectId
? projectsRef
.doc(projectId)
.collection("comments")
.orderBy("createdAt", "desc")
: null;
? projectsRef.doc(projectId).collection('comments').orderBy('createdAt', 'desc')
: null
} catch (_e) {
return null;
return null
}
}, [projectId]);
}, [projectId])
const {
data: comments = [],
@@ -96,85 +80,65 @@ const CommentsPanel = ({
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?.();
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) return;
if (!requireAuth()) return;
const value = (text || '').trim()
if (!value || !projectId) return
if (!requireAuth()) return
try {
setText("");
setText('')
const docRef = await projectsRef
.doc(projectId)
.collection("comments")
.collection('comments')
.add({
userId: currentUID,
userName: currentUserDisplayName,
profilePicture: currentUserData?.profilePictureURL || "",
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: serverTimestamp(),
});
})
try {
await projectsRef
.doc(projectId)
.set({ commentsCount: increment(1) }, { merge: true });
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: currentUserDisplayName,
profilePicture: currentUserData?.profilePictureURL || "",
profilePicture: currentUserData?.profilePictureURL || '',
text: value,
createdAt: new Date(),
};
setComments((prev = []) => [
optimistic,
...prev.filter((x) => x?.id !== optimistic.id),
]);
onCommentAdded?.();
}
setComments((prev = []) => [optimistic, ...prev.filter((x) => x?.id !== optimistic.id)])
onCommentAdded?.()
requestAnimationFrame(() => {
try {
scrollRef.current?.scrollTo({ y: 0, animated: true });
scrollRef.current?.scrollTo({ y: 0, animated: true })
} catch (_e) {}
});
})
} catch (_e) {}
}, [
currentUID,
currentUserData,
onCommentAdded,
projectId,
requireAuth,
setComments,
text,
]);
}, [currentUID, currentUserData, onCommentAdded, projectId, requireAuth, setComments, text])
const canComment = !!currentUID;
const descriptionText = (description || "").trim();
const canComment = !!currentUID
const descriptionText = (description || '').trim()
return (
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
tint="dark"
style={[
styles.commentsWrapper,
panelHeight ? { height: panelHeight } : null,
]}
style={[styles.commentsWrapper, panelHeight ? { height: panelHeight } : null]}
>
<View style={styles.commentsInner}>
<ScrollView
@@ -190,21 +154,15 @@ const CommentsPanel = ({
<Text style={styles.sectionTitle}>Description</Text>
<View style={styles.descriptionCard}>
<Text style={styles.descriptionText}>
{descriptionText || "Description chanson..."}
{descriptionText || 'Description chanson...'}
</Text>
</View>
<Text style={[styles.sectionTitle, styles.sectionSpacing]}>
{`Commentaires${commentsCount ? ` (${commentsCount})` : ""}`}
{`Commentaires${commentsCount ? ` (${commentsCount})` : ''}`}
</Text>
{Array.isArray(comments) && comments.length > 0 ? (
comments.map((c, index) => (
<View
key={c.id}
style={[
styles.commentRow,
index > 0 && styles.commentRowSpacing,
]}
>
<View key={c.id} style={[styles.commentRow, index > 0 && styles.commentRowSpacing]}>
<ProfilePicture
uri={c?.profilePicture || null}
size={48}
@@ -213,20 +171,16 @@ const CommentsPanel = ({
<View style={styles.commentBubble}>
<View style={styles.commentHeader}>
<Text style={styles.commentAuthor}>
{c?.userId === currentUID ? "vous" : c?.userName || ""}
</Text>
<Text style={styles.commentMeta}>
{formatRelativeTime(c?.createdAt)}
{c?.userId === currentUID ? 'vous' : c?.userName || ''}
</Text>
<Text style={styles.commentMeta}> {formatRelativeTime(c?.createdAt)}</Text>
</View>
<Text style={styles.commentBody}>{c?.text}</Text>
</View>
</View>
))
) : (
<Text style={styles.emptyState}>
Aucun commentaire pour le moment
</Text>
<Text style={styles.emptyState}>Aucun commentaire pour le moment</Text>
)}
</ScrollView>
<View style={styles.commentInputContainer}>
@@ -235,28 +189,24 @@ const CommentsPanel = ({
value={text}
onChangeText={(value) => {
if (!requireAuth()) {
return;
return
}
setText(value);
setText(value)
}}
placeholder={
canComment
? "Commente"
: "Connecte-toi pour laisser un commentaire"
}
placeholder={canComment ? 'Commente' : 'Connecte-toi pour laisser un commentaire'}
placeholderTextColor="#FFFFFF90"
editable
onFocus={() => {
if (!requireAuth()) {
try {
inputRef.current?.blur?.();
inputRef.current?.blur?.()
} catch (_e) {}
}
}}
onPressIn={() => {
if (!requireAuth()) {
try {
inputRef.current?.blur?.();
inputRef.current?.blur?.()
} catch (_e) {}
}
}}
@@ -271,32 +221,28 @@ const CommentsPanel = ({
pressed && text.trim() && styles.sendButtonPressed,
]}
>
<Image
source={icons.send}
style={styles.sendIcon}
resizeMode="contain"
/>
<Image source={icons.send} style={styles.sendIcon} resizeMode="contain" />
</Pressable>
</View>
</View>
</BlurView>
);
};
)
}
export default CommentsPanel;
export default CommentsPanel
const styles = StyleSheet.create({
commentsWrapper: {
flex: 1,
borderRadius: 28,
overflow: "hidden",
overflow: 'hidden',
padding: 24,
backgroundColor: Palette.glass,
minHeight: 0,
},
commentsInner: {
flex: 1,
justifyContent: "space-between",
justifyContent: 'space-between',
},
commentsScroll: {
flex: 1,
@@ -316,7 +262,7 @@ const styles = StyleSheet.create({
},
descriptionCard: {
borderRadius: 18,
backgroundColor: "#FFFFFF14",
backgroundColor: '#FFFFFF14',
paddingHorizontal: 18,
paddingVertical: 14,
},
@@ -327,8 +273,8 @@ const styles = StyleSheet.create({
lineHeight: 20,
},
commentRow: {
flexDirection: "row",
alignItems: "flex-start",
flexDirection: 'row',
alignItems: 'flex-start',
},
commentRowSpacing: {
marginTop: 16,
@@ -338,14 +284,14 @@ const styles = StyleSheet.create({
},
commentBubble: {
flex: 1,
backgroundColor: "#FFFFFF18",
backgroundColor: '#FFFFFF18',
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 12,
},
commentHeader: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
commentAuthor: {
@@ -355,7 +301,7 @@ const styles = StyleSheet.create({
},
commentMeta: {
fontSize: 12,
color: "#FFFFFFA0",
color: '#FFFFFFA0',
fontFamily: FONT_FAMILY.InterRegular,
marginLeft: 8,
},
@@ -367,14 +313,14 @@ const styles = StyleSheet.create({
},
emptyState: {
fontSize: 14,
color: "#FFFFFFBB",
color: '#FFFFFFBB',
fontFamily: FONT_FAMILY.InterRegular,
},
commentInputContainer: {
marginTop: 20,
flexDirection: "row",
alignItems: "center",
backgroundColor: "#FFFFFF14",
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF14',
borderRadius: 18,
paddingHorizontal: 18,
paddingVertical: 10,
@@ -390,19 +336,19 @@ const styles = StyleSheet.create({
sendButton: {
...size({ size: 38 }),
borderRadius: 19,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Palette.primary,
},
sendButtonPressed: {
transform: [{ scale: 0.95 }],
},
sendButtonDisabled: {
backgroundColor: "#FFFFFF33",
backgroundColor: '#FFFFFF33',
},
sendIcon: {
width: 22,
height: 22,
tintColor: Palette.white,
},
});
})
@@ -1,12 +1,12 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Image, Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { Feather } from "@expo/vector-icons";
import ProfilePicture from "../../../components/ProfilePicture";
import { icons } from "../../../assets";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Image, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import ProfilePicture from '../../../components/ProfilePicture'
import { icons } from '../../../assets'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { size } from '../../../styles/Style'
const PlaybackActions = ({
owner,
@@ -30,22 +30,20 @@ const PlaybackActions = ({
<ProfilePicture
uri={owner?.profilePictureURL || null}
size={45}
imageProps={{ priority: "high" }}
imageProps={{ priority: 'high' }}
/>
</Pressable>
{owner?.id && owner.id !== currentUID && (
<Pressable disabled={isFollowActionPending} onPress={onToggleFollow}>
<BlurView
tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS !== 'ios' ? 10 : 20}
style={[
styles.followButton,
{ backgroundColor: isFollowing ? "#FFFFFF1A" : undefined },
{ backgroundColor: isFollowing ? '#FFFFFF1A' : undefined },
]}
>
<Text style={styles.followText}>
{isFollowing ? "Suivi(e)" : "Suivre"}
</Text>
<Text style={styles.followText}>{isFollowing ? 'Suivi(e)' : 'Suivre'}</Text>
</BlurView>
</Pressable>
)}
@@ -70,38 +68,38 @@ const PlaybackActions = ({
<Text style={styles.countText}>Signaler</Text>
</Pressable>
</View>
);
};
)
}
const styles = StyleSheet.create({
actionsColumn: {
alignSelf: "flex-end",
alignItems: "center",
alignSelf: 'flex-end',
alignItems: 'center',
gap: 20,
paddingHorizontal: 13,
},
profileWrapper: { gap: 6, alignItems: "center" },
profileWrapper: { gap: 6, alignItems: 'center' },
followButton: {
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: 12,
borderWidth: 1,
borderColor: Palette.white,
overflow: "hidden",
overflow: 'hidden',
},
followText: {
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
},
centered: { alignItems: "center" },
centered: { alignItems: 'center' },
countText: {
color: Palette.white,
fontSize: 11,
marginTop: 4,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
});
})
export default PlaybackActions;
export default PlaybackActions
@@ -1,147 +1,131 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { View, StyleSheet } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { SheetManager } from "react-native-actions-sheet";
import { useUser } from "../../../providers/UserDataProvider";
import { Routes } from "../../../navigation";
import { navigate } from "../../../navigation/NavigationService";
import { openComments } from "../../../components/bottomsheets/CommentsBottomSheet";
import {
createPlaybackSharePayload,
openShareSheet,
} from "../../../utils/shareSheet";
import {
getProjectLikes,
LIKE_TARGET,
toggleProjectLike,
} from "../../../utils/likes";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import PlaybackVideo from "./PlaybackVideo";
import PlaybackActions from "./PlaybackActions";
import PlaybackLyricsCard from "./PlaybackLyricsCard";
import usePlaybackOwner from "./usePlaybackOwner";
import usePlaybackPlayer from "./usePlaybackPlayer";
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { View, StyleSheet } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { SheetManager } from 'react-native-actions-sheet'
import { useUser } from '../../../providers/UserDataProvider'
import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService'
import { openComments } from '../../../components/bottomsheets/CommentsBottomSheet'
import { createPlaybackSharePayload, openShareSheet } from '../../../utils/shareSheet'
import { getProjectLikes, LIKE_TARGET, toggleProjectLike } from '../../../utils/likes'
import { ensureAuthenticated } from '../../../utils/authRedirect'
import PlaybackVideo from './PlaybackVideo'
import PlaybackActions from './PlaybackActions'
import PlaybackLyricsCard from './PlaybackLyricsCard'
import usePlaybackOwner from './usePlaybackOwner'
import usePlaybackPlayer from './usePlaybackPlayer'
const PlaybackItem = ({ item, isActive, userCache }) => {
const { currentUID, followUser, unfollowUser, getUserByUid } = useUser() || {};
const { owner, isFollowing, isFollowActionPending, toggleFollow } =
usePlaybackOwner({
item,
userCache,
getUserByUid,
currentUID,
followUser,
unfollowUser,
});
const { currentUID, followUser, unfollowUser, getUserByUid } = useUser() || {}
const { owner, isFollowing, isFollowActionPending, toggleFollow } = usePlaybackOwner({
item,
userCache,
getUserByUid,
currentUID,
followUser,
unfollowUser,
})
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
const [isLiked, setIsLiked] = useState(
currentUID ? initialLikedBy.includes(currentUID) : false
);
const [likesCount, setLikesCount] = useState(initialLikedBy.length);
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
const [isLiked, setIsLiked] = useState(currentUID ? initialLikedBy.includes(currentUID) : false)
const [likesCount, setLikesCount] = useState(initialLikedBy.length)
useEffect(() => {
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
setLikesCount(lb.length);
setIsLiked(currentUID ? lb.includes(currentUID) : false);
}, [item?.likes?.playback, currentUID]);
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
setLikesCount(lb.length)
setIsLiked(currentUID ? lb.includes(currentUID) : false)
}, [item?.likes?.playback, currentUID])
const commentsCount = useMemo(
() => Number(item?.commentsCount || 0),
[item?.commentsCount]
);
const commentsCount = useMemo(() => Number(item?.commentsCount || 0), [item?.commentsCount])
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
const projectTitle = typeof item?.title === 'string' ? item.title.trim() : ''
const creatorName = useMemo(() => {
if (owner?.displayName) return owner.displayName;
if (owner?.artistName) return owner.artistName;
if (owner?.userName) return owner.userName;
if (typeof item?.userName === "string") return item.userName;
return "";
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
if (owner?.displayName) return owner.displayName
if (owner?.artistName) return owner.artistName
if (owner?.userName) return owner.userName
if (typeof item?.userName === 'string') return item.userName
return ''
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName])
const { videoUrl, videoPlayer, currentTimeS } = usePlaybackPlayer({
item,
isActive,
});
})
const alignedWords = useMemo(() => {
const idx = Number(item?.songIndex) || 0;
const ts = item?.musicTimestamps?.[idx];
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
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 ?? ""),
word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
}));
}, [item?.musicTimestamps, item?.songIndex]);
}))
}, [item?.musicTimestamps, item?.songIndex])
const sharePayload = useMemo(() => {
if (!item?.id) return null;
if (!item?.id) return null
return createPlaybackSharePayload({
projectId: item.id,
title: projectTitle || undefined,
artist: creatorName || undefined,
playbackUrl: videoUrl || undefined,
});
}, [creatorName, item?.id, projectTitle, videoUrl]);
})
}, [creatorName, item?.id, projectTitle, videoUrl])
const handleShare = useCallback(() => {
if (sharePayload) {
openShareSheet(sharePayload);
openShareSheet(sharePayload)
}
}, [sharePayload]);
}, [sharePayload])
const handleReport = useCallback(() => {
if (!item?.id) return;
SheetManager.show("Report", {
if (!item?.id) return
SheetManager.show('Report', {
payload: {
targetType: "playback",
targetType: 'playback',
projectId: item?.id || null,
playbackId: item?.id || null,
title: item?.title || "",
title: item?.title || '',
ownerId: item?.userId || null,
},
});
}, [item?.id, item?.title, item?.userId]);
})
}, [item?.id, item?.title, item?.userId])
const handleLike = useCallback(async () => {
try {
if (!item?.id) return;
if (!item?.id) return
if (!ensureAuthenticated(currentUID)) {
return;
return
}
const nextLiked = !isLiked;
setIsLiked(nextLiked);
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
const nextLiked = !isLiked
setIsLiked(nextLiked)
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)))
await toggleProjectLike({
projectId: item.id,
currentUID,
target: LIKE_TARGET.PLAYBACK,
next: nextLiked,
});
})
} catch (e) {
setIsLiked((v) => !v);
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)));
setIsLiked((v) => !v)
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)))
}
}, [currentUID, isLiked, item?.id]);
}, [currentUID, isLiked, item?.id])
const handleCommentPress = useCallback(() => {
if (item?.id) openComments(item.id);
}, [item?.id]);
if (item?.id) openComments(item.id)
}, [item?.id])
const onOpenProfile = useCallback(() => {
navigate(Routes.SingerProfile, { userId: item?.userId });
}, [item?.userId]);
navigate(Routes.SingerProfile, { userId: item?.userId })
}, [item?.userId])
return (
<View style={styles.container}>
<PlaybackVideo
videoUrl={videoUrl}
videoPlayer={videoPlayer}
/>
<PlaybackVideo videoUrl={videoUrl} videoPlayer={videoPlayer} />
<View style={styles.overlay}>
<PlaybackActions
owner={owner}
@@ -165,21 +149,21 @@ const PlaybackItem = ({ item, isActive, userCache }) => {
/>
</View>
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
height: responsiveHeight(100),
position: "relative",
backgroundColor: "black",
position: 'relative',
backgroundColor: 'black',
},
overlay: {
position: "absolute",
width: "100%",
position: 'absolute',
width: '100%',
bottom: 130,
gap: 18,
},
});
})
export default PlaybackItem;
export default PlaybackItem
@@ -1,365 +1,330 @@
import { BlurView } from "expo-blur";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Image,
Pressable,
StyleSheet,
Text,
View,
useWindowDimensions,
} from "react-native";
import { icons, img } from "../../../assets";
import KaraokeLyrics from "../../../components/KaraokeLyrics";
import { usersRef } from "../../../config/firebase";
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";
import CommentsPanel from "./CommentsPanel.web";
import {
getProjectLikes,
LIKE_TARGET,
toggleProjectLike,
} from "../../../utils/likes";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import {
createPlaybackSharePayload,
openShareSheet,
} from "../../../utils/shareSheet";
import { SheetManager } from "react-native-actions-sheet";
import { Feather } from "@expo/vector-icons";
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Image, Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native'
import { icons, img } from '../../../assets'
import KaraokeLyrics from '../../../components/KaraokeLyrics'
import { usersRef } from '../../../config/firebase'
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'
import CommentsPanel from './CommentsPanel.web'
import { getProjectLikes, LIKE_TARGET, toggleProjectLike } from '../../../utils/likes'
import { ensureAuthenticated } from '../../../utils/authRedirect'
import { createPlaybackSharePayload, openShareSheet } from '../../../utils/shareSheet'
import { SheetManager } from 'react-native-actions-sheet'
import { Feather } from '@expo/vector-icons'
// Debug logging toggle for web playback
const DEBUG_PLAYBACK_WEB = true;
const DEBUG_PLAYBACK_WEB = true
// NOTE: Web-only implementation that uses a native <video> element instead of expo-video
// - No Platform checks
// - Audio now comes directly from the MP4 playbackUrl
const PlaybackItem = ({
item,
isActive,
userCache,
getUserByUid,
onBackgroundSync,
}) => {
const { width: viewportWidth, height: viewportHeight } =
useWindowDimensions();
const PlaybackItem = ({ item, isActive, userCache, getUserByUid, onBackgroundSync }) => {
const { width: viewportWidth, height: viewportHeight } = useWindowDimensions()
const [layoutSize, setLayoutSize] = useState({
width: viewportWidth,
height: viewportHeight,
});
const [openComments] = useState(true);
const commentInputRef = useRef(null);
const { currentUID, followUser, unfollowUser } = useUser() || {};
})
const [openComments] = useState(true)
const commentInputRef = useRef(null)
const { currentUID, followUser, unfollowUser } = useUser() || {}
const videoUrl = item?.playbackUrl || null;
const videoRef = useRef(null); // HTMLVideoElement
const wasActiveRef = useRef(false);
const startedRef = useRef(false);
const videoUrl = item?.playbackUrl || null
const videoRef = useRef(null) // HTMLVideoElement
const wasActiveRef = useRef(false)
const startedRef = useRef(false)
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
const [isLiked, setIsLiked] = useState(
currentUID ? initialLikedBy.includes(currentUID) : false
);
const [likesCount, setLikesCount] = useState(initialLikedBy.length);
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
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);
)
const [commentsCount, setCommentsCount] = useState(computedCommentsCount)
useEffect(() => {
setCommentsCount(computedCommentsCount);
}, [computedCommentsCount]);
setCommentsCount(computedCommentsCount)
}, [computedCommentsCount])
const [owner, setOwner] = useState(
item?.userId && userCache?.current?.get(item.userId)
? userCache.current.get(item.userId)
: null
);
item?.userId && userCache?.current?.get(item.userId) ? userCache.current.get(item.userId) : null
)
useEffect(() => {
let cancelled = false;
let cancelled = false
const run = async () => {
try {
const uid = item?.userId;
if (!uid || !userCache) return;
const cached = userCache.current.get(uid);
const uid = item?.userId
if (!uid || !userCache) return
const cached = userCache.current.get(uid)
if (cached) {
if (!cancelled) setOwner(cached);
return;
if (!cancelled) setOwner(cached)
return
}
const user = await getUserByUid?.(uid);
const user = await getUserByUid?.(uid)
if (!cancelled && user) {
userCache.current.set(uid, user);
setOwner(user);
userCache.current.set(uid, user)
setOwner(user)
}
} catch (_e) {}
};
run();
}
run()
return () => {
cancelled = true;
};
}, [item?.userId, userCache, getUserByUid]);
cancelled = true
}
}, [item?.userId, userCache, getUserByUid])
useEffect(() => {
const uid = item?.userId;
if (!uid) return;
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);
const data = { id: doc.id, ...doc.data() }
setOwner(data)
try {
userCache?.current?.set(uid, data);
userCache?.current?.set(uid, data)
} catch (_e) {}
}
},
() => {}
);
return () => unsub?.();
}, [item?.userId, userCache]);
)
return () => unsub?.()
}, [item?.userId, userCache])
const [isFollowing, setIsFollowing] = useState(false);
const [isFollowing, setIsFollowing] = useState(false)
useEffect(() => {
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : [];
setIsFollowing(currentUID ? list.includes(currentUID) : false);
}, [owner?.followedBy, currentUID]);
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []
setIsFollowing(currentUID ? list.includes(currentUID) : false)
}, [owner?.followedBy, currentUID])
useEffect(() => {
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
setLikesCount(lb.length);
setIsLiked(currentUID ? lb.includes(currentUID) : false);
}, [item?.likes?.playback, currentUID]);
const lb = getProjectLikes(item, LIKE_TARGET.PLAYBACK)
setLikesCount(lb.length)
setIsLiked(currentUID ? lb.includes(currentUID) : false)
}, [item?.likes?.playback, currentUID])
useEffect(() => {
startedRef.current = false;
}, [videoUrl]);
startedRef.current = false
}, [videoUrl])
// Start/reset when active, using the video's own audio
useEffect(() => {
const el = videoRef.current;
if (!el) return;
const el = videoRef.current
if (!el) return
if (!isActive) {
try {
if (!el.paused) el.pause();
if (!el.paused) el.pause()
} catch (_e) {}
try {
el.muted = true;
el.muted = true
} catch (_e) {}
startedRef.current = false;
onBackgroundSync?.({ isPlaying: false });
return;
startedRef.current = false
onBackgroundSync?.({ isPlaying: false })
return
}
const start = async () => {
try {
if (!startedRef.current) {
startedRef.current = true;
startedRef.current = true
const resetToStart = () => {
try {
el.currentTime = 0;
el.currentTime = 0
} catch (_e) {}
};
}
if (el.readyState >= 1) {
resetToStart();
resetToStart()
} else {
const handleLoaded = () => {
resetToStart();
};
el.addEventListener("loadeddata", handleLoaded, { once: true });
resetToStart()
}
el.addEventListener('loadeddata', handleLoaded, { once: true })
}
}
el.muted = false;
el.muted = false
await el.play().catch((error) => {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] play() rejected", error);
console.log('[PlaybackItem.web] play() rejected', error)
}
});
})
onBackgroundSync?.({
currentTime: Number(el.currentTime || 0),
isPlaying: !el.paused,
});
})
} catch (_e) {
if (DEBUG_PLAYBACK_WEB) {
console.log("[PlaybackItem.web] start error", _e);
console.log('[PlaybackItem.web] start error', _e)
}
}
};
}
start();
}, [isActive, videoUrl, onBackgroundSync]);
start()
}, [isActive, videoUrl, onBackgroundSync])
// Pause all on unmount
useEffect(() => {
const el = videoRef.current;
const el = videoRef.current
return () => {
try {
if (el && !el.paused) el.pause();
if (el && !el.paused) el.pause()
} catch (_e) {}
onBackgroundSync?.({ isPlaying: false });
};
}, [onBackgroundSync]);
onBackgroundSync?.({ isPlaying: false })
}
}, [onBackgroundSync])
// Lyrics timing based on video clock
const alignedWords = useMemo(() => {
const idx = Number(item?.songIndex) || 0;
const ts = item?.musicTimestamps?.[idx];
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
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 ?? ""),
word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0),
}));
}, [item?.musicTimestamps, item?.songIndex]);
}))
}, [item?.musicTimestamps, item?.songIndex])
const [currentTimeS, setCurrentTimeS] = useState(0);
const [currentTimeS, setCurrentTimeS] = useState(0)
useEffect(() => {
if (!isActive) return;
if (!isActive) return
const id = setInterval(() => {
try {
const el = videoRef.current;
const videoTime = Number(el?.currentTime || 0);
const safeTime = Number.isFinite(videoTime) ? videoTime : 0;
setCurrentTimeS(safeTime);
const el = videoRef.current
const videoTime = Number(el?.currentTime || 0)
const safeTime = Number.isFinite(videoTime) ? videoTime : 0
setCurrentTimeS(safeTime)
onBackgroundSync?.({
currentTime: safeTime,
isPlaying: !!el && !el.paused,
});
})
} catch (_e) {}
}, 250);
return () => clearInterval(id);
}, [isActive, onBackgroundSync]);
}, 250)
return () => clearInterval(id)
}, [isActive, onBackgroundSync])
useEffect(() => {
if (!onBackgroundSync) return undefined;
if (!onBackgroundSync) return undefined
if (isActive) {
wasActiveRef.current = true;
return undefined;
wasActiveRef.current = true
return undefined
}
if (wasActiveRef.current) {
onBackgroundSync({ isPlaying: false });
wasActiveRef.current = false;
onBackgroundSync({ isPlaying: false })
wasActiveRef.current = false
}
return undefined;
}, [isActive, onBackgroundSync]);
return undefined
}, [isActive, onBackgroundSync])
const descriptionText =
item?.description || item?.title || "Description chanson";
const descriptionText = item?.description || item?.title || 'Description chanson'
const creatorName = useMemo(() => {
if (owner?.displayName) return owner.displayName;
if (owner?.artistName) return owner.artistName;
if (owner?.userName) return owner.userName;
if (typeof item?.userName === "string") return item.userName;
return "";
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
if (owner?.displayName) return owner.displayName
if (owner?.artistName) return owner.artistName
if (owner?.userName) return owner.userName
if (typeof item?.userName === 'string') return item.userName
return ''
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName])
const sharePayload = useMemo(() => {
if (!item?.id) return null;
if (!item?.id) return null
return createPlaybackSharePayload({
projectId: item.id,
title: typeof item?.title === "string" ? item.title.trim() : undefined,
title: typeof item?.title === 'string' ? item.title.trim() : undefined,
artist: creatorName || undefined,
playbackUrl: videoUrl || undefined,
});
}, [creatorName, item?.id, item?.title, videoUrl]);
})
}, [creatorName, item?.id, item?.title, videoUrl])
const handleShare = useCallback(() => {
if (sharePayload) {
openShareSheet(sharePayload);
openShareSheet(sharePayload)
}
}, [sharePayload]);
}, [sharePayload])
const handleReport = useCallback(() => {
if (!item?.id) return;
SheetManager.show("Report", {
if (!item?.id) return
SheetManager.show('Report', {
payload: {
targetType: "playback",
targetType: 'playback',
projectId: item?.id || null,
playbackId: item?.id || null,
title: item?.title || "",
title: item?.title || '',
ownerId: item?.userId || null,
},
});
}, [item?.id, item?.title, item?.userId]);
})
}, [item?.id, item?.title, item?.userId])
const handleLayout = useCallback((event) => {
const { width = 0, height = 0 } = event?.nativeEvent?.layout || {};
const { width = 0, height = 0 } = event?.nativeEvent?.layout || {}
setLayoutSize((prev) => ({
width: width > 0 ? width : prev.width,
height: height > 0 ? height : prev.height,
}));
}, []);
}))
}, [])
const layoutWidth = layoutSize.width || viewportWidth;
const layoutHeight = layoutSize.height || viewportHeight;
const layoutWidth = layoutSize.width || viewportWidth
const layoutHeight = layoutSize.height || viewportHeight
const horizontalPadding = 64;
const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0);
const gapBetweenColumns = 40;
const minVideoWidth = 420;
const maxVideoWidth = 720;
const minCommentsWidth = 300;
const maxCommentsWidth = 420;
const horizontalPadding = 64
const innerWidth = Math.max(layoutWidth - horizontalPadding * 2, 0)
const gapBetweenColumns = 40
const minVideoWidth = 420
const maxVideoWidth = 720
const minCommentsWidth = 300
const maxCommentsWidth = 420
let desiredHeight = Math.max(420, layoutHeight * 0.8);
let videoWidth = Math.min(
maxVideoWidth,
Math.max(minVideoWidth, innerWidth * 0.58)
);
let videoHeight = videoWidth * (16 / 9);
let desiredHeight = Math.max(420, layoutHeight * 0.8)
let videoWidth = Math.min(maxVideoWidth, Math.max(minVideoWidth, innerWidth * 0.58))
let videoHeight = videoWidth * (16 / 9)
if (videoHeight > desiredHeight) {
videoHeight = desiredHeight;
videoWidth = videoHeight * (9 / 16);
videoHeight = desiredHeight
videoWidth = videoHeight * (9 / 16)
}
let commentsWidth = Math.min(
maxCommentsWidth,
Math.max(minCommentsWidth, innerWidth - videoWidth - gapBetweenColumns)
);
const panelHeight = Math.min(videoHeight, desiredHeight);
)
const panelHeight = Math.min(videoHeight, desiredHeight)
// Attach verbose event listeners on the HTML video element
useEffect(() => {
const el = videoRef.current;
if (!el || !DEBUG_PLAYBACK_WEB) return undefined;
const el = videoRef.current
if (!el || !DEBUG_PLAYBACK_WEB) return undefined
const handler = (e) => {
// Avoid heavy logs: only show key events and brief state
console.log("[PlaybackItem.web] video:", e.type, {
console.log('[PlaybackItem.web] video:', e.type, {
t: Number(el.currentTime || 0).toFixed(2),
paused: el.paused,
rs: el.readyState,
});
};
})
}
const events = [
"loadedmetadata",
"loadeddata",
"play",
"playing",
"pause",
"seeking",
"seeked",
"stalled",
"waiting",
"ended",
"error",
];
events.forEach((ev) => el.addEventListener(ev, handler));
'loadedmetadata',
'loadeddata',
'play',
'playing',
'pause',
'seeking',
'seeked',
'stalled',
'waiting',
'ended',
'error',
]
events.forEach((ev) => el.addEventListener(ev, handler))
return () => {
events.forEach((ev) => el.removeEventListener(ev, handler));
};
}, []);
events.forEach((ev) => el.removeEventListener(ev, handler))
}
}, [])
return (
<View
@@ -368,18 +333,13 @@ const PlaybackItem = ({
styles.itemContainerBase,
styles.itemContainerRow,
{
height: "90%",
height: '90%',
paddingHorizontal: horizontalPadding,
paddingVertical: Math.max((layoutHeight - panelHeight) / 2, 24),
},
]}
>
<View
style={[
styles.videoColumn,
{ width: videoWidth, marginRight: gapBetweenColumns },
]}
>
<View style={[styles.videoColumn, { width: videoWidth, marginRight: gapBetweenColumns }]}>
<View style={[styles.videoSurface, { height: panelHeight }]}>
<View style={styles.videoContainer}>
{!!videoUrl ? (
@@ -392,12 +352,12 @@ const PlaybackItem = ({
preload="auto"
// Using web CSS properties here on purpose
style={{
position: "absolute",
position: 'absolute',
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block',
}}
/>
) : (
@@ -413,7 +373,7 @@ const PlaybackItem = ({
<Pressable
style={styles.ownerAvatarButton}
onPress={() => {
navigate(Routes.SingerProfile, { userId: item?.userId });
navigate(Routes.SingerProfile, { userId: item?.userId })
}}
>
{owner?.profilePictureURL && (
@@ -435,38 +395,32 @@ const PlaybackItem = ({
if (
!ensureAuthenticated(currentUID, {
onIntercept: () => {
setIsFollowing(false);
setIsFollowing(false)
},
})
) {
return;
return
}
try {
const next = !isFollowing;
setIsFollowing(next);
const next = !isFollowing
setIsFollowing(next)
setOwner((prev) => {
const fb = Array.isArray(prev?.followedBy)
? prev.followedBy
: [];
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);
: 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);
setIsFollowing((v) => !v)
}
}}
>
<BlurView
tint="dark"
intensity={20}
style={styles.followButton}
>
<BlurView tint="dark" intensity={20} style={styles.followButton}>
<Text style={styles.followButtonText}>
{isFollowing ? "Suivi(e)" : "Suivre"}
{isFollowing ? 'Suivi(e)' : 'Suivre'}
</Text>
</BlurView>
</Pressable>
@@ -475,25 +429,23 @@ const PlaybackItem = ({
<Pressable
onPress={async () => {
try {
if (!item?.id) return;
if (!item?.id) return
if (!ensureAuthenticated(currentUID)) {
return;
return
}
const nextLiked = !isLiked;
setIsLiked(nextLiked);
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
const nextLiked = !isLiked
setIsLiked(nextLiked)
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)))
await toggleProjectLike({
projectId: item.id,
currentUID,
target: LIKE_TARGET.PLAYBACK,
next: nextLiked,
});
})
} catch (_e) {
setIsLiked((v) => !v);
setLikesCount((c) =>
isLiked ? c + 1 : Math.max(0, c - 1)
);
setIsLiked((v) => !v)
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)))
}
}}
style={[styles.actionButton, styles.actionSpacing]}
@@ -503,37 +455,16 @@ const PlaybackItem = ({
style={styles.actionIcon}
resizeMode="contain"
/>
{!!likesCount && (
<Text style={styles.actionLabel}>{likesCount}</Text>
)}
{!!likesCount && <Text style={styles.actionLabel}>{likesCount}</Text>}
</Pressable>
<Pressable
onPress={() => {}}
style={[styles.actionButton, styles.actionSpacing]}
>
<Image
source={icons.chatBubble}
style={styles.actionIcon}
resizeMode="contain"
/>
{!!commentsCount && (
<Text style={styles.actionLabel}>{commentsCount}</Text>
)}
<Pressable onPress={() => {}} style={[styles.actionButton, styles.actionSpacing]}>
<Image source={icons.chatBubble} style={styles.actionIcon} resizeMode="contain" />
{!!commentsCount && <Text style={styles.actionLabel}>{commentsCount}</Text>}
</Pressable>
<Pressable
onPress={handleShare}
style={[styles.actionButton, styles.actionSpacing]}
>
<Image
source={icons.share}
style={styles.actionIcon}
resizeMode="contain"
/>
<Pressable onPress={handleShare} style={[styles.actionButton, styles.actionSpacing]}>
<Image source={icons.share} style={styles.actionIcon} resizeMode="contain" />
</Pressable>
<Pressable
onPress={handleReport}
style={[styles.actionButton, styles.actionSpacing]}
>
<Pressable onPress={handleReport} style={[styles.actionButton, styles.actionSpacing]}>
<Feather name="flag" size={20} color={Palette.white} />
<Text style={styles.actionLabel}>Signaler</Text>
</Pressable>
@@ -542,10 +473,7 @@ const PlaybackItem = ({
<View style={styles.lyricsContainer}>
<BlurView tint="dark" intensity={20} style={styles.lyricsCard}>
{alignedWords?.length > 0 ? (
<KaraokeLyrics
alignedWords={alignedWords}
currentTimeS={currentTimeS}
/>
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={currentTimeS} />
) : (
<Text style={styles.lyricsText}>{descriptionText}</Text>
)}
@@ -556,52 +484,45 @@ const PlaybackItem = ({
</View>
{openComments && (
<View
style={[
styles.commentsColumn,
{ width: commentsWidth, height: panelHeight },
]}
>
<View style={[styles.commentsColumn, { width: commentsWidth, height: panelHeight }]}>
<CommentsPanel
projectId={item?.id}
description={descriptionText}
commentsCount={commentsCount}
onCommentAdded={() =>
setCommentsCount((c) => Math.max(0, Number(c || 0) + 1))
}
onCommentAdded={() => setCommentsCount((c) => Math.max(0, Number(c || 0) + 1))}
inputRef={commentInputRef}
panelHeight={panelHeight}
/>
</View>
)}
</View>
);
};
)
}
export default PlaybackItem;
export default PlaybackItem
const styles = StyleSheet.create({
itemContainerBase: {
width: "100%",
width: '100%',
paddingVertical: 36,
alignItems: "center",
alignItems: 'center',
},
itemContainerRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
videoColumn: {
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
videoSurface: {
alignSelf: "center",
alignSelf: 'center',
aspectRatio: 9 / 16,
borderRadius: 32,
overflow: "hidden",
backgroundColor: "#07040D",
shadowColor: "#000",
overflow: 'hidden',
backgroundColor: '#07040D',
shadowColor: '#000',
shadowOpacity: 0.45,
shadowRadius: 30,
shadowOffset: { width: 0, height: 20 },
@@ -609,16 +530,16 @@ const styles = StyleSheet.create({
},
videoContainer: {
flex: 1,
position: "relative",
position: 'relative',
},
actionStack: {
position: "absolute",
position: 'absolute',
right: 18,
top: 150,
alignItems: "center",
alignItems: 'center',
},
ownerBlock: {
alignItems: "center",
alignItems: 'center',
marginBottom: 26,
},
ownerAvatarButton: {
@@ -629,7 +550,7 @@ const styles = StyleSheet.create({
borderRadius: 100,
},
followPressable: {
alignSelf: "center",
alignSelf: 'center',
},
followButton: {
paddingVertical: 8,
@@ -637,8 +558,8 @@ const styles = StyleSheet.create({
borderRadius: 14,
borderWidth: 1,
borderColor: Palette.white,
backgroundColor: "#FFFFFF20",
overflow: "hidden",
backgroundColor: '#FFFFFF20',
overflow: 'hidden',
},
followButtonText: {
fontSize: 14,
@@ -646,8 +567,8 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterMedium,
},
actionButton: {
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
},
actionSpacing: {
marginTop: 24,
@@ -660,10 +581,10 @@ const styles = StyleSheet.create({
fontSize: 13,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
},
lyricsContainer: {
position: "absolute",
position: 'absolute',
left: 24,
right: 24,
bottom: 32,
@@ -673,7 +594,7 @@ const styles = StyleSheet.create({
paddingVertical: 12,
borderRadius: 22,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
},
lyricsText: {
fontSize: 14,
@@ -681,6 +602,6 @@ const styles = StyleSheet.create({
fontFamily: FONT_FAMILY.InterRegular,
},
commentsColumn: {
alignSelf: "stretch",
alignSelf: 'stretch',
},
});
})
@@ -1,27 +1,23 @@
import { BlurView } from "expo-blur";
import React from "react";
import { Platform, StyleSheet, Text, View } from "react-native";
import KaraokeLyrics from "../../../components/KaraokeLyrics";
import { Palette } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { BlurView } from 'expo-blur'
import React from 'react'
import { Platform, StyleSheet, Text, View } from 'react-native'
import KaraokeLyrics from '../../../components/KaraokeLyrics'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
const PlaybackLyricsCard = ({ alignedWords, currentTimeS, fallbackTitle }) => {
return (
<View style={styles.container}>
<BlurView
tint="dark"
intensity={Platform.OS !== "ios" ? 10 : 20}
style={styles.blur}
>
<BlurView tint="dark" intensity={Platform.OS !== 'ios' ? 10 : 20} style={styles.blur}>
{alignedWords?.length > 0 ? (
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={currentTimeS} />
) : (
<Text style={styles.title}>{fallbackTitle || "Description chanson"}</Text>
<Text style={styles.title}>{fallbackTitle || 'Description chanson'}</Text>
)}
</BlurView>
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
@@ -32,13 +28,13 @@ const styles = StyleSheet.create({
paddingVertical: 8,
borderRadius: 20,
backgroundColor: Palette.glass,
overflow: "hidden",
overflow: 'hidden',
},
title: {
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});
})
export default PlaybackLyricsCard;
export default PlaybackLyricsCard
@@ -1,7 +1,7 @@
import { VideoView } from "expo-video";
import React from "react";
import { Image, StyleSheet, View } from "react-native";
import { img } from "../../../assets";
import { VideoView } from 'expo-video'
import React from 'react'
import { Image, StyleSheet, View } from 'react-native'
import { img } from '../../../assets'
const PlaybackVideo = ({ videoUrl, videoPlayer }) => {
return (
@@ -16,19 +16,18 @@ const PlaybackVideo = ({ videoUrl, videoPlayer }) => {
) : (
<Image source={img.placeholder3} style={styles.fill} />
)}
</View>
);
};
)
}
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
},
fill: {
width: "100%",
height: "100%",
width: '100%',
height: '100%',
},
});
})
export default PlaybackVideo;
export default PlaybackVideo
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { usersRef } from "../../../config/firebase";
import { ensureAuthenticated } from "../../../utils/authRedirect";
import { useCallback, useEffect, useState } from 'react'
import { usersRef } from '../../../config/firebase'
import { ensureAuthenticated } from '../../../utils/authRedirect'
const usePlaybackOwner = ({
item,
@@ -11,102 +11,93 @@ const usePlaybackOwner = ({
unfollowUser,
}) => {
const [owner, setOwner] = useState(
item?.userId && userCache?.current?.get(item.userId)
? userCache.current.get(item.userId)
: null
);
item?.userId && userCache?.current?.get(item.userId) ? userCache.current.get(item.userId) : null
)
useEffect(() => {
let cancelled = false;
let cancelled = false
const run = async () => {
try {
const uid = item?.userId;
if (!uid || !userCache) return;
const cached = userCache.current.get(uid);
const uid = item?.userId
if (!uid || !userCache) return
const cached = userCache.current.get(uid)
if (cached) {
if (!cancelled) setOwner(cached);
return;
if (!cancelled) setOwner(cached)
return
}
const user = await getUserByUid?.(uid);
const user = await getUserByUid?.(uid)
if (!cancelled && user) {
userCache.current.set(uid, user);
setOwner(user);
userCache.current.set(uid, user)
setOwner(user)
}
} catch (e) {}
};
run();
}
run()
return () => {
cancelled = true;
};
}, [item?.userId, userCache, getUserByUid]);
cancelled = true
}
}, [item?.userId, userCache, getUserByUid])
useEffect(() => {
const uid = item?.userId;
if (!uid) return;
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);
const data = { id: doc.id, ...doc.data() }
setOwner(data)
try {
userCache?.current?.set(uid, data);
userCache?.current?.set(uid, data)
} catch (e) {}
}
},
() => {}
);
return () => unsub?.();
}, [item?.userId, userCache]);
)
return () => unsub?.()
}, [item?.userId, userCache])
const [isFollowing, setIsFollowing] = useState(false);
const [isFollowActionPending, setIsFollowActionPending] = useState(false);
const [isFollowing, setIsFollowing] = useState(false)
const [isFollowActionPending, setIsFollowActionPending] = useState(false)
useEffect(() => {
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : [];
setIsFollowing(currentUID ? list.includes(currentUID) : false);
}, [owner?.followedBy, currentUID]);
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : []
setIsFollowing(currentUID ? list.includes(currentUID) : false)
}, [owner?.followedBy, currentUID])
const toggleFollow = useCallback(async () => {
if (!owner?.id || owner.id === currentUID) return;
if (isFollowActionPending) return;
if (!owner?.id || owner.id === currentUID) return
if (isFollowActionPending) return
if (
!ensureAuthenticated(currentUID, {
onIntercept: () => {
setIsFollowActionPending(false);
setIsFollowActionPending(false)
},
})
) {
return;
return
}
try {
setIsFollowActionPending(true);
const next = !isFollowing;
setIsFollowing(next);
setIsFollowActionPending(true)
const next = !isFollowing
setIsFollowing(next)
setOwner((prev) => {
const fb = Array.isArray(prev?.followedBy) ? prev.followedBy : [];
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);
: fb.filter((x) => x !== currentUID)
return prev ? { ...prev, followedBy: newFb } : prev
})
if (next) await followUser?.(owner.id)
else await unfollowUser?.(owner.id)
global.setTimeout(() => {
setIsFollowActionPending(false);
}, 1000);
setIsFollowActionPending(false)
}, 1000)
} catch (e) {
setIsFollowing((v) => !v);
setIsFollowActionPending(false);
setIsFollowing((v) => !v)
setIsFollowActionPending(false)
}
}, [
currentUID,
followUser,
isFollowActionPending,
isFollowing,
owner?.id,
unfollowUser,
]);
}, [currentUID, followUser, isFollowActionPending, isFollowing, owner?.id, unfollowUser])
return { owner, isFollowing, isFollowActionPending, toggleFollow };
};
return { owner, isFollowing, isFollowActionPending, toggleFollow }
}
export default usePlaybackOwner;
export default usePlaybackOwner
@@ -1,50 +1,50 @@
import { useEffect, useMemo, useState } from "react";
import { useVideoPlayer } from "expo-video";
import { useEffect, useMemo, useState } from 'react'
import { useVideoPlayer } from 'expo-video'
const usePlaybackPlayer = ({ item, isActive }) => {
const baseVideoUrl = item?.playbackUrl || null;
const videoUrl = baseVideoUrl;
const baseVideoUrl = item?.playbackUrl || null
const videoUrl = baseVideoUrl
const videoPlayer = useVideoPlayer(videoUrl || null, (player) => {
player.loop = false;
player.muted = false;
player.timeUpdateEventInterval = 0.2;
});
player.loop = false
player.muted = false
player.timeUpdateEventInterval = 0.2
})
const shouldAutoPlay = isActive;
const shouldAutoPlay = isActive
useEffect(() => {
if (!videoPlayer) return;
if (!videoPlayer) return
if (shouldAutoPlay) {
try {
videoPlayer.currentTime = 0;
videoPlayer.play();
videoPlayer.currentTime = 0
videoPlayer.play()
} catch (e) {}
} else if (videoPlayer?.playing) {
try {
videoPlayer.pause();
videoPlayer.pause()
} catch (e) {}
}
}, [shouldAutoPlay, videoPlayer]);
}, [shouldAutoPlay, videoPlayer])
useEffect(() => {
return () => {
try {
if (videoPlayer?.playing) videoPlayer.pause();
if (videoPlayer?.playing) videoPlayer.pause()
} catch (e) {}
};
}, [videoPlayer]);
}
}, [videoPlayer])
const [currentTimeS, setCurrentTimeS] = useState(0);
const [currentTimeS, setCurrentTimeS] = useState(0)
useEffect(() => {
if (!isActive) return;
if (!isActive) return
const id = global.setInterval(() => {
try {
setCurrentTimeS(Number(videoPlayer?.currentTime || 0));
setCurrentTimeS(Number(videoPlayer?.currentTime || 0))
} catch (e) {}
}, 250);
return () => global.clearInterval(id);
}, [isActive, videoPlayer]);
}, 250)
return () => global.clearInterval(id)
}, [isActive, videoPlayer])
const playerState = useMemo(
() => ({
@@ -53,9 +53,9 @@ const usePlaybackPlayer = ({ item, isActive }) => {
currentTimeS,
}),
[videoPlayer, videoUrl, currentTimeS]
);
)
return playerState;
};
return playerState
}
export default usePlaybackPlayer;
export default usePlaybackPlayer