Skip to content

Commit

Permalink
feat(messenger-feed): wire up send and fetch posts actions to ui
Browse files Browse the repository at this point in the history
  • Loading branch information
domw30 committed Aug 21, 2024
1 parent de7a708 commit 37b17f7
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 36 deletions.
10 changes: 7 additions & 3 deletions src/components/chat-view-container/chat-view-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,13 @@ export class Container extends React.Component<Properties> {
};

get messages() {
const messagesById = mapMessagesById(this.channel?.messages || []);
const messagesByRootId = mapMessagesByRootId(this.channel?.messages || []);
const messages = linkMessages(this.channel?.messages || [], messagesById, messagesByRootId);
const allMessages = this.channel?.messages || [];

const chatMessages = allMessages.filter((message) => !message.isPost);

const messagesById = mapMessagesById(chatMessages);
const messagesByRootId = mapMessagesByRootId(chatMessages);
const messages = linkMessages(chatMessages, messagesById, messagesByRootId);

return messages.sort((a, b) => compareDatesAsc(a.createdAt, b.createdAt));
}
Expand Down
36 changes: 27 additions & 9 deletions src/components/messenger/feed/components/create-post/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,52 @@ import { useEffect, useRef, useState } from 'react';
import { Avatar, Button, IconButton } from '@zero-tech/zui/components';
import { IconCamera1, IconPlus, IconMicrophone2 } from '@zero-tech/zui/icons';

import { Key } from '../../../../../lib/keyboard-search';

import styles from './styles.module.scss';

export const CreatePost = () => {
interface CreatePostProps {
isSubmitting?: boolean;

onSubmit: (message: string) => void;
}

export const CreatePost = ({ isSubmitting, onSubmit }: CreatePostProps) => {
const [value, setValue] = useState('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const isDisabled = !value.trim() || isSubmitting;

const handleOnChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setValue(event.target.value);
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!event.shiftKey && event.key === Key.Enter && value.trim()) {
event.preventDefault();
onSubmit(value);
setValue('');
}
};

const handleOnSubmit = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 2000);
if (value.trim()) {
onSubmit(value);
setValue('');
}
};

