Merge branch 'dev' into more-layout-changes

This commit is contained in:
Bill 2022-02-16 04:36:26 -05:00
commit bdc2861e20
23 changed files with 612 additions and 129 deletions

View File

@ -12,3 +12,7 @@
.nitro-cfh-sanction-status { .nitro-cfh-sanction-status {
width: 400px; width: 400px;
} }
.nitro-change-username {
width: 300px;
}

View File

@ -10,6 +10,7 @@ import { HelpContextProvider } from './HelpContext';
import { HelpMessageHandler } from './HelpMessageHandler'; import { HelpMessageHandler } from './HelpMessageHandler';
import { DescribeReportView } from './views/DescribeReportView'; import { DescribeReportView } from './views/DescribeReportView';
import { HelpIndexView } from './views/HelpIndexView'; import { HelpIndexView } from './views/HelpIndexView';
import { NameChangeView } from './views/name-change/NameChangeView';
import { SanctionSatusView } from './views/SanctionStatusView'; import { SanctionSatusView } from './views/SanctionStatusView';
import { SelectReportedChatsView } from './views/SelectReportedChatsView'; import { SelectReportedChatsView } from './views/SelectReportedChatsView';
import { SelectReportedUserView } from './views/SelectReportedUserView'; import { SelectReportedUserView } from './views/SelectReportedUserView';
@ -124,6 +125,7 @@ export const HelpView: FC<{}> = props =>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> } </NitroCardView> }
<SanctionSatusView /> <SanctionSatusView />
<NameChangeView />
</HelpContextProvider> </HelpContextProvider>
); );
} }

View File

