Messenger Chat

This commit is contained in:
MyNameIsBatman 2021-09-18 03:04:50 -03:00
parent b5850c1995
commit 7a6fe87aed
17 changed files with 309 additions and 119 deletions

View File

@ -19,6 +19,10 @@ $nitro-card-tabs-height: 33px;
height: 100%;
pointer-events: none;
.theme-primary {
border: $border-width solid $border-color;
}
@include media-breakpoint-down(lg) {
.draggable-window {

View File

@ -11,7 +11,7 @@ export const NitroCardView: FC<NitroCardViewProps> = props =>
<NitroCardContextProvider value={ { theme, simple } }>
<div className="nitro-card-responsive">
<DraggableWindow { ...rest }>
<div className={ 'nitro-card d-flex flex-column rounded border shadow overflow-hidden ' + className }>
<div className={ `nitro-card d-flex flex-column rounded shadow overflow-hidden theme-${theme} ${className}` }>
{ children }
</div>
</DraggableWindow>

View File

@ -1,6 +1,8 @@
import { FriendListFragmentEvent, FriendListUpdateEvent, FriendRequestsEvent, GetFriendRequestsComposer, MessengerInitEvent, NewConsoleMessageEvent } 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';
@ -68,17 +70,17 @@ export const FriendsMessageHandler: FC<{}> = props =>
{
const parser = event.getParser();
const activeChat = activeChats.find(c => c.friendId === parser.senderId);
let userId = parser.senderId;
if(activeChat)
{
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)
}
else
{
}
}, [ friendsState, dispatchFriendsState ]);
});
}, [ dispatchFriendsState ]);
CreateMessageHook(MessengerInitEvent, onMessengerInitEvent);
CreateMessageHook(FriendListFragmentEvent, onFriendsFragmentEvent);

View File

@ -3,3 +3,4 @@
}
@import './views/friend-bar/FriendBarView';
@import './views/messenger/FriendsMessengerView';

View File

@ -95,7 +95,7 @@ export const FriendsView: FC<{}> = props =>
return (
<FriendsContextProvider value={ { friendsState, dispatchFriendsState } }>
<FriendsMessageHandler />
{ isReady && createPortal(<FriendBarView />, document.getElementById('toolbar-friend-bar-container')) }
{ isReady && createPortal(<FriendBarView onlineFriends={ onlineFriends } />, document.getElementById('toolbar-friend-bar-container')) }
{ isListVisible && <FriendsListView onlineFriends={ onlineFriends } offlineFriends={ offlineFriends } friendRequests={ requests } onCloseClick={ () => setIsListVisible(false) } /> }
<FriendsMessengerView />
</FriendsContextProvider>

View File

@ -1,20 +1,24 @@
import { MessengerChatMessage } from './MessengerChatMessage';
import { MessengerChatMessageGroup } from './MessengerChatMessageGroup';
export class MessengerChat
{
private _friendId: number;
private _isRead: boolean;
private _messages: MessengerChatMessage[];
private _messageGroups: MessengerChatMessageGroup[];
constructor(friendId: number, isRead: boolean = true)
{
this._friendId = friendId;
this._isRead = isRead;
this._messages = [];
this._messageGroups = [];
}
public addMessage(type: number, senderId: number, message: string, sentAt: number, extraData?: string): void
public addMessage(message: MessengerChatMessage): void
{
this._messages.push(new MessengerChatMessage(type, senderId, message, sentAt, extraData));
if(!this.lastMessageGroup || this.lastMessageGroup.userId !== message.senderId) this._messageGroups.push(new MessengerChatMessageGroup(message.senderId));
this.lastMessageGroup.addMessage(message);
this._isRead = false;
}
public get friendId(): number
@ -27,8 +31,13 @@ export class MessengerChat
return this._isRead;
}
public get messages(): MessengerChatMessage[]
public get messageGroups(): MessengerChatMessageGroup[]
{
return this._messages;
return this._messageGroups;
}
public get lastMessageGroup(): MessengerChatMessageGroup
{
return this._messageGroups[this._messageGroups.length - 1];
}
}

View File

