├── .gitignore
├── .storybook
├── main.js
└── preview.js
├── LICENSE
├── README.md
├── example
├── .npmignore
├── index.html
├── index.tsx
├── package.json
└── tsconfig.json
├── package.json
├── pnpm-lock.yaml
├── src
├── components
│ ├── conversation-header
│ │ └── index.tsx
│ ├── conversation-list
│ │ └── index.tsx
│ ├── conversation
│ │ ├── index.tsx
│ │ ├── profile.png
│ │ └── profile.webp
│ ├── loading
│ │ ├── index.css
│ │ └── index.tsx
│ ├── main-container
│ │ └── index.tsx
│ ├── message-container
│ │ └── index.tsx
│ ├── message-header
│ │ └── index.tsx
│ ├── message-input
│ │ └── index.tsx
│ ├── message-list-background
│ │ └── index.tsx
│ ├── message-list
│ │ └── index.tsx
│ ├── message
│ │ ├── borderController.tsx
│ │ ├── incoming-message
│ │ │ ├── index.tsx
│ │ │ ├── profile.png
│ │ │ └── profile.webp
│ │ ├── index.tsx
│ │ ├── media-content
│ │ │ └── index.tsx
│ │ ├── outgoing-message
│ │ │ └── index.tsx
│ │ ├── text-content
│ │ │ └── index.tsx
│ │ └── timestamp
│ │ │ ├── index.tsx
│ │ │ └── loading
│ │ │ ├── index.css
│ │ │ └── index.tsx
│ ├── sidebar
│ │ └── index.tsx
│ └── typing-indicator
│ │ ├── index.css
│ │ └── index.tsx
├── contexts
│ └── MinChatUIContext.tsx
├── hooks
│ ├── useCheckIsMobile.tsx
│ ├── useColorSet.tsx
│ ├── useDetectScrollPosition.tsx
│ └── useTypingListener.tsx
├── index.css
├── index.tsx
├── providers
│ └── MinChatUiProvider.tsx
├── react-app-env.d.ts
├── types
│ ├── ConversationType.tsx
│ ├── MessageType.tsx
│ └── UserType.tsx
└── utils
│ └── date-utils
│ └── index.ts
├── stories
├── Conversation.stories.tsx
├── ConversationHeader.stories.tsx
├── ConversationList.stories.tsx
├── MainContainer.stories.tsx
├── Message.stories.tsx
├── MessageContainer.stories.tsx
├── MessageHeader.stories.tsx
├── MessageInput.stories.tsx
├── MessageList.stories.tsx
├── Sidebar.stories.tsx
├── TypingIndicator.stories.tsx
└── data.tsx
├── test
└── blah.test.tsx
├── tsconfig.json
├── tsdx.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],
3 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
4 | // https://storybook.js.org/docs/react/configure/typescript#mainjs-configuration
5 | typescript: {
6 | check: true, // type-check stories during Storybook build
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | // https://storybook.js.org/docs/react/writing-stories/parameters#global-parameters
2 | export const parameters = {
3 | // https://storybook.js.org/docs/react/essentials/actions#automatically-matching-args
4 | actions: { argTypesRegex: '^on.*' },
5 | };
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Minchat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ## Overview
8 |
9 | Build your own chat UI with React components in just a few minutes using the **React Chat UI Kit from MinChat**. Our open-source toolkit accelerates the development of web chat applications with a flexible and powerful set of components.
10 |
11 | **React Chat UI makes chat UI development faster**
12 |
13 | ### Why Choose MinChat's React Chat UI?
14 |
15 | - **Speed up development**: Quickly integrate chat functionality into your app.
16 | - **Customizable components**: Tailor the UI to fit your needs.
17 | - **Open Source**: Benefit from community-driven improvements.
18 |
19 | # Demo
20 |
21 | [View Live Demo](https://minchat.io/demo)
22 | If you would like to see the React chat UI in action, you can visit the [live demo](https://minchat.io/demo).
23 | This demo allows you to test out the various features of the react chat components and see how it can be
24 | integrated into a real-world application. We encourage you to give it a try and see for yourself the power
25 | and flexibility of our chat UI.
26 |
27 | # Documentation
28 |
29 | You can view detailed documentation [here](https://react.minchat.io)
30 |
31 | # Install
32 |
33 | Install the component library using your preferred package manager:
34 |
35 | **Using npm.**
36 |
37 | ```bash
38 | npm install @minchat/react-chat-ui
39 | ```
40 |
41 | **Using yarn.**
42 |
43 | ```bash
44 | yarn add @minchat/react-chat-ui
45 | ```
46 |
47 | # Usage
48 |
49 | Here's a quick example to get you started:
50 |
51 | ```jsx
52 | import {
53 | MinChatUiProvider,
54 | MainContainer,
55 | MessageInput,
56 | MessageContainer,
57 | MessageList,
58 | MessageHeader
59 | } from "@minchat/react-chat-ui";
60 | function App() {
61 | return (
62 |
63 |
64 |
65 |
66 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 |
83 | export default App
84 |
85 |
86 | ```
87 |
88 | # Modify Component Colors
89 |
90 | You can modify the colors of each and every component, by passing a colorSet prop to the `MinChatUiProvider` which defines the colors to use. ommited colors will use the default theme.
91 |
92 | ```jsx
93 | const myColorSet = {
94 | // input
95 | "--input-background-color": "#FF0000",
96 | "--input-text-color": "#fff",
97 | "--input-element-color": "rgb(0, 0, 255)",
98 | "--input-attach-color": "#fff",
99 | "--input-send-color": "#fff",
100 | "--input-placeholder-color": "rgb(255, 255, 255)",
101 |
102 | // message header
103 | "--message-header-background-color": "#FF0000",
104 | "--message-header-text-color": "#fff",
105 | "--message-header-last-active-color": "rgb(0, 0, 255)",
106 | "--message-header-back-color": "rgb(255, 255, 255)",
107 |
108 | // chat list header
109 | "--chatlist-header-background-color": "#FF0000",
110 | "--chatlist-header-text-color": "rgb(255, 255, 255)",
111 | "--chatlist-header-divider-color": "rgb(0, 128, 0)",
112 |
113 | //chatlist
114 | "--chatlist-background-color": "rgb(255, 192, 203)",
115 | "--no-conversation-text-color": "rgb(255, 255, 255)",
116 |
117 | //chat item
118 | "--chatitem-background-color": "rgb(0, 0, 255)",
119 | "--chatitem-selected-background-color": "rgb(255, 255, 0)",
120 | "--chatitem-title-text-color": "#FF0000",
121 | "--chatitem-content-text-color": "#FF0000",
122 | "--chatitem-hover-color": "#FF0000",
123 |
124 | //main container
125 | "--container-background-color": "rgb(255, 192, 203)",
126 |
127 | //loader
128 | "--loader-color": "rgb(0, 128, 0)",
129 |
130 | //message list
131 | "--messagelist-background-color": "rgb(0, 0, 255)",
132 | "--no-message-text-color": "rgb(255, 255, 255)",
133 |
134 | // incoming message
135 | "--incoming-message-text-color": "rgb(255, 255, 255)",
136 | "--incoming-message-name-text-color": "rgb(255, 255, 255)",
137 | "--incoming-message-background-color": "rgb(0, 128, 0)",
138 | "--incoming-message-timestamp-color": "rgb(255, 255, 255)",
139 | "--incoming-message-link-color": "#FF0000",
140 |
141 | //outgoing message
142 | "--outgoing-message-text-color": "#FF0000",
143 | "--outgoing-message-background-color": "rgb(255, 255, 0)",
144 | "--outgoing-message-timestamp-color": "#FF0000",
145 | "--outgoing-message-checkmark-color": "#FF0000",
146 | "--outgoing-message-loader-color": "#FF0000",
147 | "--outgoing-message-link-color": "rgb(0, 128, 0)",
148 | }
149 |
150 | function App() {
151 | return (
152 |
155 |
156 | {/** rest of your code*/}
157 |
158 |
159 | )
160 | }
161 | ```
162 |
163 | # Typescript
164 |
165 | Our library is written in TypeScript, offering type safety and easy integration into both TypeScript and JavaScript projects.
166 |
167 | # Show your support
168 | If you love our library, consider starring ⭐ our GitHub repository!
169 |
170 | # Community and support
171 | For articles, tutorials, and a full guide, visit our [website](https://minchat.io/blog). Connect with other developers, share ideas, and get help.
172 |
173 | # Website
174 |
175 | [https://minchat.io](https://minchat.io)
176 |
177 | Unleash the power of seamless chat functionality with MinChat's [React Chat API!](https://minchat.io) Say goodbye to backend worries and hello to effortless integration. Get started today and save months of development time. Build a full-fledged React chat application in just minutes, not months!
178 |
179 | # License
180 |
181 | React Chat UI Kit is licensed under [MIT](https://github.com/MinChatHQ/react-chat-ui/blob/master/LICENSE). Feel free to use it in your projects.
182 |
183 | ---
184 |
185 | Ready to build an amazing chat experience? Visit MinChat.io to get started and unleash the power of seamless chat functionality in your application today!
186 |
--------------------------------------------------------------------------------
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 | import { Thing } from '../.';
5 |
6 | const App = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | ReactDOM.render( , document.getElementById('root'));
15 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0"
12 | },
13 | "alias": {
14 | "react": "../node_modules/react",
15 | "react-dom": "../node_modules/react-dom/profiling",
16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^16.9.11",
20 | "@types/react-dom": "^16.8.4",
21 | "parcel": "^1.12.3",
22 | "typescript": "^3.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "types": ["node"]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.16.2",
3 | "license": "MIT",
4 | "main": "dist/index.js",
5 | "typings": "dist/index.d.ts",
6 | "files": [
7 | "dist",
8 | "src"
9 | ],
10 | "engines": {
11 | "node": ">=10"
12 | },
13 | "scripts": {
14 | "start": "tsdx watch",
15 | "build": "tsdx build",
16 | "test": "tsdx test --passWithNoTests",
17 | "lint": "tsdx lint",
18 | "prepare": "tsdx build",
19 | "size": "size-limit",
20 | "analyze": "size-limit --why",
21 | "storybook": "start-storybook -p 6006",
22 | "build-storybook": "build-storybook"
23 | },
24 | "peerDependencies": {
25 | "react": ">=16"
26 | },
27 | "husky": {
28 | "hooks": {
29 | "pre-commit": "tsdx lint"
30 | }
31 | },
32 | "prettier": {
33 | "printWidth": 80,
34 | "semi": true,
35 | "singleQuote": true,
36 | "trailingComma": "es5"
37 | },
38 | "name": "@minchat/react-chat-ui",
39 | "author": "MinChat",
40 | "module": "dist/react-chat-ui.esm.js",
41 | "size-limit": [
42 | {
43 | "path": "dist/react-chat-ui.cjs.production.min.js",
44 | "limit": "10 KB"
45 | },
46 | {
47 | "path": "dist/react-chat-ui.esm.js",
48 | "limit": "10 KB"
49 | }
50 | ],
51 | "devDependencies": {
52 | "@babel/core": "^7.23.0",
53 | "@rollup/plugin-image": "^3.0.3",
54 | "@size-limit/preset-small-lib": "^8.2.6",
55 | "@storybook/addon-essentials": "^6.5.16",
56 | "@storybook/addon-info": "^5.3.21",
57 | "@storybook/addon-links": "^6.5.16",
58 | "@storybook/addons": "^6.5.16",
59 | "@storybook/react": "^6.5.16",
60 | "@types/react": "^18.2.25",
61 | "@types/react-dom": "^18.2.11",
62 | "@types/resize-observer-browser": "^0.1.8",
63 | "babel-loader": "^9.1.3",
64 | "husky": "^8.0.3",
65 | "react": "^18.2.0",
66 | "react-dom": "^18.2.0",
67 | "react-is": "^18.2.0",
68 | "size-limit": "^8.2.6",
69 | "tsdx": "^0.14.1",
70 | "tslib": "^2.6.2",
71 | "typescript": "^4.9.5"
72 | },
73 | "dependencies": {
74 | "@types/styled-components": "^5.1.28",
75 | "postcss": "^8.4.31",
76 | "rollup-plugin-import-css": "^3.3.4",
77 | "rollup-plugin-postcss": "^4.0.2",
78 | "rollup-plugin-scss": "^4.0.0",
79 | "styled-components": "^5.3.11"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/conversation-header/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import useColorSet from '../../hooks/useColorSet'
4 |
5 | export type Props = {
6 | showHeader?: boolean
7 | loading?: boolean
8 | }
9 |
10 | const Container = styled.div<{
11 | backgroundColor?: string,
12 | dividerColor?: string
13 |
14 | }>`
15 | height:56px;
16 | padding:0px;
17 | background-color:${({ backgroundColor }) => backgroundColor || '#ffffff'};
18 |
19 | ${({ dividerColor }) => dividerColor ?
20 | `border-bottom: 1px solid ${dividerColor};`
21 | :
22 | 'box-shadow:0px 1px 0px rgba(0, 0, 0, 0.07999999821186066);'
23 |
24 | }
25 |
26 | position:absolute;
27 | top: 0px;
28 | left: 0px;
29 | right: 0px;
30 | z-index: 2;
31 | display: flex;
32 | align-items: center;
33 |
34 | `
35 |
36 | const ChatTitle = styled.div<{
37 | color?: string
38 | }>`
39 | text-align:center;
40 | vertical-align:text-top;
41 | font-size:16px;
42 | line-height:auto;
43 | color:${({ color }) => color || '#000000'};
44 | position:absolute;
45 | width: 100%;
46 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
47 | user-select: none;
48 |
49 | `
50 | const HeaderPlaceholder = styled.div`
51 | background-color: transparent;
52 | height: 56px;
53 | position: absolute;
54 | top: 0px;
55 | left: 0px;
56 | right: 0px;
57 | z-index: 1;
58 | box-sizing: border-box;
59 | `
60 |
61 | export default function ConversationHeader({ loading, showHeader = true }: Props) {
62 |
63 | const backgroundColor = useColorSet("--chatlist-header-background-color")
64 | const textColor = useColorSet("--chatlist-header-text-color")
65 | const dividerColor = useColorSet("--chatlist-header-divider-color")
66 |
67 | return (
68 | <>
69 | {
70 | loading ?
71 |
72 | :
73 | (!showHeader ?
74 |
75 | :
76 |
79 |
80 | Messages
83 |
84 |
85 | )
86 | }
87 | >
88 | )
89 | }
--------------------------------------------------------------------------------
/src/components/conversation-list/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useRef } from 'react';
2 | import styled from 'styled-components';
3 | import Loading from '../loading';
4 | import ConversationType from '../../types/ConversationType';
5 | import Conversation from '../conversation';
6 | import useColorSet from '../../hooks/useColorSet';
7 | import MinChatUIContext from '../../contexts/MinChatUIContext';
8 |
9 | export interface Props {
10 | onConversationClick?: (index: number) => void;
11 | conversations?: ConversationType[];
12 | loading?: boolean;
13 | selectedConversationId?: string;
14 | onScrollToBottom?: () => void;
15 | themeColor?: string;
16 | mobileView?: boolean;
17 | /**
18 | * the current user on the chat ui
19 | */
20 | currentUserId?: string;
21 | renderCustomConversationitem?: (conversation: ConversationType, index: number) => React.ReactNode
22 | customLoaderComponent?: React.ReactNode
23 | customEmptyConversationsComponent?: React.ReactNode
24 |
25 | }
26 |
27 | const ScrollContainer = styled.div<{
28 | loading?: boolean,
29 | backgroundColor?: string
30 | }>`
31 | position: relative;
32 | height: 100%;
33 | width: 100%;
34 | padding-top: ${({ loading }) => loading ? '0px' : '56px'};
35 | box-sizing: border-box;
36 | overflow-y: auto;
37 | max-height: 100vh;
38 | overflow-x: hidden;
39 | background-color: ${({ backgroundColor }) => backgroundColor || '#ffffff'};
40 | scrollbar-width: none; /* Firefox */
41 | -ms-overflow-style: none; /* Internet Explorer 10+ */
42 | ::-webkit-scrollbar { /* WebKit */
43 | width: 0;
44 | height: 0;
45 | }
46 | `;
47 |
48 | const Container = styled.div`
49 | height: 100%;
50 | position: relative;
51 | max-height: 100vh;
52 | overflow: hidden;
53 | `;
54 |
55 | // const SearchElement = styled.input`
56 | // width:100%;
57 | // height:40px;
58 | // padding:0px;
59 | // position:relative;
60 | // background-color:#e5e7eb;
61 | // border-radius:20px;
62 | // border:1px solid #ecebeb;
63 | // font-size:14px;
64 | // font-family:SF Pro Text;
65 | // line-height:auto;
66 | // padding-left: 16px;
67 | // text-align:left;
68 | // vertical-align:text-top;
69 | // margin-right: 56px;
70 | // &:focus{
71 | // outline: none;
72 |
73 | // }
74 | // `
75 |
76 | const NoChatsTextContainer = styled.div<{
77 | color?: string
78 | }>`
79 | color: ${({ color }) => color || 'rgba(0, 0, 0, 0.36)'};
80 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
81 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
82 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
83 | font-size: 14px;
84 | display: flex;
85 | justify-content: center;
86 | align-items: center;
87 | height: 100px;
88 | `;
89 |
90 |
91 | const LoadingContainer = styled.div`
92 | width: 100%;
93 | height: 100%;
94 | display: flex;
95 | justify-content: center;
96 | align-items: center;
97 | z-index: 1;
98 | position: relative;
99 | `
100 |
101 | export default function ConversationList({
102 | conversations,
103 | loading,
104 | onConversationClick,
105 | selectedConversationId,
106 | onScrollToBottom,
107 | currentUserId,
108 | renderCustomConversationitem,
109 | customLoaderComponent,
110 | customEmptyConversationsComponent
111 | }: Props) {
112 | const scrollContainerRef = useRef();
113 |
114 | const backgroundColor = useColorSet("--chatlist-background-color")
115 | const noConversation = useColorSet("--no-conversation-text-color")
116 |
117 | const { themeColor } = useContext(MinChatUIContext)
118 |
119 |
120 | return (
121 |
122 | {
126 | //detect when scrolled to bottom
127 | const bottom =
128 | scrollContainerRef.current.scrollHeight -
129 | scrollContainerRef.current.scrollTop ===
130 | scrollContainerRef.current.clientHeight;
131 | if (bottom) {
132 | onScrollToBottom && onScrollToBottom();
133 | }
134 | }}
135 | ref={scrollContainerRef}
136 | >
137 | {loading ?
138 |
139 | {customLoaderComponent ?
140 | customLoaderComponent :
141 | }
142 | : (
143 | <>
144 | {conversations && conversations.length <= 0 && (
145 | customEmptyConversationsComponent ?
146 | customEmptyConversationsComponent :
147 |
148 | No conversation started...
149 |
150 | )}
151 |
152 | {conversations &&
153 | conversations.map((conversation, index) => (
154 | (renderCustomConversationitem && renderCustomConversationitem(conversation, index)) ?
155 | renderCustomConversationitem(conversation, index)
156 | :
157 | onConversationClick && onConversationClick(index)}
159 | key={index}
160 | title={conversation.title}
161 | lastMessage={conversation.lastMessage}
162 | avatar={conversation.avatar}
163 | selected={selectedConversationId === conversation.id}
164 | currentUserId={currentUserId}
165 | unread={conversation.unread}
166 | />
167 | ))}
168 | >
169 | )}
170 |
171 |
172 | );
173 | }
174 |
--------------------------------------------------------------------------------
/src/components/conversation/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useRef, useState } from 'react';
2 | import styled from 'styled-components';
3 | import MessageType from '../../types/MessageType';
4 | import placeholderProfilePNG from './profile.png';
5 | import { calculateTimeAgo } from '../../utils/date-utils';
6 | import useColorSet from '../../hooks/useColorSet';
7 | import MinChatUIContext from '../../contexts/MinChatUIContext';
8 |
9 | export type Props = {
10 | title: string;
11 | lastMessage?: MessageType;
12 | unread?: boolean,
13 | avatar?: string;
14 | onClick: () => void;
15 | selected?: boolean;
16 | /**
17 | * the current user on the chat ui
18 | */
19 | currentUserId?: string;
20 | };
21 | const Container = styled.div`
22 | width: 100%;
23 | height: 88px;
24 | position: relative;
25 | margin-top: 1px;
26 | cursor: pointer;
27 | display: flex;
28 | align-items: center;
29 | box-sizing: border-box;
30 | user-select: none;
31 |
32 | `;
33 | const ContentContainer = styled.div`
34 | display: flex;
35 | position: relative;
36 | flex-direction: row;
37 | align-items: center;
38 | padding-left: 8px;
39 | width: 100%;
40 | height: 100%;
41 | box-sizing: border-box;
42 | `;
43 |
44 | const Background = styled.div<{
45 | themeColor: string
46 | selected?: boolean
47 | hoverColor?: string
48 | backgroundColor?: string
49 | selectedBackgroundColor?: string
50 | }>`
51 | position: absolute;
52 | width: 100%;
53 | height: 100%;
54 | background-color: ${({ themeColor, selected, backgroundColor, selectedBackgroundColor }) =>
55 | selected ? (selectedBackgroundColor || themeColor) : (backgroundColor || '#ffffff')};
56 | opacity: 0.2;
57 | z-index: 1;
58 | transition: all 0.3s ease-in-out;
59 |
60 | &:hover{
61 | ${({ selected }) => (!selected ? 'opacity: 0.09;' : '')}
62 | background-color: ${({ themeColor, hoverColor }) => hoverColor || themeColor};
63 |
64 | }
65 | `;
66 |
67 | const Name = styled.div<{
68 | unread?: boolean,
69 | titleTextColor?: string
70 | }>`
71 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
72 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
73 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
74 | text-align: left;
75 | vertical-align: text-top;
76 | font-size: 14px;
77 | line-height: auto;
78 | position: relative;
79 | z-index: 1;
80 | color: ${({ titleTextColor }) => titleTextColor || '#000000'};
81 |
82 | ${({ unread }) =>
83 | unread
84 | ? `
85 | font-weight: 700;
86 | `
87 | : ''}
88 | `;
89 |
90 | const NameContainer = styled.div`
91 | display: flex;
92 | width: 100%;
93 | justify-content: space-between;
94 | `
95 |
96 | const Timestamp = styled.div<{
97 | color?: string,
98 | unread?: boolean
99 | }>`
100 | text-align:right;
101 | vertical-align:text-top;
102 | font-size:12px;
103 | margin-left: 6px;
104 | margin-top:2px;
105 | margin-right:2px;
106 | align-self:flex-start;
107 | line-height:auto;
108 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
109 |
110 | ${({ unread, color }) =>
111 | unread
112 | ? `
113 | color: ${color || 'black'} ;
114 | font-weight: 600;
115 | ` : `
116 | color: ${color || 'rgb(75 85 99)'};
117 | `}
118 | `
119 |
120 | // const LastMessageUser = styled.div<{ seen?: boolean }>`
121 | // font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
122 | // text-align:left;
123 | // vertical-align:text-top;
124 | // font-size:12px;
125 | // align-self:flex-start;
126 | // position:relative;
127 | // color:#7a7a7a;
128 | // white-space: nowrap;
129 | // text-overflow: ellipsis;
130 |
131 | // ${({ seen }) => !seen ? `
132 | // color: black;
133 | // font-weight: 600;
134 | // ` : ''}
135 |
136 | // `
137 |
138 | const MessageComponent = styled.div<{
139 | unread?: boolean;
140 | width: number;
141 | media?: boolean;
142 | color?: string
143 | }>`
144 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
145 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
146 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
147 | text-align: left;
148 | vertical-align: text-top;
149 | font-size: 12px;
150 | align-self: flex-start;
151 | position: relative;
152 | color: ${({ color }) => color || '#7a7a7a'};
153 |
154 | overflow: hidden;
155 | white-space: nowrap;
156 | text-overflow: ellipsis;
157 | box-sizing: border-box;
158 | max-width: ${({ width }) => width}px;
159 | display: flex;
160 | margin-top: 4px;
161 |
162 | ${({ unread, color }) =>
163 | unread
164 | ? `
165 | color: ${color || 'black'} ;
166 | font-weight: 600;
167 | ` : ''}
168 |
169 | `;
170 |
171 |
172 | const TextContainer = styled.div`
173 | position: relative;
174 | height: 100%;
175 | width: 100%;
176 | padding-right: 20px;
177 | border-bottom: 1px solid rgba(0, 0, 0, 0.04);
178 | display: flex;
179 | justify-content: center;
180 | flex-direction: column;
181 | `;
182 |
183 | const DisplayPictureContainer = styled.div`
184 | width: 58px;
185 | height: 58px;
186 | margin-right: 12px;
187 | box-sizing: border-box;
188 | `;
189 |
190 | const DisplayPicture = styled.img`
191 | width: 58px;
192 | height: 58px;
193 | border-radius: 9999px;
194 | box-sizing: border-box;
195 | border-width: 2px;
196 | border-color: rgb(255 255 255);
197 | object-fit: cover;
198 | z-index: 1;
199 | position: relative;
200 | `;
201 |
202 | const MediaIconContainer = styled.div`
203 | width: 16px;
204 | height: 16px;
205 | margin-left: 3px;
206 | `;
207 |
208 | const MediaContainer = styled.div`
209 | display: flex;
210 | align-items: center;
211 | flex-direction: row;
212 | justify-content: center;
213 | gap: 4px;
214 | margin-left: 4px;
215 | `
216 |
217 | export default function Conversation({
218 | title,
219 | lastMessage,
220 | onClick,
221 | avatar,
222 | selected = false,
223 | currentUserId,
224 | unread
225 | }: Props) {
226 | const [containerWidth, setContainerWidth] = useState(0);
227 |
228 | const [usedAvatar, setUsedAvatar] = React.useState(
229 | placeholderProfilePNG
230 | );
231 |
232 | const [dateSent, setDateSent] = useState()
233 | const [intervalId, setIntervalId] = useState()
234 |
235 | const { themeColor } = useContext(MinChatUIContext)
236 |
237 | useEffect(() => {
238 | function updateDateSent() {
239 | if (lastMessage?.createdAt) {
240 | setDateSent(calculateTimeAgo(new Date(lastMessage.createdAt)))
241 | }
242 | }
243 |
244 | updateDateSent()
245 | clearInterval(intervalId)
246 |
247 | const id = setInterval(() => updateDateSent(), 60_000)
248 | setIntervalId(id)
249 |
250 | return () => {
251 | if (intervalId) {
252 | clearInterval(intervalId);
253 | setIntervalId(null); // Reset intervalId after clearing
254 | }
255 |
256 | };
257 | }, [lastMessage])
258 |
259 |
260 | useEffect(() => {
261 | window.addEventListener('resize', () => {
262 | calculateContainerWidth();
263 | });
264 | }, []);
265 |
266 | useEffect(() => {
267 | if (avatar && avatar.trim().length > 0) {
268 | setUsedAvatar(avatar);
269 | }
270 | }, [avatar]);
271 |
272 | const containerRef = useRef(null);
273 |
274 | useEffect(() => {
275 | calculateContainerWidth();
276 | }, [containerRef]);
277 |
278 |
279 | const backgroundColor = useColorSet("--chatitem-background-color")
280 | const titleTextColor = useColorSet("--chatitem-title-text-color")
281 | const contentTextColor = useColorSet("--chatitem-content-text-color")
282 | const hoverColor = useColorSet("--chatitem-hover-color")
283 | const selectedBackgroundColor = useColorSet("--chatitem-selected-background-color")
284 |
285 |
286 | /**
287 | *
288 | */
289 | const calculateContainerWidth = () => {
290 | if (containerRef && containerRef.current) {
291 | setContainerWidth(containerRef.current.clientWidth);
292 | }
293 | };
294 |
295 |
296 | const getMediaIcon = () => {
297 |
298 | switch (lastMessage?.media?.type) {
299 | case "image":
300 | return
308 |
309 |
310 |
311 | case "video":
312 | return
321 |
322 |
323 | case "gif":
324 | return
332 |
333 |
334 |
335 |
336 | default:
337 | return
344 |
345 |
346 |
347 | }
348 | }
349 |
350 | const getMediaText = () => {
351 |
352 | switch (lastMessage?.media?.type) {
353 | case "image":
354 | return "Image"
355 | case "video":
356 | return lastMessage?.media?.name ? lastMessage?.media?.name : "Video"
357 | case "gif":
358 | return lastMessage?.media?.name ? lastMessage?.media?.name : "Gif"
359 | default:
360 | return lastMessage?.media?.name ? lastMessage?.media?.name : "File"
361 | }
362 | }
363 |
364 | return (
365 |
366 |
371 |
372 |
373 |
374 |
375 | {
377 | setUsedAvatar(placeholderProfilePNG);
378 | }}
379 | src={usedAvatar}
380 | />
381 |
382 |
383 |
384 |
385 |
386 |
387 | {title}
390 |
391 | {dateSent}
394 |
395 |
396 |
401 | {lastMessage?.user.id === currentUserId
402 | ? 'You'
403 | : lastMessage?.user.name}
404 | :{' '}
405 | {lastMessage?.media ? (
406 |
407 |
408 | {getMediaIcon()}
409 |
410 | {getMediaText()}
411 |
412 | ) : (
413 |
415 | )}
416 |
417 |
418 |
419 |
420 | );
421 | }
422 |
--------------------------------------------------------------------------------
/src/components/conversation/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinChatHQ/react-chat-ui/498328841aa94938f1cfb2eaeb6ab200a2fb158f/src/components/conversation/profile.png
--------------------------------------------------------------------------------
/src/components/conversation/profile.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinChatHQ/react-chat-ui/498328841aa94938f1cfb2eaeb6ab200a2fb158f/src/components/conversation/profile.webp
--------------------------------------------------------------------------------
/src/components/loading/index.css:
--------------------------------------------------------------------------------
1 | .lds-ring {
2 | display: inline-block;
3 | width: 48px;
4 | height: 48px;
5 | position: absolute;
6 | top: 50%;
7 | left: 50%;
8 | transform: translate(-50%, -50%);
9 | }
10 |
11 | .lds-ring div:nth-child(1) {
12 | animation-delay: -0.45s;
13 | }
14 |
15 | .lds-ring div:nth-child(2) {
16 | animation-delay: -0.3s;
17 | }
18 |
19 | .lds-ring div:nth-child(3) {
20 | animation-delay: -0.15s;
21 | }
22 |
23 | @keyframes lds-ring {
24 | 0% {
25 | transform: rotate(0deg);
26 | }
27 |
28 | 100% {
29 | transform: rotate(360deg);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import './index.css';
4 | import useColorSet from '../../hooks/useColorSet';
5 |
6 | type Props = {
7 | themeColor?: string;
8 | };
9 |
10 | const Container = styled.div`
11 | height: 100%;
12 | width: 100%;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | box-sizing: border-box;
17 | position: relative;
18 | `;
19 |
20 | const InternalDiv = styled.div<{ themeColor?: string }>`
21 | box-sizing: border-box;
22 | width: 42px;
23 | height: 42px;
24 | margin: 8px;
25 | position: absolute;
26 | border: 6px solid #fff;
27 | border-radius: 50%;
28 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
29 | border-color: ${({ themeColor }) => themeColor} transparent transparent
30 | transparent;
31 | box-sizing: border-box;
32 | `;
33 |
34 | export default function Loading({ themeColor }: Props) {
35 |
36 | const color = useColorSet("--loader-color")
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/main-container/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { } from 'react'
2 | import styled from 'styled-components'
3 | import "../../index.css"
4 | import useColorSet from '../../hooks/useColorSet'
5 |
6 |
7 | const Container = styled.div<{
8 | backgroundColor?: string
9 | }>`
10 | height: 100%;
11 | position: relative;
12 | display: flex;
13 | width: 100%;
14 | flex-direction: row;
15 | ${({ backgroundColor }) => backgroundColor ? `background-color: ${backgroundColor};` : ""}
16 | `
17 |
18 |
19 | export interface Props {
20 | style?: React.CSSProperties | undefined
21 | children?: React.ReactNode
22 | }
23 |
24 |
25 | export default function MainContainer({
26 | children,
27 | style
28 | }: Props) {
29 |
30 | const backgroundColor = useColorSet("--container-background-color")
31 |
32 | return (
33 |
36 | {children}
37 |
38 | )
39 | }
--------------------------------------------------------------------------------
/src/components/message-container/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export type Props = {
5 | children: React.ReactNode
6 | }
7 |
8 | const Container = styled.div`
9 | position: relative;
10 | width: 100%;
11 | display: flex;
12 | flex-direction: column;
13 | height: 100%;
14 | box-sizing: border-box;
15 | `
16 |
17 |
18 | export default function MessageContainer({
19 | children
20 | }: Props) {
21 | return (
22 |
23 | {children}
24 |
25 | )
26 | }
--------------------------------------------------------------------------------
/src/components/message-header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import styled from "styled-components"
3 | import { calculateLastSeen } from '../../utils/date-utils'
4 | import useColorSet from '../../hooks/useColorSet'
5 |
6 | export type Props = {
7 | onBack?: () => void
8 | children?: React.ReactNode
9 | showBack?: boolean
10 | mobileView?: boolean
11 | lastActive?: Date
12 | }
13 |
14 |
15 | const Container = styled.div<{ mobile?: boolean }>`
16 | position: relative;
17 | width: 100%;
18 | height: 64px;
19 | display: flex;
20 | box-sizing: border-box;
21 |
22 | ${({ mobile }) => !mobile ? `
23 | padding-right: 12px;
24 | ` :
25 | `
26 | `}
27 | `
28 | const InnerContainer = styled.div<{
29 | backgroundColor?: string
30 | }>`
31 | background-color:${({ backgroundColor }) => backgroundColor || '#f3f4f6'};
32 | border-top-left-radius: 16px;
33 | border-top-right-radius: 16px;
34 | height:100%;
35 | padding:0px;
36 | box-shadow:0px 1px 0px rgba(0, 0, 0, 0.07999999821186066);
37 | position:relative;
38 | width:100%;
39 | z-index: 1;
40 | display: flex;
41 | align-items: center;
42 | box-sizing: border-box;
43 | `
44 |
45 | const HeadingContainer = styled.div`
46 | position:absolute;
47 | width: 100%;
48 |
49 | `
50 |
51 | const ChatTitle = styled.div<{
52 | color?: string
53 | }>`
54 | text-align:center;
55 | vertical-align:text-top;
56 | font-size:16px;
57 | line-height:auto;
58 | color:${({ color }) => color || '#000000'};
59 | user-select: none;
60 | position: relative;
61 | width: 100%;
62 | font-weight: 500;
63 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
64 |
65 | `
66 |
67 | const LastSeenText = styled.div<{
68 | color?: string
69 | }>`
70 | text-align:center;
71 | vertical-align:text-top;
72 | font-size:10px;
73 | line-height:auto;
74 | color:${({ color }) => color || 'rgb(107 114 128)'};
75 | user-select: none;
76 | position: relative;
77 | width: 100%;
78 | font-weight: 100;
79 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
80 |
81 | `
82 |
83 | const BackContainer = styled.div`
84 | cursor: pointer;
85 | height: 100%;
86 | padding-left: 8px;
87 | padding-right: 8px;
88 | display: flex;
89 | align-items: center;
90 | justify-content: center;
91 | width: 38px;
92 | z-index: 1;
93 | box-sizing: border-box;
94 |
95 | `
96 |
97 | const BackIcon = styled.svg<{
98 | color?: string
99 | }>`
100 | padding:0px;
101 | cursor: pointer;
102 | box-sizing: border-box;
103 |
104 | color: ${({ color }) => color ? ` ${color}` : 'black'};
105 | `
106 | export default function MessageHeader({
107 | onBack,
108 | children,
109 | showBack = true,
110 | mobileView,
111 | lastActive
112 | }: Props) {
113 |
114 | const [lastSeen, setLastSeen] = useState()
115 | const [intervalId, setIntervalId] = useState()
116 |
117 | useEffect(() => {
118 | /**
119 | *
120 | */
121 | function updateLastSeen() {
122 | if (lastActive) {
123 | setLastSeen(calculateLastSeen(lastActive))
124 | } else {
125 | setLastSeen(undefined)
126 | }
127 | }
128 |
129 |
130 | updateLastSeen()
131 |
132 | clearInterval(intervalId)
133 | if (lastActive) {
134 | const id = setInterval(() => updateLastSeen(), 5_000)
135 | setIntervalId(id)
136 | }
137 |
138 |
139 | return () => {
140 | if (intervalId) {
141 | clearInterval(intervalId);
142 | setIntervalId(null); // Reset intervalId after clearing
143 | }
144 | };
145 | }, [lastActive])
146 |
147 | const backgroundColor = useColorSet("--message-header-background-color")
148 | const textColor = useColorSet("--message-header-text-color")
149 | const lastActiveColor = useColorSet("--message-header-last-active-color")
150 | const backColor = useColorSet("--message-header-back-color")
151 |
152 | return (
153 |
155 |
156 |
158 |
159 | {showBack &&
162 |
166 |
171 |
172 |
173 | }
174 |
175 |
176 | {children}
179 |
180 | {lastSeen &&
181 | {lastSeen}
184 | }
185 |
186 |
187 |
188 | {/*
189 |
190 |
*/}
191 |
192 |
193 |
194 | )
195 | }
--------------------------------------------------------------------------------
/src/components/message-input/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useRef, useState } from 'react'
2 | import styled from 'styled-components'
3 | import useTypingListener from '../../hooks/useTypingListener'
4 | import useColorSet from '../../hooks/useColorSet'
5 | import MinChatUIContext from '../../contexts/MinChatUIContext'
6 |
7 | export type Props = {
8 | onSendMessage?: (text: string) => void
9 | mobileView?: boolean
10 | onStartTyping?: () => void
11 | onEndTyping?: () => void
12 | showAttachButton?: boolean
13 | onAttachClick?: () => void
14 | placeholder?: string
15 | disabled?: boolean
16 | showSendButton: boolean
17 |
18 | onKeyDown?: React.KeyboardEventHandler | undefined
19 | onKeyUp?: React.KeyboardEventHandler | undefined
20 |
21 |
22 | }
23 |
24 | const Container = styled.div<{
25 | mobile?: boolean,
26 | }>`
27 | box-sizing: border-box;
28 | position: relative;
29 | width: 100%;
30 | display: flex;
31 |
32 |
33 | ${({ mobile }) => mobile ? `
34 | padding-right: 0px;
35 |
36 | `:
37 | `
38 | padding-right: 12px;
39 | `}
40 | `
41 | const Form = styled.form<{
42 | backgroundColor?: string,
43 | borderColor?: string,
44 | }>`
45 | background-color:${({ backgroundColor }) => backgroundColor || "#f3f4f6"};
46 | padding-top: 8px;
47 | padding-bottom: 8px;
48 | border-bottom-right-radius: 16px;
49 | border-bottom-left-radius: 16px;
50 | box-shadow:0px -1px 0px rgba(0, 0, 0, 0.07999999821186066);
51 | position: relative;
52 | width: 100%;
53 | display: flex;
54 | align-items: end;
55 | box-sizing: border-box;
56 | `
57 |
58 | const InputContainer = styled.div`
59 | width: 100%;
60 | display: flex;
61 | align-items: center;
62 | justify-content: center;
63 | position: relative;
64 | box-sizing: border-box;
65 | `
66 |
67 | const InputBackground = styled.div<{
68 | showOpacity: boolean,
69 | bgColor?: string
70 | }>`
71 | ${({ showOpacity }) => showOpacity ? `opacity: 0.4;` : ''}
72 | height: 100%;
73 | width: 100%;
74 | border-radius:0.7rem;
75 | position: absolute;
76 | background-color: ${({ bgColor }) => bgColor};
77 | border:1px solid #ecebeb;
78 |
79 | `
80 |
81 |
82 | const InputElementContainer = styled.div`
83 | padding: 8px;
84 | padding-left: 16px;
85 | padding-right: 16px;
86 | width:100%;
87 | `
88 |
89 | const InputElement = styled.div<{
90 | color?: string
91 | }>`
92 | width:100%;
93 | border: none;
94 | max-height: 6.4em;
95 | /* Adjust this value to control the maximum number of lines */
96 | position:relative;
97 | font-size:14px;
98 | overflow: scroll;
99 |
100 | color: ${({ color }) => color || "rgba(0,0,0,.87)"};
101 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
102 | background-color: transparent;
103 | text-align:left;
104 | opacity: 1;
105 |
106 | min-height: 1.6em;
107 | line-height:1.6em;
108 | word-wrap: break-word;
109 | overflow-wrap: anywhere;
110 |
111 | &:focus{
112 | outline: none;
113 | }
114 |
115 | ::-webkit-scrollbar {
116 | display: none;
117 | }
118 |
119 | -ms-overflow-style: none; /* IE and Edge */
120 | scrollbar-width: none; /* Firefox */
121 | `
122 |
123 |
124 | const ArrowContainer = styled.div<{ showCursor: boolean, disabled: boolean }>`
125 | position: relative;
126 | padding-left:16px;
127 | padding-right:16px;
128 | cursor: ${({ showCursor, disabled }) => showCursor && !disabled ? 'pointer' : 'default'};
129 | display: flex;
130 | align-items: end;
131 | opacity: ${({ showCursor, disabled }) => showCursor && !disabled ? '1' : '0.4'};
132 | height: 100%;
133 | padding-top: 8px;
134 | padding-bottom: 8px;
135 |
136 |
137 | `
138 |
139 | const AttachmentContainer = styled.div<{ disabled: boolean }>`
140 | position: relative;
141 | padding-left:16px;
142 | padding-right:16px;
143 | display: flex;
144 |
145 | align-items: end;
146 | height: 100%;
147 | padding-top: 8px;
148 | padding-bottom: 8px;
149 |
150 | ${({ disabled }) => !disabled ? `
151 | cursor: pointer;
152 | opacity: 1;
153 | ` : `
154 | opacity: 0.6;
155 | `}
156 |
157 | `
158 |
159 | const AttachPlaceholder = styled.div`
160 | position: relative;
161 | padding:12px;
162 | `
163 | const SendPlaceholder = styled.div`
164 | position: relative;
165 | padding:12px;
166 | `
167 |
168 | const PlaceHolder = styled.span<{
169 | color?: string
170 | }>`
171 | color: ${({ color }) => color || '#9ca3af'};
172 | position: absolute;
173 | left: 0;
174 | top: 0;
175 | bottom: 0;
176 | display: flex;
177 | align-items: center;
178 | padding-left: 16px;
179 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
180 | font-size:12px;
181 | pointer-events: none;
182 | `
183 |
184 |
185 | export default function MessageInput({
186 | onSendMessage,
187 | mobileView,
188 | onStartTyping,
189 | onEndTyping,
190 | showAttachButton = true,
191 | showSendButton = true,
192 | disabled = false,
193 | onAttachClick,
194 | placeholder = 'Send a message...',
195 | onKeyDown,
196 | onKeyUp
197 | }: Props) {
198 |
199 | const { themeColor } = useContext(MinChatUIContext)
200 |
201 | const [text, setText] = useState("")
202 | const inputRef = useRef(null);
203 |
204 | const { setTyping, ...inputProps } = useTypingListener({ onStartTyping, onEndTyping })
205 |
206 | const handleSubmit = () => {
207 | if (!disabled && text.trim().length > 0) {
208 | inputRef.current.innerText = ''
209 | setTyping(false)
210 | onSendMessage && onSendMessage(text.trim())
211 | setText("")
212 |
213 | }
214 | }
215 |
216 | // colorSets
217 | const backgroundColor = useColorSet("--input-background-color")
218 | const inputTextColor = useColorSet("--input-text-color")
219 | const inputAttachColor = useColorSet("--input-attach-color")
220 | const inputSendColor = useColorSet("--input-send-color")
221 | const inputElementColor = useColorSet("--input-element-color")
222 | const inputPlaceholderColor = useColorSet("--input-placeholder-color")
223 |
224 | return (
225 |
228 |
325 |
326 |
327 | )
328 | }
--------------------------------------------------------------------------------
/src/components/message-list-background/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import useColorSet from '../../hooks/useColorSet'
4 |
5 | const ScrollBackground = styled.div<{
6 | roundedCorners?: boolean
7 | backgroundColor?: string
8 | }>`
9 | background-color:${({ backgroundColor }) => backgroundColor || '#f3f4f6'};
10 | position: relative;
11 | width: 100%;
12 | height: 100%;
13 | border-radius: ${({ roundedCorners }) => roundedCorners ? '16px' : '0px'};
14 |
15 | `
16 |
17 | const ScrollBackgroundContainer = styled.div<{
18 | mobile?: boolean,
19 | }>`
20 | position: absolute;
21 | width: 100%;
22 | height: 100%;
23 | z-index: 0;
24 | box-sizing: border-box;
25 | ${({ mobile }) => !mobile ? `
26 | padding-right: 12px;
27 | ` : ""}
28 |
29 | `
30 |
31 | type Props = {
32 | mobileView?: boolean,
33 | roundedCorners?: boolean
34 | }
35 |
36 | export default function MessageListBackground({
37 | mobileView,
38 | roundedCorners = true
39 | }: Props) {
40 |
41 | const backgroundColor = useColorSet("--messagelist-background-color")
42 |
43 | return (
44 |
45 |
48 |
49 | )
50 | }
--------------------------------------------------------------------------------
/src/components/message-list/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react'
2 | import Message from '../message'
3 | import styled from 'styled-components'
4 | import Loading from '../loading'
5 | import useDetectScrollPosition from '../../hooks/useDetectScrollPosition'
6 | import MessageType from '../../types/MessageType'
7 | import TypingIndicator from '../typing-indicator'
8 | import MessageListBackground from '../message-list-background'
9 | import useColorSet from '../../hooks/useColorSet'
10 |
11 | export type MessageListProps = {
12 | themeColor?: string
13 | messages?: MessageType[]
14 | currentUserId?: string
15 | loading?: boolean
16 | onScrollToTop?: () => void
17 | mobileView?: boolean
18 | showTypingIndicator?: boolean
19 | typingIndicatorContent?: string
20 | customTypingIndicatorComponent?: React.ReactNode
21 | customEmptyMessagesComponent?: React.ReactNode
22 | customLoaderComponent?: React.ReactNode
23 | }
24 |
25 |
26 |
27 | const Container = styled.div`
28 | height: 100%;
29 | /* display: flex;
30 | flex-direction: column; */
31 | position: relative;
32 | max-height: 100vh;
33 | overflow-y: hidden;
34 | /* background-color: #ffffff; */
35 | padding-left: 0px;
36 | padding-right: 12px;
37 | `
38 |
39 | const InnerContainer = styled.div`
40 | height: 100%;
41 | `
42 |
43 |
44 | const ScrollContainer = styled.div`
45 | overflow-y: auto;
46 | position: relative;
47 | height: 100%;
48 | width: 100%;
49 | max-height: 100vh;
50 | box-sizing: border-box;
51 | display: flex;
52 | flex-direction: column;
53 | scrollbar-width: none; /* Firefox */
54 | -ms-overflow-style: none; /* Internet Explorer 10+ */
55 | ::-webkit-scrollbar { /* WebKit */
56 | width: 0;
57 | height: 0;
58 | }
59 | `
60 |
61 | const Buffer = styled.div`
62 | height: 2px;
63 | width: 100%;
64 | position: relative;
65 | `
66 |
67 | const NoMessagesTextContainer = styled.div<{
68 | color?: string
69 | }>`
70 | color:${({ color }) => color || 'rgba(0,0,0,.36)'};
71 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
72 | font-size:14px;
73 | display: flex;
74 | justify-content: center;
75 | align-items: center;
76 | height: 100%;
77 | user-select: none;
78 |
79 | `
80 |
81 | const LoadingContainer = styled.div`
82 | width: 100%;
83 | height: 100%;
84 | display: flex;
85 | justify-content: center;
86 | align-items: center;
87 | z-index: 1;
88 | position: relative;
89 | `
90 |
91 | export default function MessageList({
92 | messages,
93 | currentUserId,
94 | loading = false,
95 | onScrollToTop,
96 | themeColor = '#6ea9d7',
97 | mobileView,
98 | typingIndicatorContent,
99 | showTypingIndicator,
100 | customTypingIndicatorComponent,
101 | customLoaderComponent,
102 | customEmptyMessagesComponent
103 | }: MessageListProps) {
104 |
105 | /** keeps track of whether messages was previously empty or whether it has already scrolled */
106 | const [messagesWasEmpty, setMessagesWasEmpty] = useState(true)
107 | const containerRef = useRef()
108 |
109 | const bottomBufferRef = useRef()
110 | const scrollContainerRef = useRef()
111 |
112 | const { detectBottom, detectTop } = useDetectScrollPosition(scrollContainerRef)
113 |
114 |
115 | useEffect(() => {
116 | //detecting when the scroll view is first rendered and messages have rendered then you can scroll to the bottom
117 | if (bottomBufferRef.current && scrollContainerRef.current && !messagesWasEmpty) {
118 | scrollToBottom()
119 | }
120 |
121 | }, [messagesWasEmpty, bottomBufferRef.current, scrollContainerRef.current])
122 |
123 |
124 | useEffect(() => {
125 | if (!messages) {
126 | setMessagesWasEmpty(true)
127 | }
128 |
129 | if (messages) {
130 | if (messagesWasEmpty) {
131 | //if the messages object was previously empty then scroll to bottom
132 | // this is for when the first page of messages arrives
133 | //if a user has instead scrolled to the top and the next page of messages arrives then don't scroll to bottom
134 |
135 | setMessagesWasEmpty(false)
136 | scrollToBottom()
137 | }
138 |
139 | // when closer to the bottom of the scroll bar and a new message arrives then scroll to bottom
140 | if (detectBottom()) {
141 | scrollToBottom()
142 | }
143 |
144 | }
145 | }, [messages])
146 |
147 |
148 | useEffect(() => {
149 | //TODO when closer to the bottom of the scroll bar and a new message arrives then scroll to bottom
150 | if (detectBottom()) {
151 | scrollToBottom()
152 | }
153 | }, [showTypingIndicator])
154 |
155 |
156 | const noMessageTextColor = useColorSet("--no-message-text-color")
157 |
158 | const scrollToBottom = async () => {
159 | if (bottomBufferRef.current && scrollContainerRef.current) {
160 | const container = scrollContainerRef.current
161 | const scrollPoint = bottomBufferRef.current
162 |
163 | const parentRect = container.getBoundingClientRect()
164 | const childRect = scrollPoint.getBoundingClientRect()
165 |
166 | // Scroll by offset relative to parent
167 | const scrollOffset = childRect.top + container.scrollTop - parentRect.top;
168 |
169 | if (container.scrollBy) {
170 | container.scrollBy({ top: scrollOffset, behavior: "auto" });
171 | } else {
172 | container.scrollTop = scrollOffset;
173 | }
174 | }
175 | }
176 |
177 |
178 |
179 | return (
180 |
183 |
184 |
187 |
188 |
189 |
190 |
191 | {loading ?
192 |
193 | {customLoaderComponent ?
194 | customLoaderComponent :
195 | }
196 |
197 | :
198 | <>
199 |
200 | {
202 | //detect when scrolled to top
203 | if (detectTop()) {
204 | onScrollToTop && onScrollToTop()
205 | }
206 | }}
207 | ref={scrollContainerRef}>
208 |
209 | {(messages && messages.length <= 0) &&
210 | (customEmptyMessagesComponent ?
211 | customEmptyMessagesComponent
212 | :
213 |
215 | No messages yet...
216 | )
217 | }
218 | {messages && scrollContainerRef.current && bottomBufferRef.current && messages.map(({ user, text, media, loading: messageLoading, seen, createdAt }, index) => {
219 | //determining the type of message to render
220 | let lastClusterMessage, firstClusterMessage, last, single
221 |
222 | //if it is the first message in the messages array then show the header
223 | if (index === 0) { firstClusterMessage = true }
224 | //if the previous message from a different user then show the header
225 | if (index > 0 && messages[index - 1].user.id !== user.id) { firstClusterMessage = true }
226 | //if it is the last message in the messages array then show the avatar and is the last incoming
227 | if (index === messages.length - 1) { lastClusterMessage = true; last = true }
228 | //if the next message from a different user then show the avatar and is last message incoming
229 | if (index < messages.length - 1 && messages[index + 1].user.id !== user.id) { lastClusterMessage = true; last = true }
230 | //if the next message and the previous message are not from the same user then single incoming is true
231 | if (index < messages.length - 1 && index > 0 && messages[index + 1].user.id !== user.id && messages[index - 1].user.id !== user.id) { single = true }
232 | //if it is the first message in the messages array and the next message is from a different user then single incoming is true
233 | if (index === 0 && index < messages.length - 1 && messages[index + 1].user.id !== user.id) { single = true }
234 | //if it is the last message in the messages array and the previous message is from a different user then single incoming is true
235 | if (index === messages.length - 1 && index > 0 && messages[index - 1].user.id !== user.id) { single = true }
236 | //if the messages array contains only 1 message then single incoming is true
237 | if (messages.length === 1) { single = true }
238 |
239 |
240 | if (user.id == (currentUserId && currentUserId.toLowerCase())) {
241 |
242 | // my message
243 | return
256 |
257 | } else {
258 |
259 | // other message
260 | return
273 | }
274 | })}
275 |
276 | {showTypingIndicator && (
277 | customTypingIndicatorComponent ?
278 | customTypingIndicatorComponent
279 | :
282 | )}
283 |
284 | {/* bottom buffer */}
285 |
286 |
287 |
288 |
289 | >
290 |
291 | }
292 |
293 |
294 |
295 | )
296 | }
--------------------------------------------------------------------------------
/src/components/message/borderController.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | type Props = {
4 | type: "incoming" | "outgoing",
5 | last?: boolean,
6 | single?: boolean,
7 |
8 | }
9 | export const getBorderCss = ({
10 | type,
11 | last,
12 | single
13 | }: Props) => {
14 |
15 | let borderTopLeft, borderTopRight, borderBottomLeft, borderBottomRight
16 |
17 | if (type === "outgoing") {
18 | borderTopLeft = true
19 | borderBottomLeft = true
20 | borderBottomRight = last ? true : false
21 | borderTopRight = !last && single ? true : false
22 | } else {
23 | borderTopRight = true
24 | borderBottomRight = true
25 | borderBottomLeft = single || last ? true : false
26 | borderTopLeft = last ? true : false
27 | }
28 |
29 | return `
30 | border-top-left-radius: ${borderTopLeft ? "8px" : "2px"};
31 | border-top-right-radius: ${borderTopRight ? "8px" : "2px"};
32 | border-bottom-left-radius: ${borderBottomLeft ? "8px" : "2px"};
33 | border-bottom-right-radius: ${borderBottomRight ? "8px" : "2px"};
34 | `
35 |
36 | }
--------------------------------------------------------------------------------
/src/components/message/incoming-message/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect } from 'react'
2 | import styled from 'styled-components'
3 | import { Container as MyMessageContainer, Wrapper as MyMessageWrapper, Background } from '../outgoing-message'
4 | import placeholderProfilePNG from './profile.webp'
5 | import MediaContent from '../media-content'
6 | import { getBorderCss } from '../borderController'
7 | import TextContent from '../text-content'
8 | import { Props } from '..'
9 | import Timestamp from '../timestamp'
10 | import useColorSet from '../../../hooks/useColorSet'
11 | import MinChatUIContext from '../../../contexts/MinChatUIContext'
12 |
13 |
14 | const MessageContainer = styled(MyMessageContainer)`
15 | margin-left: 0px;
16 | box-sizing: border-box;
17 | margin-bottom: 0px;
18 | `
19 |
20 |
21 | const Wrapper = styled(MyMessageWrapper)`
22 | justify-content: start;
23 | align-items: flex-end;
24 | `
25 |
26 | const DPContainer = styled.div`
27 | width: 32px;
28 | height: 32px;
29 | margin-left: 10px;
30 | box-sizing: border-box;
31 | user-select: none;
32 |
33 | `
34 | const DisplayPicture = styled.img`
35 | width: 32px;
36 | height: 32px;
37 | border-radius: 9999px;
38 | box-sizing: border-box;
39 | border-width: 2px;
40 | border-color: rgb(255 255 255);
41 | object-fit: cover;
42 | position: relative;
43 | z-index: 1;
44 | `
45 |
46 |
47 |
48 | const Name = styled.div<{
49 | color?: string
50 | }>`
51 | text-align:left;
52 | vertical-align:text-top;
53 | font-size:14px;
54 | align-self:flex-start;
55 | line-height:auto;
56 | color:${({ color }) => color || "#4b5563"};
57 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
58 | font-weight: 500;
59 | user-select: none;
60 |
61 | `
62 |
63 | const TextWrapper = styled.div`
64 | margin-left:8px;
65 | box-sizing: border-box;
66 | `
67 |
68 |
69 | const IncomingMessageBackground = styled(Background) <{
70 | backgroundColor?: string
71 | }>`
72 | ${({ backgroundColor }) => !backgroundColor ? "opacity: 0.5;" : ""}
73 | `
74 |
75 | const HeaderContainer = styled.div`
76 | display: flex;
77 | align-items: "center";
78 | margin-top: 16px;
79 | margin-bottom: 6px;
80 | `
81 |
82 |
83 |
84 | export default function IncomingMessage({
85 | text,
86 | media,
87 | user,
88 | showAvatar,
89 | showHeader,
90 | last,
91 | single,
92 | created_at,
93 | }: Omit) {
94 |
95 | const { themeColor } = useContext(MinChatUIContext)
96 |
97 | const [avatar, setAvatar] = React.useState(placeholderProfilePNG)
98 |
99 | useEffect(() => {
100 | if (user?.avatar && user.avatar.trim().length > 0) {
101 | setAvatar(user.avatar)
102 | }
103 | }, [user])
104 |
105 |
106 | const textColor = useColorSet("--incoming-message-text-color")
107 | const nameTextColor = useColorSet("--incoming-message-name-text-color")
108 | const linkColor = useColorSet("--incoming-message-link-color")
109 |
110 | const backgroundColor = useColorSet("--incoming-message-background-color")
111 | const timestampColor = useColorSet("--incoming-message-timestamp-color")
112 |
113 | return (
114 |
118 |
119 | {showAvatar &&
120 |
121 | {
123 | setAvatar(placeholderProfilePNG)
124 | }}
125 | src={avatar}
126 | />}
127 |
128 |
129 |
130 | {showHeader &&
131 |
132 | {user?.name}
133 |
134 | }
135 |
136 |
137 |
138 | getBorderCss({
140 | type: "incoming",
141 | last,
142 | single
143 | }))()}
144 | backgroundColor={backgroundColor}
145 | bgColor={backgroundColor || themeColor} />
146 |
147 | {media ?
152 | :
153 | {text} }
156 |
157 |
161 |
162 |
163 |
164 |
165 |
166 | )
167 | }
168 |
169 |
--------------------------------------------------------------------------------
/src/components/message/incoming-message/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinChatHQ/react-chat-ui/498328841aa94938f1cfb2eaeb6ab200a2fb158f/src/components/message/incoming-message/profile.png
--------------------------------------------------------------------------------
/src/components/message/incoming-message/profile.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinChatHQ/react-chat-ui/498328841aa94938f1cfb2eaeb6ab200a2fb158f/src/components/message/incoming-message/profile.webp
--------------------------------------------------------------------------------
/src/components/message/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import User from '../../types/UserType';
3 | import OutgoingMessage from './outgoing-message'
4 | import IncomingMessage from './incoming-message'
5 | import { MediaType } from '../../types/MessageType';
6 |
7 |
8 | export type Props = {
9 | created_at?: Date
10 | seen?: boolean
11 | text?: string,
12 | media?: MediaType,
13 | loading?: boolean
14 | type?: "incoming" | "outgoing"
15 | user?: User
16 | showAvatar?: boolean
17 | showHeader?: boolean
18 | // determines whether its the last message in the group of outgoing or incoming
19 | last?: boolean
20 | //determines whether its the only message in the group of outgoing or incoming
21 | single?: boolean
22 | clusterFirstMessage?: boolean
23 | clusterLastMessage?: boolean
24 |
25 | };
26 |
27 |
28 | export default function Message({
29 | text,
30 | media,
31 | created_at,
32 | seen,
33 | loading,
34 | type = "outgoing",
35 | user,
36 | showAvatar,
37 | showHeader,
38 | last,
39 | single,
40 | clusterFirstMessage,
41 | clusterLastMessage
42 | }: Props) {
43 |
44 | return (
45 | type === "outgoing" ?
46 |
57 |
58 | :
59 |
60 |
70 |
71 | )
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/src/components/message/media-content/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { MediaType } from '../../../types/MessageType'
4 | import { getBorderCss } from '../borderController'
5 |
6 |
7 | interface Props extends MediaType {
8 | last?: boolean
9 | single?: boolean
10 | messageType: "incoming" | "outgoing"
11 | }
12 |
13 | const ImageContainer = styled.div`
14 | width: 99%;
15 | padding: 1px;
16 | position: relative;
17 | user-select: none;
18 |
19 | `
20 |
21 | const Image = styled.img<{
22 | borderCss: string,
23 | }>`
24 | width: 100%;
25 | margin: 0px;
26 | position: relative;
27 |
28 | ${({ borderCss }) => borderCss};
29 |
30 | `
31 |
32 | const FileContainer = styled.a`
33 | text-align:left;
34 | vertical-align:text-top;
35 | font-size:14px;
36 | align-self:flex-start;
37 | line-height:auto;
38 | color:#000000;
39 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
40 | padding-left:16px;
41 | padding-right:16px;
42 | padding-top:8px;
43 | padding-bottom:8px;
44 | position: relative;
45 | box-sizing: border-box;
46 | word-wrap: break-word;
47 | width: 100%;
48 | text-decoration: none;
49 | user-select: none;
50 | `
51 |
52 | const SizeText = styled.span`
53 | margin-left: 6px;
54 | font-size: 11px;
55 |
56 | `
57 |
58 | const DownloadIcon =
67 |
68 |
69 |
70 | const Video = styled.video<{
71 | borderCss: string,
72 | }>`
73 | width: 100%;
74 | height: 240px;
75 |
76 | ${({ borderCss }) => borderCss};
77 |
78 | `
79 |
80 |
81 | export default function MediaContent({
82 | type,
83 | url,
84 | size,
85 | last,
86 | single,
87 | messageType
88 | }: Props) {
89 |
90 | return (
91 | <>
92 | {(type === 'image' || type === 'gif') &&
93 |
94 | getBorderCss({
96 | type: messageType,
97 | last,
98 | single
99 | }))()}
100 | src={url}
101 | alt={url} />
102 |
103 | }
104 |
105 |
106 |
107 | {(type === 'file' || type === 'video') &&
108 |
109 | {type === 'video' &&
110 |
getBorderCss({
113 | type: messageType,
114 | last,
115 | single
116 | }))()}
117 | >
118 |
119 |
120 | Your browser does not support the video tag.
121 |
122 | }
123 |
124 | {DownloadIcon} {url}{size && ({size}) }
127 |
128 |
129 |
130 | }
131 | >
132 | )
133 | }
--------------------------------------------------------------------------------
/src/components/message/outgoing-message/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import styled from 'styled-components'
3 | import MediaContent from '../media-content'
4 | import { getBorderCss } from '../borderController'
5 | import TextContent from '../text-content'
6 | import Timestamp from '../timestamp'
7 | import { Props } from '..'
8 | import useColorSet from '../../../hooks/useColorSet'
9 | import MinChatUIContext from '../../../contexts/MinChatUIContext'
10 |
11 |
12 | export const Wrapper = styled.div<{ firstMessage?: boolean, lastMessage?: boolean }>`
13 | display:flex;
14 | justify-content: end;
15 | margin-right: 10px;
16 | margin-top: ${({ firstMessage }) => firstMessage ? "16px" : "2px"};
17 | position: relative;
18 | box-sizing: border-box;
19 | margin-bottom: ${({ lastMessage }) => lastMessage ? "16px" : "2px"};
20 | z-index: 1;
21 | `
22 |
23 |
24 |
25 | export const Container = styled.div`
26 | max-width:272px;
27 | min-width:80px;
28 | margin-left: 10px;
29 | justify-content:flex-end;
30 | align-items:flex-end;
31 | gap:10px;
32 | position:relative;
33 | box-sizing: border-box;
34 | `
35 | export const Background = styled.div<{
36 | bgColor: string,
37 | borderCss: string,
38 | }>`
39 | position: absolute;
40 | width: 100%;
41 | height: 100%;
42 | background-color:${({ bgColor }) => bgColor};
43 |
44 | ${({ borderCss }) => borderCss};
45 | `
46 |
47 |
48 |
49 |
50 |
51 |
52 | export default function MyMessage({
53 | text,
54 | media,
55 | loading,
56 | last,
57 | single,
58 | clusterFirstMessage,
59 | clusterLastMessage,
60 | created_at,
61 | seen
62 | }: Omit) {
63 |
64 | const { themeColor } = useContext(MinChatUIContext)
65 |
66 | const textColor = useColorSet("--outgoing-message-text-color")
67 | const backgroundColor = useColorSet("--outgoing-message-background-color")
68 | const timestampColor = useColorSet("--outgoing-message-timestamp-color")
69 | const checkmarkColor = useColorSet("--outgoing-message-checkmark-color")
70 | const loaderColor = useColorSet("--outgoing-message-loader-color")
71 | const linkColor = useColorSet("--outgoing-message-link-color")
72 |
73 |
74 |
75 |
76 |
77 |
78 | return (
79 |
85 |
86 |
87 |
88 |
89 | getBorderCss({
91 | type: "outgoing",
92 | last,
93 | single
94 | }))()}
95 | bgColor={backgroundColor || themeColor} />
96 |
97 | {media ?
102 | :
103 | {text} }
107 |
108 |
116 |
117 |
118 |
119 |
120 |
121 | )
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/src/components/message/text-content/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | type Props = {
5 | children?: string
6 | color?: string
7 | linkColor?: string
8 | }
9 |
10 | export const Content = styled.div<{
11 | color?: string
12 | linkColor?: string
13 | }>`
14 | text-align:left;
15 | vertical-align:text-top;
16 | font-size:14px;
17 | align-self:flex-start;
18 | line-height:auto;
19 | color:${({ color }) => color || '#000000'};
20 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
21 | padding-left:16px;
22 | padding-right:16px;
23 | padding-top:8px;
24 | padding-bottom:8px;
25 | position: relative;
26 | box-sizing: border-box;
27 | word-wrap: break-word;
28 | width: 100%;
29 |
30 | user-select: none;
31 |
32 | a {
33 | color: ${({ linkColor }) => linkColor || 'blue'};
34 | }
35 |
36 | `
37 |
38 |
39 | export default function TextContent({
40 | linkColor,
41 | color,
42 | children = ""
43 | }: Props) {
44 |
45 | // Regular expression to match URLs
46 | const urlRegex = /(https?:\/\/[^\s]+)/g;
47 |
48 | return ($&') }} />
52 | )
53 | }
--------------------------------------------------------------------------------
/src/components/message/timestamp/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import Loading from './loading'
3 | import styled from 'styled-components'
4 | import { calculateTimeAgo } from '../../../utils/date-utils'
5 |
6 | type Props = {
7 | loading?: boolean
8 | date?: Date
9 | seen?: boolean
10 | showSeen?: boolean
11 | color?: string
12 | loaderColor?: string
13 | checkmarkColor?: string
14 | }
15 |
16 |
17 | const LoadingContainer = styled.div<{
18 | color?: string
19 | }>`
20 | position: relative;
21 | height: 100%;
22 | display: flex;
23 | align-items: center;
24 | margin-right:4px;
25 | margin-left:2px;
26 |
27 |
28 | ${({ color }) => color ? `color: ${color};` : ''}
29 | `
30 |
31 | export const Content = styled.div<{
32 | color?: string
33 | }>`
34 | text-align:right;
35 | vertical-align:text-top;
36 | font-size:12px;
37 |
38 | margin-right:2px;
39 | align-self:flex-start;
40 | line-height:auto;
41 | color: ${({ color }) => color || 'rgb(75 85 99)'};
42 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
43 | `
44 |
45 | const Check = styled.div<{
46 | color?: string
47 | }>`
48 | position:relative;
49 | width:16px;
50 | height:16px;
51 | padding-bottom:4px;
52 | padding-right:4px;
53 | color: ${({ color }) => color || 'rgb(75 85 99)'};
54 | `
55 |
56 | const PlaceholderCheck = styled(Check)`
57 | width:8px;
58 | `
59 |
60 | const Container = styled.div`
61 | display:flex;
62 | width: 100%;
63 | position: relative;
64 | justify-content: end;
65 | align-items: center;
66 | margin-top: -8px;
67 | user-select: none;
68 |
69 | `
70 |
71 | export default function Timestamp({
72 | loading,
73 | date,
74 | showSeen,
75 | seen,
76 | color,
77 | loaderColor,
78 | checkmarkColor
79 | }: Props) {
80 |
81 | const [dateSent, setDateSent] = useState()
82 |
83 | useEffect(() => {
84 | function updateDateSent() {
85 | if (date) {
86 | setDateSent(calculateTimeAgo(date))
87 | }
88 | }
89 |
90 | updateDateSent()
91 |
92 | const intervalId = setInterval(() => updateDateSent(), 60_000)
93 |
94 | return () => clearInterval(intervalId);
95 |
96 | }, [])
97 |
98 | return (
99 |
100 |
101 | {dateSent}
102 |
103 | {loading ?
104 |
105 | :
106 | (showSeen ?
107 |
108 |
109 |
111 | {seen ?
112 |
113 |
114 |
124 | :
125 |
126 |
130 |
133 |
134 |
135 | }
136 |
137 | :
138 |
139 | )
140 | }
141 |
142 | )
143 | }
--------------------------------------------------------------------------------
/src/components/message/timestamp/loading/index.css:
--------------------------------------------------------------------------------
1 | .message-lds-ring {
2 | display: inline-block;
3 | position: relative;
4 | width: 12px;
5 | height: 12px;
6 | }
7 |
8 | .message-lds-ring div:nth-child(1) {
9 | animation-delay: -0.45s;
10 | }
11 |
12 | .message-lds-ring div:nth-child(2) {
13 | animation-delay: -0.3s;
14 | }
15 |
16 | .message-lds-ring div:nth-child(3) {
17 | animation-delay: -0.15s;
18 | }
19 |
20 | @keyframes lds-ring {
21 | 0% {
22 | transform: rotate(0deg);
23 | }
24 |
25 | 100% {
26 | transform: rotate(360deg);
27 | }
28 | }
--------------------------------------------------------------------------------
/src/components/message/timestamp/loading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import "./index.css"
4 |
5 | type Props = {
6 | color?: string
7 | }
8 |
9 |
10 | const Container = styled.div`
11 | display: flex;
12 | justify-content: center;
13 | align-items: center
14 | ;
15 | `
16 |
17 | const InnerContainer = styled.div<{
18 | color?: string
19 | }>`
20 | box-sizing: border-box;
21 | display: block;
22 | position: absolute;
23 | width: 8px;
24 | height: 8px;
25 |
26 | position: absolute;
27 | left: 0;
28 | right: 0;
29 | margin-left: auto;
30 | margin-right: auto;
31 | margin-top: auto;
32 | margin-bottom: auto;
33 | top: 0;
34 | bottom: 0;
35 |
36 | /* margin: 6px; */
37 | border: 2px solid ${({ color }) => color || '#fff'};
38 | border-radius: 50%;
39 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
40 | border-color: ${({ color }) => color || '#fff'} transparent transparent transparent;
41 | `
42 |
43 | export default function Loading({ color }: Props) {
44 |
45 | return (
46 |
47 |
53 |
54 |
55 | )
56 | }
--------------------------------------------------------------------------------
/src/components/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | export type Props = {
5 | children: any
6 | }
7 |
8 | const Container = styled.div`
9 | /* max-width: 384px; */
10 | padding-top: 16px;
11 | padding-bottom: 16px;
12 | width: 35%;
13 | height: 100%;
14 | position: relative;
15 | box-sizing: border-box;
16 |
17 | `
18 |
19 | export default function Sidebar({ children }: Props) {
20 | return (
21 | {children}
22 | )
23 | }
--------------------------------------------------------------------------------
/src/components/typing-indicator/index.css:
--------------------------------------------------------------------------------
1 | .loading-animation {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
6 | @keyframes loading-animation-move {
7 | 0% {
8 | transform: translateY(0);
9 | }
10 | 50% {
11 | transform: translateY(-5px);
12 | }
13 | 100% {
14 | transform: translateY(0);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/typing-indicator/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import './index.css'
4 |
5 | export type Props = {
6 | content?: string
7 | themeColor?: string
8 | }
9 |
10 | const Container = styled.div`
11 | display: flex;
12 | align-items: center;
13 | flex-direction: row;
14 | /* this is to compensate for the width of the other message dp and its margin left */
15 | margin-left: 42px;
16 | margin-bottom: 16px;
17 | margin-top: 16px;
18 | `
19 |
20 | const Text = styled.div<{ themeColor: string }>`
21 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
22 | font-size:12px;
23 | line-height:auto;
24 | font-weight: 600;
25 | margin-left: 8px;
26 |
27 | color: ${({ themeColor }) => themeColor};
28 | `
29 |
30 | const Dot1 = styled.div<{ themeColor: string }>`
31 | width: 5px;
32 | height: 5px;
33 | border-radius: 50%;
34 | background-color: ${({ themeColor }) => themeColor};
35 | animation: loading-animation-move 0.7s ease-in-out infinite;
36 | margin-right: 4px;
37 |
38 | animation-delay: 0ms;
39 |
40 | `
41 | const Dot2 = styled(Dot1)`
42 | animation-delay: 0.2s;
43 | `
44 | const Dot3 = styled(Dot1)`
45 | animation-delay: 0.4s;
46 | margin-right: 0;
47 | `
48 |
49 | export default function TypingIndicator({
50 | content,
51 | themeColor = '#6ea9d7'
52 | }: Props) {
53 |
54 | return (
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {content}
63 |
64 | )
65 | }
--------------------------------------------------------------------------------
/src/contexts/MinChatUIContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export default createContext<{
4 | colorSet?: {}
5 | themeColor: string
6 | }>({
7 | colorSet: {},
8 | themeColor: '#6ea9d7'
9 | })
--------------------------------------------------------------------------------
/src/hooks/useCheckIsMobile.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 |
4 | const useCheckIsMobile = (containerRef: React.RefObject) => {
5 |
6 |
7 | const [isMobile, setIsMobile] = useState(false);
8 |
9 | useEffect(() => {
10 | if (containerRef?.current) {
11 | const resizeObserver = new ResizeObserver(entries => {
12 | if (!Array.isArray(entries) || !entries.length) {
13 | return;
14 | }
15 |
16 | const width = entries[0].contentRect.width
17 | setIsMobile(width < 768);
18 | });
19 |
20 | resizeObserver.observe(containerRef.current);
21 |
22 |
23 | return () => {
24 | if (containerRef.current) {
25 | resizeObserver.unobserve(containerRef.current);
26 | }
27 | }
28 | }
29 |
30 | return () =>{}
31 |
32 | }, [containerRef?.current]);
33 |
34 | return isMobile
35 | }
36 |
37 | export default useCheckIsMobile
--------------------------------------------------------------------------------
/src/hooks/useColorSet.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import MinChatUIContext from "../contexts/MinChatUIContext";
3 |
4 |
5 |
6 | export default function useColorSet(label: string): string | undefined {
7 |
8 | const { colorSet } = useContext(MinChatUIContext)
9 |
10 | return colorSet ? (colorSet as any)[label] : undefined
11 | }
--------------------------------------------------------------------------------
/src/hooks/useDetectScrollPosition.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject } from "react"
2 |
3 |
4 | const useDetectScrollPosition = (ref: RefObject) => {
5 |
6 | const detectTop = () => {
7 | if (ref.current) {
8 | return ref.current.scrollTop < 50
9 | }
10 | return false
11 | }
12 |
13 | // const detectBottom = () => {
14 | // if (ref.current) {
15 | // return ref.current.scrollHeight - ref.current.scrollTop === ref.current.clientHeight
16 | // }
17 | // return false
18 | // }
19 |
20 |
21 | const detectBottom = () => {
22 | if (ref.current) {
23 | const threshold = 100;
24 | return ref.current.scrollHeight - ref.current.scrollTop <= ref.current.clientHeight + threshold;
25 | }
26 | return false;
27 | }
28 |
29 |
30 |
31 | return { detectTop, detectBottom }
32 |
33 |
34 | }
35 |
36 | export default useDetectScrollPosition
37 |
--------------------------------------------------------------------------------
/src/hooks/useTypingListener.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 |
4 |
5 |
6 | export type Props = {
7 | onStartTyping?: () => void,
8 | onEndTyping?: () => void,
9 | }
10 |
11 | const useTypingListener = (props: Props) => {
12 | const [typing, setTyping] = useState(false);
13 |
14 | let timeout: any;
15 |
16 | useEffect(() => {
17 | //call the function when typing starts or ends but should not call it on every render and should only be called when the value of typing changes
18 | if (typing) {
19 | props.onStartTyping && props.onStartTyping()
20 | } else {
21 | props.onEndTyping && props.onEndTyping()
22 | }
23 |
24 | }, [typing])
25 |
26 | const onKeyDown = () => {
27 | clearTimeout(timeout);
28 | setTyping(true);
29 | }
30 |
31 | const onKeyUp = () => {
32 | timeout = setTimeout(() => {
33 | setTyping(false);
34 | }, 2_000);
35 | }
36 | return { setTyping, onKeyUp, onKeyDown }
37 |
38 | }
39 |
40 | export default useTypingListener
41 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .fade-animation {
2 | animation: fadeIn 0.2s ease-in-out;
3 | }
4 |
5 | .fade-animation-slow {
6 | animation: fadeIn 0.4s ease-in-out;
7 | }
8 |
9 | @keyframes fadeIn {
10 | from {
11 | opacity: 0;
12 | }
13 |
14 | to {
15 | opacity: 1;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import MessageList from "./components/message-list";
2 | import MessageListBackground from "./components/message-list-background";
3 | import ConversationList from "./components/conversation-list";
4 | import MainContainer from "./components/main-container";
5 | import Loading from "./components/loading";
6 | import Message from "./components/message"
7 | import MessageInput from "./components/message-input";
8 | import MessageHeader from "./components/message-header";
9 | import MessageContainer from "./components/message-container";
10 | import ConversationHeader from "./components/conversation-header";
11 | import TypingIndicator from "./components/typing-indicator";
12 | import useCheckIsMobile from "./hooks/useCheckIsMobile";
13 | import useTypingListener from "./hooks/useTypingListener";
14 |
15 | import Sidebar from "./components/sidebar"
16 | import MinChatUiProvider from "./providers/MinChatUiProvider";
17 |
18 |
19 |
20 |
21 | export {
22 | Loading,
23 | Message,
24 | MessageInput,
25 | MessageHeader,
26 | MessageList,
27 | ConversationList,
28 | MainContainer,
29 | ConversationHeader,
30 | TypingIndicator,
31 | useCheckIsMobile,
32 | useTypingListener,
33 | Sidebar,
34 | MessageListBackground,
35 | MessageContainer,
36 | MinChatUiProvider
37 | }
--------------------------------------------------------------------------------
/src/providers/MinChatUiProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MinChatUIContext from '../contexts/MinChatUIContext'
3 |
4 | type Props = {
5 | colorSet?: {},
6 | theme?: string
7 | children: any
8 | }
9 |
10 | export default function MinChatUiProvider({
11 | colorSet,
12 | children,
13 | theme
14 | }: Props) {
15 |
16 | return (
17 |
18 | {children}
19 |
20 | )
21 | }
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png";
2 | declare module "*.svg";
3 | declare module "*.jpeg";
4 | declare module "*.jpg";
5 | declare module "*.webp";
6 |
--------------------------------------------------------------------------------
/src/types/ConversationType.tsx:
--------------------------------------------------------------------------------
1 | import MessageType from "./MessageType"
2 |
3 | type ConversationType = {
4 | title: string,
5 | lastMessage?: MessageType
6 | unread?:boolean
7 | avatar?: string
8 | id?: string
9 | }
10 |
11 | export default ConversationType
--------------------------------------------------------------------------------
/src/types/MessageType.tsx:
--------------------------------------------------------------------------------
1 | import UserType from "./UserType"
2 |
3 | export interface MediaType {
4 | type: "image" | "video" | "file" | "gif"
5 | url: string
6 | size?: string
7 | name?: string
8 | }
9 |
10 | type MessageType = {
11 | user: UserType
12 | id?: string
13 | text?: string
14 | media?: MediaType
15 | createdAt?: Date
16 | seen?: boolean
17 | loading?: boolean
18 | }
19 |
20 | export default MessageType
--------------------------------------------------------------------------------
/src/types/UserType.tsx:
--------------------------------------------------------------------------------
1 | type UserType = {
2 | id?: string,
3 | name?: string,
4 | avatar?: string
5 | }
6 |
7 | export default UserType
--------------------------------------------------------------------------------
/src/utils/date-utils/index.ts:
--------------------------------------------------------------------------------
1 | function calculateDifferences(date: Date) {
2 | try {
3 | const currentDate = new Date()
4 | const timeDifference = (new Date(currentDate.toUTCString())).getTime() - (new Date(date ? date.toUTCString() : "")).getTime();
5 | const minutesAgo = Math.floor(timeDifference / (1000 * 60));
6 | const hoursAgo = Math.floor(minutesAgo / 60);
7 | const daysAgo = Math.floor(hoursAgo / 24);
8 |
9 | return {
10 | minutesAgo,
11 | hoursAgo,
12 | daysAgo
13 | }
14 | } catch (e) {
15 | return {
16 | minutesAgo: 0,
17 | hoursAgo: 0,
18 | daysAgo: 0
19 | }
20 | }
21 | }
22 |
23 |
24 | export function calculateLastSeen(date: Date): string {
25 | const diff = calculateDifferences(date)
26 |
27 | if (diff.minutesAgo < 1) {
28 | return 'Active now'
29 | } else if (diff.minutesAgo === 1) {
30 | return 'Seen 1 minute ago'
31 | } else if (diff.minutesAgo < 60) {
32 | return `Seen ${diff.minutesAgo} minutes ago`
33 | } else if (diff.hoursAgo === 1) {
34 | return 'Seen 1 hour ago'
35 | } else if (diff.hoursAgo < 24) {
36 | return `Seen ${diff.hoursAgo} hours ago`
37 | } else if (diff.daysAgo === 1) {
38 | return 'Seen 1 day ago'
39 | } else {
40 | return `Seen ${diff.daysAgo} days ago`
41 | }
42 | }
43 |
44 | /**
45 | *
46 | * @param date
47 | * @returns
48 | */
49 | export function calculateTimeAgo(date: Date): string {
50 |
51 | const diff = calculateDifferences(date)
52 |
53 | if (diff.minutesAgo < 1) {
54 | return 'just now';
55 | }
56 | else if (diff.minutesAgo < 60) {
57 | return `${diff.minutesAgo}m`
58 | } else if (diff.hoursAgo < 24) {
59 | return `${diff.hoursAgo}h`
60 | } else {
61 | return `${diff.daysAgo}d`
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/stories/Conversation.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import Conversation, { Props } from "../src/components/conversation"
5 |
6 | const meta: Meta = {
7 | title: 'Conversation',
8 | component: Conversation,
9 | parameters: {
10 | controls: { expanded: true },
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | const Template: Story = args => { }} />
30 |
31 | const UnseenTemplate: Story = args => { }} />
45 |
46 |
47 | const HtmlTemplate: Story = args => Hello everbody"
53 | ,
54 | user: {
55 | id: "martha_stewart",
56 | name: "Daniel",
57 | avatar: "https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg"
58 | },
59 | }}
60 | onClick={() => { }} />
61 |
62 | const UnseenFileTemplate: Story = args => { }} />
81 |
82 |
83 |
84 | const NoAvatarTemplate: Story = args => { }} />
97 |
98 | const SelectedTemplate: Story = args => { }} />
112 |
113 | const ImageMessageTemplate: Story = args => { }} />
131 |
132 | const FileMessageTemplate: Story = args => { }} />
149 |
150 | const VideoMessageTemplate: Story = args => { }} />
167 |
168 | const GifMessageTemplate: Story = args => { }} />
185 |
186 |
187 |
188 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
189 | // https://storybook.js.org/docs/react/workflows/unit-testing
190 | export const Default = Template.bind({});
191 | export const NewMessage = UnseenTemplate.bind({});
192 | export const NewFileMessage = UnseenFileTemplate.bind({});
193 | export const WithPlaceholderAvatar = NoAvatarTemplate.bind({});
194 | export const Selected = SelectedTemplate.bind({});
195 | export const ImageMessage = ImageMessageTemplate.bind({});
196 | export const VideoMessage = VideoMessageTemplate.bind({});
197 | export const FileMessage = FileMessageTemplate.bind({});
198 | export const GifMessage = GifMessageTemplate.bind({});
199 | export const Html = HtmlTemplate.bind({});
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
--------------------------------------------------------------------------------
/stories/ConversationHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import ConversationHeader, { Props } from "../src/components/conversation-header"
5 |
6 | const meta: Meta = {
7 | title: 'ConversationHeader',
8 | component: ConversationHeader,
9 | parameters: {
10 | controls: { expanded: true },
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | const Template: Story = args =>
17 |
18 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
19 | // https://storybook.js.org/docs/react/workflows/unit-testing
20 | export const Default = Template.bind({});
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/stories/ConversationList.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import ConversationList, { Props } from '../src/components/conversation-list';
4 | import styled from 'styled-components';
5 | import { chats, fewChats } from './data';
6 |
7 |
8 | const meta: Meta = {
9 | title: 'ConversationList',
10 | component: ConversationList,
11 | parameters: {
12 | controls: { expanded: true },
13 | },
14 | };
15 |
16 | export default meta
17 |
18 |
19 | const Template: Story = args => undefined}
25 | />
26 |
27 | const TemplateCustomConversationItem: Story = args => {conversation.lastMessage?.text}
}
33 | />
34 |
35 | const NoChatsTemplate: Story = args =>
42 |
43 | const CustomNoChatsTemplate: Story = args => Custom No Chats View...}
49 |
50 | />
51 |
52 | const LoadingTemplate: Story = args =>
65 |
66 |
67 | const CustomLoadingTemplate: Story = args => Custom Loading...
}
79 |
80 | />
81 |
82 |
83 | const FewChatsTemplate: Story = args =>
;
89 |
90 | const WithPaddingContainer = styled.div`
91 | height: 500px;
92 | padding: 20px;
93 | background-color: red;
94 | position: relative;
95 | width: 500px;
96 | `
97 | const TemplateWithPadding: Story = args =>
98 | ;
105 |
106 |
107 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
108 | // https://storybook.js.org/docs/react/workflows/unit-testing
109 | export const Default = Template.bind({});
110 |
111 | export const WithPadding = TemplateWithPadding.bind({});
112 | export const FewConversation = FewChatsTemplate.bind({});
113 | export const LoadingConversation = LoadingTemplate.bind({});
114 | export const CustomLoadingConversation = CustomLoadingTemplate.bind({});
115 | export const NoConversation = NoChatsTemplate.bind({});
116 | export const CustomNoConversation = CustomNoChatsTemplate.bind({});
117 | export const CustomConversationItem = TemplateCustomConversationItem.bind({});
118 |
119 |
120 |
121 |
122 |
123 | Default.args = {};
124 |
--------------------------------------------------------------------------------
/stories/MainContainer.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import MainContainer, { Props } from '../src/components/main-container';
4 | import styled from 'styled-components';
5 | import { chats, messages, fewMessages } from './data';
6 | import Sidebar from '../src/components/sidebar';
7 | import ConversationList from '../src/components/conversation-list';
8 | import ConversationHeader from '../src/components/conversation-header';
9 |
10 | import MessageHeader from '../src/components/message-header';
11 | import MessageInput from '../src/components/message-input';
12 | import MessageList from '../src/components/message-list';
13 | import MessageContainer from '../src/components/message-container';
14 | import MessageListBackground from '../src/components/message-list-background';
15 | import MinChatUIProvider from '../src/providers/MinChatUiProvider'
16 | import MinChatUiProvider from '../src/providers/MinChatUiProvider';
17 |
18 |
19 |
20 |
21 | const meta: Meta = {
22 | title: 'MainContainer',
23 | component: MainContainer,
24 | argTypes: {
25 | selectedConversation: {
26 | onSendMessage: {
27 | action: "sendMessage"
28 | }
29 | },
30 | inbox: {
31 | onConversationClick: {
32 | action: "select COnversation"
33 | }
34 | }
35 | },
36 | parameters: {
37 | controls: { expanded: true },
38 | },
39 | };
40 |
41 | export default meta
42 |
43 |
44 |
45 | const Provider = ({ children }: any) => {children}
102 |
103 |
104 | const CustomColorsTemplate: Story = args => {
105 |
106 |
107 | return
108 |
111 |
112 |
113 |
114 |
117 |
118 |
119 |
120 | Welcome
123 |
127 |
128 |
129 |
130 |
131 | }
132 |
133 |
134 | const LoadingCustomColorsTemplate: Story = args => {
135 |
136 |
137 | return
138 |
141 |
142 |
145 |
146 |
147 |
148 |
149 |
153 |
154 |
155 |
156 | }
157 |
158 | const MobileChatListCustomColorsTemplate: Story = args => {
159 | return
160 |
163 |
164 |
165 |
169 |
170 |
171 | }
172 |
173 | const MobileMessageCustomColorsTemplate: Story = args => {
174 | return
175 |
178 |
179 | Welcome
182 |
186 |
187 |
188 |
189 |
190 | }
191 |
192 |
193 | const NoMessageCustomColorsTemplate: Story = args => {
194 | return
195 |
198 |
199 | Welcome
202 |
206 |
207 |
208 |
209 |
210 | }
211 |
212 | const NoConversationCustomColorsTemplate: Story = args => {
213 | return
214 |
217 |
218 |
219 |
223 |
224 |
225 |
226 |
227 | }
228 |
229 |
230 |
231 | const Template: Story = args => {
232 |
233 |
234 | return
237 |
238 |
239 |
240 |
243 |
244 |
245 |
246 | Welcome
247 |
251 |
252 |
253 |
254 |
255 | }
256 |
257 | const NoSelectedChatTemplate: Story = args => {
258 |
259 |
260 | return
261 |
262 |
263 |
264 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | }
274 |
275 | const MobileTemplate: Story = args => {
276 | return
277 |
278 |
279 |
280 |
281 |
284 |
285 |
286 |
287 |
288 |
289 |
290 | }
291 |
292 | // const LoadingTemplate: Story = args => {
293 |
294 |
295 | // return
298 | //
299 | //
300 |
301 | //
305 | //
306 |
307 | //
308 | // Welcome
309 | //
313 | //
314 | //
315 | //
316 | // }
317 |
318 |
319 | // const MobileNoSelectedChatTemplate: Story = args =>
320 | //
321 | //
333 |
334 | // const NoChatsTemplate: Story = args => ;
344 |
345 | // const WithPaddingContainer = styled.div`
346 | // height: 500px;
347 | // padding: 20px;
348 | // border: #FF0000 1px solid;
349 | // position: relative;
350 | // width: 800px;
351 | // `
352 | // const TemplateWithPadding: Story = args =>
356 | // console.log("onSendMessage"),
370 | // onBack: () => { }
371 |
372 | // }}
373 | // />
374 | //
375 | //
376 |
377 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
378 | // https://storybook.js.org/docs/react/workflows/unit-testing
379 | export const Default = Template.bind({});
380 | export const Mobile = MobileTemplate.bind({});
381 | // export const MobileNoSelectedConversation = MobileNoSelectedChatTemplate.bind({});
382 | export const NoSelectedConversation = NoSelectedChatTemplate.bind({});
383 | // export const NoConversation = NoChatsTemplate.bind({});
384 | // export const WithPadding = TemplateWithPadding.bind({});
385 | // export const LoadingConversation = LoadingTemplate.bind({});
386 | export const CustomColors = CustomColorsTemplate.bind({});
387 |
388 | export const MobileChatListCustomColors = MobileChatListCustomColorsTemplate.bind({});
389 | export const MobileMessageCustomColors = MobileMessageCustomColorsTemplate.bind({});
390 | export const LoadingCustomColors = LoadingCustomColorsTemplate.bind({});
391 | export const NoMessageCustomColors = NoMessageCustomColorsTemplate.bind({});
392 | export const NoConversationCustomColors = NoConversationCustomColorsTemplate.bind({});
393 |
394 |
395 |
396 |
397 | Default.args = {};
398 |
--------------------------------------------------------------------------------
/stories/Message.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import Message, { Props } from "../src/components/message"
5 |
6 | const meta: Meta = {
7 | title: 'Message',
8 | component: Message,
9 | parameters: {
10 | controls: { expanded: true },
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | const date = new Date()
17 | const minutesAgoDate = new Date(date)
18 | const hoursAgoDate = new Date(date)
19 | const daysAgoDate = new Date(date)
20 | const monthsAgoDate = new Date(date)
21 |
22 |
23 |
24 | minutesAgoDate.setMinutes(minutesAgoDate.getMinutes() - 10)
25 | hoursAgoDate.setHours(hoursAgoDate.getHours() - 3)
26 | daysAgoDate.setDate(daysAgoDate.getDate() - 10)
27 | monthsAgoDate.setMonth(monthsAgoDate.getMonth() - 2)
28 |
29 | const LeftTemplate: Story = args =>
40 |
41 | const IncomingWithLinkTemplate: Story = args =>
54 |
55 | const IncomingWithHTMLTemplate: Story = args =>
68 |
69 | const IncomingWithLinkAndHTMLTemplate: Story = args =>
82 |
83 | const OutgoingWithLinkTemplate: Story = args =>
96 |
97 |
98 |
99 | const RightTemplate: Story = args =>
112 |
113 |
114 | const LoadingTemplate: Story = args =>
128 |
129 | const WithAvatarTemplate: Story = args =>
142 |
143 | const ImageContentTemplate: Story = args =>
159 |
160 | const FileContentTemplate: Story = args =>
178 |
179 | const VideoContentTemplate: Story = args =>
196 |
197 | const GifContentTemplate: Story = args =>
214 |
215 | const WithHeaderTemplate: Story = args =>
229 |
230 | const LastIncomingTemplate: Story = args =>
244 |
245 |
246 | const SingleIncomingTemplate: Story = args =>
260 |
261 |
262 |
263 | const SingleOutgoingTemplate: Story = args =>
276 |
277 | const LastOutgoingTemplate: Story = args =>
290 |
291 | const OutgoingImageContentTemplate: Story = args =>
306 |
307 | const OutgoingFileContentTemplate: Story = args =>
323 |
324 | const OutgoingVideoContentTemplate: Story = args =>
341 |
342 |
343 | const OutgoingGifContentTemplate: Story = args =>
360 |
361 |
362 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
363 | // https://storybook.js.org/docs/react/workflows/unit-testing
364 |
365 | export const Incoming = LeftTemplate.bind({});
366 | export const IncomingWithLink = IncomingWithLinkTemplate.bind({});
367 | export const IncomingWithHTML = IncomingWithHTMLTemplate.bind({});
368 | export const IncomingWithLinkAndHTML = IncomingWithLinkAndHTMLTemplate.bind({});
369 |
370 | export const IncomingWithAvatar = WithAvatarTemplate.bind({});
371 | export const IncomingWithHeader = WithHeaderTemplate.bind({});
372 | export const SingleIncoming = SingleIncomingTemplate.bind({});
373 | export const LastIncoming = LastIncomingTemplate.bind({});
374 | export const IncomingImageContent = ImageContentTemplate.bind({});
375 | export const IncomingFileContent = FileContentTemplate.bind({});
376 | export const IncomingVideoContent = VideoContentTemplate.bind({});
377 | export const IncomingGifContent = GifContentTemplate.bind({});
378 |
379 |
380 |
381 | export const Outgoing = RightTemplate.bind({});
382 | export const SingleOutgoing = SingleOutgoingTemplate.bind({});
383 | export const LastOutgoing = LastOutgoingTemplate.bind({});
384 | export const SendMessageLoading = LoadingTemplate.bind({});
385 | export const OutgoingWithLink = OutgoingWithLinkTemplate.bind({});
386 | export const OutgoingImageContent = OutgoingImageContentTemplate.bind({});
387 | export const OutgoingFileContent = OutgoingFileContentTemplate.bind({});
388 | export const OutgoingVideoContent = OutgoingVideoContentTemplate.bind({});
389 | export const OutgoingGifContent = OutgoingGifContentTemplate.bind({});
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
--------------------------------------------------------------------------------
/stories/MessageContainer.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import { chats, messages, fewMessages } from './data';
5 | import MessageHeader from '../src/components/message-header';
6 | import MessageInput from '../src/components/message-input';
7 | import MessageList from '../src/components/message-list';
8 | import MessageContainer, { Props } from '../src/components/message-container';
9 |
10 |
11 |
12 |
13 |
14 | const meta: Meta = {
15 | title: 'MessagesContainer',
16 | component: MessageContainer,
17 | argTypes: {
18 | },
19 | parameters: {
20 | controls: { expanded: true },
21 | },
22 | };
23 |
24 | export default meta
25 |
26 |
27 |
28 | const Template: Story = args => {
29 |
30 | return
31 | Welcome
32 |
36 |
37 |
38 | }
39 |
40 |
41 | const FewMessagesTemplate: Story = args => {
42 |
43 | return
44 | Welcome
45 |
49 |
50 |
51 |
52 | }
53 |
54 | const NoMessagesTemplate: Story = args => {
55 |
56 | return
57 | Welcome
58 |
62 |
63 |
64 |
65 | }
66 |
67 | const MobileTemplate: Story = args => {
68 | return
69 | Welcome
70 |
75 |
76 |
77 |
78 | }
79 |
80 |
81 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
82 | // https://storybook.js.org/docs/react/workflows/unit-testing
83 | export const Default = Template.bind({});
84 | export const Mobile = MobileTemplate.bind({});
85 | export const FewMessages = FewMessagesTemplate.bind({});
86 | export const NoMessages = NoMessagesTemplate.bind({});
87 |
88 |
89 |
90 | Default.args = {};
91 |
--------------------------------------------------------------------------------
/stories/MessageHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import MessageHeader, { Props } from "../src/components/message-header"
5 |
6 | const meta: Meta = {
7 | title: 'MessageHeader',
8 | component: MessageHeader,
9 | parameters: {
10 | controls: { expanded: true },
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 | const date = new Date()
17 | const minutesAgoDate = new Date(date)
18 | const hoursAgoDate = new Date(date)
19 | const daysAgoDate = new Date(date)
20 | const monthsAgoDate = new Date(date)
21 |
22 |
23 |
24 | minutesAgoDate.setMinutes(minutesAgoDate.getMinutes() - 10)
25 | hoursAgoDate.setHours(hoursAgoDate.getHours() - 3)
26 | daysAgoDate.setDate(daysAgoDate.getDate() - 10)
27 | monthsAgoDate.setMonth(monthsAgoDate.getMonth() - 2)
28 |
29 |
30 |
31 |
32 |
33 | const Template: Story = args => Daniel
34 | const NoBackTemplate: Story = args => Daniel
35 | const ActiveNowTemplate: Story = args => Daniel
36 | const MinutesAgoTemplate: Story = args => Daniel
37 | const HoursAgoTemplate: Story = args => Daniel
38 | const DaysAgoTemplate: Story = args => Daniel
39 | const MonthsAgoTemplate: Story = args => Daniel
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
51 | // https://storybook.js.org/docs/react/workflows/unit-testing
52 | export const Default = Template.bind({});
53 | export const WithoutBackIcon = NoBackTemplate.bind({});
54 | export const ActiveNow = ActiveNowTemplate.bind({});
55 | export const MinutesAgo = MinutesAgoTemplate.bind({});
56 | export const HoursAgo = HoursAgoTemplate.bind({});
57 | export const DaysAgo = DaysAgoTemplate.bind({});
58 | export const MonthsAgo = MonthsAgoTemplate.bind({});
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/stories/MessageInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import MessageInput, { Props } from "../src/components/message-input"
5 | import MinChatUIProvider from "../src/providers/MinChatUiProvider"
6 | const meta: Meta = {
7 | title: 'MessageInput',
8 | component: MessageInput,
9 | parameters: {
10 | controls: { expanded: true },
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 |
17 | const Template: Story = args =>
18 |
21 | console.log("onAttachClick")}
24 | >
25 |
26 |
27 | const TemplateWithoutAttach: Story = args =>
28 |
31 | console.log("onAttachClick")}
34 | showAttachButton={false}
35 | >
36 |
37 |
38 | const TemplateWithoutSend: Story = args =>
39 |
42 | console.log("onAttachClick")}
45 | showAttachButton={false}
46 | showSendButton={false}
47 | >
48 |
49 |
50 | const TemplateDisabled: Story = args =>
51 |
54 | console.log("onAttachClick")}
57 | disabled={true}
58 | >
59 |
60 |
61 |
62 |
63 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
64 | // https://storybook.js.org/docs/react/workflows/unit-testing
65 | export const Default = Template.bind({});
66 | export const WithoutAttach = TemplateWithoutAttach.bind({});
67 |
68 | export const WithoutSend = TemplateWithoutSend.bind({});
69 | export const Disabled = TemplateDisabled.bind({});
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/stories/MessageList.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import MessageList, { MessageListProps } from '../src/components/message-list';
4 | import styled from 'styled-components';
5 | import { chats, messages, fewMessages } from './data';
6 |
7 | const meta: Meta = {
8 | title: 'MessageList',
9 | component: MessageList,
10 | parameters: {
11 | controls: { expanded: true },
12 | },
13 | };
14 |
15 | export default meta;
16 |
17 |
18 | const Template: Story = args =>
23 |
;
24 |
25 | const LoadingTemplate: Story = args =>
33 |
;
34 |
35 | const CustomLoadingTemplate: Story = args => Custom Loading...
}
42 | />
43 | ;
44 |
45 | const SendMessageLoadingTemplate: Story = args =>
64 |
;
65 |
66 |
67 | const FewEntriesTemplate: Story = args =>
74 |
;
75 |
76 | const NoEntriesTemplate: Story = args =>
83 |
;
84 |
85 | const CustomNoEntriesTemplate: Story = args => Custom No Messages View...
}
91 | />
92 | ;
93 |
94 |
95 | const WithPaddingContainer = styled.div`
96 | height: 400px;
97 | padding: 20px;
98 | position: relative;
99 | width: 500px;
100 | background-color: red;
101 | border: 3px solid black;
102 | `
103 |
104 | const TemplateWithPadding: Story = args =>
;
111 |
112 | const TypingIndicatorTemplate: Story = args =>
132 |
;
133 |
134 | const CustomTypingIndicatorTemplate: Story = args => Custom typing indicator working!
}
139 | themeColor='#6ea9d7'
140 | showTypingIndicator={true}
141 | typingIndicatorContent="Mark is typing"
142 | messages={[
143 | {
144 | "user": {
145 | "id": "danny_1",
146 | "name": "Daniel Georgetown",
147 | avatar: "https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg"
148 |
149 | },
150 | "text": "this message should have loading"
151 | },
152 | ...messages
153 | ]}
154 | />
155 | ;
156 |
157 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
158 | // https://storybook.js.org/docs/react/workflows/unit-testing
159 | export const Default = Template.bind({});
160 | export const WithPadding = TemplateWithPadding.bind({});
161 | export const FewMessages = FewEntriesTemplate.bind({});
162 | export const NoMessages = NoEntriesTemplate.bind({});
163 | export const CustomNoMessages = CustomNoEntriesTemplate.bind({});
164 | export const MessagesLoading = LoadingTemplate.bind({});
165 | export const CustomMessagesLoading = CustomLoadingTemplate.bind({});
166 |
167 | export const SendMessageLoading = SendMessageLoadingTemplate.bind({});
168 | export const TypingIndicator = TypingIndicatorTemplate.bind({});
169 | export const CustomTypingIndicator = CustomTypingIndicatorTemplate.bind({});
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | Default.args = {};
179 |
--------------------------------------------------------------------------------
/stories/Sidebar.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import Sidebar, { Props } from "../src/components/sidebar"
4 | import { chats } from './data';
5 | import ConversationList from '../src/components/conversation-list';
6 |
7 | const meta: Meta = {
8 | title: 'Sidebar',
9 | component: Sidebar,
10 | parameters: {
11 | controls: { expanded: true },
12 | },
13 | };
14 |
15 | export default meta;
16 |
17 | const Template: Story = args =>
23 |
24 |
25 |
26 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
27 | // https://storybook.js.org/docs/react/workflows/unit-testing
28 | export const Default = Template.bind({});
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/stories/TypingIndicator.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Meta, Story } from '@storybook/react';
3 | import styled from 'styled-components';
4 | import TypingIndicator, { Props } from "../src/components/typing-indicator"
5 |
6 | const meta: Meta = {
7 | title: 'TypingIndicator',
8 | component: TypingIndicator,
9 | parameters: {
10 | controls: { expanded: true },
11 | },
12 | };
13 |
14 | export default meta;
15 |
16 |
17 | const Template: Story = args =>
18 | const WithoutContentTemplate: Story = args =>
19 |
20 |
21 |
22 |
23 | // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
24 | // https://storybook.js.org/docs/react/workflows/unit-testing
25 | export const Default = Template.bind({});
26 | export const WithoutContent = WithoutContentTemplate.bind({});
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/stories/data.tsx:
--------------------------------------------------------------------------------
1 | import ConversationType from "../src/types/ConversationType";
2 | import MessageType from "../src/types/MessageType";
3 |
4 | const date = new Date()
5 |
6 | export const chats: ConversationType[] = [
7 | {
8 | id: '1',
9 | title: 'Epic gamers',
10 | avatar:
11 | 'https://images.pexels.com/photos/1704488/pexels-photo-1704488.jpeg?cs=srgb&dl=pexels-suliman-sallehi-1704488.jpg&fm=jpg',
12 | unread: true,
13 | lastMessage: {
14 | seen: false,
15 | createdAt: date,
16 | text: 'Hello everbody',
17 | user: {
18 | id: 'martha_stewart',
19 | name: 'Daniel',
20 | avatar: 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
21 | },
22 | },
23 | },
24 | {
25 | id: '2',
26 |
27 | title: 'Devops',
28 | lastMessage: {
29 | createdAt: date,
30 | seen: true,
31 | text: 'How do you enable an actuator on a servo motor of a hardware and design laboratory experiment in the city,an actuator on a servo motor of a hardware and design laboratory experiment in the city',
32 | user: {
33 | avatar: 'https://fsdfsdfsdfs',
34 | id: 'daniel',
35 | name: 'Stanley Herbert Lee',
36 | },
37 | },
38 | },
39 | {
40 | // id: "3",
41 | title: 'This is a group with a long title heading that is multilines long',
42 | avatar:
43 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
44 | lastMessage: {
45 | createdAt: date,
46 | seen: true,
47 | text: 'Hello everbody',
48 | media: {
49 | type: "image",
50 | url: 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
51 | },
52 | user: {
53 | id: 'me',
54 | name: 'Martha Stewart',
55 | },
56 | },
57 | },
58 | {
59 | id: '4',
60 | title: 'Epic gamers',
61 | lastMessage: {
62 | createdAt: date,
63 | seen: true,
64 | text: 'Hello everbody',
65 | user: {
66 | id: 'daniel',
67 | name: 'Daniel',
68 | avatar:
69 | 'https://cdn.pixabay.com/photo/2015/03/03/08/55/portrait-657116_1280.jpg',
70 | },
71 | },
72 | },
73 | {
74 | id: '5',
75 | title: 'Devops',
76 | lastMessage: {
77 | createdAt: date,
78 | seen: true,
79 | text: 'How do you enable an actuator',
80 |
81 | user: {
82 | id: 'daniel',
83 | name: 'Daniel',
84 | },
85 | },
86 | },
87 | {
88 | id: '6',
89 | title: 'Pigments',
90 | lastMessage: {
91 | createdAt: date,
92 | seen: true,
93 | text: 'Hello everbody',
94 | user: {
95 | id: 'daniel',
96 | name: 'Daniel',
97 | },
98 | },
99 | },
100 | {
101 | id: '7',
102 | title: 'Epic gamers',
103 | lastMessage: {
104 | createdAt: date,
105 | seen: true,
106 | text: 'Hello everbody',
107 | user: {
108 | id: 'daniel',
109 | name: 'Daniel',
110 | },
111 | },
112 | },
113 | {
114 | id: '8',
115 | title: 'Devops',
116 | lastMessage: {
117 | createdAt: date,
118 | seen: true,
119 | text: 'How do you enable an actuator',
120 |
121 | user: {
122 | id: 'daniel',
123 | name: 'Daniel',
124 | },
125 | },
126 | },
127 | {
128 | id: '9',
129 | title: 'Pigments',
130 | lastMessage: {
131 | createdAt: date,
132 | seen: true,
133 | text: 'Hello everbody',
134 | user: {
135 | id: 'daniel',
136 | name: 'Daniel',
137 | },
138 | },
139 | },
140 | ];
141 |
142 | export const fewChats = [
143 | {
144 | title: 'Epic gamers',
145 | lastMessage: {
146 | createdAt: date,
147 | seen: false,
148 | text: 'Hello everbody',
149 | user: {
150 | id: 'daniel',
151 | name: 'Daniel',
152 | },
153 | },
154 | },
155 | {
156 | id: '2',
157 | title: 'Devops',
158 | lastMessage: {
159 | seen: true,
160 | text: 'How do you enable an actuator',
161 |
162 | user: {
163 | id: 'daniel',
164 | name: 'Daniel',
165 | },
166 | },
167 | },
168 | {
169 | id: '3',
170 | title: 'Pigments',
171 | lastMessage: {
172 | createdAt: date,
173 | seen: true,
174 | text: 'Hello everbody',
175 | user: {
176 | id: 'daniel',
177 | name: 'Daniel',
178 | },
179 | },
180 | },
181 | ];
182 |
183 |
184 | export const messages: MessageType[] = [
185 | {
186 | user: {
187 | id: 'danny_1',
188 | name: 'Daniel Georgetown',
189 | avatar:
190 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
191 | },
192 | createdAt: date,
193 | text: 'first message',
194 | },
195 | {
196 | user: {
197 | id: 'mark',
198 | name: 'Markus',
199 | },
200 | createdAt: date,
201 | text: 'hello',
202 | },
203 | {
204 | user: {
205 | id: 'danny_1',
206 | name: 'Daniel Georgetown',
207 | },
208 | createdAt: date,
209 | text: 'last message 2',
210 | },
211 | {
212 | user: {
213 | id: 'danny_1',
214 | name: 'Daniel Georgetown',
215 | },
216 | createdAt: date,
217 | text: 'how do you think we should aproach this',
218 | },
219 | {
220 | user: {
221 | id: 'danny_1',
222 | name: 'Daniel Georgetown',
223 | },
224 | createdAt: date,
225 | text: 'sdfsdf',
226 | },
227 | {
228 | user: {
229 | id: 'danny_1',
230 | name: 'Daniel Georgetown',
231 | },
232 | media: {
233 | type: "image",
234 | url: 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
235 | },
236 | createdAt: date,
237 |
238 | },
239 | {
240 | user: {
241 | id: 'danny_1',
242 | name: 'Daniel Georgetown',
243 | },
244 | createdAt: date,
245 | text: 'how do you think we should aproach this https://google.com',
246 | },
247 | {
248 | user: {
249 | id: 'mark',
250 | name: 'Markus',
251 | },
252 | createdAt: date,
253 | media: {
254 | type: "image",
255 | url: 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
256 | }
257 | },
258 | {
259 | user: {
260 | id: 'danny_1',
261 | name: 'Daniel Georgetown',
262 | },
263 | createdAt: date,
264 | text: 'determine',
265 | },
266 | {
267 | user: {
268 | id: 'danny_1',
269 | name: 'Daniel Georgetown',
270 | },
271 | createdAt: date,
272 | text: 'resolve',
273 | },
274 | {
275 | user: {
276 | id: 'danny_1',
277 | name: 'Daniel Georgetown',
278 | },
279 | createdAt: date,
280 | text: 'will',
281 | },
282 | {
283 | user: {
284 | id: 'danny_1',
285 | name: 'Daniel Georgetown',
286 | },
287 | createdAt: date,
288 | text: 'this',
289 | },
290 | {
291 | user: {
292 | id: 'danny_1',
293 | name: 'Daniel Georgetown',
294 | },
295 | createdAt: date,
296 | text: 'how',
297 | },
298 | {
299 | user: {
300 | id: 'danny_1',
301 | name: 'Daniel Georgetown',
302 | },
303 | createdAt: date,
304 | text: 'we ',
305 | },
306 | {
307 | user: {
308 | id: 'danny_1',
309 | name: 'Daniel Georgetown',
310 | avatar:
311 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
312 | },
313 | createdAt: date,
314 | text: 'foks',
315 | },
316 | {
317 | user: {
318 | id: 'danny_1',
319 | name: 'Daniel Georgetown',
320 | },
321 | createdAt: date,
322 | text: 'ipsum',
323 | },
324 | {
325 | user: {
326 | id: 'danny_1',
327 | name: 'Daniel Georgetown',
328 | },
329 | createdAt: date,
330 | text: 'lorem',
331 | },
332 | {
333 | user: {
334 | id: 'danny_1',
335 | name: 'Daniel Georgetown',
336 | avatar:
337 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
338 | },
339 | createdAt: date,
340 | text: 'totally justifiable',
341 | },
342 | {
343 | user: {
344 | id: 'danny_2',
345 | name: 'Dan',
346 | avatar:
347 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
348 | },
349 | createdAt: date,
350 | text: 'it ',
351 | },
352 | {
353 | user: {
354 | id: 'danny_2',
355 | name: 'Dan',
356 | avatar:
357 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
358 | },
359 | createdAt: date,
360 | text: 'is https://google.com ',
361 | },
362 | {
363 | createdAt: date,
364 | user: {
365 | id: 'danny_2',
366 | name: 'Dan',
367 | avatar:
368 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
369 | },
370 | media: {
371 | type: "image",
372 | url: 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
373 | },
374 | },
375 | {
376 | user: {
377 | id: 'danny_1',
378 | name: 'Daniel Georgetown',
379 | },
380 | seen: true,
381 | createdAt: date,
382 | text: 'this ',
383 | },
384 | {
385 | user: {
386 | id: 'danny_1',
387 | name: 'Daniel Georgetown',
388 | },
389 | seen: true,
390 | createdAt: date,
391 | text: 'the ',
392 | },
393 | {
394 | user: {
395 | id: 'danny_1',
396 | name: 'Daniel Georgetown',
397 | },
398 | seen: false,
399 | createdAt: date,
400 | text: 'only message you will send today',
401 | },
402 | {
403 | user: {
404 | id: 'danny_1',
405 | name: 'Daniel Georgetown',
406 | },
407 | text: 'come on man https://google.com',
408 | seen: true,
409 | createdAt: date,
410 | loading: true
411 | },
412 | {
413 | user: {
414 | id: 'danny_1',
415 | name: 'Daniel Georgetown',
416 | },
417 | text: 'this is the last message',
418 | createdAt: date,
419 | seen: true,
420 | loading: true
421 | },
422 | ];
423 |
424 | export const fewMessages = [
425 | {
426 | user: {
427 | id: 'danny_2',
428 | name: 'Daniel Georgetown',
429 | avatar:
430 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
431 | },
432 | createdAt: date,
433 | text: 'first message',
434 | },
435 | {
436 | user: {
437 | id: 'danny_1',
438 | name: 'Daniel Georgetown',
439 | avatar:
440 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
441 | },
442 | createdAt: date,
443 | text: 'come on man',
444 | },
445 | {
446 | user: {
447 | id: 'danny_1',
448 | name: 'Daniel Georgetown',
449 | avatar:
450 | 'https://media.sproutsocial.com/uploads/2022/06/profile-picture.jpeg',
451 | },
452 | createdAt: date,
453 | text: 'this is the last message',
454 | },
455 | ];
456 |
--------------------------------------------------------------------------------
/test/blah.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import { Default as Thing } from '../stories/Thing.stories';
4 |
5 | describe('Thing', () => {
6 | it('renders without crashing', () => {
7 | const div = document.createElement('div');
8 | ReactDOM.render( , div);
9 | ReactDOM.unmountComponentAtNode(div);
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | // output .d.ts declaration files for consumers
9 | "declaration": true,
10 | // output .js.map sourcemap files for consumers
11 | "sourceMap": true,
12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index
13 | "rootDir": "./src",
14 | // stricter type-checking for stronger correctness. Recommended by TS
15 | "strict": true,
16 | // linter checks for common issues
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | // use Node's module resolution algorithm, instead of the legacy TS one
23 | "moduleResolution": "node",
24 | // transpile JSX to React.createElement
25 | "jsx": "react",
26 | // interop between ESM and CJS modules. Recommended by TS
27 | "esModuleInterop": true,
28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
29 | "skipLibCheck": true,
30 | // error out if import and file system have a casing mismatch. Recommended by TS
31 | "forceConsistentCasingInFileNames": true,
32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
33 | "noEmit": true,
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tsdx.config.js:
--------------------------------------------------------------------------------
1 | const images = require('@rollup/plugin-image')
2 | const postcss = require("rollup-plugin-postcss");
3 |
4 | // Not transpiled with TypeScript or Babel, so use plain Es6/Node.js!
5 | module.exports = {
6 | // This function will run for each entry/format/env combination
7 | rollup(config, options) {
8 | // config.plugins.push(image())
9 |
10 | config.plugins = [
11 | images({ incude: ['**/*.png', '**/*.jpg', '**/*.webp'] }),
12 | postcss(),
13 | ...config.plugins,
14 | ]
15 |
16 | return config
17 | },
18 | };
--------------------------------------------------------------------------------