🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
@@ -0,0 +1,29 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
interface FriendsRemoveConfirmationViewProps
{
selectedFriendsIds: number[];
removeFriendsText: string;
removeSelectedFriends: () => void;
onCloseClick: () => void;
}
export const FriendsRemoveConfirmationView: FC<FriendsRemoveConfirmationViewProps> = props =>
{
const { selectedFriendsIds = null, removeFriendsText = null, removeSelectedFriends = null, onCloseClick = null } = props;
return (
<NitroCardView className="nitro-friends-remove-confirmation" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('friendlist.removefriendconfirm.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black">
<div>{ removeFriendsText }</div>
<div className="flex gap-1">
<Button fullWidth disabled={ (selectedFriendsIds.length === 0) } variant="danger" onClick={ removeSelectedFriends }>{ LocalizeText('generic.ok') }</Button>
<Button fullWidth onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,31 @@
import { FC, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
interface FriendsRoomInviteViewProps
{
selectedFriendsIds: number[];
onCloseClick: () => void;
sendRoomInvite: (message: string) => void;
}
export const FriendsRoomInviteView: FC<FriendsRoomInviteViewProps> = props =>
{
const { selectedFriendsIds = null, onCloseClick = null, sendRoomInvite = null } = props;
const [ roomInviteMessage, setRoomInviteMessage ] = useState<string>('');
return (
<NitroCardView className="nitro-friends-room-invite" theme="primary-slim" uniqueKey="nitro-friends-room-invite">
<NitroCardHeaderView headerText={ LocalizeText('friendlist.invite.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black">
{ LocalizeText('friendlist.invite.summary', [ 'count' ], [ selectedFriendsIds.length.toString() ]) }
<textarea className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem]" maxLength={ 255 } value={ roomInviteMessage } onChange={ event => setRoomInviteMessage(event.target.value) }></textarea>
<Text center className="bg-muted rounded p-1">{ LocalizeText('friendlist.invite.note') }</Text>
<div className="flex gap-1">
<Button fullWidth disabled={ ((roomInviteMessage.length === 0) || (selectedFriendsIds.length === 0)) } variant="success" onClick={ () => sendRoomInvite(roomInviteMessage) }>{ LocalizeText('friendlist.invite.send') }</Button>
<Button fullWidth onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</Button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,103 @@
import { HabboSearchComposer, HabboSearchResultData, HabboSearchResultEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { LocalizeText, OpenMessengerChat, SendMessageComposer } from '../../../../api';
import { Column, NitroCardAccordionItemView, NitroCardAccordionSetView, NitroCardAccordionSetViewProps, Text, UserProfileIconView } from '../../../../common';
import { useFriends, useMessageEvent } from '../../../../hooks';
interface FriendsSearchViewProps extends NitroCardAccordionSetViewProps
{
}
export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
{
const { ...rest } = props;
const [ searchValue, setSearchValue ] = useState('');
const [ friendResults, setFriendResults ] = useState<HabboSearchResultData[]>(null);
const [ otherResults, setOtherResults ] = useState<HabboSearchResultData[]>(null);
const { canRequestFriend = null, requestFriend = null } = useFriends();
useMessageEvent<HabboSearchResultEvent>(HabboSearchResultEvent, event =>
{
const parser = event.getParser();
setFriendResults(parser.friends);
setOtherResults(parser.others);
});
useEffect(() =>
{
if(!searchValue || !searchValue.length) return;
const timeout = setTimeout(() =>
{
if(!searchValue || !searchValue.length) return;
SendMessageComposer(new HabboSearchComposer(searchValue));
}, 500);
return () => clearTimeout(timeout);
}, [ searchValue ]);
return (
<NitroCardAccordionSetView { ...rest }>
<input className="search-input form-control form-control-sm w-full rounded-0" maxLength={ 50 } placeholder={ LocalizeText('generic.search') } type="text" value={ searchValue } onChange={ event => setSearchValue(event.target.value) } />
<div className="flex flex-col">
{ friendResults &&
<>
{ (friendResults.length === 0) &&
<Text bold small className="px-2 py-1">{ LocalizeText('friendlist.search.nofriendsfound') }</Text> }
{ (friendResults.length > 0) &&
<Column gap={ 0 }>
<Text bold small className="px-2 py-1">{ LocalizeText('friendlist.search.friendscaption', [ 'cnt' ], [ friendResults.length.toString() ]) }</Text>
<hr className="mx-2 mt-0 mb-1 text-black" />
<Column gap={ 0 }>
{ friendResults.map(result =>
{
return (
<NitroCardAccordionItemView key={ result.avatarId } className="px-2 py-1" justifyContent="between">
<div className="flex items-center gap-1">
<UserProfileIconView userId={ result.avatarId } />
<div>{ result.avatarName }</div>
</div>
<div className="flex items-center gap-1">
{ result.isAvatarOnline &&
<div className="nitro-friends-spritesheet icon-chat cursor-pointer" title={ LocalizeText('friendlist.tip.im') } onClick={ event => OpenMessengerChat(result.avatarId) } /> }
</div>
</NitroCardAccordionItemView>
);
}) }
</Column>
</Column> }
</> }
{ otherResults &&
<>
{ (otherResults.length === 0) &&
<Text bold small className="px-2 py-1">{ LocalizeText('friendlist.search.noothersfound') }</Text> }
{ (otherResults.length > 0) &&
<Column gap={ 0 }>
<Text bold small className="px-2 py-1">{ LocalizeText('friendlist.search.otherscaption', [ 'cnt' ], [ otherResults.length.toString() ]) }</Text>
<hr className="mx-2 mt-0 mb-1 text-black" />
<Column gap={ 0 }>
{ otherResults.map(result =>
{
return (
<NitroCardAccordionItemView key={ result.avatarId } className="px-2 py-1" justifyContent="between">
<div className="flex items-center gap-1">
<UserProfileIconView userId={ result.avatarId } />
<div>{ result.avatarName }</div>
</div>
<div className="flex items-center gap-1">
{ canRequestFriend(result.avatarId) &&
<div className="nitro-friends-spritesheet icon-add cursor-pointer" title={ LocalizeText('friendlist.tip.addfriend') } onClick={ event => requestFriend(result.avatarId, result.avatarName) } /> }
</div>
</NitroCardAccordionItemView>
);
}) }
</Column>
</Column> }
</> }
</div>
</NitroCardAccordionSetView>
);
};
@@ -0,0 +1,150 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveFriendComposer, RemoveLinkEventTracker, SendRoomInviteComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText, MessengerFriend, SendMessageComposer } from '../../../../api';
import { Button, Flex, NitroCardAccordionSetView, NitroCardAccordionView, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useFriends } from '../../../../hooks';
import { FriendsRemoveConfirmationView } from './FriendsListRemoveConfirmationView';
import { FriendsRoomInviteView } from './FriendsListRoomInviteView';
import { FriendsSearchView } from './FriendsListSearchView';
import { FriendsListGroupView } from './friends-list-group/FriendsListGroupView';
import { FriendsListRequestView } from './friends-list-request/FriendsListRequestView';
export const FriendsListView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ selectedFriendsIds, setSelectedFriendsIds ] = useState<number[]>([]);
const [ showRoomInvite, setShowRoomInvite ] = useState<boolean>(false);
const [ showRemoveFriendsConfirmation, setShowRemoveFriendsConfirmation ] = useState<boolean>(false);
const { onlineFriends = [], offlineFriends = [], requests = [], requestFriend = null } = useFriends();
const removeFriendsText = useMemo(() =>
{
if(!selectedFriendsIds || !selectedFriendsIds.length) return '';
const userNames: string[] = [];
for(const userId of selectedFriendsIds)
{
let existingFriend: MessengerFriend = onlineFriends.find(f => f.id === userId);
if(!existingFriend) existingFriend = offlineFriends.find(f => f.id === userId);
if(!existingFriend) continue;
userNames.push(existingFriend.name);
}
return LocalizeText('friendlist.removefriendconfirm.userlist', [ 'user_names' ], [ userNames.join(', ') ]);
}, [ offlineFriends, onlineFriends, selectedFriendsIds ]);
const selectFriend = useCallback((userId: number) =>
{
if(userId < 0) return;
setSelectedFriendsIds(prevValue =>
{
const newValue = [ ...prevValue ];
const existingUserIdIndex: number = newValue.indexOf(userId);
if(existingUserIdIndex > -1)
{
newValue.splice(existingUserIdIndex, 1);
}
else
{
newValue.push(userId);
}
return newValue;
});
}, [ setSelectedFriendsIds ]);
const sendRoomInvite = (message: string) =>
{
if(!selectedFriendsIds.length || !message || !message.length || (message.length > 255)) return;
SendMessageComposer(new SendRoomInviteComposer(message, selectedFriendsIds));
setShowRoomInvite(false);
};
const removeSelectedFriends = () =>
{
if(selectedFriendsIds.length === 0) return;
setSelectedFriendsIds(prevValue =>
{
SendMessageComposer(new RemoveFriendComposer(...prevValue));
return [];
});
setShowRemoveFriendsConfirmation(false);
};
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
linkReceived: (url: string) =>
{
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
setIsVisible(true);
return;
case 'hide':
setIsVisible(false);
return;
case 'toggle':
setIsVisible(prevValue => !prevValue);
return;
case 'request':
if(parts.length < 4) return;
requestFriend(parseInt(parts[2]), parts[3]);
}
},
eventUrlPrefix: 'friends/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, [ requestFriend ]);
if(!isVisible) return null;
return (
<>
<NitroCardView className="nitro-friends" theme="primary-slim" uniqueKey="nitro-friends">
<NitroCardHeaderView headerText={ LocalizeText('friendlist.friends') } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView className="text-black p-0" gap={ 1 } overflow="hidden">
<NitroCardAccordionView fullHeight overflow="hidden">
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends') + ` (${ onlineFriends.length })` } isExpanded={ true }>
<FriendsListGroupView list={ onlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
</NitroCardAccordionSetView>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }>
<FriendsListGroupView list={ offlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
</NitroCardAccordionSetView>
<FriendsListRequestView headerText={ LocalizeText('friendlist.tab.friendrequests') + ` (${ requests.length })` } isExpanded={ true } />
<FriendsSearchView headerText={ LocalizeText('people.search.title') } />
</NitroCardAccordionView>
{ selectedFriendsIds && selectedFriendsIds.length > 0 &&
<Flex className="p-1" gap={ 1 }>
<Button fullWidth onClick={ () => setShowRoomInvite(true) }>{ LocalizeText('friendlist.tip.invite') }</Button>
<Button fullWidth variant="danger" onClick={ event => setShowRemoveFriendsConfirmation(true) }>{ LocalizeText('generic.delete') }</Button>
</Flex> }
</NitroCardContentView>
</NitroCardView>
{ showRoomInvite &&
<FriendsRoomInviteView selectedFriendsIds={ selectedFriendsIds } sendRoomInvite={ sendRoomInvite } onCloseClick={ () => setShowRoomInvite(false) } /> }
{ showRemoveFriendsConfirmation &&
<FriendsRemoveConfirmationView removeFriendsText={ removeFriendsText } removeSelectedFriends={ removeSelectedFriends } selectedFriendsIds={ selectedFriendsIds } onCloseClick={ () => setShowRemoveFriendsConfirmation(false) } /> }
</>
);
};
@@ -0,0 +1,85 @@
import { FC, MouseEvent, useState } from 'react';
import { LocalizeText, MessengerFriend, OpenMessengerChat } from '../../../../../api';
import { NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
import { useFriends } from '../../../../../hooks';
export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: boolean, selectFriend: (userId: number) => void }> = props =>
{
const { friend = null, selected = false, selectFriend = null } = props;
const [ isRelationshipOpen, setIsRelationshipOpen ] = useState<boolean>(false);
const { followFriend = null, updateRelationship = null } = useFriends();
const clickFollowFriend = (event: MouseEvent<HTMLDivElement>) =>
{
event.stopPropagation();
followFriend(friend);
};
const openMessengerChat = (event: MouseEvent<HTMLDivElement>) =>
{
event.stopPropagation();
OpenMessengerChat(friend.id);
};
const openRelationship = (event: MouseEvent<HTMLDivElement>) =>
{
event.stopPropagation();
setIsRelationshipOpen(true);
};
const clickUpdateRelationship = (event: MouseEvent<HTMLDivElement>, type: number) =>
{
event.stopPropagation();
updateRelationship(friend, type);
setIsRelationshipOpen(false);
};
const getCurrentRelationshipName = () =>
{
if(!friend) return 'none';
switch(friend.relationshipStatus)
{
case MessengerFriend.RELATIONSHIP_HEART: return 'heart';
case MessengerFriend.RELATIONSHIP_SMILE: return 'smile';
case MessengerFriend.RELATIONSHIP_BOBBA: return 'bobba';
default: return 'none';
}
};
if(!friend) return null;
return (
<NitroCardAccordionItemView className={ `px-2 py-1 ${ selected && 'bg-primary text-white' }` } justifyContent="between" onClick={ event => selectFriend(friend.id) }>
<div className="flex items-center gap-1">
<div onClick={ event => event.stopPropagation() }>
<UserProfileIconView userId={ friend.id } />
</div>
<div>{ friend.name }</div>
</div>
<div className="flex items-center gap-1">
{ !isRelationshipOpen &&
<>
{ friend.followingAllowed &&
<div className="nitro-friends-spritesheet icon-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } onClick={ clickFollowFriend } /> }
{ friend.online &&
<div className="nitro-friends-spritesheet icon-chat cursor-pointer" title={ LocalizeText('friendlist.tip.im') } onClick={ openMessengerChat } /> }
{ (friend.id > 0) &&
<div className={ `nitro-friends-spritesheet icon-${ getCurrentRelationshipName() } cursor-pointer` } title={ LocalizeText('infostand.link.relationship') } onClick={ openRelationship } /> }
</> }
{ isRelationshipOpen &&
<>
<div className="nitro-friends-spritesheet icon-heart cursor-pointer" onClick={ event => clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_HEART) } />
<div className="nitro-friends-spritesheet icon-smile cursor-pointer" onClick={ event => clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_SMILE) } />
<div className="nitro-friends-spritesheet icon-bobba cursor-pointer" onClick={ event => clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_BOBBA) } />
<div className="nitro-friends-spritesheet icon-none cursor-pointer" onClick={ event => clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_NONE) } />
</> }
</div>
</NitroCardAccordionItemView>
);
};
@@ -0,0 +1,23 @@
import { FC } from 'react';
import { MessengerFriend } from '../../../../../api';
import { FriendsListGroupItemView } from './FriendsListGroupItemView';
interface FriendsListGroupViewProps
{
list: MessengerFriend[];
selectedFriendsIds: number[];
selectFriend: (userId: number) => void;
}
export const FriendsListGroupView: FC<FriendsListGroupViewProps> = props =>
{
const { list = null, selectedFriendsIds = null, selectFriend = null } = props;
if(!list || !list.length) return null;
return (
<>
{ list.map((item, index) => <FriendsListGroupItemView key={ index } friend={ item } selected={ selectedFriendsIds && (selectedFriendsIds.indexOf(item.id) >= 0) } selectFriend={ selectFriend } />) }
</>
);
};
@@ -0,0 +1,25 @@
import { FC } from 'react';
import { MessengerRequest } from '../../../../../api';
import { NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
import { useFriends } from '../../../../../hooks';
export const FriendsListRequestItemView: FC<{ request: MessengerRequest }> = props =>
{
const { request = null } = props;
const { requestResponse = null } = useFriends();
if(!request) return null;
return (
<NitroCardAccordionItemView className="px-2 py-1" justifyContent="between">
<div className="flex items-center gap-1">
<UserProfileIconView userId={ request.id } />
<div>{ request.name }</div>
</div>
<div className="flex items-center gap-1">
<div className="nitro-friends-spritesheet icon-accept cursor-pointer" onClick={ event => requestResponse(request.id, true) } />
<div className="nitro-friends-spritesheet icon-deny cursor-pointer" onClick={ event => requestResponse(request.id, false) } />
</div>
</NitroCardAccordionItemView>
);
};
@@ -0,0 +1,29 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button, Column, NitroCardAccordionSetView, NitroCardAccordionSetViewProps } from '../../../../../common';
import { useFriends } from '../../../../../hooks';
import { FriendsListRequestItemView } from './FriendsListRequestItemView';
export const FriendsListRequestView: FC<NitroCardAccordionSetViewProps> = props =>
{
const { children = null, ...rest } = props;
const { requests = [], requestResponse = null } = useFriends();
if(!requests.length) return null;
return (
<NitroCardAccordionSetView { ...rest }>
<Column fullHeight gap={ 1 } justifyContent="between">
<Column gap={ 0 }>
{ requests.map((request, index) => <FriendsListRequestItemView key={ index } request={ request } />) }
</Column>
<div className="flex justify-center px-2 py-1">
<Button onClick={ event => requestResponse(-1, false) }>
{ LocalizeText('friendlist.requests.dismissall') }
</Button>
</div>
</Column>
{ children }
</NitroCardAccordionSetView>
);
};