@ -1,5 +1,9 @@
export class MessengerChatMessage
{
public static MESSAGE: number = 0;
public static ROOM_INVITE: number = 1;
public static SYSTEM_NOTIFICATION: number = 2;
private _type: number;
private _senderId: number;
private _message: string;

View File

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

View File

@ -1,6 +1,7 @@
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';
@ -29,6 +30,8 @@ export interface IFriendsAction
update?: FriendListUpdateParser;
requests?: FriendRequestData[];
chats?: MessengerChat[];
chatMessage?: MessengerChatMessage;
numberValue?: number;
}
}
@ -40,6 +43,7 @@ export class FriendsActions
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 ADD_CHAT_MESSAGE: string = 'FA_ADD_CHAT_MESSAGE';
}
export const initialFriends: IFriendsState = {
@ -143,6 +147,24 @@ export const FriendsReducer: Reducer<IFriendsState, IFriendsAction> = (state, ac
return { ...state, activeChats };
}
case FriendsActions.ADD_CHAT_MESSAGE: {
const message = action.payload.chatMessage;
const toFriendId = action.payload.numberValue;
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, false));
activeChatIndex = activeChats.length - 1;
}
activeChats[activeChatIndex].addMessage(message);
return { ...state, activeChats };
}
default:
return state;
}

View File

