mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
chore: checkpoint current work
This commit is contained in:
@@ -1,19 +1,50 @@
|
||||
import { FC } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useFriends } from '../../hooks';
|
||||
import { FriendBarView } from './views/friends-bar/FriendsBarView';
|
||||
import { FriendsListView } from './views/friends-list/FriendsListView';
|
||||
import { FriendsMessengerView } from './views/messenger/FriendsMessengerView';
|
||||
|
||||
export const FriendsView: FC<{}> = props =>
|
||||
{
|
||||
const { settings = null, onlineFriends = [] } = useFriends();
|
||||
const FRIEND_BAR_TARGET_IDS = [ 'toolbar-friend-bar-container-desktop' ];
|
||||
|
||||
export const FriendsView: FC<{}> = props => {
|
||||
const { settings = null, onlineFriends = [], requests = [] } = useFriends();
|
||||
const [ portalTarget, setPortalTarget ] = useState<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(typeof document === 'undefined') return;
|
||||
|
||||
const resolveTarget = () =>
|
||||
{
|
||||
for(const id of FRIEND_BAR_TARGET_IDS)
|
||||
{
|
||||
const element = document.getElementById(id);
|
||||
|
||||
if(element)
|
||||
{
|
||||
setPortalTarget(previous => ((previous === element) ? previous : element));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setPortalTarget(null);
|
||||
};
|
||||
|
||||
resolveTarget();
|
||||
|
||||
const observer = new MutationObserver(resolveTarget);
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
if(!settings) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ createPortal(<FriendBarView onlineFriends={ onlineFriends } />, document.getElementById('toolbar-friend-bar-container')) }
|
||||
{ portalTarget && createPortal(<FriendBarView onlineFriends={ onlineFriends } requestsCount={ requests.length } />, portalTarget) }
|
||||
<FriendsListView />
|
||||
<FriendsMessengerView />
|
||||
</>
|
||||
|
||||
@@ -1,66 +1,108 @@
|
||||
import { FindNewFriendsMessageComposer, MouseEventType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { GetUserProfile, LocalizeText, MessengerFriend, OpenMessengerChat, SendMessageComposer } from '../../../../api';
|
||||
import { Button, LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common';
|
||||
import { LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common';
|
||||
import { useFriends } from '../../../../hooks';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
|
||||
{
|
||||
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props => {
|
||||
const { friend = null } = props;
|
||||
const [ isVisible, setVisible ] = useState(false);
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const { followFriend = null } = useFriends();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const onClick = (event: MouseEvent) =>
|
||||
{
|
||||
useEffect(() => {
|
||||
const onClick = (event: MouseEvent) => {
|
||||
const element = elementRef.current;
|
||||
|
||||
if(!element) return;
|
||||
|
||||
if((event.target !== element) && !element.contains((event.target as Node)))
|
||||
{
|
||||
if (!element) return;
|
||||
if ((event.target !== element) && !element.contains((event.target as Node))) {
|
||||
setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(MouseEventType.MOUSE_CLICK, onClick);
|
||||
|
||||
return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick);
|
||||
}, []);
|
||||
|
||||
if(!friend)
|
||||
{
|
||||
if (!friend) {
|
||||
return (
|
||||
<div ref={ elementRef } className={ 'friend-bar-item btn btn-secondary w-[130px] mx-[3px] my-0 z-0 relative pl-[37px] text-left friend-bar-search ' + (isVisible ? 'friend-bar-search-item-active' : '') } onClick={ () => setVisible(prev => !prev) }>
|
||||
<div className="friend-bar-item-head absolute -top-[3px] left-[5px] w-[31px] h-[34px] bg-[url('@/assets/images/toolbar/friend-search.png')]" />
|
||||
<div className="truncate text-white text-[13px]">{ LocalizeText('friend.bar.find.title') }</div>
|
||||
{ isVisible &&
|
||||
<div className="search-content mt-3">
|
||||
<div className="bg-white text-black px-1 py-1 text-xs">{ LocalizeText('friend.bar.find.text') }</div>
|
||||
<Button className="mt-2 mb-4" variant="secondary" onClick={ () => SendMessageComposer(new FindNewFriendsMessageComposer()) }>{ LocalizeText('friend.bar.find.button') }</Button>
|
||||
</div> }
|
||||
<div ref={elementRef} className="relative">
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
|
||||
className="relative flex h-[34px] w-[132px] items-center rounded-[7px] border border-[#9fc56f] bg-[#5f7d2f] pl-[34px] pr-[10px] text-left text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)]"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
>
|
||||
<div className="absolute left-[6px] top-1/2 h-[24px] w-[24px] -translate-y-1/2 bg-[url('@/assets/images/toolbar/friend-search.png')] bg-contain bg-center bg-no-repeat pointer-events-none" />
|
||||
<div className="truncate text-[0.8rem] font-bold">{LocalizeText('friend.bar.find.title')}</div>
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel whitespace-nowrap z-[80] flex flex-col items-center gap-2 pointer-events-auto min-w-[170px]"
|
||||
>
|
||||
<div className="text-white text-[13px] font-bold drop-shadow-[1px_1px_0_#000]">{LocalizeText('friend.bar.find.title')}</div>
|
||||
<div className="text-white/80 text-xs px-2">{LocalizeText('friend.bar.find.text')}</div>
|
||||
<button
|
||||
className="px-3 py-1 bg-black/40 hover:bg-black/60 border border-white/10 rounded-lg text-white text-[11px] font-bold transition-colors cursor-pointer mt-1"
|
||||
onClick={event => { event.stopPropagation(); SendMessageComposer(new FindNewFriendsMessageComposer()); setVisible(false); }}
|
||||
>
|
||||
{LocalizeText('friend.bar.find.button')}
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ elementRef } className={ `friend-bar-item btn btn-success ${ isVisible ? 'w-[165px]' : 'w-[130px]' } mx-[3px] my-0 z-0 relative text-left${ isVisible ? ' mb-[84px]' : '' }` } style={{ paddingLeft: friend.id > 0 ? '70px' : '46px', paddingRight: isVisible ? '4px' : undefined }} onClick={ () => setVisible(prev => !prev) }>
|
||||
<div className={ `friend-bar-item-head absolute ${ friend.id > 0 ? '-top-[31px] -left-[25px]' : '-top-[5px] -left-[3.5px]' }` }>
|
||||
{ (friend.id > 0) &&
|
||||
<LayoutAvatarImageView direction={ isVisible ? 2 : 3 } figure={ friend.figure } headOnly={ !isVisible } /> }
|
||||
{ (friend.id <= 0) &&
|
||||
<LayoutBadgeImageView badgeCode="ADM" isGroup={ false } /> }
|
||||
<div ref={elementRef} className="relative">
|
||||
<div className="absolute left-[-4px] bottom-[-2px] z-10 h-[66px] w-[34px] overflow-hidden pointer-events-none">
|
||||
{(friend.id > 0) ? (
|
||||
<LayoutAvatarImageView
|
||||
direction={2}
|
||||
figure={friend.figure}
|
||||
headOnly={false}
|
||||
className="block pointer-events-none drop-shadow-[1px_1px_0_rgba(0,0,0,0.6)]"
|
||||
style={ { marginLeft: '-28px', marginTop: '-10px' } }
|
||||
/>
|
||||
) : (
|
||||
<LayoutBadgeImageView badgeCode="ADM" isGroup={false} className="scale-75 block pointer-events-none drop-shadow-[1px_1px_0_rgba(0,0,0,0.6)]" />
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-white text-[13px]">{ friend.name }</div>
|
||||
{ isVisible &&
|
||||
<div className="flex justify-between gap-2 mt-1">
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-friendbar-chat" onClick={ event => { event.stopPropagation(); OpenMessengerChat(friend.id); } } />
|
||||
{ friend.followingAllowed &&
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-friendbar-visit" onClick={ event => { event.stopPropagation(); followFriend(friend); } } /> }
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-profile" onClick={ event => { event.stopPropagation(); GetUserProfile(friend.id); } } />
|
||||
</div> }
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
|
||||
className="relative flex h-[34px] w-[132px] items-center rounded-[7px] border border-[#9fc56f] bg-[#6f8f39] pl-[44px] pr-[10px] text-left text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)] overflow-visible"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
>
|
||||
<div className="truncate text-[0.82rem] font-bold">{friend.name}</div>
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
className="absolute bottom-[calc(100%+12px)] left-1/2 -translate-x-1/2 tbme-panel flex flex-col items-center gap-2 z-[80] pointer-events-auto min-w-[110px]"
|
||||
>
|
||||
<div className="text-white font-bold text-[13px] drop-shadow-[1px_1px_0_#000] truncate max-w-[120px] px-1">{friend.name}</div>
|
||||
<div className="flex justify-center gap-3 px-2">
|
||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-chat hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); OpenMessengerChat(friend.id); setVisible(false); }} />
|
||||
{friend.followingAllowed &&
|
||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-friendbar-visit hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); followFriend(friend); setVisible(false); }} />}
|
||||
<div className="cursor-pointer tbme-icon nitro-friends-spritesheet icon-profile hover:-translate-y-1 transition-transform" onClick={event => { event.stopPropagation(); GetUserProfile(friend.id); setVisible(false); }} />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,133 @@
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { MessengerFriend } from '../../../../api';
|
||||
import { Button } from '../../../../common';
|
||||
import { LocalizeText, MessengerFriend } from '../../../../api';
|
||||
import { FriendBarItemView } from './FriendBarItemView';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const MAX_DISPLAY_COUNT = 3;
|
||||
|
||||
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[] }> = props =>
|
||||
{
|
||||
const { onlineFriends = null } = props;
|
||||
// Mirrored from Toolbar to keep physics identical
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.05 } },
|
||||
exit: { transition: { staggerChildren: 0.03, staggerDirection: -1 as const } },
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 10, scale: 0.8 },
|
||||
visible: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 400, damping: 22 } },
|
||||
exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } },
|
||||
};
|
||||
|
||||
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[]; requestsCount?: number }> = props => {
|
||||
const { onlineFriends = [], requestsCount = 0 } = props;
|
||||
const [ indexOffset, setIndexOffset ] = useState(0);
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const hasScrollableFriends = (onlineFriends.length > MAX_DISPLAY_COUNT);
|
||||
const visibleFriends = onlineFriends.slice(indexOffset, (indexOffset + MAX_DISPLAY_COUNT));
|
||||
|
||||
return (
|
||||
<div ref={ elementRef } className="flex items-center ">
|
||||
<Button className="z-2 cursor-pointer" disabled={ (indexOffset <= 0) } variant="black" onClick={ event => setIndexOffset(indexOffset - 1) }>
|
||||
<FaChevronLeft className="fa-icon" />
|
||||
</Button>
|
||||
{ Array.from(Array(MAX_DISPLAY_COUNT), (e, i) => <FriendBarItemView key={ i } friend={ (onlineFriends[indexOffset + i] || null) } />) }
|
||||
<Button className="z-2 cursor-pointer" disabled={ !((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) } variant="black" onClick={ event => setIndexOffset(indexOffset + 1) }>
|
||||
<FaChevronRight className="fa-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<motion.div
|
||||
ref={elementRef}
|
||||
className="flex h-[40px] items-center gap-[6px] px-[2px] py-[3px]"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
>
|
||||
{ (requestsCount > 0) &&
|
||||
<motion.div variants={ itemVariants }>
|
||||
<div className="flex h-[34px] items-center rounded-[7px] border border-[#9fc56f] bg-[#5f7d2f] px-[10px] text-[0.74rem] font-bold whitespace-nowrap text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)]">
|
||||
{ requestsCount } richieste
|
||||
</div>
|
||||
</motion.div> }
|
||||
<motion.div variants={itemVariants}>
|
||||
<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' }` }
|
||||
onClick={ () => { if(indexOffset > 0) setIndexOffset(indexOffset - 1); } }
|
||||
>
|
||||
<FaChevronLeft className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
{ visibleFriends.map(friend => (
|
||||
<motion.div
|
||||
key={ friend.id }
|
||||
variants={ itemVariants }
|
||||
layout
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
>
|
||||
<FriendBarItemView friend={ friend } />
|
||||
</motion.div>
|
||||
)) }
|
||||
<motion.div
|
||||
key="friend-search"
|
||||
variants={ itemVariants }
|
||||
layout
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
>
|
||||
<FriendBarItemView friend={ null } />
|
||||
</motion.div>
|
||||
{ (!onlineFriends.length && (requestsCount <= 0)) &&
|
||||
<motion.div
|
||||
key="friend-empty"
|
||||
variants={ itemVariants }
|
||||
layout
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
>
|
||||
<div className="flex h-[34px] items-center rounded-[7px] border border-[#9fc56f] bg-[#5f7d2f] px-[10px] text-[0.74rem] font-medium whitespace-nowrap text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18),0_2px_0_rgba(0,0,0,0.25)]">
|
||||
Nessun amico online
|
||||
</div>
|
||||
</motion.div> }
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<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' }` }
|
||||
onClick={ () => { if((onlineFriends.length > MAX_DISPLAY_COUNT) && ((indexOffset + MAX_DISPLAY_COUNT) <= (onlineFriends.length - 1))) setIndexOffset(indexOffset + 1); } }
|
||||
>
|
||||
<FaChevronRight className="text-white/70 text-sm drop-shadow-[1px_1px_0_#000]" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<style>{FRIENDBAR_STYLES}</style>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const FRIENDBAR_STYLES = `
|
||||
.tbme-panel {
|
||||
background: rgba(18, 16, 14, 0.88);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 14px;
|
||||
padding: 10px 12px;
|
||||
box-shadow:
|
||||
0 10px 36px rgba(0, 0, 0, 0.65),
|
||||
0 1px 0 rgba(255, 255, 255, 0.05) inset;
|
||||
}
|
||||
|
||||
.tbme-icon {
|
||||
opacity: 0.72;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.tbme-icon:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tbme-icon:active {
|
||||
opacity: 0.85;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user