@ -0,0 +1,47 @@
import { ChangeUserNameMessageComposer, UserNameChangeMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetSessionDataManager, LocalizeText } from '../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NameChangeLayoutViewProps } from './NameChangeView.types';
export const NameChangeConfirmationView:FC<NameChangeLayoutViewProps> = props =>
{
const { username = '', onAction = null } = props;
const [ isConfirming, setIsConfirming ] = useState<boolean>(false);
const onUserNameChangeMessageEvent = useCallback((event: UserNameChangeMessageEvent) =>
{
const parser = event.getParser();
if(!parser) return;
if(parser.webId !== GetSessionDataManager().userId) return;
onAction('close');
}, [ onAction ]);
CreateMessageHook(UserNameChangeMessageEvent, onUserNameChangeMessageEvent);
const confirm = useCallback(() =>
{
if(isConfirming) return;
setIsConfirming(true);
SendMessageHook(new ChangeUserNameMessageComposer(username));
}, [ isConfirming, username ]);
return (
<div className="d-flex flex-column gap-4 h-100">
<div className="bg-muted rounded p-2 text-center">{ LocalizeText('tutorial.name_change.info.confirm') }</div>
<div className="d-flex flex-column align-items-center gap-1 h-100">
<div>{ LocalizeText('tutorial.name_change.confirm') }</div>
<div className="fw-bold">{ username }</div>
</div>
<div className="d-flex gap-2">
<button className="btn btn-success w-100" disabled={ isConfirming } onClick={ confirm }>{ LocalizeText('generic.ok') }</button>
<button className="btn btn-primary w-100" onClick={ () => onAction('close') }>{ LocalizeText('cancel') }</button>
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
import { FC } from 'react';
import { GetSessionDataManager, LocalizeText } from '../../../../api';
import { NameChangeLayoutViewProps } from './NameChangeView.types';
export const NameChangeInitView:FC<NameChangeLayoutViewProps> = props =>
{
const { onAction = null } = props;
return (
<div className="d-flex flex-column gap-4 h-100">
<div className="bg-muted rounded p-2 text-center">{ LocalizeText('tutorial.name_change.info.main') }</div>
<div className="fw-bold d-flex align-items-center justify-content-center h-100 w-100">{ LocalizeText('tutorial.name_change.current', ['name'], [GetSessionDataManager().userName]) }</div>
<div className="d-flex gap-2">
<button className="btn btn-success w-100" onClick={ () => onAction('start') }>{ LocalizeText('tutorial.name_change.change') }</button>
<button className="btn btn-primary w-100" onClick={ () => onAction('confirmation', GetSessionDataManager().userName) }>{ LocalizeText('tutorial.name_change.keep') }</button>
</div>
</div>
);
}

View File

@ -0,0 +1,102 @@
import { CheckUserNameMessageComposer, CheckUserNameResultMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { CreateMessageHook, SendMessageHook } from '../../../../hooks';
import { NameChangeLayoutViewProps } from './NameChangeView.types';
const AVAILABLE: number = 0;
const TOO_SHORT: number = 2;
const TOO_LONG: number = 3;
const NOT_VALID: number = 4;
const TAKEN_WITH_SUGGESTIONS: number = 5;
const DISABLED: number = 6;
export const NameChangeInputView:FC<NameChangeLayoutViewProps> = props =>
{
const { onAction = null } = props;
const [ newUsername, setNewUsername ] = useState<string>('');
const [ canProceed, setCanProceed ] = useState<boolean>(false);
const [ isChecking, setIsChecking ] = useState<boolean>(false);
const [ errorCode, setErrorCode ] = useState<string>(null);
const [ suggestions, setSuggestions ] = useState<string[]>([]);
const onCheckUserNameResultMessageEvent = useCallback((event: CheckUserNameResultMessageEvent) =>
{
setIsChecking(false);
const parser = event.getParser();
if(!parser) return;
switch(parser.resultCode)
{
case AVAILABLE:
setCanProceed(true);
break;
case TOO_SHORT:
setErrorCode('short');
break;
case TOO_LONG:
setErrorCode('long');
break;
case NOT_VALID:
setErrorCode('invalid');
break;
case TAKEN_WITH_SUGGESTIONS:
setSuggestions(parser.nameSuggestions);
setErrorCode('taken');
break;
case DISABLED:
setErrorCode('change_not_allowed');
}
}, []);
CreateMessageHook(CheckUserNameResultMessageEvent, onCheckUserNameResultMessageEvent);
const check = useCallback(() =>
{
if(newUsername === '') return;
setCanProceed(false);
setSuggestions([]);
setErrorCode(null);
setIsChecking(true);
SendMessageHook(new CheckUserNameMessageComposer(newUsername));
}, [ newUsername ]);
const handleUsernameChange = useCallback((username: string) =>
{
setCanProceed(false);
setSuggestions([]);
setErrorCode(null);
setNewUsername(username);
}, []);
return (
<div className="d-flex flex-column gap-3 h-100">
<div>{ LocalizeText('tutorial.name_change.info.select') }</div>
<div className="d-flex gap-2">
<input type="text" className="form-control form-control-sm" value={newUsername} onChange={ (e) => handleUsernameChange(e.target.value) } />
<button className="btn btn-primary" disabled={ newUsername === '' || isChecking } onClick={ check }>{ LocalizeText('tutorial.name_change.check') }</button>
</div>
{ !errorCode && !canProceed && <div className="bg-muted rounded p-2 text-center">{ LocalizeText('help.tutorial.name.info') }</div> }
{ errorCode && <div className="bg-danger rounded p-2 text-center text-white">{ LocalizeText(`help.tutorial.name.${errorCode}`, ['name'], [newUsername]) }</div> }
{ canProceed && <div className="bg-success rounded p-2 text-center text-white">{ LocalizeText('help.tutorial.name.available', ['name'], [newUsername]) }</div> }
{ suggestions && <div className="d-flex flex-column gap-2">
{
suggestions.map((suggestion, i) =>
{
return (<div key={ i } className="col bg-muted rounded p-1 cursor-pointer" onClick={ () => handleUsernameChange(suggestion) }>{ suggestion }</div>);
})
}
</div> }
<div className="d-flex gap-2">
<button className="btn btn-success w-100" disabled={ !canProceed } onClick={ () => onAction('confirmation', newUsername) }>{ LocalizeText('tutorial.name_change.pick') }</button>
<button className="btn btn-primary w-100" onClick={ () => onAction('close') }>{ LocalizeText('cancel') }</button>
</div>
</div>
);
}

View File

@ -0,0 +1,68 @@
import { FC, useCallback, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { HelpNameChangeEvent } from '../../../../events/help/HelpNameChangeEvent';
import { useUiEvent } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { NameChangeConfirmationView } from './NameChangeConfirmationView';
import { NameChangeInitView } from './NameChangeInitView';
import { NameChangeInputView } from './NameChangeInputView';
const INIT: string = 'INIT';
const INPUT: string = 'INPUT';
const CONFIRMATION: string = 'CONFIRMATION';
export const NameChangeView:FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState<boolean>(false);
const [ layout, setLayout ] = useState<string>(INIT);
const [ newUsername, setNewUsername ] = useState<string>('');
const onHelpNameChangeEvent = useCallback((event: HelpNameChangeEvent) =>
{
setLayout(INIT);
setIsVisible(true);
}, []);
useUiEvent(HelpNameChangeEvent.INIT, onHelpNameChangeEvent);
const onAction = useCallback((action: string, value?: string) =>
{
switch(action)
{
case 'start':
setLayout(INPUT);
break;
case 'confirmation':
setNewUsername(value);
setLayout(CONFIRMATION);
break;
case 'close':
setNewUsername('');
setIsVisible(false);
break;
}
}, []);
const titleKey = useMemo(() =>
{
switch(layout)
{
case INIT: return 'tutorial.name_change.title.main';
case INPUT: return 'tutorial.name_change.title.select';
case CONFIRMATION: return 'tutorial.name_change.title.confirm';
}
}, [layout]);
if(!isVisible) return null;
return (
<NitroCardView className="nitro-change-username" simple={ true }>
<NitroCardHeaderView headerText={LocalizeText(titleKey)} onCloseClick={ () => onAction('close') } />
<NitroCardContentView className="text-black">
{ layout === INIT && <NameChangeInitView onAction={ onAction } /> }
{ layout === INPUT && <NameChangeInputView onAction={ onAction } /> }
{ layout === CONFIRMATION && <NameChangeConfirmationView username={ newUsername } onAction={ onAction } /> }
</NitroCardContentView>
</NitroCardView>
)
}

View File

@ -0,0 +1,5 @@
export interface NameChangeLayoutViewProps
{
username?: string;
onAction: (action: string, value?: string) => void;
}

View File

@ -0,0 +1,6 @@
import { NitroEvent } from '@nitrots/nitro-renderer/src/core/events/NitroEvent';
export class HelpNameChangeEvent extends NitroEvent
{
public static INIT: string = 'HC_NAME_CHANGE_INIT';
}

View File

@ -92,5 +92,13 @@
} }
} }
.nitro-friends-room-invite {
width: $friends-list-width;
}
.nitro-friends-remove-confirmation {
width: $friends-list-width;
}
@import "./views/friend-bar/FriendBarView"; @import "./views/friend-bar/FriendBarView";
@import "./views/messenger/FriendsMessengerView"; @import "./views/messenger/FriendsMessengerView";

