feat: fixes and formatter
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user