.
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and an explanation of why the
80 | behavior was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series
85 | of actions.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the people involved, including unsolicited interaction with
89 | those enforcing the Code of Conduct, for a specified period of time. This
90 | includes avoiding interactions in community spaces as well as external channels
91 | like social media. Violating these terms may lead to a temporary or
92 | permanent ban.
93 |
94 | ### 3. Temporary Ban
95 |
96 | **Community Impact**: A serious violation of community standards, including
97 | sustained inappropriate behavior.
98 |
99 | **Consequence**: A temporary ban from any sort of interaction or public
100 | communication with the community for a specified period of time. No public or
101 | private interaction with the people involved, including unsolicited interaction
102 | with those enforcing the Code of Conduct, is allowed during this period.
103 | Violating these terms may lead to a permanent ban.
104 |
105 | ### 4. Permanent Ban
106 |
107 | **Community Impact**: Demonstrating a pattern of violation of community
108 | standards, including sustained inappropriate behavior, harassment of an
109 | individual, or aggression toward or disparagement of classes of individuals.
110 |
111 | **Consequence**: A permanent ban from any sort of public interaction within
112 | the community.
113 |
114 | ## Attribution
115 |
116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
117 | version 2.0, available at
118 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
119 |
120 | Community Impact Guidelines were inspired by
121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
122 |
123 | For answers to common questions about this code of conduct, see the FAQ at
124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available
125 | at [https://www.contributor-covenant.org/translations][translations].
126 |
127 | [homepage]: https://www.contributor-covenant.org
128 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
129 | [Mozilla CoC]: https://github.com/mozilla/diversity
130 | [FAQ]: https://www.contributor-covenant.org/faq
131 | [translations]: https://www.contributor-covenant.org/translations
132 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Flyer Chat
2 |
3 | Thank you for your interest in contributing to Flyer Chat! Our vision is to create an easy-to-use chat experience for any application and while we can't have everything right from the start, we can build it together! If you plan to implement a feature and open a pull request, make sure you don't spend a lot of time on it, otherwise open an issue so we can discuss bigger stuff.
4 |
5 | The [Open Source Guides](https://opensource.guide) website has a collection of resources for individuals, communities, and companies who want to learn how to run and contribute to an open-source project. Contributors and people new to open source alike will find the following guides especially useful:
6 |
7 | * [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
8 | * [Building Welcoming Communities](https://opensource.guide/building-community/)
9 |
10 | ## Helping with Documentation
11 |
12 | We use [Docusaurus](https://docusaurus.io) to build our documentation which is located in the [docs](https://github.com/flyerhq/react-native-chat-ui/tree/main/docs) folder. If you are adding new functionality or introducing a behavior change, we will ask you to update the documentation to reflect your changes.
13 |
14 | ## Contributing Code
15 |
16 | Code-level contributions to Flyer Chat generally come in the form of pull requests. These are done by forking the repo and making changes locally. Directly in the repo, there is the [example](https://github.com/flyerhq/react-native-chat-ui/tree/main/example) project that you can install on your device (or simulators) and use to test the changes you're making to Flyer Chat sources.
17 |
18 | [Here](https://opensource.guide/how-to-contribute/#opening-a-pull-request) you can read how to open a pull request.
19 |
20 | Please read [this section](https://github.com/demchenkoalex/react-native-module-template#how-to-see-my-changes-immediately-in-the-example) of the template library we use so the hot reload works for you when you are changing source files. Run `yarn` in the root folder to install dependencies for the library and `yarn` / `npx pod-install` in the `example` folder to install dependencies for the example project. After you are done with the changes, run `yarn compile`, `yarn lint` and `yarn type-coverage` in the root folder to make sure your code is consistent with our style.
21 |
22 | ## Community Contributions
23 |
24 | Contributions to Flyer Chat are not limited to GitHub. You can help others by sharing your experience using Flyer Chat, whether that is through blog posts, presenting talks at conferences, or simply sharing your thoughts on social media.
25 |
26 | ## License
27 |
28 | By contributing to Flyer Chat, you agree that your contributions will be licensed under its [Apache License, Version 2.0](LICENSE).
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2021 Oleksandr Demchenko
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ⚠️⚠️ Currently not maintained, please fork or look for alternatives ⚠️⚠️
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | React Native Chat UI
14 |
15 |
16 | Actively maintained, community-driven chat UI implementation with an optional Firebase BaaS .
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Flyer Chat is a platform for creating in-app chat experiences using React Native or [Flutter](https://github.com/flyerhq/flutter_chat_ui). This repository contains chat UI implementation for React Native.
48 |
49 | * **Free, open-source and community-driven**. We offer no paid plugins and strive to create an easy-to-use, almost drop-in chat experience for any application. Contributions are more than welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
50 |
51 | * **Backend agnostic**. You can choose the backend you prefer. But if you don't have one, we provide our own free and open-source [Firebase implementation](https://github.com/flyerhq/react-native-firebase-chat-core), which can be used to create a working chat in minutes. We are also working on our more advanced SaaS and self-hosted solutions.
52 |
53 | * **Customizable**. Supports custom themes, locales and more. Check our [documentation](https://docs.flyer.chat/react-native/chat-ui) for the info. More options are on the way, let us know if something is missing.
54 |
55 | * **Minimum dependencies**. Our packages are lightweight. Use your favourite packages for selecting images, opening files etc. See the [example](https://github.com/flyerhq/react-native-chat-ui/blob/main/example/src/App.tsx) for possible implementation.
56 |
57 | ## Getting Started
58 |
59 | ### Requirements
60 |
61 | `React Native >=0.60.0`
62 |
63 | Read our [documentation](https://docs.flyer.chat/react-native/chat-ui) or see the [example](https://github.com/flyerhq/react-native-chat-ui/tree/main/example) project.
64 |
65 | ## Contributing
66 |
67 | Please read our [Contributing Guide](CONTRIBUTING.md) before submitting a pull request to the project.
68 |
69 | ## Code of Conduct
70 |
71 | Flyer Chat has adopted the [Contributor Covenant](https://www.contributor-covenant.org) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
72 |
73 | ## License
74 |
75 | Licensed under the [Apache License, Version 2.0](LICENSE)
76 |
--------------------------------------------------------------------------------
/__mocks__/react-native-safe-area-context.ts:
--------------------------------------------------------------------------------
1 | export const useSafeAreaFrame = jest.fn(() => ({
2 | height: 896,
3 | width: 414,
4 | x: 0,
5 | y: 0,
6 | }))
7 |
8 | export const useSafeAreaInsets = jest.fn(() => ({
9 | top: 0,
10 | right: 0,
11 | bottom: 34,
12 | left: 0,
13 | }))
14 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | }
4 |
--------------------------------------------------------------------------------
/docs/basic-usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: basic-usage
3 | title: Basic Usage
4 | ---
5 |
6 | You start with a `Chat` component that will render a chat. It has 3 required properties:
7 |
8 | * `messages` - an array of messages to be rendered. Accepts any message, see [types](types). If you have your message types you will need to map those to any of the defined ones. Let us know if we need to add more message types or add more fields to the existing ones.
9 | * `onSendPress` - a function that will have a partial text message as a parameter. See [types](types) for more info on how types are structured. From the partial text message you need to create a text message which will at least have `author`, `id`, `text` and `type: 'text'`, this is done by you because we wanted to give you more control over those values.
10 | * `user` - a [User](types#user) object, that has only one required field - an `id`, used to determine the message author.
11 |
12 | Below you will find a drop-in example of the chat with only text messages.
13 |
14 | :::note
15 |
16 | Try to write any URL, for example, `flyer.chat`, it should be unwrapped in a rich preview.
17 |
18 | :::
19 |
20 | :::important
21 |
22 | All examples are in TypeScript.
23 |
24 | :::
25 |
26 | ```ts
27 | import { Chat, MessageType } from '@flyerhq/react-native-chat-ui'
28 | import React, { useState } from 'react'
29 | import { SafeAreaProvider } from 'react-native-safe-area-context'
30 |
31 | // For the testing purposes, you should probably use https://github.com/uuidjs/uuid
32 | const uuidv4 = () => {
33 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
34 | const r = Math.floor(Math.random() * 16)
35 | const v = c === 'x' ? r : (r % 4) + 8
36 | return v.toString(16)
37 | })
38 | }
39 |
40 | const App = () => {
41 | const [messages, setMessages] = useState([])
42 | const user = { id: '06c33e8b-e835-4736-80f4-63f44b66666c' }
43 |
44 | const addMessage = (message: MessageType.Any) => {
45 | setMessages([message, ...messages])
46 | }
47 |
48 | const handleSendPress = (message: MessageType.PartialText) => {
49 | const textMessage: MessageType.Text = {
50 | author: user,
51 | createdAt: Date.now(),
52 | id: uuidv4(),
53 | text: message.text,
54 | type: 'text',
55 | }
56 | addMessage(textMessage)
57 | }
58 |
59 | return (
60 | // Remove this provider if already registered elsewhere
61 | // or you have React Navigation set up
62 |
63 |
68 |
69 | )
70 | }
71 |
72 | export default App
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: installation
3 | title: Installation
4 | ---
5 |
6 | This library depends on [react-native-safe-area-context](https://github.com/th3rdwave/react-native-safe-area-context). If you use [React Navigation](https://reactnavigation.org) you probably already have it in your dependencies, so you're good to go. If not, please follow the instructions [here](https://github.com/th3rdwave/react-native-safe-area-context) to install it. Then run:
7 |
8 | `yarn add @flyerhq/react-native-chat-ui`
9 |
--------------------------------------------------------------------------------
/docs/localization.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: localization
3 | title: Localization
4 | ---
5 |
6 | You can pass the `locale` prop to the ` ` component. This locale will be passed to [dayjs](https://day.js.org), so we can localize dates. To see all supported locales check function `initLocale` in [this file](https://github.com/flyerhq/react-native-chat-ui/blob/main/src/utils/index.ts). Additionally, `locale` prop will be used to localize a couple of texts defined [here](https://github.com/flyerhq/react-native-chat-ui/blob/main/src/l10n.ts). You can override texts regardless of the locale by passing `l10nOverride` prop.
7 |
8 | ```ts
9 |
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: overview
3 | title: Overview
4 | slug: /
5 | ---
6 |
7 | Flyer Chat is a platform for creating in-app chat experiences using React Native or Flutter. This is the documentation for chat UI implementation for React Native.
8 |
9 | ## Motivation
10 |
11 | Ever estimated a simple chat for weeks of work? Didn't want to start because it is always the same boring work for an extended period of time? Was it moved to post MVP because of lack of time and resources? Were you left with a frustrated client, who couldn't understand why the thing that exists in almost every app takes that much time to implement?
12 |
13 | We are trying to solve all this by working on a Flyer Chat.
14 |
15 | You will say that there are libraries out there that will help create chats, but we are working on a more complete solution - very similar on completely different platforms like React Native and Flutter (we don't always work in just one) with an optional Firebase BaaS, since chat is not only UI. We are making this free and open-source so together we can create a product that works for everyone.
16 |
--------------------------------------------------------------------------------
/docs/themes.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: themes
3 | title: Themes
4 | ---
5 |
6 | You can override anything from some defined theme or create a new one from scratch. See the default theme implementation [here](https://github.com/flyerhq/react-native-chat-ui/blob/main/src/theme.ts). To override theme partially, destructure any defined theme and change what is needed, like on this example:
7 |
8 | ```ts
9 | import { Chat, defaultTheme } from '@flyerhq/react-native-chat-ui'
10 |
11 |
17 | ```
18 |
19 | If you created a theme from scratch just pass it to the `theme` prop. We also provide `darkTheme` implementation, you can import it from the library and pass to the `theme` prop.
20 |
--------------------------------------------------------------------------------
/docs/types.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: types
3 | title: Types
4 | ---
5 |
6 | There are 3 supported message types at the moment - `File`, `Image` and `Text`. All of them have corresponding "partial" message types, that include only the message's content. "Partial" messages are useful to create the content and then pass it to some kind of a backend service, which will assign fields like `id` or `author` etc, returning a "full" message which can be passed to `messages` prop of the `Chat` component. In addition to that, there are `Custom` and `Unsupported` types. `Custom` can be used to render anything you want, and `Unsupported` is just a placeholder to have backwards compatibility.
7 |
--------------------------------------------------------------------------------
/example/.buckconfig:
--------------------------------------------------------------------------------
1 |
2 | [android]
3 | target = Google Inc.:Google APIs:23
4 |
5 | [maven_repositories]
6 | central = https://repo1.maven.org/maven2
7 |
--------------------------------------------------------------------------------
/example/.editorconfig:
--------------------------------------------------------------------------------
1 | # Windows files
2 | [*.bat]
3 | end_of_line = crlf
4 |
--------------------------------------------------------------------------------
/example/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@react-native-community', 'plugin:prettier/recommended'],
3 | plugins: ['simple-import-sort'],
4 | root: true,
5 | rules: {
6 | 'import/order': 'off',
7 | radix: ['error', 'as-needed'],
8 | 'simple-import-sort/exports': 'error',
9 | 'simple-import-sort/imports': 'error',
10 | 'sort-imports': 'off',
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/example/.gitattributes:
--------------------------------------------------------------------------------
1 | # Windows files should use crlf line endings
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | *.bat text eol=crlf
4 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 |
24 | # Android/IntelliJ
25 | #
26 | build/
27 | .idea
28 | .gradle
29 | local.properties
30 | *.iml
31 | *.hprof
32 |
33 | # node.js
34 | #
35 | node_modules/
36 | npm-debug.log
37 | yarn-error.log
38 |
39 | # BUCK
40 | buck-out/
41 | \.buckd/
42 | *.keystore
43 | !debug.keystore
44 |
45 | # fastlane
46 | #
47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
48 | # screenshots whenever they are needed.
49 | # For more information about the recommended setup visit:
50 | # https://docs.fastlane.tools/best-practices/source-control/
51 |
52 | */fastlane/report.xml
53 | */fastlane/Preview.html
54 | */fastlane/screenshots
55 |
56 | # Bundle artifact
57 | *.jsbundle
58 |
59 | # CocoaPods
60 | /ios/Pods/
61 |
62 | # Messages
63 | messages.json
64 |
--------------------------------------------------------------------------------
/example/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | jsxSingleQuote: true,
3 | semi: false,
4 | singleQuote: true,
5 | }
6 |
--------------------------------------------------------------------------------
/example/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # example
2 |
3 | ## Getting Started
4 |
5 | ```bash
6 | yarn
7 | ```
8 |
9 | for iOS:
10 |
11 | ```bash
12 | npx pod-install
13 | ```
14 |
15 | To run the app use:
16 |
17 | ```bash
18 | yarn ios
19 | ```
20 |
21 | or
22 |
23 | ```bash
24 | yarn android
25 | ```
26 |
27 | ## Updating project
28 |
29 | 1. Check if there are major versions of 3rd party dependencies, update and commit these changes first
30 | 2. Remove current `example` project
31 | 3. Create a project named `example` using [react-native-better-template](https://github.com/demchenkoalex/react-native-better-template)
32 | 4. Revert `README.md` so you can see this guide
33 | 5. In `tsconfig.json` add
34 |
35 | ```json
36 | "baseUrl": ".",
37 | "paths": {
38 | "@flyerhq/react-native-chat-ui": ["../src"]
39 | },
40 | "resolveJsonModule": true,
41 | ```
42 |
43 | 6. In `package.json` scripts section add
44 |
45 | ```json
46 | "generate-messages": "node scripts/generateMessages.js",
47 | "prepare": "yarn generate-messages",
48 | ```
49 |
50 | 7. Check the difference in `metro.config.js` and combine all
51 | 8. Revert `src` folder
52 | 9. Revert `scripts` folder
53 | 10. Revert `index.js`
54 | 11. Check the difference in `.gitignore` and combine all
55 | 12. Check the difference in `.eslintrc.js` and combine all
56 | 13. Install all missing dependencies
57 | 14. Check the difference in `Info.plist` and combine all
58 | 15. Open Xcode and change build number from 1 to 2 and back in the UI, so Xcode will format `*.pbxproj` eliminating some changes
59 |
--------------------------------------------------------------------------------
/example/android/app/_BUCK:
--------------------------------------------------------------------------------
1 | # To learn about Buck see [Docs](https://buckbuild.com/).
2 | # To run your application with Buck:
3 | # - install Buck
4 | # - `npm start` - to start the packager
5 | # - `cd android`
6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
8 | # - `buck install -r android/app` - compile, install and run application
9 | #
10 |
11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
12 |
13 | lib_deps = []
14 |
15 | create_aar_targets(glob(["libs/*.aar"]))
16 |
17 | create_jar_targets(glob(["libs/*.jar"]))
18 |
19 | android_library(
20 | name = "all-libs",
21 | exported_deps = lib_deps,
22 | )
23 |
24 | android_library(
25 | name = "app-code",
26 | srcs = glob([
27 | "src/main/java/**/*.java",
28 | ]),
29 | deps = [
30 | ":all-libs",
31 | ":build_config",
32 | ":res",
33 | ],
34 | )
35 |
36 | android_build_config(
37 | name = "build_config",
38 | package = "com.example",
39 | )
40 |
41 | android_resource(
42 | name = "res",
43 | package = "com.example",
44 | res = "src/main/res",
45 | )
46 |
47 | android_binary(
48 | name = "app",
49 | keystore = "//android/keystores:debug",
50 | manifest = "src/main/AndroidManifest.xml",
51 | package_type = "debug",
52 | deps = [
53 | ":app-code",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/example/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | import com.android.build.OutputFile
5 |
6 | /**
7 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
8 | * and bundleReleaseJsAndAssets).
9 | * These basically call `react-native bundle` with the correct arguments during the Android build
10 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
11 | * bundle directly from the development server. Below you can see all the possible configurations
12 | * and their defaults. If you decide to add a configuration block, make sure to add it before the
13 | * `apply from: "../../node_modules/react-native/react.gradle"` line.
14 | *
15 | * project.ext.react = [
16 | * // the name of the generated asset file containing your JS bundle
17 | * bundleAssetName: "index.android.bundle",
18 | *
19 | * // the entry file for bundle generation. If none specified and
20 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is
21 | * // default. Can be overridden with ENTRY_FILE environment variable.
22 | * entryFile: "index.android.js",
23 | *
24 | * // https://reactnative.dev/docs/performance#enable-the-ram-format
25 | * bundleCommand: "ram-bundle",
26 | *
27 | * // whether to bundle JS and assets in debug mode
28 | * bundleInDebug: false,
29 | *
30 | * // whether to bundle JS and assets in release mode
31 | * bundleInRelease: true,
32 | *
33 | * // whether to bundle JS and assets in another build variant (if configured).
34 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
35 | * // The configuration property can be in the following formats
36 | * // 'bundleIn${productFlavor}${buildType}'
37 | * // 'bundleIn${buildType}'
38 | * // bundleInFreeDebug: true,
39 | * // bundleInPaidRelease: true,
40 | * // bundleInBeta: true,
41 | *
42 | * // whether to disable dev mode in custom build variants (by default only disabled in release)
43 | * // for example: to disable dev mode in the staging build type (if configured)
44 | * devDisabledInStaging: true,
45 | * // The configuration property can be in the following formats
46 | * // 'devDisabledIn${productFlavor}${buildType}'
47 | * // 'devDisabledIn${buildType}'
48 | *
49 | * // the root of your project, i.e. where "package.json" lives
50 | * root: "../../",
51 | *
52 | * // where to put the JS bundle asset in debug mode
53 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
54 | *
55 | * // where to put the JS bundle asset in release mode
56 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
57 | *
58 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
59 | * // require('./image.png')), in debug mode
60 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
61 | *
62 | * // where to put drawable resources / React Native assets, e.g. the ones you use via
63 | * // require('./image.png')), in release mode
64 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
65 | *
66 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
67 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
68 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle
69 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
70 | * // for example, you might want to remove it from here.
71 | * inputExcludes: ["android/**", "ios/**"],
72 | *
73 | * // override which node gets called and with what additional arguments
74 | * nodeExecutableAndArgs: ["node"],
75 | *
76 | * // supply additional arguments to the packager
77 | * extraPackagerArgs: []
78 | * ]
79 | */
80 |
81 | project.ext.react = [
82 | enableHermes: false, // clean and rebuild if changing
83 | ]
84 |
85 | apply from: '../../node_modules/react-native/react.gradle'
86 |
87 | /**
88 | * Set this to true to create two separate APKs instead of one:
89 | * - An APK that only works on ARM devices
90 | * - An APK that only works on x86 devices
91 | * The advantage is the size of the APK is reduced by about 4MB.
92 | * Upload all the APKs to the Play Store and people will download
93 | * the correct one based on the CPU architecture of their device.
94 | */
95 | def enableSeparateBuildPerCPUArchitecture = false
96 |
97 | /**
98 | * Run Proguard to shrink the Java bytecode in release builds.
99 | */
100 | def enableProguardInReleaseBuilds = false
101 |
102 | /**
103 | * The preferred build flavor of JavaScriptCore.
104 | *
105 | * For example, to use the international variant, you can use:
106 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
107 | *
108 | * The international variant includes ICU i18n library and necessary data
109 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
110 | * give correct results when using with locales other than en-US. Note that
111 | * this variant is about 6MiB larger per architecture than default.
112 | */
113 | def jscFlavor = 'org.webkit:android-jsc:+'
114 |
115 | /**
116 | * Whether to enable the Hermes VM.
117 | *
118 | * This should be set on project.ext.react and mirrored here. If it is not set
119 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
120 | * and the benefits of using Hermes will therefore be sharply reduced.
121 | */
122 | def enableHermes = project.ext.react.get('enableHermes', false);
123 |
124 | /**
125 | * Architectures to build native code for in debug.
126 | */
127 | def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
128 |
129 | android {
130 | ndkVersion rootProject.ext.ndkVersion
131 |
132 | compileSdkVersion rootProject.ext.compileSdkVersion
133 |
134 | defaultConfig {
135 | applicationId 'com.example'
136 | minSdkVersion rootProject.ext.minSdkVersion
137 | targetSdkVersion rootProject.ext.targetSdkVersion
138 | versionCode 1
139 | versionName '1.0'
140 | }
141 | splits {
142 | abi {
143 | reset()
144 | enable enableSeparateBuildPerCPUArchitecture
145 | universalApk false // If true, also generate a universal APK
146 | include 'armeabi-v7a', 'x86', 'arm64-v8a', 'x86_64'
147 | }
148 | }
149 | signingConfigs {
150 | debug {
151 | storeFile file('debug.keystore')
152 | storePassword 'android'
153 | keyAlias 'androiddebugkey'
154 | keyPassword 'android'
155 | }
156 | }
157 | buildTypes {
158 | debug {
159 | signingConfig signingConfigs.debug
160 | if (nativeArchitectures) {
161 | ndk {
162 | abiFilters nativeArchitectures.split(',')
163 | }
164 | }
165 | }
166 | release {
167 | // Caution! In production, you need to generate your own keystore file.
168 | // see https://reactnative.dev/docs/signed-apk-android.
169 | signingConfig signingConfigs.debug
170 | minifyEnabled enableProguardInReleaseBuilds
171 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
172 | }
173 | }
174 |
175 | // applicationVariants are e.g. debug, release
176 | applicationVariants.all { variant ->
177 | variant.outputs.each { output ->
178 | // For each separate APK per architecture, set a unique version code as described here:
179 | // https://developer.android.com/studio/build/configure-apk-splits.html
180 | // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
181 | def versionCodes = ['armeabi-v7a': 1, 'x86': 2, 'arm64-v8a': 3, 'x86_64': 4]
182 | def abi = output.getFilter(OutputFile.ABI)
183 | if (abi != null) { // null for the universal-debug, universal-release variants
184 | output.versionCodeOverride =
185 | defaultConfig.versionCode * 1000 + versionCodes.get(abi)
186 | }
187 |
188 | }
189 | }
190 | }
191 |
192 | dependencies {
193 | implementation fileTree(dir: 'libs', include: ['*.jar'])
194 | //noinspection GradleDynamicVersion
195 | implementation 'com.facebook.react:react-native:+' // From node_modules
196 |
197 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
198 |
199 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
200 | exclude group: 'com.facebook.fbjni'
201 | }
202 |
203 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
204 | exclude group: 'com.facebook.flipper'
205 | exclude group: 'com.squareup.okhttp3', module: 'okhttp'
206 | }
207 |
208 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
209 | exclude group: 'com.facebook.flipper'
210 | }
211 |
212 | if (enableHermes) {
213 | def hermesPath = '../../node_modules/hermes-engine/android/';
214 | debugImplementation files(hermesPath + 'hermes-debug.aar')
215 | releaseImplementation files(hermesPath + 'hermes-release.aar')
216 | } else {
217 | implementation jscFlavor
218 | }
219 | }
220 |
221 | // Run this once to be able to run the application with BUCK
222 | // puts all compile dependencies into folder libs for BUCK to use
223 | task copyDownloadableDepsToLibs(type: Copy) {
224 | from configurations.implementation
225 | into 'libs'
226 | }
227 |
228 | apply from: file('../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle'); applyNativeModulesAppBuildGradle(project)
229 |
--------------------------------------------------------------------------------
/example/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/example/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/debug.keystore
--------------------------------------------------------------------------------
/example/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/java/com/example/ReactNativeFlipper.kt:
--------------------------------------------------------------------------------
1 | package com.example
2 |
3 | import android.content.Context
4 | import com.facebook.flipper.android.AndroidFlipperClient
5 | import com.facebook.flipper.android.utils.FlipperUtils
6 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin
7 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
8 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin
9 | import com.facebook.flipper.plugins.inspector.DescriptorMapping
10 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
11 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
12 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
13 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
14 | import com.facebook.react.ReactInstanceManager
15 | import com.facebook.react.ReactInstanceManager.ReactInstanceEventListener
16 | import com.facebook.react.bridge.ReactContext
17 | import com.facebook.react.modules.network.NetworkingModule
18 |
19 | object ReactNativeFlipper {
20 | @JvmStatic
21 | fun initializeFlipper(context: Context?, reactInstanceManager: ReactInstanceManager) {
22 | if (FlipperUtils.shouldEnableFlipper(context)) {
23 | val client = AndroidFlipperClient.getInstance(context)
24 | client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
25 | client.addPlugin(DatabasesFlipperPlugin(context))
26 | client.addPlugin(SharedPreferencesFlipperPlugin(context))
27 | client.addPlugin(CrashReporterPlugin.getInstance())
28 | val networkFlipperPlugin = NetworkFlipperPlugin()
29 | NetworkingModule.setCustomClientBuilder { builder -> builder.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin)) }
30 | client.addPlugin(networkFlipperPlugin)
31 | client.start()
32 |
33 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
34 | // Hence we run if after all native modules have been initialized
35 | val reactContext = reactInstanceManager.currentReactContext
36 | if (reactContext == null) {
37 | reactInstanceManager.addReactInstanceEventListener(
38 | object : ReactInstanceEventListener {
39 | override fun onReactContextInitialized(reactContext: ReactContext) {
40 | reactInstanceManager.removeReactInstanceEventListener(this)
41 | reactContext.runOnNativeModulesQueueThread { client.addPlugin(FrescoFlipperPlugin()) }
42 | }
43 | })
44 | } else {
45 | client.addPlugin(FrescoFlipperPlugin())
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example
2 |
3 | import com.facebook.react.ReactActivity
4 |
5 | class MainActivity : ReactActivity() {
6 |
7 | override fun getMainComponentName(): String? {
8 | return "example"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import com.facebook.react.*
6 | import com.facebook.soloader.SoLoader
7 | import java.lang.reflect.InvocationTargetException
8 |
9 | class MainApplication : Application(), ReactApplication {
10 |
11 | private val mReactNativeHost = object : ReactNativeHost(this) {
12 | override fun getUseDeveloperSupport(): Boolean {
13 | return BuildConfig.DEBUG
14 | }
15 |
16 | override fun getPackages(): List {
17 | val packages = PackageList(this).packages
18 | // Packages that cannot be autolinked yet can be added manually here, for example:
19 | // packages.add(MyReactNativePackage());
20 | return packages
21 | }
22 |
23 | override fun getJSMainModuleName(): String {
24 | return "index"
25 | }
26 | }
27 |
28 | override fun getReactNativeHost(): ReactNativeHost {
29 | return mReactNativeHost
30 | }
31 |
32 | override fun onCreate() {
33 | super.onCreate()
34 | SoLoader.init(this, false)
35 | initializeFlipper(this, reactNativeHost.reactInstanceManager)
36 | }
37 |
38 | companion object {
39 |
40 | private fun initializeFlipper(context: Context, reactInstanceManager: ReactInstanceManager) {
41 | if (BuildConfig.DEBUG) {
42 | try {
43 | val aClass = Class.forName("com.example.ReactNativeFlipper")
44 | aClass
45 | .getMethod("initializeFlipper", Context::class.java, ReactInstanceManager::class.java)
46 | .invoke(null, context, reactInstanceManager)
47 | } catch (e: ClassNotFoundException) {
48 | e.printStackTrace()
49 | } catch (e: NoSuchMethodException) {
50 | e.printStackTrace()
51 | } catch (e: IllegalAccessException) {
52 | e.printStackTrace()
53 | } catch (e: InvocationTargetException) {
54 | e.printStackTrace()
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | example
3 |
4 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = '30.0.2'
6 | minSdkVersion = 21
7 | compileSdkVersion = 30
8 | targetSdkVersion = 30
9 | kotlinVersion = '1.5.31'
10 | ndkVersion = '21.4.7075529'
11 | }
12 | repositories {
13 | google()
14 | mavenCentral()
15 | }
16 | dependencies {
17 | classpath 'com.android.tools.build:gradle:4.2.2'
18 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
19 | // NOTE: Do not place your application dependencies here; they belong
20 | // in the individual module build.gradle files
21 | }
22 | }
23 |
24 | allprojects {
25 | repositories {
26 | mavenCentral()
27 | mavenLocal()
28 | maven {
29 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
30 | url "$rootDir/../node_modules/react-native/android"
31 | }
32 | maven {
33 | // Android JSC is installed from npm
34 | url "$rootDir/../node_modules/jsc-android/dist"
35 | }
36 |
37 | google()
38 | maven { url 'https://www.jitpack.io' }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.99.0
29 |
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/example/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/example/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/example/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/example/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/example/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'example'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "displayName": "example"
4 | }
5 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | }
4 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import 'react-native-get-random-values'
6 |
7 | import { AppRegistry } from 'react-native'
8 |
9 | import { name as appName } from './app.json'
10 | import AppContainer from './src/AppContainer'
11 |
12 | AppRegistry.registerComponent(appName, () => AppContainer)
13 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | require_relative '../node_modules/react-native/scripts/react_native_pods'
2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3 |
4 | platform :ios, '11.0'
5 |
6 | target 'example' do
7 | config = use_native_modules!
8 |
9 | use_react_native!(
10 | :path => config[:reactNativePath],
11 | # to enable hermes on iOS, change `false` to `true` and then install pods
12 | :hermes_enabled => false
13 | )
14 |
15 | # Enables Flipper.
16 | #
17 | # Note that if you have use_frameworks! enabled, Flipper will not work and
18 | # you should disable the next line.
19 | use_flipper!()
20 |
21 | post_install do |installer|
22 | react_native_post_install(installer)
23 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/example/ios/example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/ios/example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/example/ios/example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | #if DEBUG
3 | import FlipperKit
4 | #endif
5 |
6 | @UIApplicationMain
7 | class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
8 |
9 | var window: UIWindow?
10 |
11 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
12 | initializeFlipper(with: application)
13 |
14 | let bridge = RCTBridge(delegate: self, launchOptions: launchOptions)
15 | let rootView = RCTRootView(bridge: bridge!, moduleName: "example", initialProperties: nil)
16 |
17 | if #available(iOS 13.0, *) {
18 | rootView.backgroundColor = UIColor.systemBackground
19 | } else {
20 | rootView.backgroundColor = UIColor.white
21 | }
22 |
23 | window = UIWindow(frame: UIScreen.main.bounds)
24 | let rootViewController = UIViewController()
25 | rootViewController.view = rootView
26 | window?.rootViewController = rootViewController
27 | window?.makeKeyAndVisible()
28 |
29 | return true
30 | }
31 |
32 | func sourceURL(for bridge: RCTBridge!) -> URL! {
33 | #if DEBUG
34 | return RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
35 | #else
36 | return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
37 | #endif
38 | }
39 |
40 | private func initializeFlipper(with application: UIApplication) {
41 | #if DEBUG
42 | let client = FlipperClient.shared()
43 | let layoutDescriptionMapper = SKDescriptorMapper(defaults: ())
44 | client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptionMapper))
45 | client?.add(FKUserDefaultsPlugin(suiteName: nil))
46 | client?.add(FlipperKitReactPlugin())
47 | client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
48 | client?.start()
49 | #endif
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/example/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/example/ios/example/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/ios/example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | example
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSExceptionDomains
30 |
31 | localhost
32 |
33 | NSExceptionAllowsInsecureHTTPLoads
34 |
35 |
36 |
37 |
38 | NSCameraUsageDescription
39 | Allows you to send photos taken with the camera
40 | NSLocationWhenInUseUsageDescription
41 |
42 | NSPhotoLibraryUsageDescription
43 | Allows you to send photos from the photo library
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIRequiredDeviceCapabilities
47 |
48 | armv7
49 |
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationLandscapeLeft
54 | UIInterfaceOrientationLandscapeRight
55 |
56 | UIViewControllerBasedStatusBarAppearance
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/example/ios/example/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/example/ios/example/example-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import
6 | #import
7 | #import
8 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration for React Native
3 | * https://github.com/facebook/react-native
4 | *
5 | * @format
6 | */
7 |
8 | const path = require('path')
9 | const exclusionList = require('metro-config/src/defaults/exclusionList')
10 |
11 | const moduleRoot = path.resolve(__dirname, '..')
12 |
13 | module.exports = {
14 | watchFolders: [moduleRoot],
15 | resolver: {
16 | extraNodeModules: {
17 | react: path.resolve(__dirname, 'node_modules/react'),
18 | 'react-native': path.resolve(__dirname, 'node_modules/react-native'),
19 | 'react-native-safe-area-context': path.resolve(
20 | __dirname,
21 | 'node_modules/react-native-safe-area-context'
22 | ),
23 | },
24 | blockList: exclusionList([
25 | new RegExp(`${moduleRoot}/node_modules/react/.*`),
26 | new RegExp(`${moduleRoot}/node_modules/react-native/.*`),
27 | new RegExp(
28 | `${moduleRoot}/node_modules/react-native-safe-area-context/.*`
29 | ),
30 | ]),
31 | },
32 | transformer: {
33 | getTransformOptions: async () => ({
34 | transform: {
35 | experimentalImportSupport: false,
36 | inlineRequires: true,
37 | },
38 | }),
39 | },
40 | }
41 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "compile": "tsc -p .",
8 | "generate-messages": "node scripts/generateMessages.js",
9 | "ios": "react-native run-ios",
10 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
11 | "prepare": "yarn generate-messages",
12 | "start": "react-native start",
13 | "test": "jest"
14 | },
15 | "dependencies": {
16 | "@expo/react-native-action-sheet": "^3.12.0",
17 | "react": "^17.0.2",
18 | "react-native": "^0.66.1",
19 | "react-native-document-picker": "^7.1.1",
20 | "react-native-file-viewer": "^2.1.4",
21 | "react-native-get-random-values": "^1.7.0",
22 | "react-native-image-picker": "^4.1.2",
23 | "react-native-safe-area-context": "^3.3.2",
24 | "uuid": "^8.3.2"
25 | },
26 | "devDependencies": {
27 | "@babel/core": "^7.15.8",
28 | "@babel/runtime": "^7.15.4",
29 | "@react-native-community/eslint-config": "^3.0.1",
30 | "@types/jest": "^27.0.2",
31 | "@types/react-native": "^0.66.0",
32 | "@types/react-test-renderer": "^17.0.1",
33 | "@types/uuid": "^8.3.1",
34 | "babel-jest": "^27.3.1",
35 | "casual": "^1.6.2",
36 | "eslint": "^7.32.0",
37 | "eslint-plugin-simple-import-sort": "^7.0.0",
38 | "jest": "^27.3.1",
39 | "metro-react-native-babel-preset": "^0.66.2",
40 | "react-test-renderer": "^17.0.2",
41 | "typescript": "^4.4.4"
42 | },
43 | "resolutions": {
44 | "@types/react": "^17"
45 | },
46 | "jest": {
47 | "preset": "react-native",
48 | "moduleFileExtensions": [
49 | "ts",
50 | "tsx",
51 | "js",
52 | "jsx",
53 | "json",
54 | "node"
55 | ]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/example/scripts/generateMessages.js:
--------------------------------------------------------------------------------
1 | const casual = require('casual')
2 | const fs = require('fs')
3 | const { v4: uuidv4 } = require('uuid')
4 |
5 | const users = [
6 | {
7 | firstName: 'John',
8 | id: 'b4878b96-efbc-479a-8291-474ef323dec7',
9 | imageUrl: 'https://avatars.githubusercontent.com/u/14123304?v=4',
10 | },
11 | {
12 | firstName: 'Jane',
13 | id: '06c33e8b-e835-4736-80f4-63f44b66666c',
14 | imageUrl: 'https://avatars.githubusercontent.com/u/33809426?v=4',
15 | },
16 | ]
17 |
18 | let numberOfMessages = 10
19 | const arg = process.argv.slice(2)[0]
20 |
21 | if (!isNaN(arg) && parseInt(arg) > 0) {
22 | numberOfMessages = parseInt(arg)
23 | }
24 |
25 | const messages = [...Array(numberOfMessages)].map((_, index) => {
26 | const randomText = Math.round(Math.random())
27 | const text = randomText ? casual.text : casual.sentence
28 | const randomAuthor = Math.round(Math.random())
29 | const author = randomAuthor ? users[0] : users[1]
30 | const createdAt = Date.now() - index
31 | const data = {
32 | author,
33 | createdAt,
34 | id: uuidv4(),
35 | status: 'seen',
36 | text,
37 | type: 'text',
38 | }
39 | return data
40 | })
41 |
42 | const json = `${JSON.stringify(messages, null, 2)}\n`
43 |
44 | fs.writeFile('src/messages.json', json, () => {
45 | console.log(`Generated ${numberOfMessages} messages`)
46 | })
47 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useActionSheet } from '@expo/react-native-action-sheet'
2 | import { Chat, MessageType } from '@flyerhq/react-native-chat-ui'
3 | import { PreviewData } from '@flyerhq/react-native-link-preview'
4 | import React, { useState } from 'react'
5 | import DocumentPicker from 'react-native-document-picker'
6 | import FileViewer from 'react-native-file-viewer'
7 | import { launchImageLibrary } from 'react-native-image-picker'
8 | import { v4 as uuidv4 } from 'uuid'
9 |
10 | import data from './messages.json'
11 |
12 | const App = () => {
13 | const { showActionSheetWithOptions } = useActionSheet()
14 | const [messages, setMessages] = useState(data as MessageType.Any[])
15 | const user = { id: '06c33e8b-e835-4736-80f4-63f44b66666c' }
16 |
17 | const addMessage = (message: MessageType.Any) => {
18 | setMessages([message, ...messages])
19 | }
20 |
21 | const handleAttachmentPress = () => {
22 | showActionSheetWithOptions(
23 | {
24 | options: ['Photo', 'File', 'Cancel'],
25 | cancelButtonIndex: 2,
26 | },
27 | (buttonIndex) => {
28 | switch (buttonIndex) {
29 | case 0:
30 | handleImageSelection()
31 | break
32 | case 1:
33 | handleFileSelection()
34 | break
35 | }
36 | }
37 | )
38 | }
39 |
40 | const handleFileSelection = async () => {
41 | try {
42 | const response = await DocumentPicker.pickSingle({
43 | type: [DocumentPicker.types.allFiles],
44 | })
45 | const fileMessage: MessageType.File = {
46 | author: user,
47 | createdAt: Date.now(),
48 | id: uuidv4(),
49 | mimeType: response.type ?? undefined,
50 | name: response.name,
51 | size: response.size ?? 0,
52 | type: 'file',
53 | uri: response.uri,
54 | }
55 | addMessage(fileMessage)
56 | } catch {}
57 | }
58 |
59 | const handleImageSelection = () => {
60 | launchImageLibrary(
61 | {
62 | includeBase64: true,
63 | maxWidth: 1440,
64 | mediaType: 'photo',
65 | quality: 0.7,
66 | },
67 | ({ assets }) => {
68 | const response = assets?.[0]
69 |
70 | if (response?.base64) {
71 | const imageMessage: MessageType.Image = {
72 | author: user,
73 | createdAt: Date.now(),
74 | height: response.height,
75 | id: uuidv4(),
76 | name: response.fileName ?? response.uri?.split('/').pop() ?? '🖼',
77 | size: response.fileSize ?? 0,
78 | type: 'image',
79 | uri: `data:image/*;base64,${response.base64}`,
80 | width: response.width,
81 | }
82 | addMessage(imageMessage)
83 | }
84 | }
85 | )
86 | }
87 |
88 | const handleMessagePress = async (message: MessageType.Any) => {
89 | if (message.type === 'file') {
90 | try {
91 | await FileViewer.open(message.uri, { showOpenWithDialog: true })
92 | } catch {}
93 | }
94 | }
95 |
96 | const handlePreviewDataFetched = ({
97 | message,
98 | previewData,
99 | }: {
100 | message: MessageType.Text
101 | previewData: PreviewData
102 | }) => {
103 | setMessages(
104 | messages.map((m) =>
105 | m.id === message.id ? { ...m, previewData } : m
106 | )
107 | )
108 | }
109 |
110 | const handleSendPress = (message: MessageType.PartialText) => {
111 | const textMessage: MessageType.Text = {
112 | author: user,
113 | createdAt: Date.now(),
114 | id: uuidv4(),
115 | text: message.text,
116 | type: 'text',
117 | }
118 | addMessage(textMessage)
119 | }
120 |
121 | return (
122 |
130 | )
131 | }
132 |
133 | export default App
134 |
--------------------------------------------------------------------------------
/example/src/AppContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ActionSheetProvider } from '@expo/react-native-action-sheet'
2 | import React from 'react'
3 | import { SafeAreaProvider } from 'react-native-safe-area-context'
4 |
5 | import App from './App'
6 |
7 | const AppContainer = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default AppContainer
18 |
--------------------------------------------------------------------------------
/example/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-native/Libraries/Blob/Blob' {
2 | class Blob {
3 | constructor(parts: Array)
4 |
5 | get size(): number
6 | }
7 |
8 | export default Blob
9 | }
10 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "esModuleInterop": true,
5 | "jsx": "react-native",
6 | "lib": ["ESNext"],
7 | "module": "CommonJS",
8 | "noEmit": true,
9 | "paths": {
10 | "@flyerhq/react-native-chat-ui": ["../src"]
11 | },
12 | "resolveJsonModule": true,
13 | "skipLibCheck": true,
14 | "strict": true,
15 | "target": "ESNext",
16 | },
17 | "include": ["src"]
18 | }
19 |
--------------------------------------------------------------------------------
/jest/fixtures.ts:
--------------------------------------------------------------------------------
1 | import { MessageType, Size, User } from '../src/types'
2 |
3 | export const defaultDerivedMessageProps = {
4 | nextMessageInGroup: false,
5 | offset: 12,
6 | showName: false,
7 | showStatus: true,
8 | }
9 |
10 | export const fileMessage: MessageType.File = {
11 | author: {
12 | id: 'userId',
13 | },
14 | createdAt: 2000000,
15 | id: 'file-uuidv4',
16 | mimeType: 'application/pdf',
17 | name: 'flyer.pdf',
18 | size: 15000,
19 | status: 'seen',
20 | type: 'file',
21 | uri: 'file:///Users/admin/flyer.pdf',
22 | }
23 |
24 | export const derivedFileMessage: MessageType.DerivedFile = {
25 | ...fileMessage,
26 | ...defaultDerivedMessageProps,
27 | }
28 |
29 | export const imageMessage: MessageType.Image = {
30 | author: {
31 | id: 'image-userId',
32 | },
33 | createdAt: 0,
34 | height: 100,
35 | id: 'image-uuidv4',
36 | name: 'name',
37 | size: 15000,
38 | status: 'sending',
39 | type: 'image',
40 | uri: 'https://avatars1.githubusercontent.com/u/59206044',
41 | width: 100,
42 | }
43 |
44 | export const derivedImageMessage: MessageType.DerivedImage = {
45 | ...imageMessage,
46 | ...defaultDerivedMessageProps,
47 | }
48 |
49 | export const size: Size = {
50 | height: 896,
51 | width: 414,
52 | }
53 |
54 | export const textMessage: MessageType.Text = {
55 | author: {
56 | id: 'userId',
57 | },
58 | createdAt: 0,
59 | id: 'uuidv4',
60 | text: 'text',
61 | type: 'text',
62 | }
63 |
64 | export const derivedTextMessage: MessageType.DerivedText = {
65 | ...textMessage,
66 | ...defaultDerivedMessageProps,
67 | }
68 |
69 | export const user: User = {
70 | id: 'userId',
71 | }
72 |
--------------------------------------------------------------------------------
/jest/setup.ts:
--------------------------------------------------------------------------------
1 | jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
2 | jest.spyOn(Date, 'now').mockReturnValue(0)
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@flyerhq/react-native-chat-ui",
3 | "version": "1.4.3",
4 | "description": "Actively maintained, community-driven chat UI implementation with an optional Firebase BaaS.",
5 | "homepage": "https://flyer.chat",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/flyerhq/react-native-chat-ui.git"
9 | },
10 | "main": "lib/index.js",
11 | "types": "lib/index.d.ts",
12 | "author": "Oleksandr Demchenko ",
13 | "contributors": [
14 | "Vitalii Danylov ",
15 | "Volodymyr Smolianinov "
16 | ],
17 | "license": "Apache-2.0",
18 | "keywords": [
19 | "chat",
20 | "ui",
21 | "react-native",
22 | "react-native-component",
23 | "ios",
24 | "android",
25 | "typescript"
26 | ],
27 | "files": [
28 | "lib"
29 | ],
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "scripts": {
34 | "compile": "rm -rf lib && tsc -p . && copyup src/assets/*.png lib",
35 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
36 | "prepare": "yarn compile",
37 | "test": "jest",
38 | "type-coverage": "type-coverage"
39 | },
40 | "dependencies": {
41 | "@flyerhq/react-native-keyboard-accessory-view": "^2.3.3",
42 | "@flyerhq/react-native-link-preview": "^1.5.2",
43 | "dayjs": "^1.10.7",
44 | "react-native-image-viewing": "^0.2.1",
45 | "react-native-parsed-text": "^0.0.22"
46 | },
47 | "devDependencies": {
48 | "@babel/core": "^7.15.8",
49 | "@babel/runtime": "^7.15.4",
50 | "@react-native-community/eslint-config": "^3.0.1",
51 | "@testing-library/react-native": "^8.0.0",
52 | "@types/jest": "^27.0.2",
53 | "@types/react-native": "^0.66.0",
54 | "@types/react-test-renderer": "^17.0.1",
55 | "babel-jest": "^27.3.1",
56 | "copyfiles": "^2.4.1",
57 | "eslint": "^7.32.0",
58 | "eslint-plugin-jest": "^25.2.2",
59 | "eslint-plugin-simple-import-sort": "^7.0.0",
60 | "jest": "^27.3.1",
61 | "metro-react-native-babel-preset": "^0.66.2",
62 | "react": "^17.0.2",
63 | "react-native": "^0.66.1",
64 | "react-native-safe-area-context": "^3.3.2",
65 | "react-test-renderer": "^17.0.2",
66 | "type-coverage": "^2.18.2",
67 | "typescript": "^4.4.4"
68 | },
69 | "peerDependencies": {
70 | "react": "*",
71 | "react-native": "*"
72 | },
73 | "jest": {
74 | "collectCoverage": true,
75 | "collectCoverageFrom": [
76 | "src/**/*.{ts,tsx}",
77 | "!**/index.{ts,tsx}",
78 | "!**/styles.{ts,tsx}",
79 | "!**/types.{ts,tsx}",
80 | "!**/*.d.ts",
81 | "!**/ImageView.android.ts",
82 | "!**/ImageView.ios.ts",
83 | "!**/ImageView.tsx"
84 | ],
85 | "coverageThreshold": {
86 | "global": {
87 | "branches": 100,
88 | "functions": 100,
89 | "lines": 100,
90 | "statements": 100
91 | }
92 | },
93 | "moduleFileExtensions": [
94 | "ts",
95 | "tsx",
96 | "js",
97 | "jsx",
98 | "json",
99 | "node"
100 | ],
101 | "preset": "react-native",
102 | "setupFiles": [
103 | "./jest/setup.ts"
104 | ],
105 | "transformIgnorePatterns": [
106 | "node_modules/(?!(@flyerhq|@react-native|react-native))"
107 | ]
108 | },
109 | "typeCoverage": {
110 | "cache": true,
111 | "ignoreCatch": true,
112 | "ignoreNonNullAssertion": true,
113 | "ignoreUnread": true,
114 | "is": 100,
115 | "showRelativePath": true,
116 | "strict": true
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/assets/icon-attachment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-attachment.png
--------------------------------------------------------------------------------
/src/assets/icon-attachment@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-attachment@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-attachment@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-attachment@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-delivered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-delivered.png
--------------------------------------------------------------------------------
/src/assets/icon-delivered@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-delivered@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-delivered@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-delivered@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-document.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-document.png
--------------------------------------------------------------------------------
/src/assets/icon-document@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-document@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-document@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-document@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-error.png
--------------------------------------------------------------------------------
/src/assets/icon-error@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-error@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-error@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-error@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-reply.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-reply.png
--------------------------------------------------------------------------------
/src/assets/icon-reply@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-reply@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-reply@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-reply@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-seen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-seen.png
--------------------------------------------------------------------------------
/src/assets/icon-seen@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-seen@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-seen@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-seen@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-send.png
--------------------------------------------------------------------------------
/src/assets/icon-send@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-send@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-send@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-send@3x.png
--------------------------------------------------------------------------------
/src/assets/icon-x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-x.png
--------------------------------------------------------------------------------
/src/assets/icon-x@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-x@2x.png
--------------------------------------------------------------------------------
/src/assets/icon-x@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flyerhq/react-native-chat-ui/007ab3587d2bd96612ad9a96b80a815b5dede746/src/assets/icon-x@3x.png
--------------------------------------------------------------------------------
/src/components/AttachmentButton/AttachmentButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | GestureResponderEvent,
4 | Image,
5 | StyleSheet,
6 | TouchableOpacity,
7 | TouchableOpacityProps,
8 | } from 'react-native'
9 |
10 | import { L10nContext, ThemeContext } from '../../utils'
11 |
12 | export interface AttachmentButtonAdditionalProps {
13 | touchableOpacityProps?: TouchableOpacityProps
14 | }
15 |
16 | export interface AttachmentButtonProps extends AttachmentButtonAdditionalProps {
17 | /** Callback for attachment button tap event */
18 | onPress?: () => void
19 | }
20 |
21 | export const AttachmentButton = ({
22 | onPress,
23 | touchableOpacityProps,
24 | }: AttachmentButtonProps) => {
25 | const l10n = React.useContext(L10nContext)
26 | const theme = React.useContext(ThemeContext)
27 |
28 | const handlePress = (event: GestureResponderEvent) => {
29 | onPress?.()
30 | touchableOpacityProps?.onPress?.(event)
31 | }
32 |
33 | return (
34 |
40 | {theme.icons?.attachmentButtonIcon?.() ?? (
41 |
45 | )}
46 |
47 | )
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | image: {
52 | marginRight: 16,
53 | },
54 | })
55 |
--------------------------------------------------------------------------------
/src/components/AttachmentButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AttachmentButton'
2 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Image, StyleSheet, Text, View } from 'react-native'
3 |
4 | import { MessageType, Theme } from '../../types'
5 | import { getUserAvatarNameColor, getUserInitials } from '../../utils'
6 |
7 | export const Avatar = React.memo(
8 | ({
9 | author,
10 | currentUserIsAuthor,
11 | showAvatar,
12 | showUserAvatars,
13 | theme,
14 | }: {
15 | author: MessageType.Any['author']
16 | currentUserIsAuthor: boolean
17 | showAvatar: boolean
18 | showUserAvatars?: boolean
19 | theme: Theme
20 | }) => {
21 | const renderAvatar = () => {
22 | const color = getUserAvatarNameColor(
23 | author,
24 | theme.colors.userAvatarNameColors
25 | )
26 | const initials = getUserInitials(author)
27 |
28 | if (author.imageUrl) {
29 | return (
30 |
39 | )
40 | }
41 |
42 | return (
43 |
44 | {initials}
45 |
46 | )
47 | }
48 |
49 | return !currentUserIsAuthor && showUserAvatars ? (
50 |
51 | {showAvatar ? renderAvatar() : }
52 |
53 | ) : null
54 | }
55 | )
56 |
57 | const styles = StyleSheet.create({
58 | avatarBackground: {
59 | alignItems: 'center',
60 | borderRadius: 16,
61 | height: 32,
62 | justifyContent: 'center',
63 | marginRight: 8,
64 | width: 32,
65 | },
66 | image: {
67 | alignItems: 'center',
68 | borderRadius: 16,
69 | height: 32,
70 | justifyContent: 'center',
71 | marginRight: 8,
72 | width: 32,
73 | },
74 | placeholder: {
75 | width: 40,
76 | },
77 | })
78 |
--------------------------------------------------------------------------------
/src/components/Avatar/__tests__/Avatar.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react-native'
2 | import * as React from 'react'
3 |
4 | import { user } from '../../../../jest/fixtures'
5 | import { defaultTheme } from '../../../theme'
6 | import { Avatar } from '../Avatar'
7 |
8 | describe('avatar', () => {
9 | it(`should render container with a placeholder`, () => {
10 | expect.assertions(1)
11 | const { getByTestId } = render(
12 |
19 | )
20 | expect(getByTestId('AvatarContainer')).toBeDefined()
21 | })
22 |
23 | it('should render background with a first letter', () => {
24 | expect.assertions(1)
25 | const authorWithName = { ...user, firstName: 'John' }
26 | const { getByText } = render(
27 |
34 | )
35 | expect(getByText(authorWithName.firstName[0])).toBeDefined()
36 | })
37 |
38 | it('should render image background', () => {
39 | expect.assertions(2)
40 | const imageUrl = 'https://avatars.githubusercontent.com/u/14123304?v=4'
41 | const { getAllByRole } = render(
42 |
52 | )
53 | const image = getAllByRole('image')
54 | expect(image).toBeDefined()
55 | expect(image[0]).toHaveProperty('props.source.uri', imageUrl)
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/src/components/Avatar/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Avatar'
2 |
--------------------------------------------------------------------------------
/src/components/Chat/ImageView.android.ts:
--------------------------------------------------------------------------------
1 | import ImageView from 'react-native-image-viewing'
2 |
3 | export default ImageView
4 |
--------------------------------------------------------------------------------
/src/components/Chat/ImageView.ios.ts:
--------------------------------------------------------------------------------
1 | import ImageView from 'react-native-image-viewing'
2 |
3 | export default ImageView
4 |
--------------------------------------------------------------------------------
/src/components/Chat/ImageView.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { ImageRequireSource, ImageURISource, View } from 'react-native'
3 |
4 | interface Props {
5 | imageIndex: number
6 | images: Array
7 | onRequestClose: () => void
8 | visible: boolean
9 | }
10 |
11 | const ImageView = (_: Props) => {
12 | return
13 | }
14 |
15 | export default ImageView
16 |
--------------------------------------------------------------------------------
/src/components/Chat/__tests__/Chat.test.tsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, render } from '@testing-library/react-native'
2 | import * as React from 'react'
3 | import { Text } from 'react-native'
4 |
5 | import {
6 | fileMessage,
7 | imageMessage,
8 | textMessage,
9 | user,
10 | } from '../../../../jest/fixtures'
11 | import { l10n } from '../../../l10n'
12 | import { MessageType } from '../../../types'
13 | import { Chat } from '../Chat'
14 |
15 | describe('chat', () => {
16 | it('renders image preview', () => {
17 | expect.assertions(1)
18 | const messages = [
19 | textMessage,
20 | imageMessage,
21 | fileMessage,
22 | {
23 | ...textMessage,
24 | createdAt: 1,
25 | id: 'new-uuidv4',
26 | status: 'delivered' as const,
27 | },
28 | ]
29 | const onSendPress = jest.fn()
30 | const { getByRole, getByText } = render(
31 |
32 | )
33 | const button = getByRole('image').parent
34 | fireEvent.press(button)
35 | const closeButton = getByText('✕')
36 | expect(closeButton).toBeDefined()
37 | })
38 |
39 | it('sends a text message', () => {
40 | expect.assertions(1)
41 | const messages = [
42 | textMessage,
43 | fileMessage,
44 | {
45 | ...imageMessage,
46 | createdAt: 1,
47 | },
48 | {
49 | ...textMessage,
50 | createdAt: 2,
51 | id: 'new-uuidv4',
52 | status: 'sending' as const,
53 | },
54 | ]
55 | const onSendPress = jest.fn()
56 | const { getByLabelText } = render(
57 |
63 | )
64 | const button = getByLabelText(l10n.en.sendButtonAccessibilityLabel)
65 | fireEvent.press(button)
66 | expect(onSendPress).toHaveBeenCalledWith({ text: 'text', type: 'text' })
67 | })
68 |
69 | it('opens file on a file message tap', () => {
70 | expect.assertions(1)
71 | const messages = [fileMessage, textMessage, imageMessage]
72 | const onSendPress = jest.fn()
73 | const onFilePress = jest.fn()
74 | const onMessagePress = (message: MessageType.Any) => {
75 | if (message.type === 'file') {
76 | onFilePress(message)
77 | }
78 | }
79 | const { getByLabelText } = render(
80 |
87 | )
88 |
89 | const button = getByLabelText(l10n.en.fileButtonAccessibilityLabel)
90 | fireEvent.press(button)
91 | expect(onFilePress).toHaveBeenCalledWith(fileMessage)
92 | })
93 |
94 | it('opens image on image message press', () => {
95 | expect.assertions(1)
96 | const messages = [imageMessage]
97 | const onSendPress = jest.fn()
98 | const onImagePress = jest.fn()
99 | const onMessagePress = (message: MessageType.Any) => {
100 | if (message.type === 'image') {
101 | onImagePress(message)
102 | }
103 | }
104 |
105 | const onMessageLongPress = jest.fn()
106 |
107 | const { getByTestId } = render(
108 |
116 | )
117 |
118 | const button = getByTestId('ContentContainer')
119 | fireEvent.press(button)
120 | expect(onImagePress).toHaveBeenCalledWith(imageMessage)
121 | })
122 |
123 | it('fires image on image message long press', () => {
124 | expect.assertions(1)
125 | const messages = [imageMessage]
126 | const onSendPress = jest.fn()
127 | const onImagePress = jest.fn()
128 | const onMessagePress = (message: MessageType.Any) => {
129 | if (message.type === 'image') {
130 | onImagePress(message)
131 | }
132 | }
133 |
134 | const onMessageLongPress = jest.fn()
135 |
136 | const { getByTestId } = render(
137 |
145 | )
146 |
147 | const button = getByTestId('ContentContainer')
148 | fireEvent(button, 'onLongPress')
149 | expect(onMessageLongPress).toHaveBeenCalledWith(imageMessage)
150 | })
151 |
152 | it('renders empty chat placeholder', () => {
153 | expect.assertions(1)
154 | const messages = []
155 | const onSendPress = jest.fn()
156 | const onMessagePress = jest.fn()
157 | const { getByText } = render(
158 |
164 | )
165 |
166 | const placeholder = getByText(l10n.en.emptyChatPlaceholder)
167 | expect(placeholder).toBeDefined()
168 | })
169 |
170 | it('renders custom bottom component', () => {
171 | expect.assertions(1)
172 | const customBottomComponent = jest.fn(() => Bottom )
173 | const messages = []
174 | const onSendPress = jest.fn()
175 | const onMessagePress = jest.fn()
176 | const { getByText } = render(
177 |
186 | )
187 |
188 | const customComponent = getByText('Bottom')
189 | expect(customComponent).toBeDefined()
190 | })
191 | })
192 |
--------------------------------------------------------------------------------
/src/components/Chat/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Chat'
2 |
--------------------------------------------------------------------------------
/src/components/Chat/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | import { Theme } from '../../types'
4 |
5 | export default ({ theme }: { theme: Theme }) =>
6 | StyleSheet.create({
7 | container: {
8 | backgroundColor: theme.colors.background,
9 | flex: 1,
10 | },
11 | emptyComponentContainer: {
12 | alignItems: 'center',
13 | marginHorizontal: 24,
14 | transform: [{ rotateX: '180deg' }],
15 | },
16 | emptyComponentTitle: {
17 | ...theme.fonts.emptyChatPlaceholderTextStyle,
18 | textAlign: 'center',
19 | },
20 | flatList: {
21 | backgroundColor: theme.colors.background,
22 | height: '100%',
23 | },
24 | flatListContentContainer: {
25 | flexGrow: 1,
26 | },
27 | footer: {
28 | height: 16,
29 | },
30 | footerLoadingPage: {
31 | alignItems: 'center',
32 | justifyContent: 'center',
33 | marginTop: 16,
34 | height: 32,
35 | },
36 | header: {
37 | height: 4,
38 | },
39 | keyboardAccessoryView: {
40 | backgroundColor: theme.colors.inputBackground,
41 | borderTopLeftRadius: theme.borders.inputBorderRadius,
42 | borderTopRightRadius: theme.borders.inputBorderRadius,
43 | },
44 | })
45 |
--------------------------------------------------------------------------------
/src/components/CircularActivityIndicator/CircularActivityIndicator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | Animated,
4 | ColorValue,
5 | Easing,
6 | StyleProp,
7 | ViewStyle,
8 | } from 'react-native'
9 |
10 | import styles from './styles'
11 |
12 | export interface CircularActivityIndicatorProps {
13 | color: ColorValue
14 | size?: number
15 | style?: StyleProp
16 | }
17 |
18 | export const CircularActivityIndicator = ({
19 | color,
20 | size = 24,
21 | style,
22 | }: CircularActivityIndicatorProps) => {
23 | const spinValue = React.useRef(new Animated.Value(0)).current
24 | const { circle } = styles({ color, size })
25 |
26 | React.useEffect(() => {
27 | Animated.loop(
28 | Animated.timing(spinValue, {
29 | toValue: 1,
30 | duration: 600,
31 | easing: Easing.linear,
32 | useNativeDriver: true,
33 | })
34 | ).start()
35 | // eslint-disable-next-line react-hooks/exhaustive-deps
36 | }, [])
37 |
38 | return (
39 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/CircularActivityIndicator/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CircularActivityIndicator'
2 |
--------------------------------------------------------------------------------
/src/components/CircularActivityIndicator/styles.ts:
--------------------------------------------------------------------------------
1 | import { ColorValue, StyleSheet } from 'react-native'
2 |
3 | const styles = ({ color, size }: { color: ColorValue; size: number }) =>
4 | StyleSheet.create({
5 | circle: {
6 | backgroundColor: 'transparent',
7 | borderBottomColor: 'transparent',
8 | borderLeftColor: color,
9 | borderRadius: size / 2,
10 | borderRightColor: color,
11 | borderTopColor: color,
12 | borderWidth: 1.5,
13 | height: size,
14 | width: size,
15 | },
16 | })
17 |
18 | export default styles
19 |
--------------------------------------------------------------------------------
/src/components/FileMessage/FileMessage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Image, Text, View } from 'react-native'
3 |
4 | import { MessageType } from '../../types'
5 | import {
6 | formatBytes,
7 | L10nContext,
8 | ThemeContext,
9 | UserContext,
10 | } from '../../utils'
11 | import styles from './styles'
12 |
13 | export interface FileMessageProps {
14 | message: MessageType.DerivedFile
15 | }
16 |
17 | export const FileMessage = ({ message }: FileMessageProps) => {
18 | const l10n = React.useContext(L10nContext)
19 | const theme = React.useContext(ThemeContext)
20 | const user = React.useContext(UserContext)
21 | const { container, icon, iconContainer, name, size, textContainer } = styles({
22 | message,
23 | theme,
24 | user,
25 | })
26 |
27 | return (
28 |
32 |
33 | {theme.icons?.documentIcon?.() ?? (
34 |
38 | )}
39 |
40 |
41 | {message.name}
42 | {formatBytes(message.size)}
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/FileMessage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FileMessage'
2 |
--------------------------------------------------------------------------------
/src/components/FileMessage/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | import { MessageType, Theme, User } from '../../types'
4 |
5 | const styles = ({
6 | message,
7 | theme,
8 | user,
9 | }: {
10 | message: MessageType.DerivedFile
11 | theme: Theme
12 | user?: User
13 | }) =>
14 | StyleSheet.create({
15 | container: {
16 | alignItems: 'center',
17 | flexDirection: 'row',
18 | padding: theme.insets.messageInsetsVertical,
19 | paddingRight: theme.insets.messageInsetsHorizontal,
20 | },
21 | icon: {
22 | tintColor:
23 | user?.id === message.author.id
24 | ? theme.colors.sentMessageDocumentIcon
25 | : theme.colors.receivedMessageDocumentIcon,
26 | },
27 | iconContainer: {
28 | alignItems: 'center',
29 | backgroundColor:
30 | user?.id === message.author.id
31 | ? `${String(theme.colors.sentMessageDocumentIcon)}33`
32 | : `${String(theme.colors.receivedMessageDocumentIcon)}33`,
33 | borderRadius: 21,
34 | height: 42,
35 | justifyContent: 'center',
36 | width: 42,
37 | },
38 | name:
39 | user?.id === message.author.id
40 | ? theme.fonts.sentMessageBodyTextStyle
41 | : theme.fonts.receivedMessageBodyTextStyle,
42 | size: {
43 | ...(user?.id === message.author.id
44 | ? theme.fonts.sentMessageCaptionTextStyle
45 | : theme.fonts.receivedMessageCaptionTextStyle),
46 | marginTop: 4,
47 | },
48 | textContainer: {
49 | flexShrink: 1,
50 | marginLeft: 16,
51 | },
52 | })
53 |
54 | export default styles
55 |
--------------------------------------------------------------------------------
/src/components/ImageMessage/ImageMessage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Image, ImageBackground, Text, View } from 'react-native'
3 |
4 | import { MessageType, Size } from '../../types'
5 | import { formatBytes, ThemeContext, UserContext } from '../../utils'
6 | import styles from './styles'
7 |
8 | export interface ImageMessageProps {
9 | message: MessageType.DerivedImage
10 | /** Maximum message width */
11 | messageWidth: number
12 | }
13 |
14 | /** Image message component. Supports different
15 | * aspect ratios, renders blurred image as a background which is visible
16 | * if the image is narrow, renders image in form of a file if aspect
17 | * ratio is very small or very big. */
18 | export const ImageMessage = ({ message, messageWidth }: ImageMessageProps) => {
19 | const theme = React.useContext(ThemeContext)
20 | const user = React.useContext(UserContext)
21 | const defaultHeight = message.height ?? 0
22 | const defaultWidth = message.width ?? 0
23 | const [size, setSize] = React.useState({
24 | height: defaultHeight,
25 | width: defaultWidth,
26 | })
27 | const aspectRatio = size.width / (size.height || 1)
28 | const isMinimized = aspectRatio < 0.1 || aspectRatio > 10
29 | const {
30 | horizontalImage,
31 | minimizedImage,
32 | minimizedImageContainer,
33 | nameText,
34 | sizeText,
35 | textContainer,
36 | verticalImage,
37 | } = styles({
38 | aspectRatio,
39 | message,
40 | messageWidth,
41 | theme,
42 | user,
43 | })
44 |
45 | React.useEffect(() => {
46 | if (defaultHeight <= 0 || defaultWidth <= 0)
47 | Image.getSize(
48 | message.uri,
49 | (width, height) => setSize({ height, width }),
50 | () => setSize({ height: 0, width: 0 })
51 | )
52 | }, [defaultHeight, defaultWidth, message.uri])
53 |
54 | const renderImage = () => {
55 | return (
56 |
68 | )
69 | }
70 |
71 | return isMinimized ? (
72 |
73 | {renderImage()}
74 |
75 | {message.name}
76 | {formatBytes(message.size)}
77 |
78 |
79 | ) : (
80 |
81 | {renderImage()}
82 |
83 | )
84 | }
85 |
--------------------------------------------------------------------------------
/src/components/ImageMessage/__tests__/ImageMessage.test.tsx:
--------------------------------------------------------------------------------
1 | import { act, render } from '@testing-library/react-native'
2 | import * as React from 'react'
3 | import { Image } from 'react-native'
4 |
5 | import { derivedImageMessage, size } from '../../../../jest/fixtures'
6 | import { ImageMessage } from '../ImageMessage'
7 |
8 | describe('image message', () => {
9 | it('gets image size and renders', () => {
10 | expect.assertions(5)
11 | const getSizeMock = jest.spyOn(Image, 'getSize')
12 | getSizeMock.mockImplementation(() => {})
13 | const message = {
14 | ...derivedImageMessage,
15 | height: undefined,
16 | width: undefined,
17 | }
18 | const { getByRole } = render(
19 |
20 | )
21 | expect(getSizeMock).toHaveBeenCalledTimes(1)
22 | const getSizeArgs = getSizeMock.mock.calls[0]
23 | expect(getSizeArgs[0]).toBe(derivedImageMessage.uri)
24 | const success = getSizeArgs[1]
25 | const error = getSizeArgs[2]
26 | act(() => {
27 | success(size.width, size.height)
28 | })
29 | const successImageComponent = getByRole('image')
30 | expect(successImageComponent.props).toHaveProperty('style.height', 440)
31 | act(() => {
32 | success(size.width, size.width * 10 + 1)
33 | })
34 | const successMinimizedImageComponent = getByRole('image')
35 | expect(successMinimizedImageComponent.props).toHaveProperty(
36 | 'style.width',
37 | 64
38 | )
39 | act(() => {
40 | error(new Error())
41 | })
42 | const errorImageComponent = getByRole('image')
43 | expect(errorImageComponent.props).toHaveProperty('style.width', 64)
44 | getSizeMock.mockRestore()
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/src/components/ImageMessage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ImageMessage'
2 |
--------------------------------------------------------------------------------
/src/components/ImageMessage/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | import { MessageType, Theme, User } from '../../types'
4 |
5 | const styles = ({
6 | aspectRatio,
7 | message,
8 | messageWidth,
9 | theme,
10 | user,
11 | }: {
12 | aspectRatio: number
13 | message: MessageType.Image
14 | messageWidth: number
15 | theme: Theme
16 | user?: User
17 | }) =>
18 | StyleSheet.create({
19 | horizontalImage: {
20 | height: messageWidth / aspectRatio,
21 | maxHeight: messageWidth,
22 | width: messageWidth,
23 | },
24 | minimizedImage: {
25 | borderRadius: 15,
26 | height: 64,
27 | marginLeft: theme.insets.messageInsetsVertical,
28 | marginRight: 16,
29 | marginVertical: theme.insets.messageInsetsVertical,
30 | width: 64,
31 | },
32 | minimizedImageContainer: {
33 | alignItems: 'center',
34 | backgroundColor:
35 | user?.id === message.author.id
36 | ? theme.colors.primary
37 | : theme.colors.secondary,
38 | flexDirection: 'row',
39 | },
40 | nameText:
41 | user?.id === message.author.id
42 | ? theme.fonts.sentMessageBodyTextStyle
43 | : theme.fonts.receivedMessageBodyTextStyle,
44 | sizeText: {
45 | ...(user?.id === message.author.id
46 | ? theme.fonts.sentMessageCaptionTextStyle
47 | : theme.fonts.receivedMessageCaptionTextStyle),
48 | marginTop: 4,
49 | },
50 | textContainer: {
51 | flexShrink: 1,
52 | marginRight: theme.insets.messageInsetsHorizontal,
53 | marginVertical: theme.insets.messageInsetsVertical,
54 | },
55 | verticalImage: {
56 | height: messageWidth,
57 | minWidth: 170,
58 | width: messageWidth * aspectRatio,
59 | },
60 | })
61 |
62 | export default styles
63 |
--------------------------------------------------------------------------------
/src/components/Input/Input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { TextInput, TextInputProps, View } from 'react-native'
3 |
4 | import { MessageType } from '../../types'
5 | import { L10nContext, ThemeContext, unwrap, UserContext } from '../../utils'
6 | import {
7 | AttachmentButton,
8 | AttachmentButtonAdditionalProps,
9 | } from '../AttachmentButton'
10 | import {
11 | CircularActivityIndicator,
12 | CircularActivityIndicatorProps,
13 | } from '../CircularActivityIndicator'
14 | import { SendButton } from '../SendButton'
15 | import styles from './styles'
16 |
17 | export interface InputTopLevelProps {
18 | /** Whether attachment is uploading. Will replace attachment button with a
19 | * {@link CircularActivityIndicator}. Since we don't have libraries for
20 | * managing media in dependencies we have no way of knowing if
21 | * something is uploading so you need to set this manually. */
22 | isAttachmentUploading?: boolean
23 | /** @see {@link AttachmentButtonProps.onPress} */
24 | onAttachmentPress?: () => void
25 | /** Will be called on {@link SendButton} tap. Has {@link MessageType.PartialText} which can
26 | * be transformed to {@link MessageType.Text} and added to the messages list. */
27 | onSendPress: (message: MessageType.PartialText) => void
28 | /** Controls the visibility behavior of the {@link SendButton} based on the
29 | * `TextInput` state. Defaults to `editing`. */
30 | sendButtonVisibilityMode?: 'always' | 'editing'
31 | textInputProps?: TextInputProps
32 | }
33 |
34 | export interface InputAdditionalProps {
35 | attachmentButtonProps?: AttachmentButtonAdditionalProps
36 | attachmentCircularActivityIndicatorProps?: CircularActivityIndicatorProps
37 | }
38 |
39 | export type InputProps = InputTopLevelProps & InputAdditionalProps
40 |
41 | /** Bottom bar input component with a text input, attachment and
42 | * send buttons inside. By default hides send button when text input is empty. */
43 | export const Input = ({
44 | attachmentButtonProps,
45 | attachmentCircularActivityIndicatorProps,
46 | isAttachmentUploading,
47 | onAttachmentPress,
48 | onSendPress,
49 | sendButtonVisibilityMode,
50 | textInputProps,
51 | }: InputProps) => {
52 | const l10n = React.useContext(L10nContext)
53 | const theme = React.useContext(ThemeContext)
54 | const user = React.useContext(UserContext)
55 | const { container, input, marginRight } = styles({ theme })
56 |
57 | // Use `defaultValue` if provided
58 | const [text, setText] = React.useState(textInputProps?.defaultValue ?? '')
59 |
60 | const value = textInputProps?.value ?? text
61 |
62 | const handleChangeText = (newText: string) => {
63 | // Track local state in case `onChangeText` is provided and `value` is not
64 | setText(newText)
65 | textInputProps?.onChangeText?.(newText)
66 | }
67 |
68 | const handleSend = () => {
69 | const trimmedValue = value.trim()
70 |
71 | // Impossible to test since button is not visible when value is empty.
72 | // Additional check for the keyboard input.
73 | /* istanbul ignore next */
74 | if (trimmedValue) {
75 | onSendPress({ text: trimmedValue, type: 'text' })
76 | setText('')
77 | }
78 | }
79 |
80 | return (
81 |
82 | {user &&
83 | (isAttachmentUploading ? (
84 |
91 | ) : (
92 | !!onAttachmentPress && (
93 |
97 | )
98 | ))}
99 |
110 | {sendButtonVisibilityMode === 'always' ||
111 | (sendButtonVisibilityMode === 'editing' && user && value.trim()) ? (
112 |
113 | ) : null}
114 |
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/src/components/Input/__tests__/Input.test.tsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, render } from '@testing-library/react-native'
2 | import * as React from 'react'
3 | import { ScrollView } from 'react-native'
4 |
5 | import { user } from '../../../../jest/fixtures'
6 | import { l10n } from '../../../l10n'
7 | import { UserContext } from '../../../utils'
8 | import { Input } from '../Input'
9 |
10 | const renderScrollable = () =>
11 |
12 | describe('input', () => {
13 | it('sends a text message', () => {
14 | expect.assertions(2)
15 | const onSendPress = jest.fn()
16 | const { getByPlaceholderText, getByLabelText } = render(
17 |
18 |
25 |
26 | )
27 | const textInput = getByPlaceholderText(l10n.en.inputPlaceholder)
28 | fireEvent.changeText(textInput, 'text')
29 | const button = getByLabelText(l10n.en.sendButtonAccessibilityLabel)
30 | fireEvent.press(button)
31 | expect(onSendPress).toHaveBeenCalledWith({ text: 'text', type: 'text' })
32 | expect(textInput.props).toHaveProperty('value', '')
33 | })
34 |
35 | it('sends a text message if onChangeText and value are provided', () => {
36 | expect.assertions(2)
37 | const onSendPress = jest.fn()
38 | const value = 'value'
39 | const onChangeText = jest.fn((newValue) => {
40 | rerender(
41 |
42 |
50 |
51 | )
52 | })
53 | const { getByPlaceholderText, getByLabelText, rerender } = render(
54 |
55 |
63 |
64 | )
65 | const textInput = getByPlaceholderText(l10n.en.inputPlaceholder)
66 | fireEvent.changeText(textInput, 'text')
67 | const button = getByLabelText(l10n.en.sendButtonAccessibilityLabel)
68 | fireEvent.press(button)
69 | expect(onSendPress).toHaveBeenCalledWith({ text: 'text', type: 'text' })
70 | expect(textInput.props).toHaveProperty('value', 'text')
71 | })
72 |
73 | it('sends a text message if onChangeText is provided', () => {
74 | expect.assertions(2)
75 | const onSendPress = jest.fn()
76 | const onChangeText = jest.fn()
77 | const { getByPlaceholderText, getByLabelText } = render(
78 |
79 |
87 |
88 | )
89 | const textInput = getByPlaceholderText(l10n.en.inputPlaceholder)
90 | fireEvent.changeText(textInput, 'text')
91 | const button = getByLabelText(l10n.en.sendButtonAccessibilityLabel)
92 | fireEvent.press(button)
93 | expect(onSendPress).toHaveBeenCalledWith({ text: 'text', type: 'text' })
94 | expect(textInput.props).toHaveProperty('value', '')
95 | })
96 |
97 | it('sends a text message if value is provided', () => {
98 | expect.assertions(2)
99 | const onSendPress = jest.fn()
100 | const value = 'value'
101 | const { getByPlaceholderText, getByLabelText } = render(
102 |
103 |
111 |
112 | )
113 | const textInput = getByPlaceholderText(l10n.en.inputPlaceholder)
114 | fireEvent.changeText(textInput, 'text')
115 | const button = getByLabelText(l10n.en.sendButtonAccessibilityLabel)
116 | fireEvent.press(button)
117 | expect(onSendPress).toHaveBeenCalledWith({ text: value, type: 'text' })
118 | expect(textInput.props).toHaveProperty('value', value)
119 | })
120 |
121 | it('sends a text message if defaultValue is provided', () => {
122 | expect.assertions(2)
123 | const onSendPress = jest.fn()
124 | const defaultValue = 'defaultValue'
125 | const { getByPlaceholderText, getByLabelText } = render(
126 |
127 |
135 |
136 | )
137 | const textInput = getByPlaceholderText(l10n.en.inputPlaceholder)
138 | const button = getByLabelText(l10n.en.sendButtonAccessibilityLabel)
139 | fireEvent.press(button)
140 | expect(onSendPress).toHaveBeenCalledWith({
141 | text: defaultValue,
142 | type: 'text',
143 | })
144 | expect(textInput.props).toHaveProperty('value', '')
145 | })
146 |
147 | it('sends an image message', () => {
148 | expect.assertions(1)
149 | const onAttachmentPress = jest.fn()
150 | const onSendPress = jest.fn()
151 | const { getByLabelText } = render(
152 |
153 |
161 |
162 | )
163 | const button = getByLabelText(l10n.en.attachmentButtonAccessibilityLabel)
164 | fireEvent.press(button)
165 | expect(onAttachmentPress).toHaveBeenCalledTimes(1)
166 | })
167 |
168 | it('shows activity indicator when attachment is uploading', () => {
169 | expect.assertions(1)
170 | const isAttachmentUploading = true
171 | const onSendPress = jest.fn()
172 | const { getByTestId } = render(
173 |
174 |
186 |
187 | )
188 |
189 | const indicator = getByTestId('CircularActivityIndicator')
190 | expect(indicator).toBeDefined()
191 | })
192 | })
193 |
--------------------------------------------------------------------------------
/src/components/Input/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Input'
2 |
--------------------------------------------------------------------------------
/src/components/Input/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | import { Theme } from '../../types'
4 |
5 | export default ({ theme }: { theme: Theme }) =>
6 | StyleSheet.create({
7 | container: {
8 | alignItems: 'center',
9 | flexDirection: 'row',
10 | paddingHorizontal: 24,
11 | paddingVertical: 20,
12 | },
13 | input: {
14 | ...theme.fonts.inputTextStyle,
15 | color: theme.colors.inputText,
16 | flex: 1,
17 | maxHeight: 100,
18 | // Fixes default paddings for Android
19 | paddingBottom: 0,
20 | paddingTop: 0,
21 | },
22 | marginRight: {
23 | marginRight: 16,
24 | },
25 | })
26 |
--------------------------------------------------------------------------------
/src/components/Message/Message.tsx:
--------------------------------------------------------------------------------
1 | import { oneOf } from '@flyerhq/react-native-link-preview'
2 | import * as React from 'react'
3 | import { Pressable, Text, View } from 'react-native'
4 |
5 | import { MessageType } from '../../types'
6 | import {
7 | excludeDerivedMessageProps,
8 | ThemeContext,
9 | UserContext,
10 | } from '../../utils'
11 | import { Avatar } from '../Avatar'
12 | import { FileMessage } from '../FileMessage'
13 | import { ImageMessage } from '../ImageMessage'
14 | import { StatusIcon } from '../StatusIcon'
15 | import { TextMessage, TextMessageTopLevelProps } from '../TextMessage'
16 | import styles from './styles'
17 |
18 | export interface MessageTopLevelProps extends TextMessageTopLevelProps {
19 | /** Called when user makes a long press on any message */
20 | onMessageLongPress?: (message: MessageType.Any) => void
21 | /** Called when user taps on any message */
22 | onMessagePress?: (message: MessageType.Any) => void
23 | /** Customize the default bubble using this function. `child` is a content
24 | * you should render inside your bubble, `message` is a current message
25 | * (contains `author` inside) and `nextMessageInGroup` allows you to see
26 | * if the message is a part of a group (messages are grouped when written
27 | * in quick succession by the same author) */
28 | renderBubble?: (payload: {
29 | child: React.ReactNode
30 | message: MessageType.Any
31 | nextMessageInGroup: boolean
32 | }) => React.ReactNode
33 | /** Render a custom message inside predefined bubble */
34 | renderCustomMessage?: (
35 | message: MessageType.Custom,
36 | messageWidth: number
37 | ) => React.ReactNode
38 | /** Render a file message inside predefined bubble */
39 | renderFileMessage?: (
40 | message: MessageType.File,
41 | messageWidth: number
42 | ) => React.ReactNode
43 | /** Render an image message inside predefined bubble */
44 | renderImageMessage?: (
45 | message: MessageType.Image,
46 | messageWidth: number
47 | ) => React.ReactNode
48 | /** Render a text message inside predefined bubble */
49 | renderTextMessage?: (
50 | message: MessageType.Text,
51 | messageWidth: number,
52 | showName: boolean
53 | ) => React.ReactNode
54 | /** Show user avatars for received messages. Useful for a group chat. */
55 | showUserAvatars?: boolean
56 | }
57 |
58 | export interface MessageProps extends MessageTopLevelProps {
59 | enableAnimation?: boolean
60 | message: MessageType.DerivedAny
61 | messageWidth: number
62 | roundBorder: boolean
63 | showAvatar: boolean
64 | showName: boolean
65 | showStatus: boolean
66 | }
67 |
68 | /** Base component for all message types in the chat. Renders bubbles around
69 | * messages and status. Sets maximum width for a message for
70 | * a nice look on larger screens. */
71 | export const Message = React.memo(
72 | ({
73 | enableAnimation,
74 | message,
75 | messageWidth,
76 | onMessagePress,
77 | onMessageLongPress,
78 | onPreviewDataFetched,
79 | renderBubble,
80 | renderCustomMessage,
81 | renderFileMessage,
82 | renderImageMessage,
83 | renderTextMessage,
84 | roundBorder,
85 | showAvatar,
86 | showName,
87 | showStatus,
88 | showUserAvatars,
89 | usePreviewData,
90 | }: MessageProps) => {
91 | const theme = React.useContext(ThemeContext)
92 | const user = React.useContext(UserContext)
93 |
94 | const currentUserIsAuthor =
95 | message.type !== 'dateHeader' && user?.id === message.author.id
96 |
97 | const { container, contentContainer, dateHeader, pressable } = styles({
98 | currentUserIsAuthor,
99 | message,
100 | messageWidth,
101 | roundBorder,
102 | theme,
103 | })
104 |
105 | if (message.type === 'dateHeader') {
106 | return (
107 |
108 | {message.text}
109 |
110 | )
111 | }
112 |
113 | const renderBubbleContainer = () => {
114 | const child = renderMessage()
115 |
116 | return oneOf(
117 | renderBubble,
118 |
119 | {child}
120 |
121 | )({
122 | child,
123 | message: excludeDerivedMessageProps(message),
124 | nextMessageInGroup: roundBorder,
125 | })
126 | }
127 |
128 | const renderMessage = () => {
129 | switch (message.type) {
130 | case 'custom':
131 | return (
132 | renderCustomMessage?.(
133 | // It's okay to cast here since we checked message type above
134 | // type-coverage:ignore-next-line
135 | excludeDerivedMessageProps(message) as MessageType.Custom,
136 | messageWidth
137 | ) ?? null
138 | )
139 | case 'file':
140 | return oneOf(renderFileMessage, )(
141 | // type-coverage:ignore-next-line
142 | excludeDerivedMessageProps(message) as MessageType.File,
143 | messageWidth
144 | )
145 | case 'image':
146 | return oneOf(
147 | renderImageMessage,
148 |
154 | )(
155 | // type-coverage:ignore-next-line
156 | excludeDerivedMessageProps(message) as MessageType.Image,
157 | messageWidth
158 | )
159 | case 'text':
160 | return oneOf(
161 | renderTextMessage,
162 |
172 | )(
173 | // type-coverage:ignore-next-line
174 | excludeDerivedMessageProps(message) as MessageType.Text,
175 | messageWidth,
176 | showName
177 | )
178 | default:
179 | return null
180 | }
181 | }
182 |
183 | return (
184 |
185 |
194 |
196 | onMessageLongPress?.(excludeDerivedMessageProps(message))
197 | }
198 | onPress={() => onMessagePress?.(excludeDerivedMessageProps(message))}
199 | style={pressable}
200 | >
201 | {renderBubbleContainer()}
202 |
203 |
211 |
212 | )
213 | }
214 | )
215 |
--------------------------------------------------------------------------------
/src/components/Message/__tests__/Message.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react-native'
2 | import * as React from 'react'
3 |
4 | import { derivedTextMessage } from '../../../../jest/fixtures'
5 | import { Message } from '../Message'
6 |
7 | describe('message', () => {
8 | it('renders undefined in ContentContainer', () => {
9 | expect.assertions(2)
10 | const { getByTestId } = render(
11 |
20 | )
21 | const ContentContainer = getByTestId('ContentContainer')
22 | expect(ContentContainer).toBeDefined()
23 | expect(ContentContainer).toHaveProperty('props.children[0]', undefined)
24 | })
25 |
26 | it('renders undefined in ContentContainer with wrong message type', () => {
27 | expect.assertions(2)
28 | const { getByTestId } = render(
29 |
38 | )
39 | const ContentContainer = getByTestId('ContentContainer')
40 | expect(ContentContainer).toBeDefined()
41 | expect(ContentContainer).toHaveProperty('props.children[0]', undefined)
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/src/components/Message/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Message'
2 |
--------------------------------------------------------------------------------
/src/components/Message/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | import { MessageType, Theme } from '../../types'
4 |
5 | const styles = ({
6 | currentUserIsAuthor,
7 | message,
8 | messageWidth,
9 | roundBorder,
10 | theme,
11 | }: {
12 | currentUserIsAuthor: boolean
13 | message: MessageType.DerivedAny
14 | messageWidth: number
15 | roundBorder: boolean
16 | theme: Theme
17 | }) =>
18 | StyleSheet.create({
19 | container: {
20 | alignItems: 'flex-end',
21 | alignSelf: currentUserIsAuthor ? 'flex-end' : 'flex-start',
22 | justifyContent: !currentUserIsAuthor ? 'flex-end' : 'flex-start',
23 | flex: 1,
24 | flexDirection: 'row',
25 | marginBottom: message.type === 'dateHeader' ? 0 : 4 + message.offset,
26 | marginLeft: 20,
27 | },
28 | contentContainer: {
29 | backgroundColor:
30 | !currentUserIsAuthor || message.type === 'image'
31 | ? theme.colors.secondary
32 | : theme.colors.primary,
33 | borderBottomLeftRadius:
34 | currentUserIsAuthor || roundBorder
35 | ? theme.borders.messageBorderRadius
36 | : 0,
37 | borderBottomRightRadius: currentUserIsAuthor
38 | ? roundBorder
39 | ? theme.borders.messageBorderRadius
40 | : 0
41 | : theme.borders.messageBorderRadius,
42 | borderColor: 'transparent',
43 | borderRadius: theme.borders.messageBorderRadius,
44 | overflow: 'hidden',
45 | },
46 | dateHeader: {
47 | alignItems: 'center',
48 | justifyContent: 'center',
49 | marginBottom: 32,
50 | marginTop: 16,
51 | },
52 | pressable: {
53 | maxWidth: messageWidth,
54 | },
55 | })
56 |
57 | export default styles
58 |
--------------------------------------------------------------------------------
/src/components/SendButton/SendButton.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {
3 | GestureResponderEvent,
4 | Image,
5 | StyleSheet,
6 | TouchableOpacity,
7 | TouchableOpacityProps,
8 | } from 'react-native'
9 |
10 | import { L10nContext, ThemeContext } from '../../utils'
11 |
12 | export interface SendButtonPropsAdditionalProps {
13 | touchableOpacityProps?: TouchableOpacityProps
14 | }
15 |
16 | export interface SendButtonProps extends SendButtonPropsAdditionalProps {
17 | /** Callback for send button tap event */
18 | onPress: () => void
19 | }
20 |
21 | export const SendButton = ({
22 | onPress,
23 | touchableOpacityProps,
24 | }: SendButtonProps) => {
25 | const l10n = React.useContext(L10nContext)
26 | const theme = React.useContext(ThemeContext)
27 |
28 | const handlePress = (event: GestureResponderEvent) => {
29 | onPress()
30 | touchableOpacityProps?.onPress?.(event)
31 | }
32 |
33 | return (
34 |
41 | {theme.icons?.sendButtonIcon?.() ?? (
42 |
46 | )}
47 |
48 | )
49 | }
50 |
51 | const styles = StyleSheet.create({
52 | sendButton: {
53 | marginLeft: 16,
54 | },
55 | })
56 |
--------------------------------------------------------------------------------
/src/components/SendButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SendButton'
2 |
--------------------------------------------------------------------------------
/src/components/StatusIcon/StatusIcon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Image, StyleSheet, View } from 'react-native'
3 |
4 | import { MessageType, Theme } from '../../types'
5 | import { CircularActivityIndicator } from '../CircularActivityIndicator'
6 |
7 | export const StatusIcon = React.memo(
8 | ({
9 | currentUserIsAuthor,
10 | showStatus,
11 | status,
12 | theme,
13 | }: {
14 | currentUserIsAuthor: boolean
15 | showStatus: boolean
16 | status?: MessageType.Any['status']
17 | theme: Theme
18 | }) => {
19 | let statusIcon: React.ReactNode | null = null
20 |
21 | if (showStatus) {
22 | switch (status) {
23 | case 'delivered':
24 | case 'sent':
25 | statusIcon = theme.icons?.deliveredIcon?.() ?? (
26 |
31 | )
32 | break
33 | case 'error':
34 | statusIcon = theme.icons?.errorIcon?.() ?? (
35 |
40 | )
41 | break
42 | case 'seen':
43 | statusIcon = theme.icons?.seenIcon?.() ?? (
44 |
49 | )
50 | break
51 | case 'sending':
52 | statusIcon = theme.icons?.sendingIcon?.() ?? (
53 |
54 | )
55 | break
56 | default:
57 | break
58 | }
59 | }
60 |
61 | return currentUserIsAuthor ? (
62 |
63 | {statusIcon}
64 |
65 | ) : null
66 | }
67 | )
68 |
69 | const styles = StyleSheet.create({
70 | container: {
71 | alignItems: 'center',
72 | height: 16,
73 | justifyContent: 'center',
74 | paddingHorizontal: 4,
75 | width: 16,
76 | },
77 | })
78 |
--------------------------------------------------------------------------------
/src/components/StatusIcon/__tests__/StatusIcon.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react-native'
2 | import * as React from 'react'
3 | import { Image, View } from 'react-native'
4 |
5 | import { defaultTheme } from '../../../theme'
6 | import { StatusIcon } from '../StatusIcon'
7 |
8 | describe('status icon', () => {
9 | it('should render null if show status is false', () => {
10 | expect.assertions(1)
11 | const { queryByTestId } = render(
12 |
17 | )
18 | expect(queryByTestId('StatusIconContainer')).toBeNull()
19 | })
20 |
21 | it('should render delivered icon', () => {
22 | expect.assertions(1)
23 | const { getByTestId } = render(
24 |
30 | )
31 | expect(getByTestId('DeliveredIcon')).toBeDefined()
32 | })
33 |
34 | it('should render delivered icon from theme', () => {
35 | expect.assertions(1)
36 | const { queryByTestId } = render(
37 | (
45 |
46 | ),
47 | },
48 | }}
49 | />
50 | )
51 | expect(queryByTestId('DeliveredIcon')).toBeNull()
52 | })
53 |
54 | it('should render delivered icon with sent status', () => {
55 | expect.assertions(1)
56 | const { getByTestId } = render(
57 |
63 | )
64 | expect(getByTestId('DeliveredIcon')).toBeDefined()
65 | })
66 |
67 | it('should render delivered icon with sent status from theme', () => {
68 | expect.assertions(1)
69 | const { queryByTestId } = render(
70 | (
78 |
79 | ),
80 | },
81 | }}
82 | />
83 | )
84 | expect(queryByTestId('DeliveredIcon')).toBeNull()
85 | })
86 |
87 | it('should render error icon', () => {
88 | expect.assertions(1)
89 | const { getByTestId } = render(
90 |
96 | )
97 | expect(getByTestId('ErrorIcon')).toBeDefined()
98 | })
99 |
100 | it('should render error icon from theme', () => {
101 | expect.assertions(1)
102 | const { queryByTestId } = render(
103 | (
111 |
112 | ),
113 | },
114 | }}
115 | />
116 | )
117 | expect(queryByTestId('ErrorIcon')).toBeNull()
118 | })
119 |
120 | it('should render seen icon', () => {
121 | expect.assertions(1)
122 | const { getByTestId } = render(
123 |
129 | )
130 | expect(getByTestId('SeenIcon')).toBeDefined()
131 | })
132 |
133 | it('should render seen icon from theme', () => {
134 | expect.assertions(1)
135 | const { queryByTestId } = render(
136 | (
144 |
145 | ),
146 | },
147 | }}
148 | />
149 | )
150 | expect(queryByTestId('SeenIcon')).toBeNull()
151 | })
152 |
153 | it('should render activity indicator', () => {
154 | expect.assertions(1)
155 | const { getByTestId } = render(
156 |
162 | )
163 | expect(getByTestId('CircularActivityIndicator')).toBeDefined()
164 | })
165 |
166 | it('should render sending icon from theme', () => {
167 | expect.assertions(1)
168 | const { queryByTestId } = render(
169 | ,
177 | },
178 | }}
179 | />
180 | )
181 | expect(queryByTestId('CircularActivityIndicator')).toBeNull()
182 | })
183 | })
184 |
--------------------------------------------------------------------------------
/src/components/StatusIcon/index.ts:
--------------------------------------------------------------------------------
1 | export * from './StatusIcon'
2 |
--------------------------------------------------------------------------------
/src/components/TextMessage/TextMessage.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | LinkPreview,
3 | PreviewData,
4 | REGEX_LINK,
5 | } from '@flyerhq/react-native-link-preview'
6 | import * as React from 'react'
7 | import { Linking, Text, View } from 'react-native'
8 | import ParsedText from 'react-native-parsed-text'
9 |
10 | import { MessageType } from '../../types'
11 | import {
12 | excludeDerivedMessageProps,
13 | getUserName,
14 | ThemeContext,
15 | UserContext,
16 | } from '../../utils'
17 | import styles from './styles'
18 |
19 | export interface TextMessageTopLevelProps {
20 | /** @see {@link LinkPreviewProps.onPreviewDataFetched} */
21 | onPreviewDataFetched?: ({
22 | message,
23 | previewData,
24 | }: {
25 | message: MessageType.Text
26 | previewData: PreviewData
27 | }) => void
28 | /** Enables link (URL) preview */
29 | usePreviewData?: boolean
30 | }
31 |
32 | export interface TextMessageProps extends TextMessageTopLevelProps {
33 | enableAnimation?: boolean
34 | message: MessageType.DerivedText
35 | messageWidth: number
36 | showName: boolean
37 | }
38 |
39 | export const TextMessage = ({
40 | enableAnimation,
41 | message,
42 | messageWidth,
43 | onPreviewDataFetched,
44 | showName,
45 | usePreviewData,
46 | }: TextMessageProps) => {
47 | const theme = React.useContext(ThemeContext)
48 | const user = React.useContext(UserContext)
49 | const [previewData, setPreviewData] = React.useState(message.previewData)
50 | const { descriptionText, headerText, titleText, text, textContainer } =
51 | styles({
52 | message,
53 | theme,
54 | user,
55 | })
56 |
57 | const handleEmailPress = (email: string) => {
58 | try {
59 | Linking.openURL(`mailto:${email}`)
60 | } catch {}
61 | }
62 |
63 | const handlePreviewDataFetched = (data: PreviewData) => {
64 | setPreviewData(data)
65 | onPreviewDataFetched?.({
66 | // It's okay to cast here since we know it is a text message
67 | // type-coverage:ignore-next-line
68 | message: excludeDerivedMessageProps(message) as MessageType.Text,
69 | previewData: data,
70 | })
71 | }
72 |
73 | const handleUrlPress = (url: string) => {
74 | const uri = url.toLowerCase().startsWith('http') ? url : `https://${url}`
75 |
76 | Linking.openURL(uri)
77 | }
78 |
79 | const renderPreviewDescription = (description: string) => {
80 | return (
81 |
82 | {description}
83 |
84 | )
85 | }
86 |
87 | const renderPreviewHeader = (header: string) => {
88 | return (
89 |
90 | {header}
91 |
92 | )
93 | }
94 |
95 | const renderPreviewText = (previewText: string) => {
96 | return (
97 |
113 | {previewText}
114 |
115 | )
116 | }
117 |
118 | const renderPreviewTitle = (title: string) => {
119 | return (
120 |
121 | {title}
122 |
123 | )
124 | }
125 |
126 | return usePreviewData &&
127 | !!onPreviewDataFetched &&
128 | REGEX_LINK.test(message.text.toLowerCase()) ? (
129 |
147 | ) : (
148 |
149 | {
150 | // Tested inside the link preview
151 | /* istanbul ignore next */ showName
152 | ? renderPreviewHeader(getUserName(message.author))
153 | : null
154 | }
155 | {message.text}
156 |
157 | )
158 | }
159 |
--------------------------------------------------------------------------------
/src/components/TextMessage/__tests__/TextMessage.test.tsx:
--------------------------------------------------------------------------------
1 | import * as utils from '@flyerhq/react-native-link-preview/lib/utils'
2 | import { fireEvent, render, waitFor } from '@testing-library/react-native'
3 | import * as React from 'react'
4 | import { Linking } from 'react-native'
5 |
6 | import { derivedTextMessage } from '../../../../jest/fixtures'
7 | import { TextMessage } from '../TextMessage'
8 |
9 | describe('text message', () => {
10 | it('renders preview image and handles link press', async () => {
11 | expect.assertions(2)
12 | const link = 'https://github.com/flyerhq/'
13 | const getPreviewDataMock = jest
14 | .spyOn(utils, 'getPreviewData')
15 | .mockResolvedValue({
16 | description: 'description',
17 | image: {
18 | height: 460,
19 | url: 'https://avatars2.githubusercontent.com/u/59206044',
20 | width: 460,
21 | },
22 | link,
23 | title: 'title',
24 | })
25 | const openUrlMock = jest.spyOn(Linking, 'openURL')
26 | const { getByRole, getByText } = render(
27 |
38 | )
39 | await waitFor(() => getByRole('image'))
40 | const image = getByRole('image')
41 | expect(image).toBeDefined()
42 | const text = getByText(link)
43 | fireEvent.press(text)
44 | expect(openUrlMock).toHaveBeenCalledWith(link)
45 | getPreviewDataMock.mockRestore()
46 | openUrlMock.mockRestore()
47 | })
48 |
49 | it('renders preview image without https and handles link press', async () => {
50 | expect.assertions(2)
51 | const link = 'github.com/flyerhq/'
52 | const getPreviewDataMock = jest
53 | .spyOn(utils, 'getPreviewData')
54 | .mockResolvedValue({
55 | description: 'description',
56 | image: {
57 | height: 460,
58 | url: 'https://avatars2.githubusercontent.com/u/59206044',
59 | width: 460,
60 | },
61 | link,
62 | title: 'title',
63 | })
64 | const openUrlMock = jest.spyOn(Linking, 'openURL')
65 | const { getByRole, getByText } = render(
66 |
73 | )
74 | await waitFor(() => getByRole('image'))
75 | const image = getByRole('image')
76 | expect(image).toBeDefined()
77 | const text = getByText(link)
78 | fireEvent.press(text)
79 | expect(openUrlMock).toHaveBeenCalledWith('https://' + link)
80 | getPreviewDataMock.mockRestore()
81 | openUrlMock.mockRestore()
82 | })
83 |
84 | it('renders and handles email press', async () => {
85 | expect.assertions(1)
86 | const email = 'john@flyer.chat'
87 | const getPreviewDataMock = jest
88 | .spyOn(utils, 'getPreviewData')
89 | .mockResolvedValue({})
90 | const openUrlMock = jest.spyOn(Linking, 'openURL')
91 | const { getByText } = render(
92 |
103 | )
104 | await waitFor(() => getByText(email))
105 | const text = getByText(email)
106 | fireEvent.press(text)
107 | expect(openUrlMock).toHaveBeenCalledWith(`mailto:${email}`)
108 | getPreviewDataMock.mockRestore()
109 | openUrlMock.mockRestore()
110 | })
111 | })
112 |
--------------------------------------------------------------------------------
/src/components/TextMessage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TextMessage'
2 |
--------------------------------------------------------------------------------
/src/components/TextMessage/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | import { MessageType, Theme, User } from '../../types'
4 | import { getUserAvatarNameColor } from '../../utils'
5 |
6 | const styles = ({
7 | message,
8 | theme,
9 | user,
10 | }: {
11 | message: MessageType.Text
12 | theme: Theme
13 | user?: User
14 | }) =>
15 | StyleSheet.create({
16 | descriptionText: {
17 | ...(user?.id === message.author.id
18 | ? theme.fonts.sentMessageLinkDescriptionTextStyle
19 | : theme.fonts.receivedMessageLinkDescriptionTextStyle),
20 | marginTop: 4,
21 | },
22 | headerText: {
23 | ...theme.fonts.userNameTextStyle,
24 | color: getUserAvatarNameColor(
25 | message.author,
26 | theme.colors.userAvatarNameColors
27 | ),
28 | marginBottom: 6,
29 | },
30 | titleText:
31 | user?.id === message.author.id
32 | ? theme.fonts.sentMessageLinkTitleTextStyle
33 | : theme.fonts.receivedMessageLinkTitleTextStyle,
34 | text:
35 | user?.id === message.author.id
36 | ? theme.fonts.sentMessageBodyTextStyle
37 | : theme.fonts.receivedMessageBodyTextStyle,
38 | textContainer: {
39 | marginHorizontal: theme.insets.messageInsetsHorizontal,
40 | marginVertical: theme.insets.messageInsetsVertical,
41 | },
42 | })
43 |
44 | export default styles
45 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AttachmentButton'
2 | export * from './Avatar'
3 | export * from './Chat'
4 | export * from './CircularActivityIndicator'
5 | export * from './FileMessage'
6 | export * from './ImageMessage'
7 | export * from './Input'
8 | export * from './Message'
9 | export * from './SendButton'
10 | export * from './StatusIcon'
11 | export * from './TextMessage'
12 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-native/Libraries/Blob/Blob' {
2 | class Blob {
3 | constructor(parts: Array)
4 |
5 | get size(): number
6 | }
7 |
8 | export default Blob
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './usePrevious'
2 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export const usePrevious = (value: T) => {
4 | const ref = React.useRef()
5 |
6 | React.useEffect(() => {
7 | ref.current = value
8 | }, [value])
9 |
10 | return ref.current
11 | }
12 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components'
2 | export * from './hooks'
3 | export * from './l10n'
4 | export * from './theme'
5 | export * from './types'
6 | export * from './utils'
7 |
--------------------------------------------------------------------------------
/src/l10n.ts:
--------------------------------------------------------------------------------
1 | /** Base chat l10n containing all required properties to provide localized copy. */
2 | export const l10n = {
3 | en: {
4 | attachmentButtonAccessibilityLabel: 'Send media',
5 | emptyChatPlaceholder: 'No messages here yet',
6 | fileButtonAccessibilityLabel: 'File',
7 | inputPlaceholder: 'Message',
8 | sendButtonAccessibilityLabel: 'Send',
9 | },
10 | es: {
11 | attachmentButtonAccessibilityLabel: 'Enviar multimedia',
12 | emptyChatPlaceholder: 'Aún no hay mensajes',
13 | fileButtonAccessibilityLabel: 'Archivo',
14 | inputPlaceholder: 'Mensaje',
15 | sendButtonAccessibilityLabel: 'Enviar',
16 | },
17 | ko: {
18 | attachmentButtonAccessibilityLabel: '미디어 보내기',
19 | emptyChatPlaceholder: '주고받은 메시지가 없습니다',
20 | fileButtonAccessibilityLabel: '파일',
21 | inputPlaceholder: '메시지',
22 | sendButtonAccessibilityLabel: '보내기',
23 | },
24 | pl: {
25 | attachmentButtonAccessibilityLabel: 'Wyślij multimedia',
26 | emptyChatPlaceholder: 'Tu jeszcze nie ma wiadomości',
27 | fileButtonAccessibilityLabel: 'Plik',
28 | inputPlaceholder: 'Napisz wiadomość',
29 | sendButtonAccessibilityLabel: 'Wyślij',
30 | },
31 | pt: {
32 | attachmentButtonAccessibilityLabel: 'Envia mídia',
33 | emptyChatPlaceholder: 'Ainda não há mensagens aqui',
34 | fileButtonAccessibilityLabel: 'Arquivo',
35 | inputPlaceholder: 'Mensagem',
36 | sendButtonAccessibilityLabel: 'Enviar',
37 | },
38 | ru: {
39 | attachmentButtonAccessibilityLabel: 'Отправить медиа',
40 | emptyChatPlaceholder: 'Пока что у вас нет сообщений',
41 | fileButtonAccessibilityLabel: 'Файл',
42 | inputPlaceholder: 'Сообщение',
43 | sendButtonAccessibilityLabel: 'Отправить',
44 | },
45 | tr: {
46 | attachmentButtonAccessibilityLabel: 'Medya gönder',
47 | emptyChatPlaceholder: 'Henüz mesaj yok',
48 | fileButtonAccessibilityLabel: 'Dosya',
49 | inputPlaceholder: 'Mesaj yazın',
50 | sendButtonAccessibilityLabel: 'Gönder',
51 | },
52 | uk: {
53 | attachmentButtonAccessibilityLabel: 'Надіслати медіа',
54 | emptyChatPlaceholder: 'Повідомлень ще немає',
55 | fileButtonAccessibilityLabel: 'Файл',
56 | inputPlaceholder: 'Повідомлення',
57 | sendButtonAccessibilityLabel: 'Надіслати',
58 | },
59 | }
60 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { ColorValue } from 'react-native'
2 |
3 | import { Theme } from './types'
4 |
5 | // For internal usage only. Use values from theme itself.
6 |
7 | /** @see {@link ThemeColors.userAvatarNameColors} */
8 | export const COLORS: ColorValue[] = [
9 | '#ff6767',
10 | '#66e0da',
11 | '#f5a2d9',
12 | '#f0c722',
13 | '#6a85e5',
14 | '#fd9a6f',
15 | '#92db6e',
16 | '#73b8e5',
17 | '#fd7590',
18 | '#c78ae5',
19 | ]
20 |
21 | /** Dark */
22 | const DARK = '#1f1c38'
23 |
24 | /** Error */
25 | const ERROR = '#ff6767'
26 |
27 | /** N0 */
28 | const NEUTRAL_0 = '#1d1c21'
29 |
30 | /** N2 */
31 | const NEUTRAL_2 = '#9e9cab'
32 |
33 | /** N7 */
34 | const NEUTRAL_7 = '#ffffff'
35 |
36 | /** N7 with opacity */
37 | const NEUTRAL_7_WITH_OPACITY = '#ffffff80'
38 |
39 | /** Primary */
40 | const PRIMARY = '#6f61e8'
41 |
42 | /** Secondary */
43 | const SECONDARY = '#f5f5f7'
44 |
45 | /** Secondary dark */
46 | const SECONDARY_DARK = '#2b2250'
47 |
48 | /** Default chat theme which implements {@link Theme} */
49 | export const defaultTheme: Theme = {
50 | borders: {
51 | inputBorderRadius: 20,
52 | messageBorderRadius: 20,
53 | },
54 | colors: {
55 | background: NEUTRAL_7,
56 | error: ERROR,
57 | inputBackground: NEUTRAL_0,
58 | inputText: NEUTRAL_7,
59 | primary: PRIMARY,
60 | receivedMessageDocumentIcon: PRIMARY,
61 | secondary: SECONDARY,
62 | sentMessageDocumentIcon: NEUTRAL_7,
63 | userAvatarImageBackground: 'transparent',
64 | userAvatarNameColors: COLORS,
65 | },
66 | fonts: {
67 | dateDividerTextStyle: {
68 | color: NEUTRAL_2,
69 | fontSize: 12,
70 | fontWeight: '800',
71 | lineHeight: 16,
72 | },
73 | emptyChatPlaceholderTextStyle: {
74 | color: NEUTRAL_2,
75 | fontSize: 16,
76 | fontWeight: '500',
77 | lineHeight: 24,
78 | },
79 | inputTextStyle: {
80 | fontSize: 16,
81 | fontWeight: '500',
82 | lineHeight: 24,
83 | },
84 | receivedMessageBodyTextStyle: {
85 | color: NEUTRAL_0,
86 | fontSize: 16,
87 | fontWeight: '500',
88 | lineHeight: 24,
89 | },
90 | receivedMessageCaptionTextStyle: {
91 | color: NEUTRAL_2,
92 | fontSize: 12,
93 | fontWeight: '500',
94 | lineHeight: 16,
95 | },
96 | receivedMessageLinkDescriptionTextStyle: {
97 | color: NEUTRAL_0,
98 | fontSize: 14,
99 | fontWeight: '400',
100 | lineHeight: 20,
101 | },
102 | receivedMessageLinkTitleTextStyle: {
103 | color: NEUTRAL_0,
104 | fontSize: 16,
105 | fontWeight: '800',
106 | lineHeight: 22,
107 | },
108 | sentMessageBodyTextStyle: {
109 | color: NEUTRAL_7,
110 | fontSize: 16,
111 | fontWeight: '500',
112 | lineHeight: 24,
113 | },
114 | sentMessageCaptionTextStyle: {
115 | color: NEUTRAL_7_WITH_OPACITY,
116 | fontSize: 12,
117 | fontWeight: '500',
118 | lineHeight: 16,
119 | },
120 | sentMessageLinkDescriptionTextStyle: {
121 | color: NEUTRAL_7,
122 | fontSize: 14,
123 | fontWeight: '400',
124 | lineHeight: 20,
125 | },
126 | sentMessageLinkTitleTextStyle: {
127 | color: NEUTRAL_7,
128 | fontSize: 16,
129 | fontWeight: '800',
130 | lineHeight: 22,
131 | },
132 | userAvatarTextStyle: {
133 | color: NEUTRAL_7,
134 | fontSize: 12,
135 | fontWeight: '800',
136 | lineHeight: 16,
137 | },
138 | userNameTextStyle: {
139 | fontSize: 12,
140 | fontWeight: '800',
141 | lineHeight: 16,
142 | },
143 | },
144 | insets: {
145 | messageInsetsHorizontal: 20,
146 | messageInsetsVertical: 16,
147 | },
148 | }
149 |
150 | /** Dark chat theme which implements {@link Theme} */
151 | export const darkTheme: Theme = {
152 | ...defaultTheme,
153 | colors: {
154 | ...defaultTheme.colors,
155 | background: DARK,
156 | inputBackground: SECONDARY_DARK,
157 | secondary: SECONDARY_DARK,
158 | },
159 | fonts: {
160 | ...defaultTheme.fonts,
161 | dateDividerTextStyle: {
162 | ...defaultTheme.fonts.dateDividerTextStyle,
163 | color: NEUTRAL_7,
164 | },
165 | receivedMessageBodyTextStyle: {
166 | ...defaultTheme.fonts.receivedMessageBodyTextStyle,
167 | color: NEUTRAL_7,
168 | },
169 | receivedMessageCaptionTextStyle: {
170 | ...defaultTheme.fonts.receivedMessageCaptionTextStyle,
171 | color: NEUTRAL_7_WITH_OPACITY,
172 | },
173 | receivedMessageLinkDescriptionTextStyle: {
174 | ...defaultTheme.fonts.receivedMessageLinkDescriptionTextStyle,
175 | color: NEUTRAL_7,
176 | },
177 | receivedMessageLinkTitleTextStyle: {
178 | ...defaultTheme.fonts.receivedMessageLinkTitleTextStyle,
179 | color: NEUTRAL_7,
180 | },
181 | },
182 | }
183 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { PreviewData } from '@flyerhq/react-native-link-preview'
2 | import * as React from 'react'
3 | import { ColorValue, ImageURISource, TextStyle } from 'react-native'
4 |
5 | export namespace MessageType {
6 | export type Any = Custom | File | Image | Text | Unsupported
7 |
8 | export type DerivedMessage =
9 | | DerivedCustom
10 | | DerivedFile
11 | | DerivedImage
12 | | DerivedText
13 | | DerivedUnsupported
14 | export type DerivedAny = DateHeader | DerivedMessage
15 |
16 | export type PartialAny =
17 | | PartialCustom
18 | | PartialFile
19 | | PartialImage
20 | | PartialText
21 |
22 | interface Base {
23 | author: User
24 | createdAt?: number
25 | id: string
26 | metadata?: Record
27 | roomId?: string
28 | status?: 'delivered' | 'error' | 'seen' | 'sending' | 'sent'
29 | type: 'custom' | 'file' | 'image' | 'text' | 'unsupported'
30 | updatedAt?: number
31 | }
32 |
33 | export interface DerivedMessageProps extends Base {
34 | nextMessageInGroup: boolean
35 | // TODO: Check name?
36 | offset: number
37 | showName: boolean
38 | showStatus: boolean
39 | }
40 |
41 | export interface DerivedCustom extends DerivedMessageProps, Custom {
42 | type: Custom['type']
43 | }
44 |
45 | export interface DerivedFile extends DerivedMessageProps, File {
46 | type: File['type']
47 | }
48 |
49 | export interface DerivedImage extends DerivedMessageProps, Image {
50 | type: Image['type']
51 | }
52 |
53 | export interface DerivedText extends DerivedMessageProps, Text {
54 | type: Text['type']
55 | }
56 |
57 | export interface DerivedUnsupported extends DerivedMessageProps, Unsupported {
58 | type: Unsupported['type']
59 | }
60 |
61 | export interface PartialCustom extends Base {
62 | metadata?: Record
63 | type: 'custom'
64 | }
65 |
66 | export interface Custom extends Base, PartialCustom {
67 | type: 'custom'
68 | }
69 |
70 | export interface PartialFile {
71 | metadata?: Record
72 | mimeType?: string
73 | name: string
74 | size: number
75 | type: 'file'
76 | uri: string
77 | }
78 |
79 | export interface File extends Base, PartialFile {
80 | type: 'file'
81 | }
82 |
83 | export interface PartialImage {
84 | height?: number
85 | metadata?: Record
86 | name: string
87 | size: number
88 | type: 'image'
89 | uri: string
90 | width?: number
91 | }
92 |
93 | export interface Image extends Base, PartialImage {
94 | type: 'image'
95 | }
96 |
97 | export interface PartialText {
98 | metadata?: Record
99 | previewData?: PreviewData
100 | text: string
101 | type: 'text'
102 | }
103 |
104 | export interface Text extends Base, PartialText {
105 | type: 'text'
106 | }
107 |
108 | export interface Unsupported extends Base {
109 | type: 'unsupported'
110 | }
111 |
112 | export interface DateHeader {
113 | id: string
114 | text: string
115 | type: 'dateHeader'
116 | }
117 | }
118 |
119 | export interface PreviewImage {
120 | id: string
121 | uri: ImageURISource['uri']
122 | }
123 |
124 | export interface Size {
125 | height: number
126 | width: number
127 | }
128 |
129 | /** Base chat theme containing all required properties to make a theme.
130 | * Implement this interface if you want to create a custom theme. */
131 | export interface Theme {
132 | borders: ThemeBorders
133 | colors: ThemeColors
134 | fonts: ThemeFonts
135 | icons?: ThemeIcons
136 | insets: ThemeInsets
137 | }
138 |
139 | export interface ThemeBorders {
140 | inputBorderRadius: number
141 | messageBorderRadius: number
142 | }
143 |
144 | export interface ThemeColors {
145 | background: ColorValue
146 | error: ColorValue
147 | inputBackground: ColorValue
148 | inputText: ColorValue
149 | primary: ColorValue
150 | secondary: ColorValue
151 | receivedMessageDocumentIcon: ColorValue
152 | sentMessageDocumentIcon: ColorValue
153 | userAvatarImageBackground: ColorValue
154 | userAvatarNameColors: ColorValue[]
155 | }
156 |
157 | export interface ThemeFonts {
158 | dateDividerTextStyle: TextStyle
159 | emptyChatPlaceholderTextStyle: TextStyle
160 | inputTextStyle: TextStyle
161 | receivedMessageBodyTextStyle: TextStyle
162 | receivedMessageCaptionTextStyle: TextStyle
163 | receivedMessageLinkDescriptionTextStyle: TextStyle
164 | receivedMessageLinkTitleTextStyle: TextStyle
165 | sentMessageBodyTextStyle: TextStyle
166 | sentMessageCaptionTextStyle: TextStyle
167 | sentMessageLinkDescriptionTextStyle: TextStyle
168 | sentMessageLinkTitleTextStyle: TextStyle
169 | userAvatarTextStyle: TextStyle
170 | userNameTextStyle: TextStyle
171 | }
172 |
173 | export interface ThemeIcons {
174 | attachmentButtonIcon?: () => React.ReactNode
175 | deliveredIcon?: () => React.ReactNode
176 | documentIcon?: () => React.ReactNode
177 | errorIcon?: () => React.ReactNode
178 | seenIcon?: () => React.ReactNode
179 | sendButtonIcon?: () => React.ReactNode
180 | sendingIcon?: () => React.ReactNode
181 | }
182 |
183 | export interface ThemeInsets {
184 | messageInsetsHorizontal: number
185 | messageInsetsVertical: number
186 | }
187 |
188 | export interface User {
189 | createdAt?: number
190 | firstName?: string
191 | id: string
192 | imageUrl?: ImageURISource['uri']
193 | lastName?: string
194 | lastSeen?: number
195 | metadata?: Record
196 | role?: 'admin' | 'agent' | 'moderator' | 'user'
197 | updatedAt?: number
198 | }
199 |
--------------------------------------------------------------------------------
/src/utils/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { formatBytes, getTextSizeInBytes, unwrap } from '..'
2 |
3 | describe('formatBytes', () => {
4 | it('formats bytes correctly when the size is 0', () => {
5 | expect.assertions(1)
6 | expect(formatBytes(0)).toBe('0 B')
7 | })
8 |
9 | it('formats bytes correctly', () => {
10 | expect.assertions(1)
11 | expect(formatBytes(1024)).toBe('1 kB')
12 | })
13 | })
14 |
15 | describe('getTextSizeInBytes', () => {
16 | it('calculates the size for a simple text', () => {
17 | expect.assertions(1)
18 | const text = 'text'
19 | expect(getTextSizeInBytes(text)).toBe(4)
20 | })
21 |
22 | it('calculates the size for an emoji text', () => {
23 | expect.assertions(1)
24 | const text = '🤔 🤓'
25 | expect(getTextSizeInBytes(text)).toBe(9)
26 | })
27 | })
28 |
29 | describe('unwrap', () => {
30 | it('returns an empty object', () => {
31 | expect.assertions(1)
32 | expect(unwrap(undefined)).toStrictEqual({})
33 | })
34 |
35 | it('returns a provided prop', () => {
36 | expect.assertions(1)
37 | const prop = 'prop'
38 | expect(unwrap(prop)).toStrictEqual(prop)
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import * as React from 'react'
3 | import { ColorValue } from 'react-native'
4 | import Blob from 'react-native/Libraries/Blob/Blob'
5 |
6 | import { l10n } from '../l10n'
7 | import { defaultTheme } from '../theme'
8 | import { MessageType, PreviewImage, Theme, User } from '../types'
9 |
10 | export const L10nContext = React.createContext(
11 | l10n.en
12 | )
13 | export const ThemeContext = React.createContext(defaultTheme)
14 | export const UserContext = React.createContext(undefined)
15 |
16 | /** Returns text representation of a provided bytes value (e.g. 1kB, 1GB) */
17 | export const formatBytes = (size: number, fractionDigits = 2) => {
18 | if (size <= 0) return '0 B'
19 | const multiple = Math.floor(Math.log(size) / Math.log(1024))
20 | return (
21 | parseFloat((size / Math.pow(1024, multiple)).toFixed(fractionDigits)) +
22 | ' ' +
23 | ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][multiple]
24 | )
25 | }
26 |
27 | /** Returns size in bytes of the provided text */
28 | export const getTextSizeInBytes = (text: string) => new Blob([text]).size
29 |
30 | /** Returns user avatar and name color based on the ID */
31 | export const getUserAvatarNameColor = (user: User, colors: ColorValue[]) =>
32 | colors[hashCode(user.id) % colors.length]
33 |
34 | /** Returns user initials (can have only first letter of firstName/lastName or both) */
35 | export const getUserInitials = ({ firstName, lastName }: User) =>
36 | `${firstName?.charAt(0) ?? ''}${lastName?.charAt(0) ?? ''}`
37 | .toUpperCase()
38 | .trim()
39 |
40 | /** Returns user name as joined firstName and lastName */
41 | export const getUserName = ({ firstName, lastName }: User) =>
42 | `${firstName ?? ''} ${lastName ?? ''}`.trim()
43 |
44 | /** Returns hash code of the provided text */
45 | export const hashCode = (text = '') => {
46 | let i,
47 | chr,
48 | hash = 0
49 | if (text.length === 0) return hash
50 | for (i = 0; i < text.length; i++) {
51 | chr = text.charCodeAt(i)
52 | // eslint-disable-next-line no-bitwise
53 | hash = (hash << 5) - hash + chr
54 | // eslint-disable-next-line no-bitwise
55 | hash |= 0 // Convert to 32bit integer
56 | }
57 | return Math.abs(hash)
58 | }
59 |
60 | /** Inits dayjs locale */
61 | export const initLocale = (locale?: keyof typeof l10n) => {
62 | const locales: { [key in keyof typeof l10n]: unknown } = {
63 | en: require('dayjs/locale/en'),
64 | es: require('dayjs/locale/es'),
65 | ko: require('dayjs/locale/ko'),
66 | pl: require('dayjs/locale/pl'),
67 | pt: require('dayjs/locale/pt'),
68 | ru: require('dayjs/locale/ru'),
69 | tr: require('dayjs/locale/tr'),
70 | uk: require('dayjs/locale/uk'),
71 | }
72 |
73 | locale ? locales[locale] : locales.en
74 | dayjs.locale(locale)
75 | }
76 |
77 | /** Returns either prop or empty object if null or undefined */
78 | export const unwrap = (prop: T) => prop ?? {}
79 |
80 | /** Returns formatted date used as a divider between different days in the chat history */
81 | const getVerboseDateTimeRepresentation = (
82 | dateTime: number,
83 | {
84 | dateFormat,
85 | timeFormat,
86 | }: {
87 | dateFormat?: string
88 | timeFormat?: string
89 | }
90 | ) => {
91 | const formattedDate = dateFormat
92 | ? dayjs(dateTime).format(dateFormat)
93 | : dayjs(dateTime).format('MMM D')
94 |
95 | const formattedTime = timeFormat
96 | ? dayjs(dateTime).format(timeFormat)
97 | : dayjs(dateTime).format('HH:mm')
98 |
99 | const localDateTime = dayjs(dateTime)
100 | const now = dayjs()
101 |
102 | if (
103 | localDateTime.isSame(now, 'day') &&
104 | localDateTime.isSame(now, 'month') &&
105 | localDateTime.isSame(now, 'year')
106 | ) {
107 | return formattedTime
108 | }
109 |
110 | return `${formattedDate}, ${formattedTime}`
111 | }
112 |
113 | /** Parses provided messages to chat messages (with headers) and returns them with a gallery */
114 | export const calculateChatMessages = (
115 | messages: MessageType.Any[],
116 | user: User,
117 | {
118 | customDateHeaderText,
119 | dateFormat,
120 | showUserNames,
121 | timeFormat,
122 | }: {
123 | customDateHeaderText?: (dateTime: number) => string
124 | dateFormat?: string
125 | showUserNames: boolean
126 | timeFormat?: string
127 | }
128 | ) => {
129 | let chatMessages: MessageType.DerivedAny[] = []
130 | let gallery: PreviewImage[] = []
131 |
132 | let shouldShowName = false
133 |
134 | for (let i = messages.length - 1; i >= 0; i--) {
135 | const isFirst = i === messages.length - 1
136 | const isLast = i === 0
137 | const message = messages[i]
138 | const messageHasCreatedAt = !!message.createdAt
139 | const nextMessage = isLast ? undefined : messages[i - 1]
140 | const nextMessageHasCreatedAt = !!nextMessage?.createdAt
141 | const nextMessageSameAuthor = message.author.id === nextMessage?.author.id
142 | const notMyMessage = message.author.id !== user.id
143 |
144 | let nextMessageDateThreshold = false
145 | let nextMessageDifferentDay = false
146 | let nextMessageInGroup = false
147 | let showName = false
148 |
149 | if (showUserNames) {
150 | const previousMessage = isFirst ? undefined : messages[i + 1]
151 |
152 | const isFirstInGroup =
153 | notMyMessage &&
154 | (message.author.id !== previousMessage?.author.id ||
155 | (messageHasCreatedAt &&
156 | !!previousMessage?.createdAt &&
157 | message.createdAt! - previousMessage!.createdAt! > 60000))
158 |
159 | if (isFirstInGroup) {
160 | shouldShowName = false
161 | if (message.type === 'text') {
162 | showName = true
163 | } else {
164 | shouldShowName = true
165 | }
166 | }
167 |
168 | if (message.type === 'text' && shouldShowName) {
169 | showName = true
170 | shouldShowName = false
171 | }
172 | }
173 |
174 | if (messageHasCreatedAt && nextMessageHasCreatedAt) {
175 | nextMessageDateThreshold =
176 | nextMessage!.createdAt! - message.createdAt! >= 900000
177 |
178 | nextMessageDifferentDay = !dayjs(message.createdAt!).isSame(
179 | nextMessage!.createdAt!,
180 | 'day'
181 | )
182 |
183 | nextMessageInGroup =
184 | nextMessageSameAuthor &&
185 | nextMessage!.createdAt! - message.createdAt! <= 60000
186 | }
187 |
188 | if (isFirst && messageHasCreatedAt) {
189 | const text =
190 | customDateHeaderText?.(message.createdAt!) ??
191 | getVerboseDateTimeRepresentation(message.createdAt!, {
192 | dateFormat,
193 | timeFormat,
194 | })
195 | chatMessages = [{ id: text, text, type: 'dateHeader' }, ...chatMessages]
196 | }
197 |
198 | chatMessages = [
199 | {
200 | ...message,
201 | nextMessageInGroup,
202 | // TODO: Check this
203 | offset: !nextMessageInGroup ? 12 : 0,
204 | showName:
205 | notMyMessage &&
206 | showUserNames &&
207 | showName &&
208 | !!getUserName(message.author),
209 | showStatus: true,
210 | },
211 | ...chatMessages,
212 | ]
213 |
214 | if (nextMessageDifferentDay || nextMessageDateThreshold) {
215 | const text =
216 | customDateHeaderText?.(nextMessage!.createdAt!) ??
217 | getVerboseDateTimeRepresentation(nextMessage!.createdAt!, {
218 | dateFormat,
219 | timeFormat,
220 | })
221 |
222 | chatMessages = [
223 | {
224 | id: text,
225 | text,
226 | type: 'dateHeader',
227 | },
228 | ...chatMessages,
229 | ]
230 | }
231 |
232 | if (message.type === 'image') {
233 | gallery = [...gallery, { id: message.id, uri: message.uri }]
234 | }
235 | }
236 |
237 | return {
238 | chatMessages,
239 | gallery,
240 | }
241 | }
242 |
243 | /** Removes all derived message props from the derived message */
244 | export const excludeDerivedMessageProps = (
245 | message: MessageType.DerivedMessage
246 | ) => {
247 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
248 | const { nextMessageInGroup, offset, showName, showStatus, ...rest } = message
249 | return { ...rest } as MessageType.Any
250 | }
251 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "esModuleInterop": true,
5 | "jsx": "react",
6 | "lib": ["ESNext"],
7 | "module": "ESNext",
8 | "moduleResolution": "Node",
9 | "noEmitOnError": true,
10 | "outDir": "./lib",
11 | "skipLibCheck": true,
12 | "sourceMap": true,
13 | "strict": true,
14 | "target": "ES2018",
15 | },
16 | "exclude": ["**/__tests__/*"],
17 | "include": ["src"]
18 | }
19 |
--------------------------------------------------------------------------------