View File

@ -1,4 +1,5 @@
import { FollowFriendMessageComposer, SetRelationshipStatusComposer } from '@nitrots/nitro-renderer'; import { FollowFriendMessageComposer, SetRelationshipStatusComposer } from '@nitrots/nitro-renderer';
import classNames from 'classnames';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useState } from 'react';
import { LocalizeText, OpenMessengerChat } from '../../../../api'; import { LocalizeText, OpenMessengerChat } from '../../../../api';
import { SendMessageHook } from '../../../../hooks'; import { SendMessageHook } from '../../../../hooks';
@ -9,7 +10,7 @@ import { FriendsGroupItemViewProps } from './FriendsGroupItemView.types';
export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props => export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
{ {
const { friend = null, selected = false, children = null, ...rest } = props; const { friend = null, selected = false, selectFriend = null, children = null, ...rest } = props;
const [ isExpanded, setIsExpanded ] = useState<boolean>(false); const [ isExpanded, setIsExpanded ] = useState<boolean>(false);
@ -20,8 +21,10 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
SendMessageHook(new FollowFriendMessageComposer(friend.id)); SendMessageHook(new FollowFriendMessageComposer(friend.id));
}, [ friend ]); }, [ friend ]);
const openMessengerChat = useCallback(() => const openMessengerChat = useCallback((e) =>
{ {
e.stopPropagation();
if(!friend) return; if(!friend) return;
OpenMessengerChat(friend.id); OpenMessengerChat(friend.id);
@ -40,8 +43,16 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
} }
}, [ friend ]); }, [ friend ]);
const updateRelationship = useCallback((type: number) => const initUpdateRelationship = useCallback((e) =>
{ {
e.stopPropagation();
setIsExpanded(true);
}, []);
const updateRelationship = useCallback((e, type: number) =>
{
e.stopPropagation();
if(type !== friend.relationshipStatus) SendMessageHook(new SetRelationshipStatusComposer(friend.id, type)); if(type !== friend.relationshipStatus) SendMessageHook(new SetRelationshipStatusComposer(friend.id, type));
setIsExpanded(false); setIsExpanded(false);
@ -50,8 +61,10 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
if(!friend) return null; if(!friend) return null;
return ( return (
<NitroLayoutFlex className="px-2 py-1 align-items-center" gap={ 1 } { ...rest }> <NitroLayoutFlex className={ 'px-2 py-1 align-items-center' + classNames({ ' bg-primary text-white': selected }) } gap={ 1 } { ...rest } onClick={ selectFriend }>
<div onClick={ (e) => e.stopPropagation() }>
<UserProfileIconView userId={ friend.id } /> <UserProfileIconView userId={ friend.id } />
</div>
<div>{ friend.name }</div> <div>{ friend.name }</div>
<NitroLayoutFlex className="ms-auto align-items-center" gap={ 1 }> <NitroLayoutFlex className="ms-auto align-items-center" gap={ 1 }>
{ !isExpanded && { !isExpanded &&
@ -60,14 +73,14 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
<NitroLayoutBase onClick={ followFriend } className="nitro-friends-spritesheet icon-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } /> } <NitroLayoutBase onClick={ followFriend } className="nitro-friends-spritesheet icon-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } /> }
{ friend.online && { friend.online &&
<NitroLayoutBase className="nitro-friends-spritesheet icon-chat cursor-pointer" onClick={ openMessengerChat } title={ LocalizeText('friendlist.tip.im') } /> } <NitroLayoutBase className="nitro-friends-spritesheet icon-chat cursor-pointer" onClick={ openMessengerChat } title={ LocalizeText('friendlist.tip.im') } /> }
<NitroLayoutBase className={ `nitro-friends-spritesheet icon-${ getCurrentRelationshipName() } cursor-pointer` }onClick={ event => setIsExpanded(true) } title={ LocalizeText('infostand.link.relationship') } /> <NitroLayoutBase className={ `nitro-friends-spritesheet icon-${ getCurrentRelationshipName() } cursor-pointer` } onClick={ initUpdateRelationship } title={ LocalizeText('infostand.link.relationship') } />
</> } </> }
{ isExpanded && { isExpanded &&
<> <>
<NitroLayoutBase className="nitro-friends-spritesheet icon-heart cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_HEART) } /> <NitroLayoutBase className="nitro-friends-spritesheet icon-heart cursor-pointer" onClick={ (e) => updateRelationship(e, MessengerFriend.RELATIONSHIP_HEART) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-smile cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_SMILE) } /> <NitroLayoutBase className="nitro-friends-spritesheet icon-smile cursor-pointer" onClick={ (e) => updateRelationship(e, MessengerFriend.RELATIONSHIP_SMILE) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-bobba cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_BOBBA) } /> <NitroLayoutBase className="nitro-friends-spritesheet icon-bobba cursor-pointer" onClick={ (e) => updateRelationship(e, MessengerFriend.RELATIONSHIP_BOBBA) } />
<NitroLayoutBase className="nitro-friends-spritesheet icon-none cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_NONE) } /> <NitroLayoutBase className="nitro-friends-spritesheet icon-none cursor-pointer" onClick={ (e) => updateRelationship(e, MessengerFriend.RELATIONSHIP_NONE) } />
</> } </> }
</NitroLayoutFlex> </NitroLayoutFlex>
{ children } { children }

View File

@ -5,4 +5,5 @@ export interface FriendsGroupItemViewProps extends NitroLayoutFlexProps
{ {
friend: MessengerFriend; friend: MessengerFriend;
selected?: boolean; selected?: boolean;
selectFriend: () => void;
} }

View File

@ -4,15 +4,15 @@ import { FriendsGroupViewProps } from './FriendsGroupView.types';
export const FriendsGroupView: FC<FriendsGroupViewProps> = props => export const FriendsGroupView: FC<FriendsGroupViewProps> = props =>
{ {
const { list = null } = props; const { list = null, selectedFriendsIds = null, selectFriend = null } = props;
if(!list) return null; if(!list) return null;
return ( return (
<> <>
{ list.map((item, index) => { selectedFriendsIds && list && list.map((item, index) =>
{ {
return <FriendsGroupItemView key={ index } friend={ item } />; return <FriendsGroupItemView key={ index } friend={ item } selected={ selectedFriendsIds.includes(item.id) } selectFriend={ () => selectFriend(item.id) } />;
}) } }) }
</> </>
); );

View File

@ -3,4 +3,6 @@ import { MessengerFriend } from '../../common/MessengerFriend';
export interface FriendsGroupViewProps export interface FriendsGroupViewProps
{ {
list: MessengerFriend[]; list: MessengerFriend[];
selectedFriendsIds: number[];
selectFriend: (userId: number) => void;
} }

View File

@ -1,9 +1,13 @@
import { FC, useEffect, useState } from 'react'; import { RemoveFriendComposer, SendRoomInviteComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
import { LocalizeText } from '../../../../api'; import { LocalizeText } from '../../../../api';
import { SendMessageHook } from '../../../../hooks';
import { NitroCardAccordionSetView, NitroCardAccordionView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout'; import { NitroCardAccordionSetView, NitroCardAccordionView, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
import { MessengerFriend } from '../../common/MessengerFriend'; import { MessengerFriend } from '../../common/MessengerFriend';
import { FriendsGroupView } from '../friends-group/FriendsGroupView'; import { FriendsGroupView } from '../friends-group/FriendsGroupView';
import { FriendsRemoveConfirmationView } from '../friends-remove-confirmation/FriendsRemoveConfirmationView';
import { FriendsRequestView } from '../friends-request/FriendsRequestView'; import { FriendsRequestView } from '../friends-request/FriendsRequestView';
import { FriendsRoomInviteView } from '../friends-room-invite/FriendsRoomInviteView';
import { FriendsSearchView } from '../friends-search/FriendsSearchView'; import { FriendsSearchView } from '../friends-search/FriendsSearchView';
import { FriendsListViewProps } from './FriendsListView.types'; import { FriendsListViewProps } from './FriendsListView.types';
@ -13,15 +17,71 @@ const MODE_SEARCH: number = 1;
export const FriendsListView: FC<FriendsListViewProps> = props => export const FriendsListView: FC<FriendsListViewProps> = props =>
{ {
const { onlineFriends = [], offlineFriends = [], friendRequests = [], onCloseClick = null } = props; const { onlineFriends = [], offlineFriends = [], friendRequests = [], onCloseClick = null } = props;
const [ selectedFriends, setSelectedFriends ] = useState<MessengerFriend[]>([]);
const [ selectedFriendsIds, setSelectedFriendsIds ] = useState<number[]>([]);
const [ mode, setMode ] = useState<number>(0); const [ mode, setMode ] = useState<number>(0);
useEffect(() => const [ showRoomInvite, setShowRoomInvite ] = useState<boolean>(false);
const [ showRemoveFriendsConfirmation, setShowRemoveFriendsConfirmation ] = useState<boolean>(false);
const removeFriendsText = useMemo(() =>
{ {
setSelectedFriends([]); if(!selectedFriendsIds || !selectedFriendsIds.length) return '';
}, [ onlineFriends, offlineFriends ]);
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;
const existingUserIdIndex: number = selectedFriendsIds.indexOf(userId);
if(existingUserIdIndex > -1)
{
const clonedFriend = [...selectedFriendsIds];
clonedFriend.splice(existingUserIdIndex, 1)
setSelectedFriendsIds([...clonedFriend]);
}
else
{
setSelectedFriendsIds([...selectedFriendsIds, userId]);
}
}, [ selectedFriendsIds, setSelectedFriendsIds ]);
const sendRoomInvite = useCallback((message: string) =>
{
if(selectedFriendsIds.length === 0 || !message || message.length === 0) return;
SendMessageHook(new SendRoomInviteComposer(message, ...selectedFriendsIds));
setShowRoomInvite(false);
}, [ selectedFriendsIds, setShowRoomInvite ]);
const removeSelectedFriends = useCallback(() =>
{
if(selectedFriendsIds.length === 0) return;
SendMessageHook(new RemoveFriendComposer(...selectedFriendsIds));
setSelectedFriendsIds([]);
setShowRemoveFriendsConfirmation(false);
}, [ selectedFriendsIds ]);
return ( return (
<>
<NitroCardView className="nitro-friends" uniqueKey="nitro-friends"> <NitroCardView className="nitro-friends" uniqueKey="nitro-friends">
<NitroCardHeaderView headerText={ LocalizeText('friendlist.friends') } onCloseClick={ onCloseClick } /> <NitroCardHeaderView headerText={ LocalizeText('friendlist.friends') } onCloseClick={ onCloseClick } />
<NitroCardTabsView> <NitroCardTabsView>
@ -34,18 +94,30 @@ export const FriendsListView: FC<FriendsListViewProps> = props =>
</NitroCardTabsView> </NitroCardTabsView>
<NitroCardContentView className="p-0 text-black"> <NitroCardContentView className="p-0 text-black">
{ (mode === MODE_FRIENDS) && { (mode === MODE_FRIENDS) &&
<NitroCardAccordionView> <>
<NitroCardAccordionView className="overflow-y-auto">
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends') + ` (${onlineFriends.length})` } isExpanded={ true }> <NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends') + ` (${onlineFriends.length})` } isExpanded={ true }>
<FriendsGroupView list={ onlineFriends } /> <FriendsGroupView list={ onlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
</NitroCardAccordionSetView> </NitroCardAccordionSetView>
<NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${offlineFriends.length})` }> <NitroCardAccordionSetView headerText={ LocalizeText('friendlist.friends.offlinecaption') + ` (${offlineFriends.length})` }>
<FriendsGroupView list={ offlineFriends } /> <FriendsGroupView list={ offlineFriends } selectedFriendsIds={ selectedFriendsIds } selectFriend={ selectFriend } />
</NitroCardAccordionSetView> </NitroCardAccordionSetView>
<FriendsRequestView requests={ friendRequests } /> <FriendsRequestView requests={ friendRequests } />
</NitroCardAccordionView> } </NitroCardAccordionView>
{ selectedFriendsIds && selectedFriendsIds.length > 0 && <div className="d-flex gap-2 p-2">
<button className="btn btn-primary w-100" onClick={ () => setShowRoomInvite(true) }>Invite</button>
<button className="btn btn-danger w-100" onClick={ () => setShowRemoveFriendsConfirmation(true) }>Delete</button>
</div> }
</>
}
{ (mode === MODE_SEARCH) && { (mode === MODE_SEARCH) &&
<FriendsSearchView /> } <FriendsSearchView /> }
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
{ showRoomInvite &&
<FriendsRoomInviteView selectedFriendsIds={ selectedFriendsIds } onCloseClick={ () => setShowRoomInvite(false) } sendRoomInvite={ sendRoomInvite } /> }
{ showRemoveFriendsConfirmation &&
<FriendsRemoveConfirmationView selectedFriendsIds={ selectedFriendsIds } removeFriendsText={ removeFriendsText } onCloseClick={ () => setShowRemoveFriendsConfirmation(false) } removeSelectedFriends={ removeSelectedFriends } /> }
</>
); );
}; };

View File

@ -0,0 +1,22 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { FriendsRemoveConfirmationViewProps } from './FriendsRemoveConfirmationView.types';
export const FriendsRemoveConfirmationView: FC<FriendsRemoveConfirmationViewProps> = props =>
{
const { selectedFriendsIds = null, removeFriendsText = null, removeSelectedFriends = null, onCloseClick = null } = props;
return (
<NitroCardView className="nitro-friends-remove-confirmation" uniqueKey="nitro-friends-remove-confirmation" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('friendlist.removefriendconfirm.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black d-flex flex-column gap-3">
<div>{ removeFriendsText }</div>
<div className="d-flex gap-2">
<button className="btn btn-danger w-100" disabled={ selectedFriendsIds.length === 0 } onClick={ removeSelectedFriends }>{ LocalizeText('generic.ok') }</button>
<button className="btn btn-primary w-100" onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};

View File

@ -0,0 +1,7 @@
export interface FriendsRemoveConfirmationViewProps
{
selectedFriendsIds: number[];
removeFriendsText: string;
removeSelectedFriends: () => void;
onCloseClick: () => void;
}

View File

@ -0,0 +1,26 @@
import { FC, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { FriendsRoomInviteViewProps } from './FriendsRoomInviteView.types';
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" uniqueKey="nitro-friends-room-invite" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('friendlist.invite.title') } onCloseClick={ onCloseClick } />
<NitroCardContentView className="text-black d-flex flex-column gap-2">
{ LocalizeText('friendlist.invite.summary', ['count'], [selectedFriendsIds.length.toString()]) }
<textarea className="form-control" value={roomInviteMessage} onChange={e => setRoomInviteMessage(e.target.value)}></textarea>
<div className="bg-muted rounded text-center p-2">{ LocalizeText('friendlist.invite.note') }</div>
<div className="d-flex gap-2">
<button className="btn btn-success w-100" disabled={ roomInviteMessage.length === 0 || selectedFriendsIds.length === 0 } onClick={ () => sendRoomInvite(roomInviteMessage) }>{ LocalizeText('friendlist.invite.send') }</button>
<button className="btn btn-primary w-100" onClick={ onCloseClick }>{ LocalizeText('generic.cancel') }</button>
</div>
</NitroCardContentView>
</NitroCardView>
);
};

View File

@ -0,0 +1,6 @@
export interface FriendsRoomInviteViewProps
{
selectedFriendsIds: number[];
onCloseClick: () => void;
sendRoomInvite: (message: string) => void;
}

View File

@ -1,4 +1,4 @@
import { FollowFriendMessageComposer, ILinkEventTracker, NewConsoleMessageEvent, SendMessageComposer } from '@nitrots/nitro-renderer'; import { FollowFriendMessageComposer, ILinkEventTracker, NewConsoleMessageEvent, RoomInviteEvent, SendMessageComposer } from '@nitrots/nitro-renderer';
import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AddEventLinkTracker, GetSessionDataManager, GetUserProfile, LocalizeText, RemoveLinkEventTracker } from '../../../../api'; import { AddEventLinkTracker, GetSessionDataManager, GetUserProfile, LocalizeText, RemoveLinkEventTracker } from '../../../../api';
import { MESSENGER_MESSAGE_RECEIVED, MESSENGER_NEW_THREAD, PlaySound } from '../../../../api/utils/PlaySound'; import { MESSENGER_MESSAGE_RECEIVED, MESSENGER_NEW_THREAD, PlaySound } from '../../../../api/utils/PlaySound';
@ -105,6 +105,21 @@ export const FriendsMessengerView: FC<{}> = props =>
CreateMessageHook(NewConsoleMessageEvent, onNewConsoleMessageEvent); CreateMessageHook(NewConsoleMessageEvent, onNewConsoleMessageEvent);
const onRoomInviteEvent = useCallback((event: RoomInviteEvent) =>
{
const parser = event.getParser();
const [threadIndex, thread] = getMessageThreadWithIndex(parser.senderId);
if((threadIndex === -1) || !thread) return;
thread.addMessage(parser.senderId, parser.messageText, 0, null, MessengerThreadChat.ROOM_INVITE);
setMessageThreads(prevValue => [...prevValue]);
}, [getMessageThreadWithIndex]);
CreateMessageHook(RoomInviteEvent, onRoomInviteEvent);
const sendMessage = useCallback(() => const sendMessage = useCallback(() =>
{ {
if(!messageText || !messageText.length) return; if(!messageText || !messageText.length) return;

View File

@ -2,6 +2,7 @@ import { AvatarAction, AvatarExpressionEnum, RoomControllerLevel, RoomObjectCate
import { FC, useCallback, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
import { GetCanStandUp, GetCanUseExpression, GetOwnPosture, GetUserProfile, HasHabboClub, HasHabboVip, IsRidingHorse, LocalizeText, RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetUpdateDecorateModeEvent, RoomWidgetUserActionMessage } from '../../../../../../api'; import { GetCanStandUp, GetCanUseExpression, GetOwnPosture, GetUserProfile, HasHabboClub, HasHabboVip, IsRidingHorse, LocalizeText, RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetUpdateDecorateModeEvent, RoomWidgetUserActionMessage } from '../../../../../../api';
import { AvatarEditorEvent } from '../../../../../../events'; import { AvatarEditorEvent } from '../../../../../../events';
import { HelpNameChangeEvent } from '../../../../../../events/help/HelpNameChangeEvent';
import { dispatchUiEvent } from '../../../../../../hooks'; import { dispatchUiEvent } from '../../../../../../hooks';
import { CurrencyIcon } from '../../../../../shared/currency-icon/CurrencyIcon'; import { CurrencyIcon } from '../../../../../shared/currency-icon/CurrencyIcon';
import { useRoomContext } from '../../../../context/RoomContext'; import { useRoomContext } from '../../../../context/RoomContext';
@ -42,6 +43,9 @@ export const AvatarInfoWidgetOwnAvatarView: FC<AvatarInfoWidgetOwnAvatarViewProp
case 'decorate': case 'decorate':
eventDispatcher.dispatchEvent(new RoomWidgetUpdateDecorateModeEvent(true)); eventDispatcher.dispatchEvent(new RoomWidgetUpdateDecorateModeEvent(true));
break; break;
case 'change_name':
dispatchUiEvent(new HelpNameChangeEvent(HelpNameChangeEvent.INIT));
break;
case 'change_looks': case 'change_looks':
dispatchUiEvent(new AvatarEditorEvent(AvatarEditorEvent.SHOW_EDITOR)); dispatchUiEvent(new AvatarEditorEvent(AvatarEditorEvent.SHOW_EDITOR));
break; break;

View File

@ -1,7 +1,9 @@
.nitro-room-tools { .nitro-room-tools-container {
position: absolute; position: absolute;
bottom: $toolbar-height + 65px; bottom: $toolbar-height + 65px;
left: 0; left: 0;
.nitro-room-tools {
background: rgba($dark,.95); background: rgba($dark,.95);
box-shadow: inset 0px 5px lighten(rgba($dark,.6),2.5), inset 0 -4px darken(rgba($dark,.6),4); box-shadow: inset 0px 5px lighten(rgba($dark,.6),2.5), inset 0 -4px darken(rgba($dark,.6),4);
border-top-right-radius: $border-radius; border-top-right-radius: $border-radius;
@ -28,11 +30,6 @@
padding-bottom: 8px; padding-bottom: 8px;
} }
&.disabled {
opacity: .5;
}
}
.tools-item { .tools-item {
.icon { .icon {
width: 22px; width: 22px;
@ -45,3 +42,14 @@
} }
} }
} }
}
.nitro-room-tools-info {
margin-left: 10px;
background: rgba($dark,.95);
box-shadow: inset 0px 5px lighten(rgba($dark,.6),2.5), inset 0 -4px darken(rgba($dark,.6),4);
transition: all .2s ease;
pointer-events: none;
max-width: 250px;
}
}

View File

@ -1,13 +1,12 @@
import { RoomLikeRoomComposer } from '@nitrots/nitro-renderer'; import { GetGuestRoomResultEvent, RoomLikeRoomComposer } from '@nitrots/nitro-renderer';
import classNames from 'classnames'; import classNames from 'classnames';
import { FC, useCallback, useState } from 'react'; import { FC, useCallback, useEffect, useState } from 'react';
import { LocalizeText, RoomWidgetZoomToggleMessage } from '../../../../api'; import { LocalizeText, RoomWidgetZoomToggleMessage } from '../../../../api';
import { Base } from '../../../../common/Base'; import { Base, Column, Flex, Text } from '../../../../common';
import { Column } from '../../../../common/Column';
import { NavigatorEvent } from '../../../../events'; import { NavigatorEvent } from '../../../../events';
import { ChatHistoryEvent } from '../../../../events/chat-history/ChatHistoryEvent'; import { ChatHistoryEvent } from '../../../../events/chat-history/ChatHistoryEvent';
import { dispatchUiEvent } from '../../../../hooks/events'; import { dispatchUiEvent } from '../../../../hooks/events';
import { SendMessageHook } from '../../../../hooks/messages'; import { CreateMessageHook, SendMessageHook } from '../../../../hooks/messages';
import { useRoomContext } from '../../context/RoomContext'; import { useRoomContext } from '../../context/RoomContext';
export const RoomToolsWidgetView: FC<{}> = props => export const RoomToolsWidgetView: FC<{}> = props =>
@ -16,6 +15,12 @@ export const RoomToolsWidgetView: FC<{}> = props =>
const [ isLiked, setIsLiked ] = useState(false); const [ isLiked, setIsLiked ] = useState(false);
const { widgetHandler = null } = useRoomContext(); const { widgetHandler = null } = useRoomContext();
const [ roomName, setRoomName ] = useState(null);
const [ roomOwner, setRoomOwner ] = useState(null);
const [ roomTags, setRoomTags ] = useState(null);
const [ roomInfoDisplay, setRoomInfoDisplay ] = useState(false);
const [ isOpen, setIsOpen ] = useState(false);
const handleToolClick = useCallback((action: string) => const handleToolClick = useCallback((action: string) =>
{ {
switch(action) switch(action)
@ -42,12 +47,49 @@ export const RoomToolsWidgetView: FC<{}> = props =>
} }
}, [ isZoomedIn, isLiked, widgetHandler ]); }, [ isZoomedIn, isLiked, widgetHandler ]);
const onGetGuestRoomResultEvent = useCallback((event: GetGuestRoomResultEvent) =>
{
const parser = event.getParser();
if(roomName !== parser.data.roomName) setRoomName(parser.data.roomName);
if(roomOwner !== parser.data.ownerName) setRoomOwner(parser.data.ownerName);
if(roomTags !== parser.data.tags) setRoomTags(parser.data.tags);
}, [ roomName, roomOwner, roomTags ]);
CreateMessageHook(GetGuestRoomResultEvent, onGetGuestRoomResultEvent);
useEffect(() =>
{
setIsOpen(true);
const timeout = setTimeout(() => setIsOpen(false), 5000);
return () => clearTimeout(timeout);
}, [ roomName, roomOwner, roomTags ]);
return ( return (
<Flex className="nitro-room-tools-container">
<Column center className="nitro-room-tools p-2"> <Column center className="nitro-room-tools p-2">
<Base pointer title={ LocalizeText('room.settings.button.text') } className="icon icon-cog" onClick={ () => handleToolClick('settings') } /> <Base pointer title={ LocalizeText('room.settings.button.text') } className="icon icon-cog" onClick={ () => handleToolClick('settings') } />
<Base pointer title={ LocalizeText('room.zoom.button.text') } onClick={ () => handleToolClick('zoom') } className={ 'icon ' + classNames({ 'icon-zoom-less': !isZoomedIn, 'icon-zoom-more': isZoomedIn }) } /> <Base pointer title={ LocalizeText('room.zoom.button.text') } onClick={ () => handleToolClick('zoom') } className={ 'icon ' + classNames({ 'icon-zoom-less': !isZoomedIn, 'icon-zoom-more': isZoomedIn }) } />
<Base pointer title={ LocalizeText('room.chathistory.button.text') } onClick={ () => handleToolClick('chat_history') } className="icon icon-chat-history" /> <Base pointer title={ LocalizeText('room.chathistory.button.text') } onClick={ () => handleToolClick('chat_history') } className="icon icon-chat-history" />
{ !isLiked && <Base pointer title={ LocalizeText('room.like.button.text') } onClick={ () => handleToolClick('like_room') } className="icon icon-like-room" /> } { !isLiked && <Base pointer title={ LocalizeText('room.like.button.text') } onClick={ () => handleToolClick('like_room') } className="icon icon-like-room" /> }
</Column> </Column>
{ isOpen &&
<Column center>
<Column className="nitro-room-tools-info rounded py-2 px-3">
<Column gap={ 1 }>
<Text variant="white" fontSize={ 4 }>{ roomName }</Text>
<Text variant="muted" fontSize={ 5 }>{ roomOwner }</Text>
</Column>
{ roomTags && roomTags.length > 0 &&
<div className="d-flex gap-2">
{ roomTags.map((tag: string) => <div className="rounded bg-primary text-white p-1 text-sm">#{ tag }</div>) }
</div> }
</Column>
</Column> }
</Flex>
); );
} }

View File

@ -114,9 +114,10 @@ export const UserSettingsView: FC<{}> = props =>
if(!isVisible) return null; if(!isVisible) return null;
return ( return (
<NitroCardView uniqueKey="user-settings" className="user-settings-window"> <NitroCardView uniqueKey="user-settings" className="user-settings-window" simple>
<NitroCardHeaderView headerText={ LocalizeText('widget.memenu.settings.title') } onCloseClick={event => processAction('close_view')} /> <NitroCardHeaderView headerText={ LocalizeText('widget.memenu.settings.title') } onCloseClick={event => processAction('close_view')} />
<NitroCardContentView className="text-black"> <NitroCardContentView className="text-black d-flex flex-column gap-2">
<div className="d-flex flex-column gap-1">
<div className="form-check"> <div className="form-check">
<input className="form-check-input" type="checkbox" checked={ userSettings.oldChat } onChange={ event => processAction('oldchat', event.target.checked) } /> <input className="form-check-input" type="checkbox" checked={ userSettings.oldChat } onChange={ event => processAction('oldchat', event.target.checked) } />
<label className="form-check-label">{ LocalizeText('memenu.settings.chat.prefer.old.chat') }</label> <label className="form-check-label">{ LocalizeText('memenu.settings.chat.prefer.old.chat') }</label>
@ -129,8 +130,10 @@ export const UserSettingsView: FC<{}> = props =>
<input className="form-check-input" type="checkbox" checked={ userSettings.cameraFollow } onChange={ event => processAction('camera_follow', event.target.checked) } /> <input className="form-check-input" type="checkbox" checked={ userSettings.cameraFollow } onChange={ event => processAction('camera_follow', event.target.checked) } />
<label className="form-check-label">{ LocalizeText('memenu.settings.other.disable.room.camera.follow') }</label> <label className="form-check-label">{ LocalizeText('memenu.settings.other.disable.room.camera.follow') }</label>
</div> </div>
<div className="mt-3 mb-2">{ LocalizeText('widget.memenu.settings.volume') }</div> </div>
<div className="mb-2"> <div className="d-flex flex-column gap-2">
<div className="fw-bold">{ LocalizeText('widget.memenu.settings.volume') }</div>
<div>
<label>{ LocalizeText('widget.memenu.settings.volume.ui') }</label> <label>{ LocalizeText('widget.memenu.settings.volume.ui') }</label>
<div className={ 'd-flex align-items-center justify-content-center' }> <div className={ 'd-flex align-items-center justify-content-center' }>
<i className={ 'fas' + ((userSettings.volumeSystem === 0) ? ' fa-volume-mute' : '') + ((userSettings.volumeSystem > 0) ? ' fa-volume-down' : '') + ((userSettings.volumeSystem >= 50) ? ' text-muted' : '') } /> <i className={ 'fas' + ((userSettings.volumeSystem === 0) ? ' fa-volume-mute' : '') + ((userSettings.volumeSystem > 0) ? ' fa-volume-down' : '') + ((userSettings.volumeSystem >= 50) ? ' text-muted' : '') } />
@ -138,7 +141,7 @@ export const UserSettingsView: FC<{}> = props =>
<i className={ 'fas fa-volume-up' + ((userSettings.volumeSystem < 50) ? ' text-muted': '') } /> <i className={ 'fas fa-volume-up' + ((userSettings.volumeSystem < 50) ? ' text-muted': '') } />
</div> </div>
</div> </div>
<div className="mb-2"> <div>
<label>{ LocalizeText('widget.memenu.settings.volume.furni') }</label> <label>{ LocalizeText('widget.memenu.settings.volume.furni') }</label>
<div className={ 'd-flex align-items-center justify-content-center' }> <div className={ 'd-flex align-items-center justify-content-center' }>
<i className={ 'fas' + ((userSettings.volumeFurni === 0) ? ' fa-volume-mute' : '') + ((userSettings.volumeFurni > 0) ? ' fa-volume-down' : '') + ((userSettings.volumeFurni >= 50) ? ' text-muted' : '') } /> <i className={ 'fas' + ((userSettings.volumeFurni === 0) ? ' fa-volume-mute' : '') + ((userSettings.volumeFurni > 0) ? ' fa-volume-down' : '') + ((userSettings.volumeFurni >= 50) ? ' text-muted' : '') } />
@ -146,7 +149,7 @@ export const UserSettingsView: FC<{}> = props =>
<i className={ 'fas fa-volume-up' + ((userSettings.volumeFurni < 50) ? ' text-muted': '') } /> <i className={ 'fas fa-volume-up' + ((userSettings.volumeFurni < 50) ? ' text-muted': '') } />
</div> </div>
</div> </div>
<div className="mb-2"> <div>
<label>{ LocalizeText('widget.memenu.settings.volume.trax') }</label> <label>{ LocalizeText('widget.memenu.settings.volume.trax') }</label>
<div className={ 'd-flex align-items-center justify-content-center' }> <div className={ 'd-flex align-items-center justify-content-center' }>
<i className={ 'fas' + ((userSettings.volumeTrax === 0) ? ' fa-volume-mute' : '') + ((userSettings.volumeTrax > 0) ? ' fa-volume-down' : '') + ((userSettings.volumeTrax >= 50) ? ' text-muted' : '') } /> <i className={ 'fas' + ((userSettings.volumeTrax === 0) ? ' fa-volume-mute' : '') + ((userSettings.volumeTrax > 0) ? ' fa-volume-down' : '') + ((userSettings.volumeTrax >= 50) ? ' text-muted' : '') } />
@ -154,6 +157,7 @@ export const UserSettingsView: FC<{}> = props =>
<i className={ 'fas fa-volume-up' + ((userSettings.volumeTrax < 50) ? ' text-muted': '') } /> <i className={ 'fas fa-volume-up' + ((userSettings.volumeTrax < 50) ? ' text-muted': '') } />
</div> </div>
</div> </div>
</div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );