Messenger updates

This commit is contained in:
Bill 2021-09-21 21:48:00 -04:00
parent e97f18c53c
commit 6a28213896
29 changed files with 569 additions and 366 deletions

View File

@ -1,6 +1,7 @@
import { CreateLinkEvent } from '..';
export function OpenMessengerChat(friendId: number): void
export function OpenMessengerChat(friendId: number = -1): void
{
CreateLinkEvent(`friends/messenger/${friendId}`);
if(friendId === -1) CreateLinkEvent('friends/messenger/open');
else CreateLinkEvent(`friends/messenger/${friendId}`);
}

View File

@ -0,0 +1,23 @@
import { NitroEvent } from '@nitrots/nitro-renderer';
export class FriendsMessengerIconEvent extends NitroEvent
{
public static UPDATE_ICON: string = 'FMIE_UPDATE_ICON';
public static HIDE_ICON: number = 0;
public static SHOW_ICON: number = 1;
public static UNREAD_ICON: number = 2;
private _iconType: number;
constructor(type: string, subType: number = FriendsMessengerIconEvent.SHOW_ICON)
{
super(type);
this._iconType = subType;
}
public get iconType(): number
{
return this._iconType;
}
}

View File

@ -1,4 +1,5 @@
export * from './FriendEnteredRoomEvent';
export * from './FriendListContentEvent';
export * from './FriendsEvent';
export * from './FriendsMessengerIconEvent';
export * from './FriendsSendFriendRequestEvent';

View File

@ -1,4 +1,5 @@
.content-area {
height: 100%;
padding-top: $container-padding-x;
padding-bottom: $container-padding-x;
overflow: auto;

View File

@ -0,0 +1,30 @@
import { CatalogPageMessageOfferData, RoomObjectCategory, RoomObjectPlacementSource } from '@nitrots/nitro-renderer';
import { GetRoomEngine } from '../../../api';
import { IsCatalogOfferDraggable } from './IsCatalogOfferDraggable';
import { ProductTypeEnum } from './ProductTypeEnum';
export const AttemptCatalogPlacement = (offer: CatalogPageMessageOfferData) =>
{
if(!IsCatalogOfferDraggable(offer)) return;
const product = offer.products[0];
let category: number = -1;
switch(product.productType)
{
case ProductTypeEnum.FLOOR:
category = RoomObjectCategory.FLOOR;
break;
case ProductTypeEnum.WALL:
category = RoomObjectCategory.WALL;
break;
}
if(category === -1) return;
if(GetRoomEngine().processRoomObjectPlacement(RoomObjectPlacementSource.CATALOG, -(offer.offerId), category, product.furniClassId, (product.extraParam) ? product.extraParam.toString() : null))
{
}
}

View File

@ -0,0 +1,8 @@
import { CatalogPageMessageOfferData, RoomControllerLevel } from '@nitrots/nitro-renderer';
import { GetRoomSession } from '../../../api';
import { ProductTypeEnum } from './ProductTypeEnum';
export const IsCatalogOfferDraggable = (offer: CatalogPageMessageOfferData) =>
{
return ((GetRoomSession().isRoomOwner || (GetRoomSession().isGuildRoom && (GetRoomSession().controllerLevel >= RoomControllerLevel.GUILD_MEMBER))) && (offer.products.length === 1) && (offer.products[0].productType !== ProductTypeEnum.EFFECT) && (offer.products[0].productType !== ProductTypeEnum.HABBO_CLUB))
}

View File

@ -1,5 +1,5 @@
import { MouseEventType } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useCallback } from 'react';
import { FC, MouseEvent, useCallback, useState } from 'react';
import { useCatalogContext } from '../../../context/CatalogContext';
import { CatalogActions } from '../../../reducers/CatalogReducer';
import { CatalogProductView } from '../product/CatalogProductView';
@ -8,6 +8,7 @@ import { CatalogPageOfferViewProps } from './CatalogPageOfferView.types';
export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props =>
{
const { isActive = false, offer = null } = props;
const [ isMouseDown, setMouseDown ] = useState(false);
const { dispatchCatalogState = null } = useCatalogContext();
const onMouseEvent = useCallback((event: MouseEvent) =>
@ -24,8 +25,18 @@ export const CatalogPageOfferView: FC<CatalogPageOfferViewProps> = props =>
}
});
return;
case MouseEventType.MOUSE_DOWN:
console.log('ye')
setMouseDown(true);
return;
case MouseEventType.MOUSE_UP:
setMouseDown(false);
return;
case MouseEventType.ROLL_OUT:
if(!isMouseDown || !isActive) return;
return;
}
}, [ isActive, offer, dispatchCatalogState ]);
}, [ isActive, offer, isMouseDown, dispatchCatalogState ]);
const product = ((offer.products && offer.products[0]) || null);

View File

@ -65,7 +65,7 @@ export const CatalogProductView: FC<CatalogProductViewProps> = props =>
return (
<div className="col pe-1 pb-1 catalog-offer-item-container">
<div className={ 'position-relative border border-2 rounded catalog-offer-item cursor-pointer ' + (isActive ? 'active ' : '') + (product.uniqueLimitedItem ? 'unique-item ' : '') + ((product.uniqueLimitedItem && !product.uniqueLimitedItemsLeft) ? 'sold-out ' : '') } style={ { backgroundImage: imageUrl }} onClick={ onMouseEvent }>
<div className={ 'position-relative border border-2 rounded catalog-offer-item cursor-pointer ' + (isActive ? 'active ' : '') + (product.uniqueLimitedItem ? 'unique-item ' : '') + ((product.uniqueLimitedItem && !product.uniqueLimitedItemsLeft) ? 'sold-out ' : '') } style={ { backgroundImage: imageUrl }} onClick={ onMouseEvent } onMouseDown={ onMouseEvent } onMouseUp={ onMouseEvent } onMouseOut={ onMouseEvent }>
{ !imageUrl && (product.productType === ProductTypeEnum.ROBOT) &&
<AvatarImageView figure={ product.extraParam } direction={ 3 } headOnly={ true } /> }
{ (product.productCount > 1) && <span className="position-absolute badge border bg-danger px-1 rounded-circle">{ product.productCount }</span> }

View File

@ -1,8 +1,6 @@
import { FriendListFragmentEvent, FriendListUpdateEvent, FriendRequestsEvent, GetFriendRequestsComposer, MessengerInitEvent, NewConsoleMessageEvent } from '@nitrots/nitro-renderer';
import { FriendListFragmentEvent, FriendListUpdateEvent, FriendRequestsEvent, GetFriendRequestsComposer, MessengerInitEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback } from 'react';
import { GetSessionDataManager } from '../../api';
import { CreateMessageHook, SendMessageHook } from '../../hooks/messages/message-event';
import { MessengerChatMessage } from './common/MessengerChatMessage';
import { MessengerSettings } from './common/MessengerSettings';
import { useFriendsContext } from './context/FriendsContext';
import { FriendsActions } from './reducers/FriendsReducer';
@ -10,7 +8,6 @@ import { FriendsActions } from './reducers/FriendsReducer';
export const FriendsMessageHandler: FC<{}> = props =>
{
const { friendsState = null, dispatchFriendsState = null } = useFriendsContext();
const { activeChats = [] } = friendsState;
const onMessengerInitEvent = useCallback((event: MessengerInitEvent) =>
{
@ -66,28 +63,10 @@ export const FriendsMessageHandler: FC<{}> = props =>
});
}, [ dispatchFriendsState ]);
const onNewConsoleMessageEvent = useCallback((event: NewConsoleMessageEvent) =>
{
const parser = event.getParser();
let userId = parser.senderId;
if(userId === GetSessionDataManager().userId) userId = 0;
dispatchFriendsState({
type: FriendsActions.ADD_CHAT_MESSAGE,
payload: {
chatMessage: new MessengerChatMessage(MessengerChatMessage.MESSAGE, userId, parser.messageText, parser.secondsSinceSent, parser.extraData),
boolValue: true
}
});
}, [ dispatchFriendsState ]);
CreateMessageHook(MessengerInitEvent, onMessengerInitEvent);
CreateMessageHook(FriendListFragmentEvent, onFriendsFragmentEvent);
CreateMessageHook(FriendListUpdateEvent, onFriendsUpdateEvent);
CreateMessageHook(FriendRequestsEvent, onFriendRequestsEvent);
CreateMessageHook(NewConsoleMessageEvent, onNewConsoleMessageEvent);
return null;
}

View File

@ -1,49 +0,0 @@
import { MessengerChatMessage } from './MessengerChatMessage';
import { MessengerChatMessageGroup } from './MessengerChatMessageGroup';
export class MessengerChat
{
private _friendId: number;
private _isRead: boolean;
private _messageGroups: MessengerChatMessageGroup[];
constructor(friendId: number)
{
this._friendId = friendId;
this._isRead = true;
this._messageGroups = [];
}
public addMessage(message: MessengerChatMessage, setAsNotRead: boolean = true, isSystem: boolean = false): void
{
if(!this.lastMessageGroup || this.lastMessageGroup.userId !== message.senderId || isSystem || this.lastMessageGroup.isSystem) this._messageGroups.push(new MessengerChatMessageGroup(message.senderId, isSystem));
this.lastMessageGroup.addMessage(message);
if(setAsNotRead) this._isRead = false;
}
public read(): void
{
this._isRead = true;
}
public get friendId(): number
{
return this._friendId;
}
public get isRead(): boolean
{
return this._isRead;
}
public get messageGroups(): MessengerChatMessageGroup[]
{
return this._messageGroups;
}
public get lastMessageGroup(): MessengerChatMessageGroup
{
return this._messageGroups[this._messageGroups.length - 1];
}
}

View File

@ -1,35 +0,0 @@
import { MessengerChatMessage } from './MessengerChatMessage';
export class MessengerChatMessageGroup
{
private _userId: number;
private _messages: MessengerChatMessage[];
private _isSystem: boolean;
constructor(userId: number, isSystem: boolean)
{
this._userId = userId;
this._messages = [];
this._isSystem = isSystem;
}
public addMessage(message: MessengerChatMessage): void
{
this._messages.push(message);
}
public get userId(): number
{
return this._userId;
}
public get messages(): MessengerChatMessage[]
{
return this._messages;
}
public get isSystem(): boolean
{
return this._isSystem;
}
}

View File

@ -0,0 +1,83 @@
import { LocalizeText } from '../../../api';
import { MessengerFriend } from './MessengerFriend';
import { MessengerThreadChat } from './MessengerThreadChat';
import { MessengerThreadChatGroup } from './MessengerThreadChatGroup';
export class MessengerThread
{
public static MESSAGE_RECEIVED: string = 'MT_MESSAGE_RECEIVED';
private _participant: MessengerFriend;
private _groups: MessengerThreadChatGroup[];
private _lastUpdated: Date;
private _unread: boolean;
constructor(participant: MessengerFriend, isNew: boolean = true)
{
this._participant = participant;
this._groups = [];
this._lastUpdated = new Date();
this._unread = false;
if(isNew)
{
this.addMessage(-1, LocalizeText('messenger.moderationinfo'), 0, null, MessengerThreadChat.SECURITY_NOTIFICATION);
this._unread = false;
}
}
public addMessage(senderId: number, message: string, secondsSinceSent: number = 0, extraData: string = null, type: number = 0): MessengerThreadChat
{
const group = this.getLastGroup(senderId);
if(!group) return;
const chat = new MessengerThreadChat(senderId, message, secondsSinceSent, extraData, type);
group.addChat(chat);
this._lastUpdated = new Date();
this._unread = true;
return chat;
}
private getLastGroup(userId: number): MessengerThreadChatGroup
{
let group = this._groups[(this._groups.length - 1)];
if(group && (group.userId === userId)) return group;
group = new MessengerThreadChatGroup(userId);
this._groups.push(group);
return group;
}
public setRead(): void
{
this._unread = false;
}
public get participant(): MessengerFriend
{
return this._participant;
}
public get groups(): MessengerThreadChatGroup[]
{
return this._groups;
}
public get lastUpdated(): Date
{
return this._lastUpdated;
}
public get unread(): boolean
{
return this._unread;
}
}

View File

@ -1,23 +1,25 @@
export class MessengerChatMessage
export class MessengerThreadChat
{
public static MESSAGE: number = 0;
public static CHAT: number = 0;
public static ROOM_INVITE: number = 1;
public static SECURITY_ALERT: number = 2;
public static STATUS_ALERT: number = 3;
public static STATUS_NOTIFICATION: number = 2;
public static SECURITY_NOTIFICATION: number = 3;
private _type: number;
private _senderId: number;
private _message: string;
private _sentAt: number;
private _secondsSinceSent: number;
private _extraData: string;
private _date: Date;
constructor(type: number, senderId: number, message: string, sentAt: number, extraData?: string)
constructor(senderId: number, message: string, secondsSinceSent: number = 0, extraData: string = null, type: number = 0)
{
this._type = type;
this._senderId = senderId;
this._message = message;
this._sentAt = sentAt;
this._secondsSinceSent = secondsSinceSent;
this._extraData = extraData;
this._date = new Date();
}
public get type(): number
@ -35,13 +37,18 @@ export class MessengerChatMessage
return this._message;
}
public get sentAt(): number
public get secondsSinceSent(): number
{
return this._sentAt;
return this._secondsSinceSent;
}
public get extraData(): string
{
return this._extraData;
}
public get date(): Date
{
return this._date;
}
}

View File

@ -0,0 +1,28 @@
import { MessengerThreadChat } from './MessengerThreadChat';
export class MessengerThreadChatGroup
{
private _userId: number;
private _chats: MessengerThreadChat[];
constructor(userId: number)
{
this._userId = userId;
this._chats = [];
}
public addChat(message: MessengerThreadChat): void
{
this._chats.push(message);
}
public get userId(): number
{
return this._userId;
}
public get chats(): MessengerThreadChat[]
{
return this._chats;
}
}

View File

@ -1,7 +1,5 @@
import { FriendListUpdateParser, FriendParser, FriendRequestData } from '@nitrots/nitro-renderer';
import { Reducer } from 'react';
import { MessengerChat } from '../common/MessengerChat';
import { MessengerChatMessage } from '../common/MessengerChatMessage';
import { MessengerFriend } from '../common/MessengerFriend';
import { MessengerRequest } from '../common/MessengerRequest';
import { MessengerSettings } from '../common/MessengerSettings';
@ -18,8 +16,6 @@ export interface IFriendsState
settings: MessengerSettings;
friends: MessengerFriend[];
requests: MessengerRequest[];
activeChats: MessengerChat[];
firstChatEverOpen: boolean;
}
export interface IFriendsAction
@ -30,8 +26,6 @@ export interface IFriendsAction
fragment?: FriendParser[];
update?: FriendListUpdateParser;
requests?: FriendRequestData[];
chats?: MessengerChat[];
chatMessage?: MessengerChatMessage;
numberValue?: number;
boolValue?: boolean;
}
@ -44,17 +38,12 @@ export class FriendsActions
public static PROCESS_FRAGMENT: string = 'FA_PROCESS_FRAGMENT';
public static PROCESS_UPDATE: string = 'FA_PROCESS_UPDATE';
public static PROCESS_REQUESTS: string = 'FA_PROCESS_REQUESTS';
public static SET_ACTIVE_CHATS: string = 'FA_SET_ACTIVE_CHATS';
public static SET_CHAT_READ: string = 'FA_SET_CHAT_READ';
public static ADD_CHAT_MESSAGE: string = 'FA_ADD_CHAT_MESSAGE';
}
export const initialFriends: IFriendsState = {
settings: null,
friends: [],
requests: [],
activeChats: [],
firstChatEverOpen: false
requests: []
}
export const FriendsReducer: Reducer<IFriendsState, IFriendsAction> = (state, action) =>
@ -108,11 +97,17 @@ export const FriendsReducer: Reducer<IFriendsState, IFriendsAction> = (state, ac
{
const index = friends.findIndex(existingFriend => (existingFriend.id === friend.id));
const newFriend = new MessengerFriend();
newFriend.populate(friend);
if(index === -1)
{
const newFriend = new MessengerFriend();
newFriend.populate(friend);
if(index > -1) friends[index] = newFriend;
else friends.unshift(newFriend);
friends.unshift(newFriend);
}
else
{
friends[index].populate(friend);
}
}
for(const friend of update.addedFriends) processUpdate(friend);
@ -146,43 +141,6 @@ export const FriendsReducer: Reducer<IFriendsState, IFriendsAction> = (state, ac
return { ...state, requests };
}
case FriendsActions.SET_ACTIVE_CHATS: {
const activeChats = (action.payload.chats || []);
if(!state.firstChatEverOpen && activeChats.length > 0) activeChats[0].addMessage(new MessengerChatMessage(MessengerChatMessage.SECURITY_ALERT, 0, null, 0), false, true);
return { ...state, activeChats, firstChatEverOpen: true };
}
case FriendsActions.SET_CHAT_READ: {
const friendId = action.payload.numberValue;
const activeChats = Array.from(state.activeChats);
let activeChatIndex = activeChats.findIndex(c => c.friendId === friendId);
if(activeChatIndex > -1) activeChats[activeChatIndex].read();
return { ...state, activeChats };
}
case FriendsActions.ADD_CHAT_MESSAGE: {
const message = action.payload.chatMessage;
const toFriendId = action.payload.numberValue;
const setAsNotRead = action.payload.boolValue;
const activeChats = Array.from(state.activeChats);
let activeChatIndex = activeChats.findIndex(c => c.friendId === toFriendId ? toFriendId : message.senderId);
if(activeChatIndex === -1)
{
activeChats.push(new MessengerChat(message.senderId));
activeChatIndex = activeChats.length - 1;
}
activeChats[activeChatIndex].addMessage(message, setAsNotRead);
return { ...state, activeChats };
}
default:
return state;
}

View File

@ -0,0 +1,48 @@
import { FC } from 'react';
import { GetSessionDataManager } from '../../../../api';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { MessengerThreadChat } from '../../common/MessengerThreadChat';
import { FriendsMessengerThreadGroupProps } from './FriendsMessengerThreadGroup.types';
export const FriendsMessengerThreadGroup: FC<FriendsMessengerThreadGroupProps> = props =>
{
const { thread = null, group = null } = props;
if(!thread || !group) return null;
if(group.userId === -1)
{
return (
<div className="d-flex gap-2 w-100 justify-content-start">
{ group.chats.map((chat, index) =>
{
return (
<div key={ index } className="text-break">
{ chat.type === MessengerThreadChat.SECURITY_NOTIFICATION &&
<div className="bg-light rounded mb-2 d-flex gap-2 px-2 py-1 small text-muted align-items-center">
<i className="icon icon-friendlist-warning flex-shrink-0" />
<div>{ chat.message }</div>
</div> }
</div>
);
}) }
</div>
);
}
return (
<div className={ 'd-flex gap-2 w-100 justify-content-' + (group.userId === 0 ? 'end' : 'start') }>
{ (group.userId > 0) &&
<div className="message-avatar flex-shrink-0">
<AvatarImageView figure={ thread.participant.figure } direction={ 2 } />
</div> }
<div className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (group.userId === 0 ? 'right' : 'left') }>
{ group.chats.map((chat, index) => <div key={ index } className="text-break">{ chat.message }</div>) }
</div>
{ (group.userId === 0) &&
<div className="message-avatar flex-shrink-0">
<AvatarImageView figure={ GetSessionDataManager().figure } direction={ 4 } />
</div> }
</div>
);
}

View File

@ -0,0 +1,8 @@
import { MessengerThread } from '../../common/MessengerThread';
import { MessengerThreadChatGroup } from '../../common/MessengerThreadChatGroup';
export interface FriendsMessengerThreadGroupProps
{
thread: MessengerThread;
group: MessengerThreadChatGroup;
}

View File

@ -0,0 +1,17 @@
import { FC } from 'react';
import { FriendsMessengerThreadGroup } from '../messenger-thread-group/FriendsMessengerThreadGroup';
import { FriendsMessengerThreadViewProps } from './FriendsMessengerThreadView.types';
export const FriendsMessengerThreadView: FC<FriendsMessengerThreadViewProps> = props =>
{
const { thread = null } = props;
return (
<>
{ (thread.groups.length > 0) && thread.groups.map((group, index) =>
{
return <FriendsMessengerThreadGroup key={ index } thread={ thread } group={ group } />;
}) }
</>
);
}

View File

@ -0,0 +1,6 @@
import { MessengerThread } from '../../common/MessengerThread';
export interface FriendsMessengerThreadViewProps
{
thread: MessengerThread;
}

View File

@ -1,5 +1,6 @@
.nitro-friends-messenger {
width: 280px;
resize: both;
.friend-head {
position: relative;

View File

@ -1,41 +1,134 @@
import { FollowFriendMessageComposer, ILinkEventTracker, NitroEvent, SendMessageComposer, UserProfileComposer } from '@nitrots/nitro-renderer';
import { FollowFriendMessageComposer, ILinkEventTracker, NewConsoleMessageEvent, SendMessageComposer, UserProfileComposer } from '@nitrots/nitro-renderer';
import { FC, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AddEventLinkTracker, GetSessionDataManager, LocalizeText, RemoveLinkEventTracker } from '../../../../api';
import { FriendsEvent } from '../../../../events/friends/FriendsEvent';
import { SendMessageHook, useUiEvent } from '../../../../hooks';
import { AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker } from '../../../../api';
import { FriendsMessengerIconEvent } from '../../../../events';
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { MessengerChat } from '../../common/MessengerChat';
import { MessengerChatMessage } from '../../common/MessengerChatMessage';
import { MessengerThread } from '../../common/MessengerThread';
import { MessengerThreadChat } from '../../common/MessengerThreadChat';
import { useFriendsContext } from '../../context/FriendsContext';
import { FriendsActions } from '../../reducers/FriendsReducer';
import { FriendsMessengerThreadView } from '../messenger-thread/FriendsMessengerThreadView';
export const FriendsMessengerView: FC<{}> = props =>
{
const { friendsState = null, dispatchFriendsState = null } = useFriendsContext();
const { activeChats = [], friends = [] } = friendsState;
const [ isVisible, setIsVisible ] = useState(false);
const [ selectedChatIndex, setSelectedChatIndex ] = useState(0);
const [ message, setMessage ] = useState('');
const [ messageThreads, setMessageThreads ] = useState<MessengerThread[]>([]);
const [ activeThreadIndex, setActiveThreadIndex ] = useState(-1);
const [ hiddenThreadIndexes, setHiddenThreadIndexes ] = useState<number[]>([]);
const [ messageText, setMessageText ] = useState('');
const { friendsState = null } = useFriendsContext();
const { friends = [] } = friendsState;
const messagesBox = useRef<HTMLDivElement>();
const onNitroEvent = useCallback((event: NitroEvent) =>
{
switch(event.type)
{
case FriendsEvent.SHOW_FRIEND_MESSENGER:
setIsVisible(true);
return;
case FriendsEvent.TOGGLE_FRIEND_MESSENGER:
setIsVisible(value => !value);
return;
}
}, []);
const [ updateValue, setUpdateValue ] = useState({});
useUiEvent(FriendsEvent.SHOW_FRIEND_MESSENGER, onNitroEvent);
useUiEvent(FriendsEvent.TOGGLE_FRIEND_MESSENGER, onNitroEvent);
const followFriend = useCallback(() =>
{
SendMessageHook(new FollowFriendMessageComposer(messageThreads[activeThreadIndex].participant.id));
}, [ messageThreads, activeThreadIndex ]);
const openProfile = useCallback(() =>
{
SendMessageHook(new UserProfileComposer(messageThreads[activeThreadIndex].participant.id));
}, [ messageThreads, activeThreadIndex ]);
const getFriend = useCallback((userId: number) =>
{
return ((friends.find(friend => (friend.id === userId))) || null);
}, [ friends ]);
const visibleThreads = useMemo(() =>
{
return messageThreads.filter((thread, index) =>
{
if(hiddenThreadIndexes.indexOf(index) >= 0) return false;
return true;
});
}, [ messageThreads, hiddenThreadIndexes ]);
const getMessageThreadWithIndex = useCallback<(userId: number) => [ number, MessengerThread ]>((userId: number) =>
{
if(messageThreads.length > 0)
{
for(let i = 0; i < messageThreads.length; i++)
{
const thread = messageThreads[i];
if(thread.participant && (thread.participant.id === userId))
{
const hiddenIndex = hiddenThreadIndexes.indexOf(i);
if(hiddenIndex >= 0)
{
setHiddenThreadIndexes(prevValue =>
{
const newIndexes = [ ...prevValue ];
newIndexes.splice(hiddenIndex, 1);
return newIndexes;
});
}
return [ i, thread ];
}
}
}
const friend = getFriend(userId);
if(!friend) return [ -1, null ];
const thread = new MessengerThread(friend);
const newThreads = [ ...messageThreads, thread ];
setMessageThreads(newThreads);
return [ (newThreads.length - 1), thread ];
}, [ messageThreads, hiddenThreadIndexes, getFriend ]);
const onNewConsoleMessageEvent = useCallback((event: NewConsoleMessageEvent) =>
{
const parser = event.getParser();
const [ threadIndex, thread ] = getMessageThreadWithIndex(parser.senderId);
if((threadIndex === -1) || !thread) return;
thread.addMessage(parser.senderId, parser.messageText, parser.secondsSinceSent, parser.extraData);
setMessageThreads(prevValue => [ ...prevValue ]);
}, [ getMessageThreadWithIndex ]);
CreateMessageHook(NewConsoleMessageEvent, onNewConsoleMessageEvent);
const sendMessage = useCallback(() =>
{
if(!messageText || !messageText.length) return;
if(activeThreadIndex === -1) return;
const thread = messageThreads[activeThreadIndex];
if(!thread) return;
SendMessageHook(new SendMessageComposer(thread.participant.id, messageText));
thread.addMessage(0, messageText, 0, null, MessengerThreadChat.CHAT);
BatchUpdates(() =>
{
setMessageThreads(prevValue => [ ...prevValue ]);
setMessageText('');
});
}, [ messageThreads, activeThreadIndex, messageText ]);
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key !== 'Enter') return;
sendMessage();
}, [ sendMessage ]);
const linkReceived = useCallback((url: string) =>
{
@ -43,69 +136,35 @@ export const FriendsMessengerView: FC<{}> = props =>
if(parts.length < 3) return;
const friendId = parseInt(parts[2]);
let existingChatIndex = activeChats.findIndex(c => c.friendId === friendId);
if(existingChatIndex === -1)
if(parts[2] === 'open')
{
const clonedActiveChats = Array.from(activeChats);
clonedActiveChats.push(new MessengerChat(friendId));
setIsVisible(true);
dispatchFriendsState({
type: FriendsActions.SET_ACTIVE_CHATS,
payload: {
chats: clonedActiveChats
}
});
existingChatIndex = clonedActiveChats.length - 1;
return;
}
setSelectedChatIndex(existingChatIndex);
setIsVisible(true);
}, [ activeChats, dispatchFriendsState ]);
const [ threadIndex ] = getMessageThreadWithIndex(parseInt(parts[2]));
const getFriendFigure = useCallback((id: number) =>
{
const friend = friends.find(f => f.id === id);
if(threadIndex === -1) return;
if(!friend) return null;
return friend.figure;
}, [ friends ]);
const selectChat = useCallback((index: number) =>
{
const chat = activeChats[index];
if(!chat) return;
dispatchFriendsState({
type: FriendsActions.SET_CHAT_READ,
payload: {
numberValue: chat.friendId
}
BatchUpdates(() =>
{
setActiveThreadIndex(threadIndex);
setIsVisible(true);
});
setSelectedChatIndex(index);
}, [ activeChats, dispatchFriendsState ]);
}, [ getMessageThreadWithIndex ]);
const selectedChat = useMemo(() =>
{
return activeChats[selectedChatIndex];
}, [ activeChats, selectedChatIndex ]);
const selectedChatFriend = useMemo(() =>
const closeThread = useCallback((threadIndex: number) =>
{
if(!selectedChat) return null;
setHiddenThreadIndexes(prevValue =>
{
const values = [ ...prevValue ];
const friend = friends.find(f => f.id === selectedChat.friendId);
if(values.indexOf(threadIndex) === -1) values.push(threadIndex);
if(!friend) return null;
return friend;
}, [ friends, selectedChat ]);
return values;
});
}, []);
useEffect(() =>
{
@ -121,112 +180,102 @@ export const FriendsMessengerView: FC<{}> = props =>
useEffect(() =>
{
if(!messagesBox || !messagesBox.current) return;
messagesBox.current.scrollTop = messagesBox.current.scrollHeight;
if(!isVisible) return;
}, [ selectedChat ]);
if(activeThreadIndex === -1) setActiveThreadIndex(0);
}, [ isVisible, activeThreadIndex ]);
const followFriend = useCallback(() =>
useEffect(() =>
{
SendMessageHook(new FollowFriendMessageComposer(selectedChatFriend.id));
}, [ selectedChatFriend ]);
if(hiddenThreadIndexes.indexOf(activeThreadIndex) >= 0) setActiveThreadIndex(0);
}, [ activeThreadIndex, hiddenThreadIndexes ]);
const openProfile = useCallback(() =>
useEffect(() =>
{
SendMessageHook(new UserProfileComposer(selectedChatFriend.id));
}, [ selectedChatFriend ]);
if(!isVisible || (activeThreadIndex === -1)) return;
const sendMessage = useCallback(() =>
const activeThread = messageThreads[activeThreadIndex];
if(activeThread.unread)
{
messagesBox.current.scrollTop = messagesBox.current.scrollHeight;
activeThread.setRead();
setUpdateValue({});
}
}, [ isVisible, messageThreads, activeThreadIndex ]);
useEffect(() =>
{
if(message.length === 0) return;
if(!visibleThreads.length)
{
setIsVisible(false);
SendMessageHook(new SendMessageComposer(selectedChat.friendId, message));
dispatchUiEvent(new FriendsMessengerIconEvent(FriendsMessengerIconEvent.UPDATE_ICON, FriendsMessengerIconEvent.HIDE_ICON));
dispatchFriendsState({
type: FriendsActions.ADD_CHAT_MESSAGE,
payload: {
chatMessage: new MessengerChatMessage(MessengerChatMessage.MESSAGE, 0, message, (new Date().getMilliseconds())),
numberValue: selectedChat.friendId,
boolValue: false
return;
}
let isUnread = false;
for(const thread of visibleThreads)
{
if(thread.unread)
{
isUnread = true;
break;
}
});
setMessage('');
}, [ selectedChat, message, dispatchFriendsState ]);
}
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key !== 'Enter') return;
sendMessage();
}, [ sendMessage ]);
dispatchUiEvent(new FriendsMessengerIconEvent(FriendsMessengerIconEvent.UPDATE_ICON, isUnread ? FriendsMessengerIconEvent.UNREAD_ICON : FriendsMessengerIconEvent.SHOW_ICON));
}, [ visibleThreads, updateValue ]);
if(!isVisible) return null;
return (<NitroCardView className="nitro-friends-messenger" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('messenger.window.title', ['OPEN_CHAT_COUNT'], [activeChats.length.toString()]) } onCloseClick={ () => setIsVisible(false) } />
<NitroCardContentView>
<div className="d-flex gap-2 overflow-auto pb-1">
{ activeChats && activeChats.map((chat, index) =>
return (
<NitroCardView className="nitro-friends-messenger" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('messenger.window.title', [ 'OPEN_CHAT_COUNT' ], [ visibleThreads.length.toString() ]) } onCloseClick={ event => setIsVisible(false) } />
<NitroCardContentView>
<div className="d-flex gap-2 overflow-auto pb-1">
{ visibleThreads && (visibleThreads.length > 0) && visibleThreads.map((thread, index) =>
{
return <div key={ index } className="friend-head rounded flex-shrink-0 cursor-pointer bg-muted" onClick={ () => selectChat(index) }>
{ !chat.isRead && <i className="icon icon-friendlist-new-message" /> }
<AvatarImageView figure={ getFriendFigure(chat.friendId) } headOnly={true} direction={3} />
</div>;
const messageThreadIndex = messageThreads.indexOf(thread);
return (
<div key={ index } className="friend-head rounded flex-shrink-0 cursor-pointer bg-muted" onClick={ event => setActiveThreadIndex(messageThreadIndex) }>
{ thread.unread && <i className="icon icon-friendlist-new-message" /> }
<AvatarImageView figure={ thread.participant.figure } headOnly={ true } direction={ 3 } />
</div>
);
}) }
</div>
<hr className="bg-dark mt-3 mb-2" />
{ selectedChat && selectedChatFriend && <>
<div className="text-black chat-title bg-light pe-2 position-absolute">{ LocalizeText('messenger.window.separator', ['FRIEND_NAME'], [ selectedChatFriend.name ]) }</div>
</div>
<hr className="bg-dark mt-3 mb-2" />
{ (activeThreadIndex >= 0) &&
<>
<div className="text-black chat-title bg-light pe-2 position-absolute">
{ LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ messageThreads[activeThreadIndex].participant.name ]) }
</div>
<div className="d-flex gap-2 mb-2">
<div className="btn-group">
<button className="btn btn-sm btn-primary" onClick={ followFriend }>
<button className="d-flex justify-content-center align-items-center btn btn-sm btn-primary" onClick={ followFriend }>
<i className="icon icon-friendlist-follow" />
</button>
<button className="btn btn-sm btn-primary" onClick={ openProfile }>
<button className="d-flex justify-content-center align-items-center btn btn-sm btn-primary" onClick={ openProfile }>
<i className="icon icon-user-profile" />
</button>
</div>
<button className="btn btn-sm btn-danger">{ LocalizeText('messenger.window.button.report') }</button>
<button className="btn btn-sm btn-primary ms-auto"><i className="fas fa-times" /></button>
<button className="btn btn-sm btn-primary ms-auto" onClick={ event => closeThread(activeThreadIndex) }><i className="fas fa-times" /></button>
</div>
<div ref={ messagesBox } className="bg-muted p-2 rounded chat-messages mb-2 d-flex flex-column">
{ selectedChat.messageGroups.map((group, groupIndex) =>
{
return <div key={ groupIndex } className={ 'd-flex gap-2 w-100 justify-content-' + (group.userId === 0 ? 'end' : 'start') }>
{ group.isSystem && <>
{ group.messages.map((message, messageIndex) =>
{
return <div key={ messageIndex } className="text-break">
{ message.type === MessengerChatMessage.SECURITY_ALERT && <div className="bg-light rounded mb-2 d-flex gap-2 px-2 py-1 small text-muted align-items-center">
<i className="icon icon-friendlist-warning flex-shrink-0" />
<div>{ LocalizeText('messenger.moderationinfo') }</div>
</div> }
</div>
}) }
</> }
{ !group.isSystem && <>
{ group.userId !== 0 && <div className="message-avatar flex-shrink-0">
<AvatarImageView figure={ selectedChatFriend.figure } direction={ 2 } />
</div> }
<div className={ 'bg-light text-black border-radius mb-2 rounded py-1 px-2 messages-group-' + (group.userId === 0 ? 'right' : 'left') }>
{ group.messages.map((message, messageIndex) =>
{
return <div key={ messageIndex } className="text-break">{ message.message }</div>
}) }
</div>
{ group.userId === 0 && <div className="message-avatar flex-shrink-0">
<AvatarImageView figure={ GetSessionDataManager().figure } direction={ 4 } />
</div> }
</> }
</div>;
}) }
<FriendsMessengerThreadView thread={ messageThreads[activeThreadIndex] } />
</div>
<div className="d-flex gap-2">
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('messenger.window.input.default', ['FRIEND_NAME'], [ selectedChatFriend.name ]) } value={ message } onChange={ (e) => setMessage(e.target.value) } onKeyDown={ onKeyDown } />
<input type="text" className="form-control form-control-sm" placeholder={ LocalizeText('messenger.window.input.default', [ 'FRIEND_NAME' ], [ messageThreads[activeThreadIndex].participant.name ]) } value={ messageText } onChange={ event => setMessageText(event.target.value) } onKeyDown={ onKeyDown } />
<button className="btn btn-sm btn-success" onClick={ sendMessage }>{ LocalizeText('widgets.chatinput.say') }</button>
</div>
</> }
</NitroCardContentView>
</NitroCardView>);
};
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -696,7 +696,6 @@
justify-content: center;
height: 100%;
max-height: 24px;
image-rendering: -webkit-optimize-contrast;
overflow: hidden;
.user-image {
@ -707,8 +706,9 @@
height: 130px;
background-repeat: no-repeat;
background-position: center;
transform: scale(0.5) translateZ(0);
transform: scale(0.5);
overflow: hidden;
image-rendering: -webkit-optimize-contrast;
}
}

