Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Direct message chat #334

Merged
merged 9 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
684 changes: 442 additions & 242 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@zero-tech/zapp-daos": "^0.4.0",
"@zero-tech/zapp-nfts": "^0.7.5",
"@zero-tech/zapp-staking": "0.4.8",
"@zero-tech/zui": "^0.12.0",
"classnames": "^2.3.1",
"es6-promise-debounce": "^1.0.1",
"ethers": "^5.5.2",
Expand Down
10 changes: 10 additions & 0 deletions src/Main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { WalletManager } from './components/wallet-manager';
import { ThemeEngine } from './components/theme-engine';
import { ViewModeToggle } from './components/view-mode-toggle';
import { AddressBarContainer } from './components/address-bar/container';
import { DirectMessageChat } from './platform-apps/channels/direct-message-chat';

describe('Main', () => {
const subject = (props: Partial<Properties> = {}) => {
const allProps = {
hasContextPanel: false,
isContextPanelOpen: false,
context: {
isAuthenticated: false,
},
...props,
};

Expand Down Expand Up @@ -66,6 +70,12 @@ describe('Main', () => {
expect(wrapper.find('.main').hasClass('context-panel-open')).toBe(true);
});

it('renders direct message chat component', () => {
const wrapper = subject({ context: { isAuthenticated: true } });

expect(wrapper.find(DirectMessageChat).exists()).toBe(true);
});

describe('mapState', () => {
const subject = (state: any) =>
Main.mapState({
Expand Down
3 changes: 3 additions & 0 deletions src/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import './main.scss';
import classNames from 'classnames';
import { Sidekick } from './components/sidekick/index';
import { withContext as withAuthenticationContext } from './components/authentication/context';
import { DirectMessageChat } from './platform-apps/channels/direct-message-chat';

export interface Properties {
hasContextPanel: boolean;
Expand Down Expand Up @@ -67,6 +68,8 @@ export class Container extends React.Component<Properties> {
</div>
<Sidekick className='main__sidekick' />
<ThemeEngine />

{this.props.context.isAuthenticated && <DirectMessageChat />}
</div>
);
}
Expand Down
28 changes: 28 additions & 0 deletions src/components/sidekick/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { shallow } from 'enzyme';

import { Container } from '.';
import { IfAuthenticated } from '../authentication/if-authenticated';
import { DirectMessageMembers } from '../../platform-apps/channels/direct-message-members';

describe('Sidekick', () => {
const subject = (props: any = {}) => {
Expand Down Expand Up @@ -40,4 +41,31 @@ describe('Sidekick', () => {

expect(ifAuthenticated.find('.sidekick__slide-out').exists()).toBe(false);
});

it('renders default active tab', () => {
const wrapper = subject();

expect(wrapper.find(DirectMessageMembers).exists()).toBe(true);
});

it('handle network tab content', () => {
const wrapper = subject();
wrapper.find('.sidekick__tabs-network').simulate('click');

expect(wrapper.find('.sidekick__tab-content--network').exists()).toBe(true);
});

it('handle messages tab content', () => {
const wrapper = subject();
wrapper.find('.sidekick__tabs-messages').simulate('click');

expect(wrapper.find('.sidekick__tab-content--messages').exists()).toBe(true);
});

it('handle notifications tab content', () => {
const wrapper = subject();
wrapper.find('.sidekick__tabs-notifications').simulate('click');

expect(wrapper.find('.sidekick__tab-content--notifications').exists()).toBe(true);
});
});
87 changes: 60 additions & 27 deletions src/components/sidekick/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';
import { RootState } from '../../store';
import { connectContainer } from '../../store/redux-container';

import { IfAuthenticated } from '../authentication/if-authenticated';
import { IconButton, Icons } from '@zer0-os/zos-component-library';
import classNames from 'classnames';
import { AuthenticationState } from '../../store/authentication/types';
import { AppLayout, update as updateLayout } from '../../store/layout';
import { DirectMessageMembers } from '../../platform-apps/channels/direct-message-members';

import './styles.scss';

require('./styles.scss');
enum Tabs {
NETWORK,
MESSAGES,
NOTIFICATIONS,
}

interface PublicProperties {
className?: string;
Expand All @@ -21,9 +27,12 @@ export interface Properties extends PublicProperties {

export interface State {
isOpen: boolean;
activeTab: Tabs;
}

export class Container extends React.Component<Properties, State> {
state = { isOpen: true, activeTab: Tabs.MESSAGES };

static mapState(state: RootState): Partial<Properties> {
const {
authentication: { user },
Expand All @@ -38,22 +47,24 @@ export class Container extends React.Component<Properties, State> {
return { updateLayout };
}

state = { isOpen: true };

slideAnimationEnded = () => {
slideAnimationEnded = (): void => {
if (!this.state.isOpen) {
this.setState({ isOpen: false });
}
};

clickTab = () => {};
clickTab(tab: Tabs): void {
this.setState({
activeTab: tab,
});
}

handleSidekickPanel = () => {
handleSidekickPanel = (): void => {
this.props.updateLayout({ isSidekickOpen: !this.state.isOpen });
this.setState({ isOpen: !this.state.isOpen });
};

renderSidekickPanel() {
renderSidekickPanel(): JSX.Element {
return (
<div
className='app-sidekick-panel__target'
Expand All @@ -76,6 +87,45 @@ export class Container extends React.Component<Properties, State> {
);
}

renderTabs(): JSX.Element {
return (
<div className='sidekick__tabs'>
<IconButton
className='sidekick__tabs-network'
icon={Icons.Network}
onClick={this.clickTab.bind(this, Tabs.NETWORK)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we changed our clickTab method above can we get rid of the bind?

 clickTab = (tab: Tabs) =>
onClick={this.clickTab(Tabs.NETWORK)}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I remove the .bind and keep onClick={this.clickTab(Tabs.NETWORK)} it will call the method on each render and to solve it I have to do onClick={() => this.clickTab(Tabs.NETWORK)} which it will create an anonymous function on each render which do not want.

Copy link
Contributor

@dalefukami dalefukami Jan 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...it will create an anonymous function on each render which do not want.

Just as an FYI, bind is creating an entirely new function on every render too. So, using bind isn't really a solution to the problem you've mentioned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, I was wrong about it, it's also written in react documentation.

@dalefukami for learning purposes..., do you have an idea how we can make it better in performance ?

Copy link
Contributor

@dalefukami dalefukami Jan 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of strategies that I tend towards:

  1. If the list is fairly short and static (like this case) I'll just write 3 separate functions on the class. this.clickNetworkTab: () => this.clickTab(Tabs.NETWORK). They can all call the general case and they're super short so it's pretty obvious that they're just shortcut methods for this purpose.
  2. If the list is dynamic then the main strategy is to cache the functions as they're created the first time. I saw this used in zero-web and it's pretty similar to what I've done previously.
  3. If the child component is a specific thing that is always supposed to render something of a "type" or "id"...then I'll usually make the child component call the onClick with the appropriate value. In this case, it probably doesn't make sense to use this strategy.

/>
<IconButton
className='sidekick__tabs-messages'
icon={Icons.Messages}
onClick={this.clickTab.bind(this, Tabs.MESSAGES)}
/>
<IconButton
className='sidekick__tabs-notifications'
icon={Icons.Notifications}
onClick={this.clickTab.bind(this, Tabs.NOTIFICATIONS)}
/>
</div>
);
}

renderTabContent(): JSX.Element {
switch (this.state.activeTab) {
case Tabs.NETWORK:
return <div className='sidekick__tab-content--network'>NETWORK</div>;
case Tabs.MESSAGES:
return (
<div className='sidekick__tab-content--messages'>
<DirectMessageMembers />
</div>
);
case Tabs.NOTIFICATIONS:
return <div className='sidekick__tab-content--notifications'>NOTIFICATIONS</div>;
default:
return null;
}
}

render() {
return (
<IfAuthenticated showChildren>
Expand All @@ -84,25 +134,8 @@ export class Container extends React.Component<Properties, State> {
onAnimationEnd={this.slideAnimationEnded}
>
{this.renderSidekickPanel()}
<div className='sidekick-panel'>
<div className='sidekick__tabs'>
<IconButton
className='sidekick__tabs-network'
icon={Icons.Network}
onClick={this.clickTab}
/>
<IconButton
className='sidekick__tabs-messages'
icon={Icons.Messages}
onClick={this.clickTab}
/>
<IconButton
className='sidekick__tabs-notifications'
icon={Icons.Notifications}
onClick={this.clickTab}
/>
</div>
</div>
<div className='sidekick-panel'>{this.renderTabs()}</div>
<div className='sidekick__tab-content'>{this.renderTabContent()}</div>
</div>
</IfAuthenticated>
);
Expand Down
5 changes: 5 additions & 0 deletions src/components/sidekick/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
}
}
}

&__tab-content {
width: 100%;
padding-top: 56px;
}
}

.app-sidekick-panel__target {
Expand Down
7 changes: 7 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,10 @@ hr.glow:before {
* {
@include scrollbar();
}

.button-reset {
cursor: pointer;
background: none;
border: none;
display: inline-block;
}
18 changes: 18 additions & 0 deletions src/platform-apps/channels/channel-view-container.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ describe('ChannelViewContainer', () => {
expect(wrapper.find(ChannelView).prop('name')).toStrictEqual('first channel');
});

it('passes hasJoined to channel view', () => {
const wrapper = subject({ channel: { hasJoined: true } });

expect(wrapper.find(ChannelView).prop('hasJoined')).toStrictEqual(true);
});

it('passes hasJoined or is direct message to channel view', () => {
const wrapper = subject({ channel: { hasJoined: false }, isDirectMessage: true });

expect(wrapper.find(ChannelView).prop('hasJoined')).toStrictEqual(true);
});

it('passes is direct message to channel view', () => {
const wrapper = subject({ channel: {}, isDirectMessage: true });

expect(wrapper.find(ChannelView).prop('isDirectMessage')).toStrictEqual(true);
});

it('should mark all messages as read when unReadCount > 0', () => {
const markAllMessagesAsReadInChannel = jest.fn();
const messages = [
Expand Down
10 changes: 8 additions & 2 deletions src/platform-apps/channels/channel-view-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface Properties extends PublicProperties {

interface PublicProperties {
channelId: string;
className?: string;
isDirectMessage?: boolean;
}
export interface State {
countNewMessages: number;
Expand Down Expand Up @@ -239,7 +241,10 @@ export class Container extends React.Component<Properties, State> {
<ChatConnect />
</IfAuthenticated>
<ChannelView
className={classNames({ 'channel-view--messages-fetched': this.state.isFirstMessagesFetchDone })}
className={classNames(
{ 'channel-view--messages-fetched': this.state.isFirstMessagesFetchDone },
this.props.className
)}
name={this.channel.name}
messages={this.channel.messages || []}
onFetchMore={this.fetchMore}
Expand All @@ -249,10 +254,11 @@ export class Container extends React.Component<Properties, State> {
sendMessage={this.handleSendMessage}
joinChannel={this.handleJoinChannel}
users={this.channel.users || []}
hasJoined={this.channel.hasJoined || false}
hasJoined={this.channel.hasJoined || this.props.isDirectMessage}
countNewMessages={this.state.countNewMessages}
resetCountNewMessage={this.resetCountNewMessage}
onMessageInputRendered={this.onMessageInputRendered}
isDirectMessage={this.props.isDirectMessage}
/>
</>
);
Expand Down
31 changes: 30 additions & 1 deletion src/platform-apps/channels/channel-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ describe('ChannelView', () => {
expect(wrapper.find(Lightbox).prop('items')).toEqual([imageMedia]);
});

it('does not render', () => {
it('does not render Lightbox', () => {
const messages = [
{
id: 'message-1',
Expand Down Expand Up @@ -258,4 +258,33 @@ describe('ChannelView', () => {
expect(wrapper.find(Lightbox).exists()).toBeFalsy();
});
});

it('does not render channel name in case of a direct message', () => {
const messages = [
{
id: 'message-1',
message: 'image message',
media: { url: 'image.jpg', type: MediaType.Image },
sender: { userId: 1 },
createdAt: 1658776625730,
},
{
id: 'message-2',
message: 'video message',
media: { url: 'video.avi', type: MediaType.Video },
sender: { userId: 1 },
createdAt: 1658776625731,
},
{
id: 'message-3',
message: 'audio message',
media: { url: 'video.mp3', type: MediaType.Audio },
sender: { userId: 1 },
createdAt: 1658776625732,
},
];
const wrapper = subject({ messages, isDirectMessage: true });

expect(wrapper.find('.channel-view__name').exists()).toBeFalsy();
});
});
11 changes: 7 additions & 4 deletions src/platform-apps/channels/channel-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface Properties {
users: UserModel[];
className?: string;
onMessageInputRendered: (ref: RefObject<HTMLTextAreaElement>) => void;
isDirectMessage: boolean;
}

export interface State {
Expand Down Expand Up @@ -187,10 +188,12 @@ export class ChannelView extends React.Component<Properties, State> {
)}
<InvertedScroll className='channel-view__inverted-scroll'>
<div className='channel-view__main'>
<div className='channel-view__name'>
<h1>Welcome to #{this.props.name}</h1>
<span>This is the start of the channel.</span>
</div>
{!this.props.isDirectMessage && (
<div className='channel-view__name'>
<h1>Welcome to #{this.props.name}</h1>
<span>This is the start of the channel.</span>
</div>
)}
{this.props.messages.length > 0 && <Waypoint onEnter={this.props.onFetchMore} />}
{this.props.messages.length > 0 && this.renderMessages()}
<IfAuthenticated showChildren>
Expand Down
Loading