mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge pull request #205 from simoleo89/fix/friendbar-overflow-autofit
fix(friendbar): auto-fit visible friend count so the bar stops clipping
This commit is contained in:
@@ -1,11 +1,23 @@
|
|||||||
import { FC, useRef, useState } from 'react';
|
import { FC, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||||
import { LocalizeText, MessengerFriend } from '../../../../api';
|
import { LocalizeText, MessengerFriend } from '../../../../api';
|
||||||
import { FriendBarItemView } from './FriendBarItemView';
|
import { FriendBarItemView } from './FriendBarItemView';
|
||||||
import { motion, AnimatePresence, Variants } from 'framer-motion';
|
import { motion, AnimatePresence, Variants } from 'framer-motion';
|
||||||
|
|
||||||
|
// Hard cap on simultaneously-shown friend chips. The effective count is
|
||||||
|
// reduced below this when the bar would otherwise overflow its (clipped)
|
||||||
|
// slot in the toolbar — see the width measurement below.
|
||||||
const MAX_DISPLAY_COUNT = 3;
|
const MAX_DISPLAY_COUNT = 3;
|
||||||
|
|
||||||
|
// Layout constants mirrored from FriendBarItemView / the flex gaps here, used
|
||||||
|
// to compute how many friend chips fit in the available width. A "slot" is one
|
||||||
|
// w-[132px] button plus the gap-[6px] that precedes it.
|
||||||
|
const ITEM_SLOT = 138; // 132px chip + 6px gap (friend chip and search chip)
|
||||||
|
const ARROWS_WIDTH = 52; // two w-[20px] arrows, each + 6px gap
|
||||||
|
const REQUEST_SLOT = 120; // requests chip (only present when requestsCount > 0)
|
||||||
|
const BASE_PAD = 8; // container px-[2px] + a little slack
|
||||||
|
const RIGHT_SAFE = 24; // right inset (right-0/right-3) + pr-3 safety margin
|
||||||
|
|
||||||
// Mirrored from Toolbar to keep physics identical
|
// Mirrored from Toolbar to keep physics identical
|
||||||
const containerVariants: Variants = {
|
const containerVariants: Variants = {
|
||||||
hidden: {},
|
hidden: {},
|
||||||
@@ -23,9 +35,55 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
|||||||
{
|
{
|
||||||
const { onlineFriends = [], requestsCount = 0 } = props;
|
const { onlineFriends = [], requestsCount = 0 } = props;
|
||||||
const [ indexOffset, setIndexOffset ] = useState(0);
|
const [ indexOffset, setIndexOffset ] = useState(0);
|
||||||
|
const [ maxVisible, setMaxVisible ] = useState(MAX_DISPLAY_COUNT);
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
const hasScrollableFriends = (onlineFriends.length > MAX_DISPLAY_COUNT);
|
|
||||||
const visibleFriends = onlineFriends.slice(indexOffset, (indexOffset + MAX_DISPLAY_COUNT));
|
// Auto-fit the visible friend count to the room actually available between
|
||||||
|
// the bar's left edge and the right side of the viewport. The bar lives in
|
||||||
|
// a `overflow-x: clip` toolbar slot, so anything that doesn't fit would be
|
||||||
|
// silently cut off (the scroll arrow / search button disappear). The bar's
|
||||||
|
// left edge is stable (it sits after fixed-width toolbar icons), so growing
|
||||||
|
// or shrinking the chip count never moves it — no measurement feedback loop.
|
||||||
|
useLayoutEffect(() =>
|
||||||
|
{
|
||||||
|
const element = elementRef.current;
|
||||||
|
|
||||||
|
if(!element) return;
|
||||||
|
|
||||||
|
const measure = () =>
|
||||||
|
{
|
||||||
|
const left = element.getBoundingClientRect().left;
|
||||||
|
const available = window.innerWidth - left - RIGHT_SAFE;
|
||||||
|
const fixed = ARROWS_WIDTH + ITEM_SLOT /* search chip */ + BASE_PAD + ((requestsCount > 0) ? REQUEST_SLOT : 0);
|
||||||
|
const fit = Math.floor((available - fixed) / ITEM_SLOT);
|
||||||
|
const next = Math.max(1, Math.min(MAX_DISPLAY_COUNT, fit));
|
||||||
|
|
||||||
|
setMaxVisible(prev => ((prev === next) ? prev : next));
|
||||||
|
};
|
||||||
|
|
||||||
|
measure();
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(measure);
|
||||||
|
|
||||||
|
observer.observe(document.documentElement);
|
||||||
|
window.addEventListener('resize', measure);
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
observer.disconnect();
|
||||||
|
window.removeEventListener('resize', measure);
|
||||||
|
};
|
||||||
|
}, [ requestsCount, onlineFriends.length ]);
|
||||||
|
|
||||||
|
// `safeOffset` is the offset clamped to the current list/fit. Every read
|
||||||
|
// below uses it, so a stale `indexOffset` (after the list shrinks or the fit
|
||||||
|
// grows) renders correctly and self-corrects on the next arrow click — no
|
||||||
|
// write-back effect needed.
|
||||||
|
const maxOffset = Math.max(0, (onlineFriends.length - maxVisible));
|
||||||
|
const safeOffset = Math.min(indexOffset, maxOffset);
|
||||||
|
const canScrollLeft = (safeOffset > 0);
|
||||||
|
const canScrollRight = (safeOffset < maxOffset);
|
||||||
|
const visibleFriends = onlineFriends.slice(safeOffset, (safeOffset + maxVisible));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -44,10 +102,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
|||||||
</motion.div> }
|
</motion.div> }
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<div
|
<div
|
||||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || (indexOffset <= 0)) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ !canScrollLeft ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||||
onClick={ () =>
|
onClick={ () =>
|
||||||
{
|
{
|
||||||
if(indexOffset > 0) setIndexOffset(indexOffset - 1);
|
if(canScrollLeft) setIndexOffset(safeOffset - 1);
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||||
@@ -94,10 +152,10 @@ export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount
|
|||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<div
|
<div
|
||||||
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ (!hasScrollableFriends || !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1)))) ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
className={ `flex h-[34px] w-[20px] items-center justify-center text-white/80 transition-all ${ !canScrollRight ? 'opacity-30 cursor-not-allowed' : 'cursor-pointer hover:text-white active:scale-95' }` }
|
||||||
onClick={ () =>
|
onClick={ () =>
|
||||||
{
|
{
|
||||||
if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1);
|
if(canScrollRight) setIndexOffset(safeOffset + 1);
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||||
|
|||||||
Reference in New Issue
Block a user