feat(reels): user info/caption also hide on collapse; auto-scroll + speed; save-block deterrent

- Username/caption block, and the long-video progress bar, only checked
  chromeHidden (the long-press hide-everything state) — not
  actionsExpanded (the 3-dot-adjacent collapse toggle), so collapsing icons
  never hid them. Now both also respect actionsExpanded.
- Wired playbackSpeed (via a playbackRate effect on the <video> ref, since
  it isn't a JSX prop) and autoScrollEnabled (loop={!autoScrollEnabled} +
  onEnded calling the new onRequestNext prop, which both reels feeds
  (ExploreReelsView, PostFeedView) implement by scrollTo-ing to the next
  item).
- Added onContextMenu prevention (+ draggable=false on the poster image)
  when author_allow_save is false — this is a best-effort deterrent against
  casual right-click/drag saving only. No browser exposes an API to block
  actual OS-level screenshots; that isn't something any website can do.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
payacom
2026-09-17 17:13:26 +03:30
parent 1d1d3cd461
commit 0602bde788
3 changed files with 45 additions and 3 deletions

View File

@@ -406,6 +406,13 @@ export default function ExploreReelsView({
<ReelsPostCard
postData={item.post}
isActive={item.key === activeKey}
onRequestNext={() => {
if (!scrollRef.current || index + 1 >= items.length) return;
scrollRef.current.scrollTo({
top: (index + 1) * getReelsStepHeight(scrollRef.current),
behavior: "smooth",
});
}}
/>
) : (
<ReelsAcademyCard

View File

@@ -800,6 +800,13 @@ export default function PostFeedView({
isActive={String(post._id) === String(activePostId)}
postPage={feedMode === "user" && showClose}
preloadPriority={preloadPriority}
onRequestNext={() => {
if (!scrollRef.current || index + 1 >= displayPosts.length) return;
scrollRef.current.scrollTo({
top: (index + 1) * getReelsStepHeight(scrollRef.current),
behavior: "smooth",
});
}}
/>
</ReelsScrollSlot>
);

View File

@@ -68,6 +68,8 @@ interface ReelsPostCardProps {
isActive?: boolean;
postPage?: boolean;
preloadPriority?: VideoPreloadPriority;
/** وقتی پیمایش خودکار فعاله و این ریلز (فعال) به انتهای ویدیو رسید */
onRequestNext?: () => void;
}
export default function ReelsPostCard({
@@ -75,6 +77,7 @@ export default function ReelsPostCard({
isActive = false,
postPage = false,
preloadPriority = "idle",
onRequestNext,
}: ReelsPostCardProps) {
const { t } = useTranslation("common");
const {
@@ -146,6 +149,11 @@ export default function ReelsPostCard({
const social = usePostSocialState(postData);
const requireAuth = useRequireAuth();
const chromeHidden = exploreUi ? exploreUi.chromeHidden : uiHidden;
// اطلاعات کاربر/کپشن هم باید با دکمه‌ی جمع‌کردن آیکون‌ها مخفی بشن، نه فقط با
// hold روی تصویر (chromeHidden) — قبلاً این بلوک فقط chromeHidden رو چک می‌کرد
const actionsExpanded = exploreUi ? exploreUi.actionsExpanded : true;
const autoScrollEnabled = exploreUi ? exploreUi.autoScrollEnabled : false;
const playbackSpeed = exploreUi ? exploreUi.playbackSpeed : 1;
const allowSave = postData.author_allow_save !== false;
const useCommentsSheet = !postPage;
@@ -155,7 +163,8 @@ export default function ReelsPostCard({
videoDuration > LONG_VIDEO_SEC &&
isActive &&
!chromeHidden &&
!anyCommentsOpen;
!anyCommentsOpen &&
actionsExpanded;
const mediaFiles =
files?.map((file) => ({
@@ -315,6 +324,12 @@ export default function ReelsPostCard({
setVideoPaused(false);
}, [videoSrc]);
// <video> عنصرِ HTML سرعتِ پخش رو به‌عنوان JSX prop نمی‌گیره، باید مستقیم روی خودِ
// المنت ست بشه — هم موقع تغییرِ سرعت از منو، هم موقع لود شدنِ منبعِ جدید
useEffect(() => {
if (videoRef.current) videoRef.current.playbackRate = playbackSpeed;
}, [playbackSpeed, videoSrc]);
useEffect(() => {
if (type !== "video" || !videoSrc) return;
if (preloadPriority === "active" || preloadPriority === "nearby") {
@@ -440,20 +455,30 @@ export default function ReelsPostCard({
sizes="100vw"
className="object-contain"
priority={isActive || preloadPriority === "nearby"}
draggable={!allowSave ? false : undefined}
onContextMenu={
!allowSave ? (e) => e.preventDefault() : undefined
}
/>
)}
<video
ref={videoRef}
src={videoSrc ?? mediaFiles[0]?.src}
poster={videoPoster}
loop
loop={!autoScrollEnabled}
playsInline
preload={videoPreload}
controlsList="nodownload"
className={cn(
"h-full w-full object-contain",
videoPoster && !videoReady && "opacity-0"
)}
onClick={handleVideoClick}
// مرورگر نمی‌تونه از اسکرین‌شات واقعی جلوگیری کنه (هیچ API مرورگری برای این
// وجود نداره)؛ این فقط راه‌های ساده‌ی دانلود/ذخیره (کلیک راست، درگ) رو می‌بندد
onContextMenu={
!allowSave ? (e) => e.preventDefault() : undefined
}
onLoadedData={() => {
setImageLoading(false);
setVideoReady(true);
@@ -469,6 +494,9 @@ export default function ReelsPostCard({
onProgress={handleVideoTimeUpdate}
onPlay={() => setVideoPaused(false)}
onPause={() => setVideoPaused(true)}
onEnded={() => {
if (isActive && autoScrollEnabled) onRequestNext?.();
}}
muted={muted}
/>
{videoPaused && isActive && !commentsOpen && (
@@ -578,7 +606,7 @@ export default function ReelsPostCard({
)}
<AnimatePresence>
{!chromeHidden && !anyCommentsOpen && (
{!chromeHidden && !anyCommentsOpen && actionsExpanded && (
<>
<motion.div
initial={{ opacity: 0 }}