View File

@ -6,4 +6,10 @@
background-position-x: center;
background-position-y: -8px !important;
pointer-events: none;
image-rendering: pixelated;
&.scale-0-5,
&.scale-0-75 {
image-rendering: -webkit-optimize-contrast;
}
}

View File

@ -1,5 +1,5 @@
import { AvatarScaleType, AvatarSetType } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { GetAvatarRenderManager } from '../../../api';
import { AvatarImageViewProps } from './AvatarImageView.types';
@ -10,6 +10,13 @@ export const AvatarImageView: FC<AvatarImageViewProps> = props =>
const [ randomValue, setRandomValue ] = useState(-1);
const isDisposed = useRef(false);
const getScaleStyle = useMemo(() =>
{
if(scale === .5) return '0-5';
return scale.toString();
}, [ scale ]);
useEffect(() =>
{
const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, gender, {
@ -50,5 +57,5 @@ export const AvatarImageView: FC<AvatarImageViewProps> = props =>
const url = `url('${ avatarUrl }')`;
return <div className={ 'avatar-image scale-' + scale } style={ (avatarUrl && url.length) ? { backgroundImage: url } : {} }></div>;
return <div className={ 'avatar-image scale-' + getScaleStyle } style={ (avatarUrl && url.length) ? { backgroundImage: url } : {} }></div>;
}

View File

@ -12,7 +12,6 @@
background-color: $light;
background-repeat: no-repeat;
background-position: center;
image-rendering: auto;
&.border-0 {
&::after {

View File

@ -29,31 +29,28 @@
align-items: center;
justify-content: center;
cursor: pointer;
width: 50px;
margin: 0 1px;
//margin: 0 1px;
position: relative;
.toolbar-avatar {
height: 50px;
&.item-avatar {
width: 50px;
height: 45px;
overflow: hidden;
.avatar-image {
margin-left: -5px;
margin-top: -30px;
}
&:hover, &.active {
height: 53px;
margin-top: 25px;
}
}
.icon,
.toolbar-avatar {
&.item-avatar {
position: relative;
transition: transform .2s ease-out;
//transition: transform .2s ease-out;
&:hover, &.active {
-webkit-transform: translate(0, -3px);
transform: translate(0, -3px);
-webkit-transform: translate(-1px, -1px);
transform: translate(-1px, -1px);
filter: drop-shadow(2px 2px 0 rgba($black, 0.8));
}
}
@ -68,7 +65,7 @@
.count {
top: 0rem;
right: 5px;
right: 2px;
font-size: 10px;
}
}

View File

@ -1,7 +1,7 @@
import { Dispose, DropBounce, EaseOut, FigureUpdateEvent, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserInfoDataParser, UserInfoEvent, UserProfileComposer, Wait } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetRoomSession, GetRoomSessionManager, GetSessionDataManager, GoToDesktop } from '../../api';
import { AvatarEditorEvent, CatalogEvent, FriendsEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent } from '../../events';
import { GetRoomSession, GetRoomSessionManager, GetSessionDataManager, GoToDesktop, OpenMessengerChat } from '../../api';
import { AvatarEditorEvent, CatalogEvent, FriendsEvent, FriendsMessengerIconEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent } from '../../events';
import { AchievementsUIEvent } from '../../events/achievements';
import { UnseenItemTrackerUpdateEvent } from '../../events/inventory/UnseenItemTrackerUpdateEvent';
import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent';
@ -14,6 +14,10 @@ import { AvatarImageView } from '../shared/avatar-image/AvatarImageView';
import { ToolbarMeView } from './me/ToolbarMeView';
import { ToolbarViewItems, ToolbarViewProps } from './ToolbarView.types';
const CHAT_ICON_HIDDEN: number = 0;
const CHAT_ICON_SHOWING: number = 1;
const CHAT_ICON_UNREAD: number = 2;
export const ToolbarView: FC<ToolbarViewProps> = props =>
{
const { isInRoom } = props;
@ -21,6 +25,7 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
const [ userInfo, setUserInfo ] = useState<UserInfoDataParser>(null);
const [ userFigure, setUserFigure ] = useState<string>(null);
const [ isMeExpanded, setMeExpanded ] = useState(false);
const [ chatIconType, setChatIconType ] = useState(CHAT_ICON_HIDDEN);
const [ unseenInventoryCount, setUnseenInventoryCount ] = useState(0);
const unseenFriendListCount = 0;
@ -45,6 +50,13 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
CreateMessageHook(FigureUpdateEvent, onUserFigureEvent);
const onFriendsMessengerIconEvent = useCallback((event: FriendsMessengerIconEvent) =>
{
setChatIconType(event.iconType);
}, []);
useUiEvent(FriendsMessengerIconEvent.UPDATE_ICON, onFriendsMessengerIconEvent);
const onUnseenItemTrackerUpdateEvent = useCallback((event: UnseenItemTrackerUpdateEvent) =>
{
setUnseenInventoryCount(event.count);
@ -131,6 +143,9 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
dispatchUiEvent(new UserSettingsUIEvent(UserSettingsUIEvent.TOGGLE_USER_SETTINGS));
setMeExpanded(false);
return;
case ToolbarViewItems.FRIEND_CHAT_ITEM:
OpenMessengerChat();
return;
}
}, []);
@ -148,20 +163,16 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
<ToolbarMeView handleToolbarItemClick={ handleToolbarItemClick } />
</TransitionAnimation>
<div className="d-flex justify-content-between align-items-center nitro-toolbar py-1 px-3">
<div className="d-flex align-items-center toolbar-left-side">
<div className="navigation-items navigation-avatar pe-1 me-2">
<div className="navigation-item">
<div className={ 'toolbar-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
<AvatarImageView figure={ userFigure } direction={ 2 } />
</div>
<div className="d-flex align-items-center">
<div className="navigation-items gap-2">
<div className={ 'navigation-item item-avatar ' + (isMeExpanded ? 'active ' : '') } onClick={ event => setMeExpanded(!isMeExpanded) }>
<AvatarImageView figure={ userFigure } direction={ 2 } />
{ (unseenAchievementsCount > 0) &&
<div className="position-absolute bg-danger px-1 py-0 rounded shadow count">{ unseenAchievementsCount }</div> }
</div>
{ (unseenAchievementsCount > 0) && (
<div className="position-absolute bg-danger px-1 py-0 rounded shadow count">{ unseenAchievementsCount }</div>) }
</div>
<div className="navigation-items">
{ isInRoom && (
<div className="navigation-item" onClick={ visitDesktop }>
<i className="icon icon-hotelview icon-nitro-light"></i>
<i className="icon icon-habbo"></i>
</div>) }
{ !isInRoom && (
<div className="navigation-item">
@ -188,13 +199,20 @@ export const ToolbarView: FC<ToolbarViewProps> = props =>
</div>
<div id="toolbar-chat-input-container" className="d-flex align-items-center" />
</div>
<div className="d-flex toolbar-right-side">
<div className="navigation-items">
<div className="d-flex align-items-center gap-2">
<div className="navigation-items gap-2">
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_LIST_ITEM) }>
<i className="icon icon-friendall"></i>
{ (unseenFriendListCount > 0) && (
<div className="position-absolute bg-danger px-1 py-0 rounded shadow count">{ unseenFriendListCount }</div>) }
</div>
{ ((chatIconType === CHAT_ICON_SHOWING) || (chatIconType === CHAT_ICON_UNREAD)) &&
<div className="navigation-item" onClick={ event => handleToolbarItemClick(ToolbarViewItems.FRIEND_CHAT_ITEM) }>
{ (chatIconType === CHAT_ICON_SHOWING) && <i className="icon icon-message" /> }
{ (chatIconType === CHAT_ICON_UNREAD) && <i className="icon icon-message is-unseen" /> }
{ (unseenFriendListCount > 0) &&
<div className="position-absolute bg-danger px-1 py-0 rounded shadow count">{ unseenFriendListCount }</div> }
</div> }
</div>
<div id="toolbar-friend-bar-container" />
</div>

View File

@ -9,6 +9,7 @@ export class ToolbarViewItems
public static INVENTORY_ITEM: string = 'TVI_INVENTORY_ITEM';
public static CATALOG_ITEM: string = 'TVI_CATALOG_ITEM';
public static FRIEND_LIST_ITEM: string = 'TVI_FRIEND_LIST_ITEM';
public static FRIEND_CHAT_ITEM: string = 'TVI_FRIEND_CHAT_ITEM';
public static CLOTHING_ITEM: string = 'TVI_CLOTHING_ITEM';
public static CAMERA_ITEM: string = 'TVI_CAMERA_ITEM';
public static MOD_TOOLS_ITEM: string = 'TVI_MOD_TOOLS_ITEM';

View File

@ -24,6 +24,6 @@
},
"include": [
"src",
"node_modules/@nitrots/nitro-renderer/**/*.ts",
"node_modules/@nitrots/nitro-renderer/src/**/*.ts",
]
}