@ -1,6 +1,6 @@
import { FollowFriendMessageComposer, MouseEventType, UserProfileComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { LocalizeText, OpenMessengerChat } from '../../../../api';
import { SendMessageHook } from '../../../../hooks/messages';
import { AvatarImageView } from '../../../shared/avatar-image/AvatarImageView';
import { FriendBarItemViewProps } from './FriendBarItemView.types';
@ -16,6 +16,13 @@ export const FriendBarItemView: FC<FriendBarItemViewProps> = props =>
SendMessageHook(new FollowFriendMessageComposer(friend.id));
}, [ friend ]);
const openMessengerChat = useCallback(() =>
{
if(!friend) return;
OpenMessengerChat(friend.id);
}, [ friend ]);
const openProfile = useCallback(() =>
{
SendMessageHook(new UserProfileComposer(friend.id));
@ -59,7 +66,7 @@ export const FriendBarItemView: FC<FriendBarItemViewProps> = props =>
<div className="text-truncate">{ friend.name }</div>
{ isVisible &&
<div className="d-flex justify-content-between">
<i className="icon icon-fb-chat cursor-pointer" />
<i onClick={ openMessengerChat } className="icon icon-fb-chat cursor-pointer" />
{ friend.followingAllowed && <i onClick={ followFriend } className="icon icon-fb-visit cursor-pointer" /> }
<i onClick={ openProfile } className="icon icon-fb-profile cursor-pointer" />
</div> }

View File

@ -1,20 +1,14 @@
import { FC, useMemo, useState } from 'react';
import { useFriendsContext } from '../../context/FriendsContext';
import { FriendBarItemView } from '../friend-bar-item/FriendBarItemView';
import { FriendBarViewProps } from './FriendBarView.types';
export const FriendBarView: FC<FriendBarViewProps> = props =>
{
const { friendsState = null } = useFriendsContext();
const { friends = null } = friendsState;
const { onlineFriends = null } = props;
const [ indexOffset, setIndexOffset ] = useState(0);
const [ maxDisplayCount, setMaxDisplayCount ] = useState(3);
const onlineFriends = useMemo(() =>
{
return friends.filter(friend => friend.online);
}, [ friends ]);
const canDecreaseIndex = useMemo(() =>
{
if(indexOffset === 0) return false;

View File

@ -1,4 +1,5 @@
import { MessengerFriend } from './../../common/MessengerFriend';
export interface FriendBarViewProps
{
onlineFriends: MessengerFriend[];
}

View File

@ -1,71 +0,0 @@
import { SetRelationshipStatusComposer } from '@nitrots/nitro-renderer';
import { FollowFriendMessageComposer } from '@nitrots/nitro-renderer/src/nitro/communication/messages/outgoing/friendlist/FollowFriendMessageComposer';
import { FC, useCallback, useState } from 'react';
import { LocalizeText, OpenMessengerChat } from '../../../../api';
import { SendMessageHook } from '../../../../hooks';
import { UserProfileIconView } from '../../../shared/user-profile-icon/UserProfileIconView';
import { MessengerFriend } from '../../common/MessengerFriend';
import { FriendsListItemViewProps } from './FriendsListItemView.types';
export const FriendsListItemView: FC<FriendsListItemViewProps> = props =>
{
const { friend = null } = props;
const [ isExpanded, setIsExpanded ] = useState<boolean>(false);
const followFriend = useCallback(() =>
{
if(!friend) return;
SendMessageHook(new FollowFriendMessageComposer(friend.id));
}, [ friend ]);
const openMessengerChat = useCallback(() =>
{
if(!friend) return;
OpenMessengerChat(friend.id);
}, [ friend ]);
const getCurrentRelationshipName = useCallback(() =>
{
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';
}
}, [ friend ]);
const updateRelationship = useCallback((type: number) =>
{
if(type !== friend.relationshipStatus) SendMessageHook(new SetRelationshipStatusComposer(friend.id, type));
setIsExpanded(false);
}, [ friend ]);
if(!friend) return null;
return (
<div className="px-2 py-1 d-flex gap-1 align-items-center">
<UserProfileIconView userId={ friend.id } />
<div>{ friend.name }</div>
<div className="ms-auto d-flex align-items-center gap-1">
{ !isExpanded && <>
{ friend.followingAllowed && <i className="icon icon-friendlist-follow cursor-pointer" onClick={ followFriend } title={ LocalizeText('friendlist.tip.follow') } /> }
{ friend.online && <i className="icon icon-friendlist-chat cursor-pointer" onClick={ openMessengerChat } title={ LocalizeText('friendlist.tip.im') } /> }
<i className={ 'icon cursor-pointer icon-relationship-' + getCurrentRelationshipName() } onClick={ () => setIsExpanded(true) } title={ LocalizeText('infostand.link.relationship') } />
</> }
{ isExpanded && <>
<i className="icon icon-relationship-heart cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_HEART) } />
<i className="icon icon-relationship-smile cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_SMILE) } />
<i className="icon icon-relationship-bobba cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_BOBBA) } />
<i className="icon icon-relationship-none cursor-pointer" onClick={ () => updateRelationship(MessengerFriend.RELATIONSHIP_NONE) } />
</> }
</div>
</div>
);
}

View File

@ -1,6 +0,0 @@
import { MessengerFriend } from '../../common/MessengerFriend';
export interface FriendsListItemViewProps
{
friend: MessengerFriend;
}

View File

@ -1,6 +1,6 @@
import { FollowFriendMessageComposer, SetRelationshipStatusComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { LocalizeText } from '../../../../api';
import { LocalizeText, OpenMessengerChat } from '../../../../api';
import { SendMessageHook } from '../../../../hooks';
import { UserProfileIconView } from '../../../shared/user-profile-icon/UserProfileIconView';
import { MessengerFriend } from '../../common/MessengerFriend';
@ -19,6 +19,13 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
SendMessageHook(new FollowFriendMessageComposer(friend.id));
}, [ friend ]);
const openMessengerChat = useCallback(() =>
{
if(!friend) return;
OpenMessengerChat(friend.id);
}, [ friend ]);
const getCurrentRelationshipName = useCallback(() =>
{
if(!friend) return 'none';
@ -48,7 +55,7 @@ export const FriendsGroupItemView: FC<FriendsGroupItemViewProps> = props =>
<div className="ms-auto d-flex align-items-center gap-1">
{ !isExpanded && <>
{ friend.followingAllowed && <i onClick={ followFriend } className="icon icon-friendlist-follow cursor-pointer" title={ LocalizeText('friendlist.tip.follow') } /> }
{ friend.online && <i className="icon icon-friendlist-chat cursor-pointer" title={ LocalizeText('friendlist.tip.im') } /> }
{ friend.online && <i className="icon icon-friendlist-chat cursor-pointer" onClick={ openMessengerChat } title={ LocalizeText('friendlist.tip.im') } /> }
<i className={ 'icon cursor-pointer icon-relationship-' + getCurrentRelationshipName() } onClick={ () => setIsExpanded(true) } title={ LocalizeText('infostand.link.relationship') } />
</> }
{ isExpanded && <>

View File

@ -0,0 +1,71 @@
.nitro-friends-messenger {
width: 300px;
.friend-head {
position: relative;
width: 40px;
height: 40px;
overflow: hidden;
.avatar-image {
position: absolute;
margin-left: -27px;
margin-top: -27px;
}
}
.chat-title {
margin-top: -21px;
}
.chat-messages {
height: 200px;
min-height: 200px;
overflow-y: auto;
.message-avatar {
position: relative;
overflow: hidden;
width: 50px;
height: 50px;
.avatar-image {
position: absolute;
margin-left: -22px;
margin-top: -25px;
}
}
.messages-group-left {
position: relative;
&:before {
position: absolute;
content: ' ';
width: 0;
height: 0;
border-right: 8px solid rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
top: 10px;
left: -8px;
}
}
.messages-group-right {
position: relative;
&:before {
position: absolute;
content: ' ';
width: 0;
height: 0;
border-left: 8px solid rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
top: 10px;
right: -8px;
}
}
}
}

View File

@ -1,20 +1,25 @@
import { ILinkEventTracker, NitroEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker } from '../../../../api';
import { FollowFriendMessageComposer, ILinkEventTracker, NitroEvent, 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 { useUiEvent } from '../../../../hooks';
import { SendMessageHook, useUiEvent } 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 { useFriendsContext } from '../../context/FriendsContext';
import { FriendsActions } from '../../reducers/FriendsReducer';
export const FriendsMessengerView: FC<{}> = props =>
{
const { friendsState = null, dispatchFriendsState = null } = useFriendsContext();
const { activeChats = [] } = friendsState;
const { activeChats = [], friends = [] } = friendsState;
const [ isVisible, setIsVisible ] = useState(false);
const [ activeChatIndex, setActiveChatIndex ] = useState(0);
const [ selectedChatIndex, setSelectedChatIndex ] = useState(0);
const [ message, setMessage ] = useState('');
const messagesBox = useRef<HTMLDivElement>();
const onNitroEvent = useCallback((event: NitroEvent) =>
{
@ -35,11 +40,11 @@ export const FriendsMessengerView: FC<{}> = props =>
const linkReceived = useCallback((url: string) =>
{
const parts = url.split('/');
console.log(parts);
if(parts.length < 3) return;
const friendId = parseInt(parts[2]);
console.log(friendId);
let existingChatIndex = activeChats.findIndex(c => c.friendId === friendId);
if(existingChatIndex === -1)
@ -57,10 +62,35 @@ export const FriendsMessengerView: FC<{}> = props =>
existingChatIndex = clonedActiveChats.length - 1;
}
setActiveChatIndex(existingChatIndex);
setSelectedChatIndex(existingChatIndex);
setIsVisible(true);
}, [ activeChats, dispatchFriendsState ]);
const getFriendFigure = useCallback((id: number) =>
{
const friend = friends.find(f => f.id === id);
if(!friend) return null;
return friend.figure;
}, [ friends ]);
const selectedChat = useMemo(() =>
{
return activeChats[selectedChatIndex];
}, [ activeChats, selectedChatIndex ]);
const selectedChatFriend = useMemo(() =>
{
if(!selectedChat) return null;
const friend = friends.find(f => f.id === selectedChat.friendId);
if(!friend) return null;
return friend;
}, [ friends, selectedChat ]);
useEffect(() =>
{
const linkTracker: ILinkEventTracker = {
@ -73,12 +103,99 @@ export const FriendsMessengerView: FC<{}> = props =>
return () => RemoveLinkEventTracker(linkTracker);
}, [ linkReceived ]);
useEffect(() =>
{
if(!messagesBox || !messagesBox.current) return;
messagesBox.current.scrollTop = messagesBox.current.scrollHeight;
}, [ selectedChat ]);
const followFriend = useCallback(() =>
{
SendMessageHook(new FollowFriendMessageComposer(selectedChatFriend.id));
}, [ selectedChatFriend ]);
const openProfile = useCallback(() =>
{
SendMessageHook(new UserProfileComposer(selectedChatFriend.id));
}, [ selectedChatFriend ]);
const sendMessage = useCallback(() =>
{
if(message.length === 0) return;
SendMessageHook(new SendMessageComposer(selectedChat.friendId, message));
dispatchFriendsState({
type: FriendsActions.ADD_CHAT_MESSAGE,
payload: {
chatMessage: new MessengerChatMessage(MessengerChatMessage.MESSAGE, 0, message, (new Date().getMilliseconds())),
numberValue: selectedChat.friendId
}
});
setMessage('');
}, [ selectedChat, message, dispatchFriendsState ]);
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) =>
{
if(event.key !== 'Enter') return;
sendMessage();
}, [ sendMessage ]);
if(!isVisible) return null;
return (<NitroCardView className="nitro-friends-messenger" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('friendlist.friends') } onCloseClick={ () => {} } />
<NitroCardContentView className="p-0">
<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 <div key={ index } className="friend-head bg-muted rounded flex-shrink-0 cursor-pointer" onClick={ () => setSelectedChatIndex(index) }>
<AvatarImageView figure={ getFriendFigure(chat.friendId) } 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 className="d-flex gap-2 mb-2">
<div className="btn-group">
<button className="btn btn-sm btn-primary" onClick={ followFriend }>
<i className="icon icon-friendlist-follow" />
</button>
<button className="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>
</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.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>;
}) }
</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 } />
<button className="btn btn-sm btn-success" onClick={ sendMessage }>{ LocalizeText('widgets.chatinput.say') }</button>
</div>
</> }
</NitroCardContentView>
</NitroCardView>);
};