mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { FC } 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();
|
||||
|
||||
if(!settings) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{ createPortal(<FriendBarView onlineFriends={ onlineFriends } />, document.getElementById('toolbar-friend-bar-container')) }
|
||||
<FriendsListView />
|
||||
<FriendsMessengerView />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MouseEventType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { GetUserProfile, LocalizeText, MessengerFriend, OpenMessengerChat } from '../../../../api';
|
||||
import { Button, LayoutAvatarImageView, LayoutBadgeImageView } from '../../../../common';
|
||||
import { useFriends } from '../../../../hooks';
|
||||
|
||||
export const FriendBarItemView: FC<{ friend: MessengerFriend }> = props =>
|
||||
{
|
||||
const { friend = null } = props;
|
||||
const [ isVisible, setVisible ] = useState(false);
|
||||
const { followFriend = null } = useFriends();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const onClick = (event: MouseEvent) =>
|
||||
{
|
||||
const element = elementRef.current;
|
||||
|
||||
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)
|
||||
{
|
||||
return (
|
||||
<Button className="border w-[130px] mx-[3px] my-[0] z-0 relative pl-[37px] text-left friend-bar-search" justifyContent="start" size="md">
|
||||
<div className="absolute -top-[3px] left-[5px] w-[31px] h-[34px] bg-[url('@/assets/images/toolbar/friend-search.png')]" />
|
||||
<div className="truncate">{ LocalizeText('friend.bar.find.title') }</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className={ ' block w-[130px] mx-[3px] my-[0] z-0 relative pl-[37px] text-left' + (isVisible ? 'mb-[21px]' : '') } justifyContent="start" size="md" variant={ 'success' } onClick={ event => setVisible(prevValue => !prevValue) }>
|
||||
<div className={ `friend-bar-item-head absolute ${ friend.id > 0 ? '-top-[30px] -left-[30px]' : '-top-[5px] -left-[3.5px]' }` }>
|
||||
{ (friend.id > 0) && <LayoutAvatarImageView direction={ 2 } figure={ friend.figure } headOnly={ true } /> }
|
||||
{ (friend.id <= 0) && <LayoutBadgeImageView badgeCode={ friend.figure } isGroup={ true } /> }
|
||||
</div>
|
||||
<div className="truncate">{ friend.name }</div>
|
||||
{ isVisible &&
|
||||
<div className="flex justify-between">
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-friendbar-chat" onClick={ event => OpenMessengerChat(friend.id) } />
|
||||
{ friend.followingAllowed &&
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-friendbar-visit" onClick={ event => followFriend(friend) } /> }
|
||||
<div className="cursor-pointer nitro-friends-spritesheet icon-profile" onClick={ event => GetUserProfile(friend.id) } />
|
||||
</div> }
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { MessengerFriend } from '../../../../api';
|
||||
import { Button } from '../../../../common';
|
||||
import { FriendBarItemView } from './FriendBarItemView';
|
||||
|
||||
const MAX_DISPLAY_COUNT = 3;
|
||||
|
||||
export const FriendBarView: FC<{ onlineFriends: MessengerFriend[] }> = props =>
|
||||
{
|
||||
const { onlineFriends = null } = props;
|
||||
const [ indexOffset, setIndexOffset ] = useState(0);
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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) } /> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
+85
@@ -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 } />) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
+25
@@ -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>
|
||||
);
|
||||
};
|
||||
+29
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
import { AddLinkEventTracker, FollowFriendMessageComposer, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { GetUserProfile, LocalizeText, ReportType, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutAvatarImageView, LayoutBadgeImageView, LayoutGridItem, LayoutItemCountView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { useHelp, useMessenger } from '../../../../hooks';
|
||||
import { NitroInput } from '../../../../layout';
|
||||
import { FriendsMessengerThreadView } from './messenger-thread/FriendsMessengerThreadView';
|
||||
|
||||
export const FriendsMessengerView: FC<{}> = props =>
|
||||
{
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ lastThreadId, setLastThreadId ] = useState(-1);
|
||||
const [ messageText, setMessageText ] = useState('');
|
||||
const { visibleThreads = [], activeThread = null, getMessageThread = null, sendMessage = null, setActiveThreadId = null, closeThread = null } = useMessenger();
|
||||
const { report = null } = useHelp();
|
||||
const messagesBox = useRef<HTMLDivElement>();
|
||||
|
||||
const followFriend = () => (activeThread && activeThread.participant && SendMessageComposer(new FollowFriendMessageComposer(activeThread.participant.id)));
|
||||
const openProfile = () => (activeThread && activeThread.participant && GetUserProfile(activeThread.participant.id));
|
||||
|
||||
const send = () =>
|
||||
{
|
||||
if(!activeThread || !messageText.length) return;
|
||||
|
||||
sendMessage(activeThread, GetSessionDataManager().userId, messageText);
|
||||
|
||||
setMessageText('');
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) =>
|
||||
{
|
||||
if(event.key !== 'Enter') return;
|
||||
|
||||
send();
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const linkTracker: ILinkEventTracker = {
|
||||
linkReceived: (url: string) =>
|
||||
{
|
||||
const parts = url.split('/');
|
||||
|
||||
if(parts.length === 2)
|
||||
{
|
||||
if(parts[1] === 'open')
|
||||
{
|
||||
setIsVisible(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(parts[1] === 'toggle')
|
||||
{
|
||||
setIsVisible(prevValue => !prevValue);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const thread = getMessageThread(parseInt(parts[1]));
|
||||
|
||||
if(!thread) return;
|
||||
|
||||
setActiveThreadId(thread.threadId);
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
eventUrlPrefix: 'friends-messenger/'
|
||||
};
|
||||
|
||||
AddLinkEventTracker(linkTracker);
|
||||
|
||||
return () => RemoveLinkEventTracker(linkTracker);
|
||||
}, [ getMessageThread, setActiveThreadId ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!isVisible || !activeThread) return;
|
||||
|
||||
messagesBox.current.scrollTop = messagesBox.current.scrollHeight;
|
||||
}, [ isVisible, activeThread ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isVisible && !activeThread)
|
||||
{
|
||||
if(lastThreadId > 0)
|
||||
{
|
||||
setActiveThreadId(lastThreadId);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(visibleThreads.length > 0) setActiveThreadId(visibleThreads[0].threadId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isVisible && activeThread)
|
||||
{
|
||||
setLastThreadId(activeThread.threadId);
|
||||
setActiveThreadId(-1);
|
||||
}
|
||||
}, [ isVisible, activeThread, lastThreadId, visibleThreads, setActiveThreadId ]);
|
||||
|
||||
if(!isVisible) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-friends-messenger" theme="primary-slim" uniqueKey="nitro-friends-messenger">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('messenger.window.title', [ 'OPEN_CHAT_COUNT' ], [ visibleThreads.length.toString() ]) } onCloseClick={ event => setIsVisible(false) } />
|
||||
<NitroCardContentView>
|
||||
<Grid overflow="hidden">
|
||||
<Column overflow="hidden" size={ 4 }>
|
||||
<Text bold>{ LocalizeText('toolbar.icon.label.messenger') }</Text>
|
||||
<Column fit overflow="auto">
|
||||
<div className="flex flex-col">
|
||||
{ visibleThreads && (visibleThreads.length > 0) && visibleThreads.map(thread =>
|
||||
{
|
||||
return (
|
||||
<LayoutGridItem key={ thread.threadId } itemActive={ (activeThread === thread) } onClick={ event => setActiveThreadId(thread.threadId) }>
|
||||
{ thread.unread &&
|
||||
<LayoutItemCountView count={ thread.unreadCount } /> }
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<div className="flex items-center friend-head px-1">
|
||||
{ (thread.participant.id > 0) &&
|
||||
<LayoutAvatarImageView direction={ 3 } figure={ thread.participant.figure } headOnly={ true } /> }
|
||||
{ (thread.participant.id <= 0) &&
|
||||
<LayoutBadgeImageView badgeCode={ thread.participant.figure } isGroup={ true } /> }
|
||||
</div>
|
||||
<Text grow truncate>{ thread.participant.name }</Text>
|
||||
</div>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
<Column overflow="hidden" size={ 8 }>
|
||||
{ activeThread &&
|
||||
<>
|
||||
<Text bold center>{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) }</Text>
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="between">
|
||||
<div className="flex gap-1">
|
||||
<div className="relative inline-flex align-middle">
|
||||
<Button onClick={ followFriend }>
|
||||
<div className="nitro-friends-spritesheet icon-follow" />
|
||||
</Button>
|
||||
<Button onClick={ openProfile }>
|
||||
<div className="nitro-friends-spritesheet icon-profile-sm" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="danger" onClick={ () => report(ReportType.IM, { reportedUserId: activeThread.participant.id }) }>
|
||||
{ LocalizeText('messenger.window.button.report') }
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={ event => closeThread(activeThread.threadId) }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</Button>
|
||||
</Flex>
|
||||
<Column fit className="bg-muted p-2 rounded chat-messages">
|
||||
<Column innerRef={ messagesBox } overflow="auto">
|
||||
<FriendsMessengerThreadView thread={ activeThread } />
|
||||
</Column>
|
||||
</Column>
|
||||
<div className="flex gap-1">
|
||||
<NitroInput maxLength={ 255 } placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } type="text" value={ messageText } onChange={ event => setMessageText(event.target.value) } onKeyDown={ onKeyDown } />
|
||||
<Button variant="success" onClick={ send }>
|
||||
{ LocalizeText('widgets.chatinput.say') }
|
||||
</Button>
|
||||
</div>
|
||||
</> }
|
||||
</Column>
|
||||
</Grid>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { GetSessionDataManager } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { GetGroupChatData, LocalizeText, MessengerGroupType, MessengerThread, MessengerThreadChat, MessengerThreadChatGroup } from '../../../../../api';
|
||||
import { Base, Flex, LayoutAvatarImageView } from '../../../../../common';
|
||||
|
||||
export const FriendsMessengerThreadGroup: FC<{ thread: MessengerThread, group: MessengerThreadChatGroup }> = props =>
|
||||
{
|
||||
const { thread = null, group = null } = props;
|
||||
|
||||
const groupChatData = useMemo(() => ((group.type === MessengerGroupType.GROUP_CHAT) && GetGroupChatData(group.chats[0].extraData)), [ group ]);
|
||||
|
||||
const isOwnChat = useMemo(() =>
|
||||
{
|
||||
if(!thread || !group) return false;
|
||||
|
||||
if((group.type === MessengerGroupType.PRIVATE_CHAT) && (group.userId === GetSessionDataManager().userId)) return true;
|
||||
|
||||
if(groupChatData && group.chats.length && (groupChatData.userId === GetSessionDataManager().userId)) return true;
|
||||
|
||||
return false;
|
||||
}, [ thread, group, groupChatData ]);
|
||||
|
||||
if(!thread || !group) return null;
|
||||
|
||||
if(!group.userId)
|
||||
{
|
||||
return (
|
||||
<>
|
||||
{ group.chats.map((chat, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } fullWidth gap={ 2 } justifyContent="start">
|
||||
<Base className="w-full text-break">
|
||||
{ (chat.type === MessengerThreadChat.SECURITY_NOTIFICATION) &&
|
||||
<Flex alignItems="center" className="bg-light rounded mb-2 px-2 py-1 small text-muted" gap={ 2 }>
|
||||
<Base className="nitro-friends-spritesheet icon-warning flex-shrink-0" />
|
||||
<Base>{ chat.message }</Base>
|
||||
</Flex> }
|
||||
{ (chat.type === MessengerThreadChat.ROOM_INVITE) &&
|
||||
<Flex alignItems="center" className="bg-light rounded mb-2 px-2 py-1 small text-black" gap={ 2 }>
|
||||
<Base className="messenger-notification-icon flex-shrink-0" />
|
||||
<Base>{ (LocalizeText('messenger.invitation') + ' ') }{ chat.message }</Base>
|
||||
</Flex> }
|
||||
</Base>
|
||||
</Flex>
|
||||
);
|
||||
}) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex fullWidth gap={ 2 } justifyContent={ isOwnChat ? 'end' : 'start' }>
|
||||
<Base shrink className="message-avatar">
|
||||
{ ((group.type === MessengerGroupType.PRIVATE_CHAT) && !isOwnChat) &&
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ thread.participant.figure } /> }
|
||||
{ (groupChatData && !isOwnChat) &&
|
||||
<LayoutAvatarImageView direction={ 2 } figure={ groupChatData.figure } /> }
|
||||
</Base>
|
||||
<Base className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (isOwnChat ? 'right' : 'left') }>
|
||||
<Base className="font-bold ">
|
||||
{ isOwnChat && GetSessionDataManager().userName }
|
||||
{ !isOwnChat && (groupChatData ? groupChatData.username : thread.participant.name) }
|
||||
</Base>
|
||||
{ group.chats.map((chat, index) => <Base key={ index } className="text-break">{ chat.message }</Base>) }
|
||||
</Base>
|
||||
{ isOwnChat &&
|
||||
<Base shrink className="message-avatar">
|
||||
<LayoutAvatarImageView direction={ 4 } figure={ GetSessionDataManager().figure } />
|
||||
</Base> }
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { FC } from 'react';
|
||||
import { MessengerThread } from '../../../../../api';
|
||||
import { FriendsMessengerThreadGroup } from './FriendsMessengerThreadGroup';
|
||||
|
||||
export const FriendsMessengerThreadView: FC<{ thread: MessengerThread }> = props =>
|
||||
{
|
||||
const { thread = null } = props;
|
||||
|
||||
thread.setRead();
|
||||
|
||||
return (
|
||||
<>
|
||||
{ (thread.groups.length > 0) && thread.groups.map((group, index) => <FriendsMessengerThreadGroup key={ index } group={ group } thread={ thread } />) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user