Skip to content

Commit

Permalink
More new components
Browse files Browse the repository at this point in the history
  • Loading branch information
arnautov-anton committed Jun 15, 2024
1 parent ffc1389 commit cb06b65
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 100 deletions.
57 changes: 23 additions & 34 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Thread as ThreadType, ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import React from 'react';
import { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import {
Channel,
ChannelHeader,
Expand All @@ -11,7 +11,7 @@ import {
Window,
useCreateChatClient,
ThreadList,
ThreadProvider,
Views,
} from 'stream-chat-react';
import '@stream-io/stream-chat-css/dist/v2/css/index.css';

Expand Down Expand Up @@ -52,8 +52,6 @@ type StreamChatGenerics = {
userType: LocalUserType;
};

const threadOnly = userId !== 'john';

const App = () => {
const chatClient = useCreateChatClient<StreamChatGenerics>({
apiKey,
Expand All @@ -65,37 +63,28 @@ const App = () => {

return (
<Chat client={chatClient}>
<div className='str-chat'>
{!threadOnly && (
<>
<ChannelList filters={filters} options={options} sort={sort} />
<Channel>
<Window>
<ChannelHeader />
<MessageList returnAllReadData />
<MessageInput focus />
</Window>
<Thread virtualized />
</Channel>
</>
)}
{threadOnly && <Threads />}
</div>
<Views>
<Views.Selector />
<Views.Channel>
<ChannelList filters={filters} options={options} sort={sort} />
<Channel>
<Window>
<ChannelHeader />
<MessageList returnAllReadData />
<MessageInput focus />
</Window>
<Thread virtualized />
</Channel>
</Views.Channel>
<Views.Threads>
<ThreadList />
<Views.ThreadAdapter>
<Thread virtualized />
</Views.ThreadAdapter>
</Views.Threads>
</Views>
</Chat>
);
};

const Threads = () => {
const [state, setState] = useState<ThreadType | undefined>(undefined);

return (
<div className='str-chat__threads'>
<ThreadList threadListItemProps={{ onPointerDown: (_, t) => setState(t) }} />
<ThreadProvider thread={state}>
<Thread virtualized />
</ThreadProvider>
</div>
);
};

export default App;
23 changes: 15 additions & 8 deletions examples/vite/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ body,
// position: initial;
// z-index: 0;
//}

.str-chat__thread {
width: 45%;
position: initial;
z-index: 0;
}
Expand All @@ -107,16 +106,24 @@ body,
}
}

.str-chat__threads {
.str-chat__views {
display: flex;
width: 100%;
height: 100%;

.str-chat__thread {
width: 100%;
&__selector {
display: flex;
flex-direction: column;
list-style: none;
}

.str-chat__thread-list {
height: 100%;
max-width: 420px;
&__channel {
display: flex;
flex-grow: 1;
}

&__threads {
display: flex;
flex-grow: 1;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
"react-dom": "^18.1.0",
"react-test-renderer": "^18.1.0",
"semantic-release": "^19.0.5",
"stream-chat": "^8.30.0",
"stream-chat": "link:../stream-chat-js/",
"ts-jest": "^29.1.4",
"typescript": "^5.4.5"
},
Expand Down
5 changes: 1 addition & 4 deletions src/components/Threads/ThreadContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ export const useThreadContext = () => {
const { client } = useChatContext();
const thread = useContext(ThreadContext);

const placeholder = useMemo(
() => new Thread({ client, registerSubscriptions: false, threadData: {} }),
[client],
);
const placeholder = useMemo(() => new Thread({ client, threadData: {} }), [client]);

if (!thread) return placeholder;

Expand Down
25 changes: 8 additions & 17 deletions src/components/Threads/ThreadList/ThreadList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useEffect } from 'react';
import { ComputeItemKey, Virtuoso, VirtuosoProps } from 'react-virtuoso';

import type { ComponentType, PointerEvent } from 'react';
import type { ComponentType } from 'react';
import type { InferStoreValueType, Thread, ThreadManager } from 'stream-chat';

import { ThreadListItem } from './ThreadListItem';
import { ThreadListItem, ThreadListItemUi } from './ThreadListItem';
import { Icon } from '../icons';
import { useChatContext } from '../../../context';
import { useSimpleStateStore } from '../hooks/useSimpleStateStore';

import type { ThreadListItemProps } from './ThreadListItem';
import type { ThreadListItemProps, ThreadListItemUiProps } from './ThreadListItem';

/**
* TODO:
Expand All @@ -31,16 +31,15 @@ const computeItemKey: ComputeItemKey<Thread, unknown> = (_, item) => item.id;

type ThreadListProps = {
ThreadListItem?: ComponentType<ThreadListItemProps>;
threadListItemProps?: Omit<ThreadListItemProps, 'thread' | 'onPointerDown'> & {
onPointerDown?: (event: PointerEvent<HTMLButtonElement>, thread: Thread) => void;
};
ThreadListItemUi?: ComponentType<ThreadListItemUiProps>;
virtuosoProps?: VirtuosoProps<Thread, unknown>;
};

export const ThreadList = ({
// FIXME: temporary, should be removed when ComponentOverride component is out
ThreadListItemUi: PropsThreadListItemUi = ThreadListItemUi,
ThreadListItem: PropsThreadListItem = ThreadListItem,
virtuosoProps,
threadListItemProps: { onPointerDown, ...restThreadListItemProps } = {},
}: ThreadListProps) => {
const { client } = useChatContext();
const [unreadThreadIds, threads] = useSimpleStateStore(client.threads.state, selector);
Expand All @@ -51,11 +50,7 @@ export const ThreadList = ({

return (
<div className='str-chat__thread-list-container'>
{/* TODO: create a replaceable banner component, wait for BE to support "in" keyword for query threads */}
{/* TODO: use query threads with limit (unreadThreadsId.length) - should be top of the list, and prepend
- this does not work when we reply to an non-loaded thread and then reply to a loaded thread
- querying afterwards will return only the latest, which was already in the list but not the one we need
*/}
{/* TODO: create a replaceable banner component */}
{unreadThreadIds.length > 0 && (
<div className='str-chat__unread-threads-banner'>
{unreadThreadIds.length} unread threads
Expand All @@ -73,11 +68,7 @@ export const ThreadList = ({
computeItemKey={computeItemKey}
data={threads}
itemContent={(_, thread) => (
<PropsThreadListItem
onPointerDown={(e) => onPointerDown?.(e, thread)}
thread={thread}
{...restThreadListItemProps}
/>
<PropsThreadListItem thread={thread} ThreadListItemUi={PropsThreadListItemUi} />
)}
{...virtuosoProps}
/>
Expand Down
88 changes: 66 additions & 22 deletions src/components/Threads/ThreadList/ThreadListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import React from 'react';
import React, { createContext, useContext } from 'react';

import type { ComponentPropsWithoutRef, ComponentType } from 'react';
import type { InferStoreValueType, Thread } from 'stream-chat';
import { InferStoreValueType, Thread } from 'stream-chat';

import { useSimpleStateStore } from '../hooks/useSimpleStateStore';
import { Avatar } from '../../Avatar';
import { Icon } from '../icons';
import { useChatContext } from '../../../context';
import { useThreadsViewContext } from '../../Views';
import clsx from 'clsx';

export type ThreadListItemProps = {
thread: Thread;
ThreadListItemUi?: ComponentType<ThreadListItemUiProps>;
} & ComponentPropsWithoutRef<'button'>;
};

export type ThreadListItemUiProps = Omit<ThreadListItemProps, 'ThreadListItemUi'>;
export type ThreadListItemUiProps = ComponentPropsWithoutRef<'button'>;

// Bl - business logic
// Ui - user interface

/**
* TODO:
* - replace 💬 with proper icon
* - add selected class name "str-chat__thread-list-item--selected"
* - maybe hover state? ask design
* - move styling to CSS library and clean it up (separate layout and theme)
* - figure out why some data is unavailable/adjust types accordingly
* - use Moment/DayJs for proper created_at formatting (replace toLocaleTimeString)
* - handle deleted message
* - handle deleted message [in progress]
* - handle markRead when loading a thread
*/

Expand All @@ -36,20 +37,32 @@ const selector = (nextValue: InferStoreValueType<Thread>) =>
nextValue.read,
nextValue.parentMessage,
nextValue.channelData,
nextValue.deletedAt,
] as const;

export const ThreadListItemUi = ({ thread, ...rest }: ThreadListItemUiProps) => {
export const ThreadListItemUi = (props: ThreadListItemUiProps) => {
const { client } = useChatContext();
const [latestReply, read, parentMessage, channelData] = useSimpleStateStore(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const thread = useThreadListItemContext()!;
const [latestReply, read, parentMessage, channelData, deletedAt] = useSimpleStateStore(
thread.state,
selector,
);

const { activeThread, setActiveThread } = useThreadsViewContext();

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const unreadMessagesCount = read[client.user!.id]?.unread_messages ?? 0;
const avatarProps = deletedAt ? null : latestReply?.user;

return (
<button className='str-chat__thread-list-item' {...rest}>
<button
className={clsx('str-chat__thread-list-item', {
'str-chat__thread-list-item--active': activeThread === thread,
})}
onPointerDown={() => setActiveThread(thread)}
{...props}
>
<div className='str-chat__thread-list-item__channel'>
<Icon.MessageBubble className='str-chat__thread-list-item__channel-icon' />
<div className='str-chat__thread-list-item__channel-text'>{channelData?.name || 'N/A'}</div>
Expand All @@ -58,27 +71,28 @@ export const ThreadListItemUi = ({ thread, ...rest }: ThreadListItemUiProps) =>
<div className='str-chat__thread-list-item__parent-message-text'>
replied to: {parentMessage?.text || 'Unknown message'}
</div>
{unreadMessagesCount > 0 && (
{unreadMessagesCount > 0 && !deletedAt && (
<div className='str-chat__thread-list-item__parent-message-unread-count'>
{unreadMessagesCount}
</div>
)}
</div>
<div className='str-chat__thread-list-item__latest-reply'>
<Avatar
image={latestReply?.user?.image as string | undefined}
name={latestReply?.user?.name}
/>
<Avatar {...avatarProps} />
<div className='str-chat__thread-list-item__latest-reply-details'>
<div className='str-chat__thread-list-item__latest-reply-created-by'>
{latestReply?.user?.name || latestReply?.user?.id || 'Unknown sender'}
</div>
{!deletedAt && (
<div className='str-chat__thread-list-item__latest-reply-created-by'>
{latestReply?.user?.name || latestReply?.user?.id || 'Unknown sender'}
</div>
)}
<div className='str-chat__thread-list-item__latest-reply-text-and-timestamp'>
<div className='str-chat__thread-list-item__latest-reply-text'>
{latestReply?.text || 'N/A'}
{deletedAt ? 'This thread has been deleted' : latestReply?.text || 'N/A'}
</div>
<div className='str-chat__thread-list-item__latest-reply-timestamp'>
{latestReply?.created_at.toLocaleTimeString() || 'N/A'}
{deletedAt
? deletedAt.toLocaleTimeString()
: latestReply?.created_at.toLocaleTimeString() || 'N/A'}
</div>
</div>
</div>
Expand All @@ -87,13 +101,20 @@ export const ThreadListItemUi = ({ thread, ...rest }: ThreadListItemUiProps) =>
);
};

// could be context provider? (to be able to pass down Thread instance and simplify SimpleStore subbing)
const ThreadListItemContext = createContext<Thread | undefined>(undefined);

export const useThreadListItemContext = () => useContext(ThreadListItemContext);

export const ThreadListItem = (props: ThreadListItemProps) => {
const { ThreadListItemUi: PropsThreadListItemUi = ThreadListItemUi, ...rest } = props;
const { ThreadListItemUi: PropsThreadListItemUi = ThreadListItemUi, thread } = props;

// useThreadListItemBl();

return <PropsThreadListItemUi {...rest} />;
return (
<ThreadListItemContext.Provider value={thread}>
<PropsThreadListItemUi />
</ThreadListItemContext.Provider>
);
};

// const App = () => {
Expand All @@ -118,3 +139,26 @@ export const ThreadListItem = (props: ThreadListItemProps) => {
// </Chat>
// );
// };

// pre-built layout

{
/*
<Chat client={chatClient}>
<Views>
// has default
<ViewSelector onItemPointerDown={} />
<View.Chat>
<Channel>
<MessageList />
<MessageInput />
</Channel>
</View.Chat>
<View.Thread> <-- activeThread state
<ThreadList /> <-- uses context for click handler
<WrappedThread /> <-- ThreadProvider + Channel combo
</View.Thread>
</Views>
</Chat>;
*/
}
Loading

0 comments on commit cb06b65

Please sign in to comment.