return (
<div className={styles.Container}>
<Avatar size={'regular'} />
<div className={styles.Create}>
<PostInput value={value} onChange={handleOnChange} />
<PostInput value={value} onChange={handleOnChange} onKeyDown={handleKeyDown} />
<hr />
<div className={styles.Actions}>
<div className={styles.Media}>
<IconButton Icon={IconPlus} onClick={() => {}} />
<IconButton Icon={IconCamera1} onClick={() => {}} />
<IconButton Icon={IconMicrophone2} onClick={() => {}} />
</div>
<Button isDisabled={!value} isLoading={isLoading} className={styles.Button} onPress={handleOnSubmit}>
<Button isDisabled={isDisabled} isLoading={isSubmitting} className={styles.Button} onPress={handleOnSubmit}>
Create
</Button>
</div>
Expand All @@ -43,10 +59,11 @@ export const CreatePost = () => {

interface PostInputProps {
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
value: string;
}

const PostInput = ({ onChange, value }: PostInputProps) => {
const PostInput = ({ onChange, onKeyDown, value }: PostInputProps) => {
const ref = useRef<HTMLTextAreaElement>(null);

useEffect(() => {
Expand All @@ -66,6 +83,7 @@ const PostInput = ({ onChange, value }: PostInputProps) => {
<textarea
className={styles.Input}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder='Write a post'
ref={ref}
rows={2}
Expand Down
16 changes: 9 additions & 7 deletions src/components/messenger/feed/components/post/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import { Action, Name, Post as ZUIPost } from '@zero-tech/zui/components/Post';
import { Timestamp } from '@zero-tech/zui/components/Post/components/Timestamp';
import type { Post as PostType } from '../../lib/types';
import { IconMessageSquare2, IconShare7 } from '@zero-tech/zui/icons';
import { Avatar } from '@zero-tech/zui/components';

import styles from './styles.module.scss';

export interface PostProps {
post: PostType;
timestamp: number;
author?: string;
nickname: string;
text: string;
}

export const Post = ({ post }: PostProps) => {
export const Post = ({ text, nickname, author, timestamp }: PostProps) => {
return (
<div className={styles.Container}>
<div>
<Avatar size='regular' />
</div>
<ZUIPost
className={styles.Post}
body={post.text}
body={text}
details={
<>
{/* @ts-ignore */}
<Name variant='name'>{post.nickname}</Name>
<Name variant='name'>{nickname}</Name>
{/* @ts-ignore */}
<Name variant='username'>{post.author}</Name>
<Name variant='username'>{author}</Name>
</>
}
options={<Timestamp timestamp={post.timestamp} />}
options={<Timestamp timestamp={timestamp} />}
actions={
<>
<Action>
Expand Down
7 changes: 3 additions & 4 deletions src/components/messenger/feed/components/posts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Post } from '../post';
import { MOCK_POSTS } from '../../mock';

import styles from './styles.module.scss';

export const Posts = () => {
export const Posts = ({ postMessages }) => {
return (
<ol className={styles.Posts}>
{MOCK_POSTS.map((post) => (
{postMessages.map((post) => (
<li key={post.id}>
<Post post={post} />
<Post text={post.message} nickname={post.sender.firstName} timestamp={post.createdAt} />
</li>
))}
</ol>
Expand Down
108 changes: 95 additions & 13 deletions src/components/messenger/feed/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React from 'react';
import { RootState } from '../../../store/reducer';
import { connectContainer } from '../../../store/redux-container';
import { rawChannelSelector } from '../../../store/channels/saga';
import { Posts } from './components/posts';
import { ScrollbarContainer } from '../../scrollbar-container';
import { CreatePost } from './components/create-post';
import { PostPayload as PayloadPostMessage, Payload as PayloadFetchPosts } from '../../../store/posts/saga';
import { Channel, denormalize } from '../../../store/channels';
import { MessageSendStatus } from '../../../store/messages';
import { fetchPosts, sendPost } from '../../../store/posts';
import { AuthenticationState } from '../../../store/authentication/types';
import { Waypoint } from 'react-waypoint';

import { bemClassName } from '../../../lib/bem';
import './styles.scss';
Expand All @@ -14,39 +19,116 @@ const cn = bemClassName('messenger-feed');
export interface PublicProperties {}

export interface Properties extends PublicProperties {
user: AuthenticationState['user'];
channel: Channel;
activeConversationId: string;
isSocialChannel: boolean;
isJoiningConversation: boolean;

sendPost: (payload: PayloadPostMessage) => void;
fetchPosts: (payload: PayloadFetchPosts) => void;
}

export class Container extends React.Component<Properties> {
static mapState(state: RootState): Partial<Properties> {
const {
chat: { activeConversationId },
authentication: { user },
chat: { activeConversationId, isJoiningConversation },
} = state;

const currentChannel = rawChannelSelector(activeConversationId)(state);
const currentChannel = denormalize(activeConversationId, state) || null;

return { isSocialChannel: currentChannel?.isSocialChannel };
return {
user,
channel: currentChannel,
isJoiningConversation,
activeConversationId,
isSocialChannel: currentChannel?.isSocialChannel,
};
}

static mapActions(): Partial<Properties> {
return {};
return {
sendPost,
fetchPosts,
};
}

componentDidMount() {
const { activeConversationId, channel } = this.props;
if (activeConversationId && !channel.hasLoadedMessages) {
this.props.fetchPosts({ channelId: activeConversationId });
}
}

componentDidUpdate(prevProps: Properties) {
const { activeConversationId } = this.props;

if (activeConversationId && activeConversationId !== prevProps.activeConversationId) {
this.props.fetchPosts({ channelId: activeConversationId });
}

if (activeConversationId && prevProps.user.data === null && this.props.user.data !== null) {
this.props.fetchPosts({ channelId: activeConversationId });
}
}

getOldestTimestamp(messages = []) {
return messages.reduce((previousTimestamp, message) => {
return message.createdAt < previousTimestamp ? message.createdAt : previousTimestamp;
}, Date.now());
}

fetchMorePosts = () => {
const { activeConversationId, channel } = this.props;

if (channel.hasMorePosts) {
const referenceTimestamp = this.getOldestTimestamp(this.postMessages);
this.props.fetchPosts({ channelId: activeConversationId, referenceTimestamp });
}
};

submitPost = (message: string) => {
const { activeConversationId } = this.props;

let payloadPostMessage = {
channelId: activeConversationId,
message: message,
};

this.props.sendPost(payloadPostMessage);
};

get postMessages() {
const messages = this.props.channel?.messages || [];
return messages.filter((message) => message.isPost && message.sendStatus === MessageSendStatus.SUCCESS).reverse();
}

get isSubmitting() {
return this.props.channel?.messages.some((message) => message.sendStatus === MessageSendStatus.IN_PROGRESS);
}

render() {
const { isSocialChannel } = this.props;
const { channel, isJoiningConversation, activeConversationId, isSocialChannel } = this.props;

if (!isSocialChannel) {
if (!activeConversationId || !isSocialChannel || isJoiningConversation) {
return null;
}

return (
<>
<ScrollbarContainer>
<div {...cn('')}>
<CreatePost />
<Posts />
</div>
</ScrollbarContainer>
<div {...cn('')}>
<ScrollbarContainer>
<CreatePost onSubmit={this.submitPost} isSubmitting={this.isSubmitting} />
{channel.hasLoadedMessages && (
<>
<Posts postMessages={this.postMessages} />
<Waypoint onEnter={this.fetchMorePosts} />
</>
)}
</ScrollbarContainer>
</div>

<div {...cn('divider')} />
</>
);
Expand Down
22 changes: 22 additions & 0 deletions src/lib/chat/chat-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,28 @@ describe(getMessagePreview, () => {

expect(preview).toEqual('You: Failed to send');
});

it('returns post preview for isPost messages', function () {
const state = new StoreBuilder().withCurrentUser({ id: 'current-user' }).build();

const preview = getMessagePreview(
{ message: '', isAdmin: false, isPost: true, sender: { userId: 'current-user' } } as Message,
state
);

expect(preview).toEqual('You: shared a new post');
});

it('returns post preview with sender firstName for non-current user', function () {
const state = new StoreBuilder().withCurrentUser({ id: 'current-user' }).build();

const preview = getMessagePreview(
{ message: '', isAdmin: false, isPost: true, sender: { userId: 'another-user', firstName: 'Jack' } } as Message,
state
);

expect(preview).toEqual('Jack: shared a new post');
});
});

describe(previewDisplayDate, () => {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/chat/chat-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export function getMessagePreview(message: Message, state: RootState) {
return adminMessageText(message, state);
}

if (message.isPost) {
let prefix = previewPrefix(message.sender, state);
return `${prefix}: shared a new post`;
}

let prefix = previewPrefix(message.sender, state);
return `${prefix}: ${message.message || getMediaPreview(message)}`;
}
Expand Down

0 comments on commit 37b17f7

Please sign in to comment.