WIP preserve local changes before duckie merge

This commit is contained in:
Lorenzune
2026-04-21 11:13:32 +02:00
parent e0174e450c
commit 9b36513def
74 changed files with 4419 additions and 408 deletions
@@ -13,13 +13,19 @@ interface FriendsRemoveConfirmationViewProps
export const FriendsRemoveConfirmationView: FC<FriendsRemoveConfirmationViewProps> = props =>
{
const { selectedFriendsIds = null, removeFriendsText = null, removeSelectedFriends = null, onCloseClick = null } = props;
const separatorIndex = removeFriendsText.indexOf(':');
const removeFriendsLeadText = (separatorIndex >= 0) ? removeFriendsText.substring(0, separatorIndex + 1) : removeFriendsText;
const removeFriendsNamesText = (separatorIndex >= 0) ? removeFriendsText.substring(separatorIndex + 1).trimStart() : '';
return (
<NitroCardView className="nitro-friends-remove-confirmation" theme="primary-slim">
<NitroCardView className="nitro-friends-remove-confirmation" theme="primary-slim" isResizable={ false } style={ { width: 270, height: 225, minWidth: 270, minHeight: 225, maxWidth: 270, maxHeight: 225 } }>
<NitroCardHeaderView headerText={ LocalizeText('friendlist.removefriendconfirm.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black">
<div>{ removeFriendsText }</div>
<div className="flex gap-1">
<NitroCardContentView className="nitro-friends-remove-confirmation-content text-black">
<div className="nitro-friends-remove-confirmation-text">
<div>{ removeFriendsLeadText }</div>
{ removeFriendsNamesText.length > 0 && <div className="nitro-friends-remove-confirmation-names">{ removeFriendsNamesText }</div> }
</div>
<div className="nitro-friends-remove-confirmation-actions">
<Button fullWidth disabled={ (selectedFriendsIds.length === 0) } variant="danger" onClick={ removeSelectedFriends }>{ LocalizeText('generic.ok') }</Button>
<Button fullWidth onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</Button>
</div>
@@ -15,13 +15,13 @@ export const FriendsRoomInviteView: FC<FriendsRoomInviteViewProps> = props =>
const [ roomInviteMessage, setRoomInviteMessage ] = useState<string>('');
return (
<NitroCardView className="nitro-friends-room-invite" theme="primary-slim" uniqueKey="nitro-friends-room-invite">
<NitroCardView className="nitro-friends-room-invite" theme="primary-slim" uniqueKey="nitro-friends-room-invite" isResizable={ false } style={ { width: 270, height: 225, minWidth: 270, minHeight: 225, maxWidth: 270, maxHeight: 225 } }>
<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">
<NitroCardContentView className="nitro-friends-room-invite-content text-black" gap={ 2 }>
<Text className="nitro-friends-room-invite-summary">{ LocalizeText('friendlist.invite.summary', [ 'count' ], [ selectedFriendsIds.length.toString() ]) }</Text>
<textarea className="nitro-friends-room-invite-textarea" maxLength={ 255 } value={ roomInviteMessage } onChange={ event => setRoomInviteMessage(event.target.value) }></textarea>
<Text center className="nitro-friends-room-invite-note">{ LocalizeText('friendlist.invite.note') }</Text>
<div className="nitro-friends-room-invite-actions">
<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>
@@ -1,8 +1,10 @@
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 { Column, LayoutAvatarImageView, NitroCardAccordionItemView, NitroCardAccordionSetView, NitroCardAccordionSetViewProps, Text, UserProfileIconView } from '../../../../common';
import { useFriends, useMessageEvent } from '../../../../hooks';
import { resolveAvatarFigure } from './resolveAvatarFigure';
import { resolveAvatarGender } from './resolveAvatarGender';
interface FriendsSearchViewProps extends NitroCardAccordionSetViewProps
{
@@ -17,6 +19,22 @@ export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
const [ otherResults, setOtherResults ] = useState<HabboSearchResultData[]>(null);
const { canRequestFriend = null, requestFriend = null } = useFriends();
const getSearchResultFigure = (result: HabboSearchResultData) =>
{
if(!result) return null;
const typedResult = (result as HabboSearchResultData & { figureString?: string; avatarFigure?: string; figure?: string; avatarFigureString?: string });
return typedResult.figureString || typedResult.avatarFigure || typedResult.figure || typedResult.avatarFigureString || null;
};
const getSearchResultGender = (result: HabboSearchResultData) =>
{
const typedResult = (result as HabboSearchResultData & { gender?: string | number; avatarGender?: string | number });
return resolveAvatarGender(typedResult.avatarGender ?? typedResult.gender);
};
useMessageEvent<HabboSearchResultEvent>(HabboSearchResultEvent, event =>
{
const parser = event.getParser();
@@ -55,10 +73,15 @@ export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
{ 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>
<NitroCardAccordionItemView key={ result.avatarId } className="friends-list-item px-2 py-1" justifyContent="between">
<div className="friends-list-user">
<div className="friends-list-avatar">
<LayoutAvatarImageView figure={ resolveAvatarFigure(getSearchResultFigure(result), getSearchResultGender(result)) } gender={ getSearchResultGender(result) } headOnly={ true } direction={ 2 } />
</div>
<div>
<UserProfileIconView userId={ result.avatarId } />
</div>
<div className="friends-list-name">{ result.avatarName }</div>
</div>
<div className="flex items-center gap-1">
{ result.isAvatarOnline &&
@@ -82,10 +105,15 @@ export const FriendsSearchView: FC<FriendsSearchViewProps> = props =>
{ 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>
<NitroCardAccordionItemView key={ result.avatarId } className="friends-list-item px-2 py-1" justifyContent="between">
<div className="friends-list-user">
<div className="friends-list-avatar">
<LayoutAvatarImageView figure={ resolveAvatarFigure(getSearchResultFigure(result), getSearchResultGender(result)) } gender={ getSearchResultGender(result) } headOnly={ true } direction={ 2 } />
</div>
<div>
<UserProfileIconView userId={ result.avatarId } />
</div>
<div className="friends-list-name">{ result.avatarName }</div>
</div>
<div className="flex items-center gap-1">
{ canRequestFriend(result.avatarId) &&
@@ -34,7 +34,7 @@ export const FriendsListView: FC<{}> = props =>
userNames.push(existingFriend.name);
}
return LocalizeText('friendlist.removefriendconfirm.userlist', [ 'user_names' ], [ userNames.join(', ') ]);
return LocalizeText('friendlist.removefriendconfirm.userlist', [ 'user_names' ], [ userNames.join('\n') ]);
}, [ offlineFriends, onlineFriends, selectedFriendsIds ]);
const selectFriend = useCallback((userId: number) =>
@@ -60,6 +60,27 @@ export const FriendsListView: FC<{}> = props =>
});
}, [ setSelectedFriendsIds ]);
const toggleSelectFriends = useCallback((friendIds: number[]) =>
{
if(!friendIds.length) return;
setSelectedFriendsIds(prevValue =>
{
const allSelected = friendIds.every(friendId => (prevValue.indexOf(friendId) >= 0));
if(allSelected) return prevValue.filter(friendId => (friendIds.indexOf(friendId) === -1));
const nextValue = [ ...prevValue ];
for(const friendId of friendIds)
{
if(nextValue.indexOf(friendId) === -1) nextValue.push(friendId);
}
return nextValue;
});
}, []);
const sendRoomInvite = (message: string) =>
{
if(!selectedFriendsIds.length || !message || !message.length || (message.length > 255)) return;
@@ -125,10 +146,24 @@ export const FriendsListView: FC<{}> = props =>
<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 }>
<NitroCardAccordionSetView className="friends-list-section" headerText={ LocalizeText('friendlist.friends') + ` (${ onlineFriends.length })` } isExpanded={ true }>
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
<span className="friends-list-toolbar-link" onClick={ event => { event.stopPropagation(); toggleSelectFriends(onlineFriends.map(friend => friend.id)); } }>
{ onlineFriends.length && onlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
? LocalizeText('friendlist.unselect_all')
: LocalizeText('friendlist.select_all') }
</span>
</Flex>
<FriendsListGroupView list={ onlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
</NitroCardAccordionSetView>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${ offlineFriends.length })` }>
<Flex className="friends-list-toolbar px-2 py-1" justifyContent="end">
<span className="friends-list-toolbar-link" onClick={ event => { event.stopPropagation(); toggleSelectFriends(offlineFriends.map(friend => friend.id)); } }>
{ offlineFriends.length && offlineFriends.every(friend => (selectedFriendsIds.indexOf(friend.id) >= 0))
? LocalizeText('friendlist.unselect_all')
: LocalizeText('friendlist.select_all') }
</span>
</Flex>
<FriendsListGroupView list={ offlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
</NitroCardAccordionSetView>
<FriendsListRequestView headerText={ LocalizeText('friendlist.tab.friendrequests') + ` (${ requests.length })` } isExpanded={ true } />
@@ -1,7 +1,9 @@
import { FC, MouseEvent, useState } from 'react';
import { LocalizeText, MessengerFriend, OpenMessengerChat } from '../../../../../api';
import { NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
import { LayoutAvatarImageView, NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
import { useFriends } from '../../../../../hooks';
import { resolveAvatarFigure } from '../resolveAvatarFigure';
import { resolveAvatarGender } from '../resolveAvatarGender';
export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: boolean, selectFriend: (userId: number) => void }> = props =>
{
@@ -55,14 +57,17 @@ export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: b
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">
<NitroCardAccordionItemView className={ `friends-list-item ${ selected ? 'selected' : '' }` } justifyContent="between" onClick={ event => selectFriend(friend.id) }>
<div className="friends-list-user">
<div className="friends-list-avatar">
<LayoutAvatarImageView figure={ resolveAvatarFigure(friend.figure, friend.gender) } gender={ resolveAvatarGender(friend.gender) } headOnly={ true } direction={ 2 } />
</div>
<div onClick={ event => event.stopPropagation() }>
<UserProfileIconView userId={ friend.id } />
</div>
<div>{ friend.name }</div>
<div className="friends-list-name">{ friend.name }</div>
</div>
<div className="flex items-center gap-1">
<div className="friends-list-actions">
{ !isRelationshipOpen &&
<>
{ friend.online &&
@@ -1,7 +1,9 @@
import { FC } from 'react';
import { MessengerRequest } from '../../../../../api';
import { NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
import { LocalizeText, MessengerRequest } from '../../../../../api';
import { Button, LayoutAvatarImageView, NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common';
import { useFriends } from '../../../../../hooks';
import { resolveAvatarFigure } from '../resolveAvatarFigure';
import { resolveAvatarGender } from '../resolveAvatarGender';
export const FriendsListRequestItemView: FC<{ request: MessengerRequest }> = props =>
{
@@ -11,14 +13,23 @@ export const FriendsListRequestItemView: FC<{ request: MessengerRequest }> = pro
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>
<NitroCardAccordionItemView className="friends-list-item px-2 py-1" justifyContent="between">
<div className="friends-list-user">
<div className="friends-list-avatar">
<LayoutAvatarImageView figure={ resolveAvatarFigure(request.figureString) } gender={ resolveAvatarGender(undefined) } headOnly={ true } direction={ 2 } />
</div>
<div>
<UserProfileIconView userId={ request.requesterUserId } />
</div>
<div className="friends-list-name">{ 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) } />
<Button size="sm" onClick={ event => requestResponse(request.id, true) }>
{ LocalizeText('friendlist.request_accept') }
</Button>
<Button size="sm" variant="danger" onClick={ event => requestResponse(request.id, false) }>
{ LocalizeText('friendlist.request_decline') }
</Button>
</div>
</NitroCardAccordionItemView>
);
@@ -17,8 +17,11 @@ export const FriendsListRequestView: FC<NitroCardAccordionSetViewProps> = props
<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) }>
<div className="flex justify-center gap-2 px-2 py-1">
<Button onClick={ event => requests.forEach(request => requestResponse(request.id, true)) }>
{ LocalizeText('friendlist.requests.acceptall') }
</Button>
<Button variant="danger" onClick={ event => requestResponse(-1, false) }>
{ LocalizeText('friendlist.requests.dismissall') }
</Button>
</div>
@@ -0,0 +1,15 @@
import { resolveAvatarGender } from './resolveAvatarGender';
const DEFAULT_AVATAR_FIGURES: Record<string, string> = {
M: 'hd-180-1.ch-210-66.lg-270-82.sh-290-80',
F: 'hd-600-1.ch-630-66.lg-695-82.sh-725-80'
};
export const resolveAvatarFigure = (figure: string | null | undefined, gender?: string | number | null) =>
{
const normalizedFigure = (figure || '').trim();
if(normalizedFigure.length && normalizedFigure.includes('hd-')) return normalizedFigure;
return DEFAULT_AVATAR_FIGURES[resolveAvatarGender(gender)] || DEFAULT_AVATAR_FIGURES.M;
};
@@ -0,0 +1,20 @@
export const resolveAvatarGender = (value: string | number | null | undefined) =>
{
if(typeof value === 'string')
{
const normalized = value.trim().toUpperCase();
if(normalized === 'F') return 'F';
if(normalized === 'M') return 'M';
if(normalized === 'FEMALE') return 'F';
if(normalized === 'MALE') return 'M';
}
if(typeof value === 'number')
{
if(value === 2) return 'F';
if(value === 1) return 'M';
}
return 'M';
};